diff --git a/static/assets/HaveBlueTransWh.png b/static/assets/HaveBlueTransWh.png new file mode 100644 index 0000000..d9a123d Binary files /dev/null and b/static/assets/HaveBlueTransWh.png differ diff --git a/static/assets/favicon.png b/static/assets/favicon.png new file mode 100644 index 0000000..3683c35 Binary files /dev/null and b/static/assets/favicon.png differ diff --git a/static/assets/header.png b/static/assets/header.png new file mode 100644 index 0000000..f1677ed Binary files /dev/null and b/static/assets/header.png differ diff --git a/static/assets/timeturner_2398.png b/static/assets/timeturner_2398.png new file mode 100644 index 0000000..763bcba Binary files /dev/null and b/static/assets/timeturner_2398.png differ diff --git a/static/assets/timeturner_24.png b/static/assets/timeturner_24.png new file mode 100644 index 0000000..ffc75d0 Binary files /dev/null and b/static/assets/timeturner_24.png differ diff --git a/static/assets/timeturner_25.png b/static/assets/timeturner_25.png new file mode 100644 index 0000000..3b44c93 Binary files /dev/null and b/static/assets/timeturner_25.png differ diff --git a/static/assets/timeturner_2997.png b/static/assets/timeturner_2997.png new file mode 100644 index 0000000..0bd27fd Binary files /dev/null and b/static/assets/timeturner_2997.png differ diff --git a/static/assets/timeturner_2997DF.png b/static/assets/timeturner_2997DF.png new file mode 100644 index 0000000..bf03215 Binary files /dev/null and b/static/assets/timeturner_2997DF.png differ diff --git a/static/assets/timeturner_30.png b/static/assets/timeturner_30.png new file mode 100644 index 0000000..4ce0211 Binary files /dev/null and b/static/assets/timeturner_30.png differ diff --git a/static/assets/timeturner_default.png b/static/assets/timeturner_default.png new file mode 100644 index 0000000..734aa8d Binary files /dev/null and b/static/assets/timeturner_default.png differ diff --git a/static/assets/timeturner_lock_green.png b/static/assets/timeturner_lock_green.png new file mode 100644 index 0000000..0659c60 Binary files /dev/null and b/static/assets/timeturner_lock_green.png differ diff --git a/static/assets/timeturner_lock_orange.png b/static/assets/timeturner_lock_orange.png new file mode 100644 index 0000000..836a376 Binary files /dev/null and b/static/assets/timeturner_lock_orange.png differ diff --git a/static/assets/timeturner_lock_red.png b/static/assets/timeturner_lock_red.png new file mode 100644 index 0000000..aa8740d Binary files /dev/null and b/static/assets/timeturner_lock_red.png differ diff --git a/static/assets/timeturner_timeturning.png b/static/assets/timeturner_timeturning.png new file mode 100644 index 0000000..fd3eaeb Binary files /dev/null and b/static/assets/timeturner_timeturning.png differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..83d6317 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/icon-map.js b/static/icon-map.js new file mode 100644 index 0000000..64336b3 --- /dev/null +++ b/static/icon-map.js @@ -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%.' } + } +}; diff --git a/static/index.html b/static/index.html index 26bf760..7178fb9 100644 --- a/static/index.html +++ b/static/index.html @@ -5,47 +5,63 @@ NTP TimeTurner +
-

NTP TimeTurner

+ + + + +
-

LTC Status

+

LTC Input

--:--:--:--

-

--

-

-- fps

-

Lock Ratio: --%

+
+ + + +
-

System Clock

+

NTP Clock

--:--:--.---

-

Date: ---- -- --

-

NTP Service: --

-

Sync Status: --

-
- - -
-

Clock Offset

-

Delta: -- ms (-- frames)

-

Jitter: --

+

---- -- --

+
+ + + + +
+

Δ -- ms (-- frames)

-

Network

-
    -
  • --
  • -
+
+ Network Icon +

Network

+
+

--

-
-

Controls

+
+
+ Controls Icon +

Controls

+
+
@@ -97,15 +113,29 @@
+
-
-

Logs

-

+            
+
+ Logs Icon +

Logs

+
+
+

+                
+
+ + diff --git a/static/mock-data.js b/static/mock-data.js new file mode 100644 index 0000000..a953e59 --- /dev/null +++ b/static/mock-data.js @@ -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.' ] + } +}; diff --git a/static/script.js b/static/script.js index 6fd3475..634ed33 100644 --- a/static/script.js +++ b/static/script.js @@ -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 = ``; + 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 = ``; + + 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 = ``; 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 = ``; + statusElements.ntpActive.className = 'active'; } else { - const li = document.createElement('li'); - li.textContent = 'No active interfaces found.'; - statusElements.interfaces.appendChild(li); + statusElements.ntpActive.innerHTML = ``; + statusElements.ntpActive.className = 'inactive'; + } + + const syncStatus = data.sync_status || 'UNKNOWN'; + const syncIconInfo = iconMap.syncStatus[syncStatus] || iconMap.syncStatus.default; + statusElements.syncStatus.innerHTML = ``; + 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 = ``; + + 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 = ``; + 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 - setInterval(fetchStatus, 2000); - setInterval(fetchLogs, 2000); + // 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 }); diff --git a/static/style.css b/static/style.css index 7bd9c20..51bc097 100644 --- a/static/style.css +++ b/static/style.css @@ -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; }