Merge pull request #31 from cjfranko/webui_updates

web ui update! We are for initial release!
This commit is contained in:
Chris Frankland-Wright 2025-08-08 01:32:14 +01:00 committed by GitHub
commit cf24c9029e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 581 additions and 69 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
static/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

BIN
static/assets/header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

43
static/icon-map.js Normal file
View file

@ -0,0 +1,43 @@
// In this file, you can define the paths to your local icon image files.
const iconMap = {
ltcStatus: {
'LOCK': { src: 'assets/timeturner_ltc_green.png', tooltip: 'LTC signal is locked and stable.' },
'FREE': { src: 'assets/timeturner_ltc_orange.png', tooltip: 'LTC signal is in freewheel mode.' },
'default': { src: 'assets/timeturner_ltc_red.png', tooltip: 'LTC signal is not detected.' }
},
ntpActive: {
true: { src: 'assets/timeturner_ntp_green.png', tooltip: 'NTP service is active.' },
false: { src: 'assets/timeturner_ntp_red.png', tooltip: 'NTP service is inactive.' }
},
syncStatus: {
'IN SYNC': { src: 'assets/timeturner_sync_green.png', tooltip: 'System clock is in sync with LTC source.' },
'CLOCK AHEAD': { src: 'assets/timeturner_sync_orange.png', tooltip: 'System clock is ahead of the LTC source.' },
'CLOCK BEHIND': { src: 'assets/timeturner_sync_orange.png', tooltip: 'System clock is behind the LTC source.' },
'TIMETURNING': { src: 'assets/timeturner_timeturning.png', tooltip: 'Timeturner offset is active.' },
'default': { src: 'assets/timeturner_sync_red.png', tooltip: 'Sync status is unknown.' }
},
jitterStatus: {
'GOOD': { src: 'assets/timeturner_jitter_green.png', tooltip: 'Clock jitter is within acceptable limits.' },
'AVERAGE': { src: 'assets/timeturner_jitter_orange.png', tooltip: 'Clock jitter is moderate.' },
'BAD': { src: 'assets/timeturner_jitter_red.png', tooltip: 'Clock jitter is high and may affect accuracy.' },
'default': { src: 'assets/timeturner_jitter_red.png', tooltip: 'Jitter status is unknown.' }
},
deltaStatus: {
'good': { src: 'assets/timeturner_delta_green.png', tooltip: 'Clock delta is 0ms.' },
'average': { src: 'assets/timeturner_delta_orange.png', tooltip: 'Clock delta is less than 10ms.' },
'bad': { src: 'assets/timeturner_delta_red.png', tooltip: 'Clock delta is 10ms or greater.' }
},
frameRate: {
'23.98fps': { src: 'assets/timeturner_2398.png', tooltip: '23.98 frames per second' },
'24.00fps': { src: 'assets/timeturner_24.png', tooltip: '24.00 frames per second' },
'25.00fps': { src: 'assets/timeturner_25.png', tooltip: '25.00 frames per second' },
'29.97fps': { src: 'assets/timeturner_2997.png', tooltip: '29.97 frames per second' },
'30.00fps': { src: 'assets/timeturner_30.png', tooltip: '30.00 frames per second' },
'default': { src: 'assets/timeturner_default.png', tooltip: 'Unknown frame rate' }
},
lockRatio: {
'good': { src: 'assets/timeturner_lock_green.png', tooltip: 'Lock ratio is 100%.' },
'average': { src: 'assets/timeturner_lock_orange.png', tooltip: 'Lock ratio is 90% or higher.' },
'bad': { src: 'assets/timeturner_lock_red.png', tooltip: 'Lock ratio is below 90%.' }
}
};

View file

@ -5,47 +5,63 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NTP TimeTurner</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" href="favicon.ico" type="image/x-icon">
</head>
<body>
<div class="container">
<h1>NTP TimeTurner</h1>
<img src="assets/header.png" alt="NTP Timeturner" class="header-logo">
<!-- Mock Data Controls (hidden by default) -->
<div id="mock-controls" class="card full-width" style="display: none;">
<h2>Mock Data Controls</h2>
<div class="control-group">
<label for="mock-data-selector">Select Mock Data Scenario:</label>
<select id="mock-data-selector"></select>
</div>
</div>
<div class="grid">
<!-- LTC Status -->
<div class="card">
<h2>LTC Status</h2>
<h2>LTC Input</h2>
<p id="ltc-timecode">--:--:--:--</p>
<p id="ltc-status">--</p>
<p id="frame-rate">-- fps</p>
<p>Lock Ratio: <span id="lock-ratio">--</span>%</p>
<div class="icon-group">
<span id="ltc-status"></span>
<span id="frame-rate"></span>
<span id="lock-ratio"></span>
</div>
</div>
<!-- System Clock & Sync -->
<div class="card">
<h2>System Clock</h2>
<h2>NTP Clock</h2>
<p id="system-clock">--:--:--.---</p>
<p>Date: <span id="system-date">---- -- --</span></p>
<p>NTP Service: <span id="ntp-active">--</span></p>
<p>Sync Status: <span id="sync-status">--</span></p>
<p class="system-date-display"><span id="system-date">---- -- --</span></p>
<div class="icon-group">
<span id="ntp-active"></span>
<span id="sync-status"></span>
<span id="jitter-status"></span>
<span id="delta-status"></span>
</div>
<!-- Delta & Jitter -->
<div class="card">
<h2>Clock Offset</h2>
<p>Delta: <span id="delta-ms">--</span> ms (<span id="delta-frames">--</span> frames)</p>
<p>Jitter: <span id="jitter-status">--</span></p>
<p id="delta-text">Δ -- ms (-- frames)</p>
</div>
<!-- Network Interfaces -->
<div class="card">
<div class="card-header">
<img src="assets/timeturner_network.png" class="header-icon" alt="Network Icon">
<h2>Network</h2>
<ul id="interfaces">
<li>--</li>
</ul>
</div>
<p id="interfaces">--</p>
</div>
<!-- Controls -->
<div class="card full-width">
<div class="card full-width collapsible-card">
<div class="toggle-header" id="controls-toggle">
<img src="assets/timeturner_controls.png" class="toggle-icon" alt="Controls Icon">
<h2>Controls</h2>
</div>
<div class="collapsible-content" id="controls-content">
<div class="control-group">
<label for="hw-offset">Hardware Offset (ms):</label>
<input type="number" id="hw-offset" name="hw-offset">
@ -98,14 +114,28 @@
<span id="date-message"></span>
</div>
</div>
</div>
<!-- Logs -->
<div class="card full-width">
<div class="card full-width collapsible-card">
<div class="toggle-header" id="logs-toggle">
<img src="assets/timeturner_logs.png" class="toggle-icon" alt="Logs Icon">
<h2>Logs</h2>
</div>
<div class="collapsible-content" id="logs-content">
<pre id="logs" class="log-box"></pre>
</div>
</div>
</div>
<footer>
<p>
Built by Chris Frankland-Wright and John Rogers | Have Blue Broadcast Media |
<a href="https://github.com/cjfranko/NTP-Timeturner" target="_blank" rel="noopener noreferrer">https://github.com/cjfranko/NTP-Timeturner</a>
</p>
</footer>
</div>
<script src="icon-map.js"></script>
<script src="mock-data.js"></script>
<script src="script.js"></script>
</body>
</html>

168
static/mock-data.js Normal file
View file

@ -0,0 +1,168 @@
// This file contains mock data sets for UI development and testing without a live backend.
const mockApiDataSets = {
allGood: {
status: {
ltc_status: 'LOCK',
ltc_timecode: '10:20:30:00',
frame_rate: '25.00fps',
lock_ratio: 99.5,
system_clock: '10:20:30.500',
system_date: '2025-08-07',
ntp_active: true,
sync_status: 'IN SYNC',
timecode_delta_ms: 5,
timecode_delta_frames: 0.125,
jitter_status: 'GOOD',
interfaces: ['192.168.1.100/24 (eth0)', '10.0.0.5/8 (wlan0)'],
},
config: {
hardwareOffsetMs: 10,
autoSyncEnabled: true,
defaultNudgeMs: 2,
timeturnerOffset: { hours: 1, minutes: 2, seconds: 3, frames: 4, milliseconds: 50 },
},
logs: [
'2025-08-07 10:20:30 [INFO] Starting up...',
'2025-08-07 10:20:32 [INFO] LTC LOCK detected. Frame rate: 25.00fps.',
'2025-08-07 10:20:35 [INFO] Initial sync complete. Clock adjusted by -15ms.',
]
},
ltcFree: {
status: {
ltc_status: 'FREE',
ltc_timecode: '11:22:33:11',
frame_rate: '25.00fps',
lock_ratio: 40.2,
system_clock: '11:22:33.800',
system_date: '2025-08-07',
ntp_active: true,
sync_status: 'IN SYNC',
timecode_delta_ms: 3,
timecode_delta_frames: 0.075,
jitter_status: 'GOOD',
interfaces: ['192.168.1.100/24 (eth0)'],
},
config: {
hardwareOffsetMs: 10,
autoSyncEnabled: true,
defaultNudgeMs: 2,
timeturnerOffset: { hours: 0, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 },
},
logs: [ '2025-08-07 11:22:30 [WARN] LTC signal lost, entering freewheel.' ]
},
clockAhead: {
status: {
ltc_status: 'LOCK',
ltc_timecode: '12:00:05:00',
frame_rate: '25.00fps',
lock_ratio: 98.1,
system_clock: '12:00:04.500',
system_date: '2025-08-07',
ntp_active: true,
sync_status: 'CLOCK AHEAD',
timecode_delta_ms: -500,
timecode_delta_frames: -12.5,
jitter_status: 'AVERAGE',
interfaces: ['192.168.1.100/24 (eth0)'],
},
config: {
hardwareOffsetMs: 10,
autoSyncEnabled: true,
defaultNudgeMs: 2,
timeturnerOffset: { hours: 0, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 },
},
logs: [ '2025-08-07 12:00:00 [WARN] System clock is ahead of LTC source by 500ms.' ]
},
clockBehind: {
status: {
ltc_status: 'LOCK',
ltc_timecode: '13:30:10:00',
frame_rate: '25.00fps',
lock_ratio: 99.9,
system_clock: '13:30:10.800',
system_date: '2025-08-07',
ntp_active: true,
sync_status: 'CLOCK BEHIND',
timecode_delta_ms: 800,
timecode_delta_frames: 20,
jitter_status: 'AVERAGE',
interfaces: ['192.168.1.100/24 (eth0)'],
},
config: {
hardwareOffsetMs: 10,
autoSyncEnabled: true,
defaultNudgeMs: 2,
timeturnerOffset: { hours: 0, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 },
},
logs: [ '2025-08-07 13:30:00 [WARN] System clock is behind LTC source by 800ms.' ]
},
timeturning: {
status: {
ltc_status: 'LOCK',
ltc_timecode: '14:00:00:00',
frame_rate: '25.00fps',
lock_ratio: 100,
system_clock: '15:02:03.050',
system_date: '2025-08-07',
ntp_active: true,
sync_status: 'TIMETURNING',
timecode_delta_ms: 3723050, // a big number
timecode_delta_frames: 93076,
jitter_status: 'GOOD',
interfaces: ['192.168.1.100/24 (eth0)'],
},
config: {
hardwareOffsetMs: 10,
autoSyncEnabled: false,
defaultNudgeMs: 2,
timeturnerOffset: { hours: 1, minutes: 2, seconds: 3, frames: 4, milliseconds: 50 },
},
logs: [ '2025-08-07 14:00:00 [INFO] Timeturner offset is active.' ]
},
badJitter: {
status: {
ltc_status: 'LOCK',
ltc_timecode: '15:15:15:15',
frame_rate: '25.00fps',
lock_ratio: 95.0,
system_clock: '15:15:15.515',
system_date: '2025-08-07',
ntp_active: true,
sync_status: 'IN SYNC',
timecode_delta_ms: 10,
timecode_delta_frames: 0.25,
jitter_status: 'BAD',
interfaces: ['192.168.1.100/24 (eth0)'],
},
config: {
hardwareOffsetMs: 10,
autoSyncEnabled: true,
defaultNudgeMs: 2,
timeturnerOffset: { hours: 0, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 },
},
logs: [ '2025-08-07 15:15:00 [ERROR] High jitter detected on LTC source.' ]
},
ntpInactive: {
status: {
ltc_status: 'UNKNOWN',
ltc_timecode: '--:--:--:--',
frame_rate: '--',
lock_ratio: 0,
system_clock: '16:00:00.000',
system_date: '2025-08-07',
ntp_active: false,
sync_status: 'UNKNOWN',
timecode_delta_ms: 0,
timecode_delta_frames: 0,
jitter_status: 'UNKNOWN',
interfaces: [],
},
config: {
hardwareOffsetMs: 0,
autoSyncEnabled: false,
defaultNudgeMs: 2,
timeturnerOffset: { hours: 0, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 },
},
logs: [ '2025-08-07 16:00:00 [INFO] NTP service is inactive.' ]
}
};

View file

@ -1,4 +1,9 @@
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', () => {
// --- Mock Data Configuration ---
// Set to true to use mock data, false for live API.
const useMockData = false;
let currentMockSetKey = 'allGood'; // Default mock data set
let lastApiData = null;
let lastApiFetchTime = null;
@ -11,9 +16,9 @@ document.addEventListener('DOMContentLoaded', () => {
systemDate: document.getElementById('system-date'),
ntpActive: document.getElementById('ntp-active'),
syncStatus: document.getElementById('sync-status'),
deltaMs: document.getElementById('delta-ms'),
deltaFrames: document.getElementById('delta-frames'),
deltaStatus: document.getElementById('delta-status'),
jitterStatus: document.getElementById('jitter-status'),
deltaText: document.getElementById('delta-text'),
interfaces: document.getElementById('interfaces'),
logs: document.getElementById('logs'),
};
@ -40,37 +45,110 @@ document.addEventListener('DOMContentLoaded', () => {
const setDateButton = document.getElementById('set-date');
const dateMessage = document.getElementById('date-message');
// --- Collapsible Sections ---
const controlsToggle = document.getElementById('controls-toggle');
const controlsContent = document.getElementById('controls-content');
const logsToggle = document.getElementById('logs-toggle');
const logsContent = document.getElementById('logs-content');
// --- Mock Controls Setup ---
const mockControls = document.getElementById('mock-controls');
const mockDataSelector = document.getElementById('mock-data-selector');
function setupMockControls() {
if (useMockData) {
mockControls.style.display = 'block';
// Populate dropdown
Object.keys(mockApiDataSets).forEach(key => {
const option = document.createElement('option');
option.value = key;
option.textContent = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
mockDataSelector.appendChild(option);
});
mockDataSelector.value = currentMockSetKey;
// Handle selection change
mockDataSelector.addEventListener('change', (event) => {
currentMockSetKey = event.target.value;
// Re-fetch all data from the new mock set
fetchStatus();
fetchConfig();
fetchLogs();
});
}
}
function updateStatus(data) {
statusElements.ltcStatus.textContent = data.ltc_status;
const ltcStatus = data.ltc_status || 'UNKNOWN';
const ltcIconInfo = iconMap.ltcStatus[ltcStatus] || iconMap.ltcStatus.default;
statusElements.ltcStatus.innerHTML = `<img src="${ltcIconInfo.src}" class="status-icon" alt="" title="${ltcIconInfo.tooltip}">`;
statusElements.ltcStatus.className = ltcStatus.toLowerCase();
statusElements.ltcTimecode.textContent = data.ltc_timecode;
statusElements.frameRate.textContent = data.frame_rate;
statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2);
const frameRate = data.frame_rate || 'unknown';
const frameRateIconInfo = iconMap.frameRate[frameRate] || iconMap.frameRate.default;
statusElements.frameRate.innerHTML = `<img src="${frameRateIconInfo.src}" class="status-icon" alt="" title="${frameRateIconInfo.tooltip}">`;
const lockRatio = data.lock_ratio;
let lockRatioCategory;
if (lockRatio === 100) {
lockRatioCategory = 'good';
} else if (lockRatio >= 90) {
lockRatioCategory = 'average';
} else {
lockRatioCategory = 'bad';
}
const lockRatioIconInfo = iconMap.lockRatio[lockRatioCategory];
statusElements.lockRatio.innerHTML = `<img src="${lockRatioIconInfo.src}" class="status-icon" alt="" title="${lockRatioIconInfo.tooltip}">`;
statusElements.systemClock.textContent = data.system_clock;
statusElements.systemDate.textContent = data.system_date;
statusElements.ntpActive.textContent = data.ntp_active ? 'Active' : 'Inactive';
statusElements.ntpActive.className = data.ntp_active ? 'active' : 'inactive';
// Autofill the date input, but don't overwrite user edits.
if (!lastApiData || dateInput.value === lastApiData.system_date) {
dateInput.value = data.system_date;
}
statusElements.syncStatus.textContent = data.sync_status;
statusElements.syncStatus.className = data.sync_status.replace(/\s+/g, '-').toLowerCase();
statusElements.deltaMs.textContent = data.timecode_delta_ms;
statusElements.deltaFrames.textContent = data.timecode_delta_frames;
statusElements.jitterStatus.textContent = data.jitter_status;
statusElements.jitterStatus.className = data.jitter_status.toLowerCase();
statusElements.interfaces.innerHTML = '';
if (data.interfaces.length > 0) {
data.interfaces.forEach(ip => {
const li = document.createElement('li');
li.textContent = ip;
statusElements.interfaces.appendChild(li);
});
const ntpIconInfo = iconMap.ntpActive[!!data.ntp_active];
if (data.ntp_active) {
statusElements.ntpActive.innerHTML = `<img src="${ntpIconInfo.src}" class="status-icon" alt="" title="${ntpIconInfo.tooltip}">`;
statusElements.ntpActive.className = 'active';
} else {
const li = document.createElement('li');
li.textContent = 'No active interfaces found.';
statusElements.interfaces.appendChild(li);
statusElements.ntpActive.innerHTML = `<img src="${ntpIconInfo.src}" class="status-icon" alt="" title="${ntpIconInfo.tooltip}">`;
statusElements.ntpActive.className = 'inactive';
}
const syncStatus = data.sync_status || 'UNKNOWN';
const syncIconInfo = iconMap.syncStatus[syncStatus] || iconMap.syncStatus.default;
statusElements.syncStatus.innerHTML = `<img src="${syncIconInfo.src}" class="status-icon" alt="" title="${syncIconInfo.tooltip}">`;
statusElements.syncStatus.className = syncStatus.replace(/\s+/g, '-').toLowerCase();
// Delta Status
const deltaMs = data.timecode_delta_ms;
let deltaCategory;
if (deltaMs === 0) {
deltaCategory = 'good';
} else if (Math.abs(deltaMs) < 10) {
deltaCategory = 'average';
} else {
deltaCategory = 'bad';
}
const deltaIconInfo = iconMap.deltaStatus[deltaCategory];
statusElements.deltaStatus.innerHTML = `<img src="${deltaIconInfo.src}" class="status-icon" alt="" title="${deltaIconInfo.tooltip}">`;
const deltaTextValue = `${data.timecode_delta_ms} ms (${data.timecode_delta_frames} frames)`;
statusElements.deltaText.textContent = `Δ ${deltaTextValue}`;
const jitterStatus = data.jitter_status || 'UNKNOWN';
const jitterIconInfo = iconMap.jitterStatus[jitterStatus] || iconMap.jitterStatus.default;
statusElements.jitterStatus.innerHTML = `<img src="${jitterIconInfo.src}" class="status-icon" alt="" title="${jitterIconInfo.tooltip}">`;
statusElements.jitterStatus.className = jitterStatus.toLowerCase();
if (data.interfaces.length > 0) {
statusElements.interfaces.textContent = data.interfaces.join(' | ');
} else {
statusElements.interfaces.textContent = 'No active interfaces found.';
}
}
@ -134,6 +212,13 @@ document.addEventListener('DOMContentLoaded', () => {
}
async function fetchStatus() {
if (useMockData) {
const data = mockApiDataSets[currentMockSetKey].status;
updateStatus(data);
lastApiData = data;
lastApiFetchTime = new Date();
return;
}
try {
const response = await fetch('/api/status');
if (!response.ok) throw new Error('Failed to fetch status');
@ -149,6 +234,18 @@ document.addEventListener('DOMContentLoaded', () => {
}
async function fetchConfig() {
if (useMockData) {
const data = mockApiDataSets[currentMockSetKey].config;
hwOffsetInput.value = data.hardwareOffsetMs;
autoSyncCheckbox.checked = data.autoSyncEnabled;
offsetInputs.h.value = data.timeturnerOffset.hours;
offsetInputs.m.value = data.timeturnerOffset.minutes;
offsetInputs.s.value = data.timeturnerOffset.seconds;
offsetInputs.f.value = data.timeturnerOffset.frames;
offsetInputs.ms.value = data.timeturnerOffset.milliseconds || 0;
nudgeValueInput.value = data.defaultNudgeMs;
return;
}
try {
const response = await fetch('/api/config');
if (!response.ok) throw new Error('Failed to fetch config');
@ -180,6 +277,14 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
if (useMockData) {
console.log('Mock save:', config);
alert('Configuration saved (mock).');
// We can also update the mock data in memory to see changes reflected
mockApiDataSets[currentMockSetKey].config = config;
return;
}
try {
const response = await fetch('/api/config', {
method: 'POST',
@ -195,13 +300,21 @@ document.addEventListener('DOMContentLoaded', () => {
}
async function fetchLogs() {
if (useMockData) {
// Use a copy to avoid mutating the original mock data array
const logs = mockApiDataSets[currentMockSetKey].logs.slice();
// Show latest 20 logs, with the newest at the top.
logs.reverse();
statusElements.logs.textContent = logs.slice(0, 20).join('\n');
return;
}
try {
const response = await fetch('/api/logs');
if (!response.ok) throw new Error('Failed to fetch logs');
const logs = await response.json();
statusElements.logs.textContent = logs.join('\n');
// Auto-scroll to the bottom
statusElements.logs.scrollTop = statusElements.logs.scrollHeight;
// Show latest 20 logs, with the newest at the top.
logs.reverse();
statusElements.logs.textContent = logs.slice(0, 20).join('\n');
} catch (error) {
console.error('Error fetching logs:', error);
statusElements.logs.textContent = 'Error fetching logs.';
@ -210,6 +323,11 @@ document.addEventListener('DOMContentLoaded', () => {
async function triggerManualSync() {
syncMessage.textContent = 'Issuing sync command...';
if (useMockData) {
syncMessage.textContent = 'Success: Manual sync triggered (mock).';
setTimeout(() => { syncMessage.textContent = ''; }, 5000);
return;
}
try {
const response = await fetch('/api/sync', { method: 'POST' });
const data = await response.json();
@ -227,6 +345,11 @@ document.addEventListener('DOMContentLoaded', () => {
async function nudgeClock(ms) {
nudgeMessage.textContent = 'Nudging clock...';
if (useMockData) {
nudgeMessage.textContent = `Success: Clock nudged by ${ms}ms (mock).`;
setTimeout(() => { nudgeMessage.textContent = ''; }, 3000);
return;
}
try {
const response = await fetch('/api/nudge_clock', {
method: 'POST',
@ -254,6 +377,13 @@ document.addEventListener('DOMContentLoaded', () => {
}
dateMessage.textContent = 'Setting date...';
if (useMockData) {
mockApiDataSets[currentMockSetKey].status.system_date = date;
dateMessage.textContent = `Success: Date set to ${date} (mock).`;
fetchStatus(); // re-render
setTimeout(() => { dateMessage.textContent = ''; }, 5000);
return;
}
try {
const response = await fetch('/api/set_date', {
method: 'POST',
@ -287,13 +417,27 @@ document.addEventListener('DOMContentLoaded', () => {
});
setDateButton.addEventListener('click', setDate);
// --- Collapsible Section Listeners ---
controlsToggle.addEventListener('click', () => {
const isActive = controlsContent.classList.toggle('active');
controlsToggle.classList.toggle('active', isActive);
});
logsToggle.addEventListener('click', () => {
const isActive = logsContent.classList.toggle('active');
logsToggle.classList.toggle('active', isActive);
});
// Initial data load
setupMockControls();
fetchStatus();
fetchConfig();
fetchLogs();
// Refresh data every 2 seconds
// Refresh data every 2 seconds if not using mock data
if (!useMockData) {
setInterval(fetchStatus, 2000);
setInterval(fetchLogs, 2000);
}
setInterval(animateClocks, 50); // High-frequency clock animation
});

View file

@ -1,6 +1,21 @@
@font-face {
font-family: 'FuturaStdHeavy';
src: url('assets/FuturaStdHeavy.otf') format('opentype');
}
@font-face {
font-family: 'Quartz';
src: url('assets/quartz-ms-regular.ttf') format('truetype');
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f4f4f9;
font-family: 'FuturaStdHeavy', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #221f1f;
background-image: url('assets/HaveBlueTransWh.png');
background-repeat: no-repeat;
background-position: bottom 20px right 20px;
background-attachment: fixed;
background-size: 300px;
color: #333;
margin: 0;
padding: 20px;
@ -13,19 +28,20 @@ body {
max-width: 960px;
}
h1 {
text-align: center;
color: #444;
.header-logo {
display: block;
margin: 0 auto 20px auto;
max-width: 60%;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-template-columns: 1fr;
gap: 20px;
}
.card {
background: #fff;
background: #c5ced6;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
@ -33,16 +49,32 @@ h1 {
.card h2 {
margin-top: 0;
color: #0056b3;
color: #1a7db6;
}
#ltc-timecode, #system-clock {
font-family: 'Quartz', monospace;
font-size: 2em;
text-align: center;
letter-spacing: 2px;
}
.card p, .card ul {
margin: 10px 0;
}
.card ul {
padding-left: 20px;
list-style: none;
.system-date-display {
text-align: center;
font-size: 1.5em;
font-family: 'Quartz', monospace;
letter-spacing: 2px;
}
#interfaces {
text-align: center;
white-space: nowrap;
overflow-x: auto;
padding-bottom: 5px; /* Add some space for the scrollbar if it appears */
}
.full-width {
@ -82,6 +114,98 @@ button:hover {
color: #555;
}
.icon-group {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin: 10px 0;
}
#delta-text {
text-align: center;
}
#ltc-status, #ntp-active, #sync-status, #jitter-status, #frame-rate, #lock-ratio, #delta-status {
display: flex;
justify-content: center;
align-items: center;
}
.status-icon {
width: 60px;
height: 60px;
}
.collapsible-card {
padding: 0;
}
.collapsible-card .toggle-header {
display: flex;
align-items: center;
gap: 15px;
padding: 20px;
cursor: pointer;
border-radius: 8px;
}
.collapsible-card .toggle-header.active {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom: 1px solid #eee;
}
.collapsible-card .toggle-header:hover {
background-color: #e9e9f3;
}
.toggle-icon {
width: 40px;
height: 40px;
}
.header-icon {
width: 40px;
height: 40px;
}
.card-header {
display: flex;
align-items: center;
gap: 15px;
}
.collapsible-content {
display: none;
padding: 20px;
}
.collapsible-content.active {
display: block;
}
footer {
text-align: center;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #444;
color: #c5ced6;
}
footer p {
margin: 0;
}
footer a {
color: #1a7db6;
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
/* Status-specific colors */
#sync-status.in-sync, #jitter-status.good { font-weight: bold; color: #28a745; }
#sync-status.clock-ahead, #sync-status.clock-behind, #jitter-status.average { font-weight: bold; color: #ffc107; }
@ -89,3 +213,6 @@ button:hover {
#jitter-status.bad { font-weight: bold; color: #dc3545; }
#ntp-active.active { font-weight: bold; color: #28a745; }
#ntp-active.inactive { font-weight: bold; color: #dc3545; }
#ltc-status.lock { font-weight: bold; color: #28a745; }
#ltc-status.free { font-weight: bold; color: #ffc107; }