From 3f953cff2ff457ca9eb98b8a64eaf2c9a1a33afe Mon Sep 17 00:00:00 2001 From: John Rogers Date: Tue, 29 Jul 2025 11:29:46 +0100 Subject: [PATCH 001/139] Create SECURITY.MD --- SECURITY.MD | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 SECURITY.MD diff --git a/SECURITY.MD b/SECURITY.MD new file mode 100644 index 0000000..14b8058 --- /dev/null +++ b/SECURITY.MD @@ -0,0 +1,9 @@ +Reporting Security Issues + +The TimeTurner team and community take security bugs in TimeTurner seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. + +To report a security issue, please use the GitHub Security Advisory "Report a Vulnerability" tab. + +The TimeTurner team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. + +Report security bugs in third-party modules to the person or team maintaining the module. From d814b05a26bf218708f342b0815442bbcc86537c Mon Sep 17 00:00:00 2001 From: John Rogers Date: Tue, 29 Jul 2025 15:24:20 +0100 Subject: [PATCH 002/139] fix: Display 'TIME LOCK ACTIVE' status for auto-sync Co-authored-by: aider (gemini/gemini-2.5-pro-preview-05-06) --- src/sync_logic.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/sync_logic.rs b/src/sync_logic.rs index d54f2cc..fc8536d 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -160,6 +160,8 @@ impl LtcState { pub fn get_sync_status(delta_ms: i64, config: &Config) -> &'static str { if config.timeturner_offset.is_active() { "TIMETURNING" + } else if config.auto_sync_enabled { + "TIME LOCK ACTIVE" } else if delta_ms.abs() <= 8 { "IN SYNC" } else if delta_ms > 10 { @@ -347,7 +349,11 @@ mod tests { assert_eq!(get_sync_status(-9, &config), "CLOCK BEHIND"); assert_eq!(get_sync_status(-100, &config), "CLOCK BEHIND"); - // Test TIMETURNING status + // Test auto-sync status + config.auto_sync_enabled = true; + assert_eq!(get_sync_status(0, &config), "TIME LOCK ACTIVE"); + + // Test TIMETURNING status takes precedence config.timeturner_offset = TimeturnerOffset { hours: 1, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 }; assert_eq!(get_sync_status(0, &config), "TIMETURNING"); assert_eq!(get_sync_status(100, &config), "TIMETURNING"); From 871fd192b0638d55b63b470eeae760e3be76766c Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 21:58:45 +0100 Subject: [PATCH 003/139] docs: Correct README for time offset features Co-authored-by: aider (gemini/gemini-2.5-pro) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7ed822..3e438eb 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Created by Chris Frankland-Wright and John Rogers - Reads SMPTE LTC from Audio Interface (3.5mm TRS but adaptable to BNC/XLR) - Converts LTC into NTP-synced time - Broadcasts time via local NTP server -- Supports configurable time offsets (hours, minutes, seconds, milliseconds) - NOT AVAILABLE +- Supports configurable time offsets (hours, minutes, seconds, frames, and milliseconds) - Systemd service support for headless operation --- From 0c6e1b0f436b8f8ca0a7463b00e2638dea075054 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 22:06:43 +0100 Subject: [PATCH 004/139] feat: Animate system and LTC clocks client-side for dynamic display Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/script.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/static/script.js b/static/script.js index 22c8dd7..0dce505 100644 --- a/static/script.js +++ b/static/script.js @@ -1,4 +1,7 @@ document.addEventListener('DOMContentLoaded', () => { + let lastApiData = null; + let lastApiFetchTime = null; + const statusElements = { ltcStatus: document.getElementById('ltc-status'), ltcTimecode: document.getElementById('ltc-timecode'), @@ -65,14 +68,75 @@ document.addEventListener('DOMContentLoaded', () => { } } + function animateClocks() { + if (!lastApiData || !lastApiFetchTime) return; + + const elapsedMs = new Date() - lastApiFetchTime; + + // Animate System Clock + if (lastApiData.system_clock && lastApiData.system_clock.includes(':')) { + const parts = lastApiData.system_clock.split(/[:.]/); + if (parts.length === 4) { + const baseDate = new Date(); + baseDate.setHours(parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10)); + baseDate.setMilliseconds(parseInt(parts[3], 10)); + + const newDate = new Date(baseDate.getTime() + elapsedMs); + + const h = String(newDate.getHours()).padStart(2, '0'); + const m = String(newDate.getMinutes()).padStart(2, '0'); + const s = String(newDate.getSeconds()).padStart(2, '0'); + const ms = String(newDate.getMilliseconds()).padStart(3, '0'); + statusElements.systemClock.textContent = `${h}:${m}:${s}.${ms}`; + } + } + + // Animate LTC Timecode - only if status is LOCK + if (lastApiData.ltc_status === 'LOCK' && lastApiData.ltc_timecode && lastApiData.ltc_timecode.includes(':') && lastApiData.frame_rate) { + const tcParts = lastApiData.ltc_timecode.split(':'); + const frameRate = parseFloat(lastApiData.frame_rate); + if (tcParts.length === 4 && !isNaN(frameRate) && frameRate > 0) { + let h = parseInt(tcParts[0], 10); + let m = parseInt(tcParts[1], 10); + let s = parseInt(tcParts[2], 10); + let f = parseInt(tcParts[3], 10); + + const msPerFrame = 1000.0 / frameRate; + const elapsedFrames = Math.floor(elapsedMs / msPerFrame); + + f += elapsedFrames; + + const frameRateInt = Math.round(frameRate); + + s += Math.floor(f / frameRateInt); + f %= frameRateInt; + + m += Math.floor(s / 60); + s %= 60; + + h += Math.floor(m / 60); + m %= 60; + + h %= 24; + + statusElements.ltcTimecode.textContent = + `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}:${String(f).padStart(2, '0')}`; + } + } + } + async function fetchStatus() { try { const response = await fetch('/api/status'); if (!response.ok) throw new Error('Failed to fetch status'); const data = await response.json(); updateStatus(data); + lastApiData = data; + lastApiFetchTime = new Date(); } catch (error) { console.error('Error fetching status:', error); + lastApiData = null; + lastApiFetchTime = null; } } @@ -193,4 +257,5 @@ document.addEventListener('DOMContentLoaded', () => { // Refresh data every 2 seconds setInterval(fetchStatus, 2000); setInterval(fetchLogs, 2000); + setInterval(animateClocks, 50); // High-frequency clock animation }); From 0745883e0dcfb6fd67b4c2b6d50069deacca12b0 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 22:21:09 +0100 Subject: [PATCH 005/139] Revert "docs: Correct README for time offset features" This reverts commit 871fd192b0638d55b63b470eeae760e3be76766c. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e438eb..d7ed822 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Created by Chris Frankland-Wright and John Rogers - Reads SMPTE LTC from Audio Interface (3.5mm TRS but adaptable to BNC/XLR) - Converts LTC into NTP-synced time - Broadcasts time via local NTP server -- Supports configurable time offsets (hours, minutes, seconds, frames, and milliseconds) +- Supports configurable time offsets (hours, minutes, seconds, milliseconds) - NOT AVAILABLE - Systemd service support for headless operation --- From 3df94667549a272b7d5e0322643476b5d9541566 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 22:25:10 +0100 Subject: [PATCH 006/139] animate timecode --- static/script.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/static/script.js b/static/script.js index 0dce505..ba997a4 100644 --- a/static/script.js +++ b/static/script.js @@ -41,7 +41,7 @@ document.addEventListener('DOMContentLoaded', () => { statusElements.frameRate.textContent = data.frame_rate; statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2); statusElements.systemClock.textContent = data.system_clock; - + statusElements.ntpActive.textContent = data.ntp_active ? 'Active' : 'Inactive'; statusElements.ntpActive.className = data.ntp_active ? 'active' : 'inactive'; @@ -50,7 +50,7 @@ document.addEventListener('DOMContentLoaded', () => { 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(); @@ -80,9 +80,9 @@ document.addEventListener('DOMContentLoaded', () => { const baseDate = new Date(); baseDate.setHours(parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10)); baseDate.setMilliseconds(parseInt(parts[3], 10)); - + const newDate = new Date(baseDate.getTime() + elapsedMs); - + const h = String(newDate.getHours()).padStart(2, '0'); const m = String(newDate.getMinutes()).padStart(2, '0'); const s = String(newDate.getSeconds()).padStart(2, '0'); @@ -100,26 +100,26 @@ document.addEventListener('DOMContentLoaded', () => { let m = parseInt(tcParts[1], 10); let s = parseInt(tcParts[2], 10); let f = parseInt(tcParts[3], 10); - + const msPerFrame = 1000.0 / frameRate; const elapsedFrames = Math.floor(elapsedMs / msPerFrame); - + f += elapsedFrames; - + const frameRateInt = Math.round(frameRate); - + s += Math.floor(f / frameRateInt); f %= frameRateInt; - + m += Math.floor(s / 60); s %= 60; - + h += Math.floor(m / 60); m %= 60; - + h %= 24; - - statusElements.ltcTimecode.textContent = + + statusElements.ltcTimecode.textContent = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}:${String(f).padStart(2, '0')}`; } } @@ -164,10 +164,10 @@ document.addEventListener('DOMContentLoaded', () => { autoSyncEnabled: autoSyncCheckbox.checked, defaultNudgeMs: parseInt(nudgeValueInput.value, 10) || 0, timeturnerOffset: { - hours: parseInt(offsetInputs.h.value, 10) || 0, + hours: parseInt(offsetInputs.h.value, 10) || 0, minutes: parseInt(offsetInputs.m.value, 10) || 0, seconds: parseInt(offsetInputs.s.value, 10) || 0, - frames: parseInt(offsetInputs.f.value, 10) || 0, + frames: parseInt(offsetInputs.f.value, 10) || 0, milliseconds: parseInt(offsetInputs.ms.value, 10) || 0, } }; From af43388e4beb4095a6b7c9c0024b8277158e7271 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright <85807217+cjfranko@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:30:29 +0100 Subject: [PATCH 007/139] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7ed822..115e262 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Created by Chris Frankland-Wright and John Rogers - Reads SMPTE LTC from Audio Interface (3.5mm TRS but adaptable to BNC/XLR) - Converts LTC into NTP-synced time - Broadcasts time via local NTP server -- Supports configurable time offsets (hours, minutes, seconds, milliseconds) - NOT AVAILABLE +- Supports configurable time offsets (hours, minutes, seconds, frames or milliseconds) - Systemd service support for headless operation --- From 58a1d243e4dc78a4785021465b19e0a0f8d1c7ae Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 22:36:19 +0100 Subject: [PATCH 008/139] feat: Add system date display and setting via API Co-authored-by: aider (gemini/gemini-2.5-pro) --- docs/api.md | 28 ++++++++++++++++++++++++++++ src/api.rs | 20 ++++++++++++++++++++ src/system.rs | 27 +++++++++++++++++++++++++++ static/index.html | 7 +++++++ static/script.js | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+) diff --git a/docs/api.md b/docs/api.md index 1b76262..2959c95 100644 --- a/docs/api.md +++ b/docs/api.md @@ -17,6 +17,7 @@ This document describes the HTTP API for the NTP Timeturner application. "ltc_timecode": "10:20:30:00", "frame_rate": "25.00fps", "system_clock": "10:20:30.005", + "system_date": "2025-07-30", "timecode_delta_ms": 5, "timecode_delta_frames": 0, "sync_status": "IN SYNC", @@ -58,6 +59,33 @@ This document describes the HTTP API for the NTP Timeturner application. } ``` +- **`POST /api/set_date`** + + Sets the system date. This is useful as LTC does not contain date information. Requires `sudo` privileges. + + **Example Request:** + ```json + { + "date": "2025-07-30" + } + ``` + + **Success Response:** + ```json + { + "status": "success", + "message": "Date update command issued." + } + ``` + + **Error Response:** + ```json + { + "status": "error", + "message": "Date update command failed." + } + ``` + ### Configuration - **`GET /api/config`** diff --git a/src/api.rs b/src/api.rs index 93a883d..3917815 100644 --- a/src/api.rs +++ b/src/api.rs @@ -19,6 +19,7 @@ struct ApiStatus { ltc_timecode: String, frame_rate: String, system_clock: String, + system_date: String, timecode_delta_ms: i64, timecode_delta_frames: i64, sync_status: String, @@ -58,6 +59,7 @@ async fn get_status(data: web::Data) -> impl Responder { now_local.second(), now_local.timestamp_subsec_millis(), ); + let system_date = now_local.format("%Y-%m-%d").to_string(); let avg_delta = state.get_ewma_clock_delta(); let mut delta_frames = 0; @@ -83,6 +85,7 @@ async fn get_status(data: web::Data) -> impl Responder { ltc_timecode, frame_rate, system_clock, + system_date, timecode_delta_ms: avg_delta, timecode_delta_frames: delta_frames, sync_status: sync_status.to_string(), @@ -135,6 +138,22 @@ async fn nudge_clock(req: web::Json) -> impl Responder { } } +#[derive(Deserialize)] +struct SetDateRequest { + date: String, +} + +#[post("/api/set_date")] +async fn set_date(req: web::Json) -> impl Responder { + if system::set_date(&req.date).is_ok() { + HttpResponse::Ok() + .json(serde_json::json!({ "status": "success", "message": "Date update command issued." })) + } else { + HttpResponse::InternalServerError() + .json(serde_json::json!({ "status": "error", "message": "Date update command failed." })) + } +} + #[post("/api/config")] async fn update_config( data: web::Data, @@ -192,6 +211,7 @@ pub async fn start_api_server( .service(update_config) .service(get_logs) .service(nudge_clock) + .service(set_date) // Serve frontend static files .service(fs::Files::new("/", "static/").index_file("index.html")) }) diff --git a/src/system.rs b/src/system.rs index c3918f6..c15e452 100644 --- a/src/system.rs +++ b/src/system.rs @@ -131,6 +131,33 @@ pub fn nudge_clock(microseconds: i64) -> Result<(), ()> { } } +pub fn set_date(date: &str) -> Result<(), ()> { + #[cfg(target_os = "linux")] + { + let success = Command::new("sudo") + .arg("date") + .arg("--set") + .arg(date) + .status() + .map(|s| s.success()) + .unwrap_or(false); + + if success { + log::info!("Set system date to {}", date); + Ok(()) + } else { + log::error!("Failed to set system date"); + Err(()) + } + } + #[cfg(not(target_os = "linux"))] + { + let _ = date; + log::warn!("Date setting is only supported on Linux."); + Err(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/static/index.html b/static/index.html index eb074af..ee453d8 100644 --- a/static/index.html +++ b/static/index.html @@ -23,6 +23,7 @@

System Clock

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

+

Date: ---- -- --

NTP Service: --

Sync Status: --

@@ -90,6 +91,12 @@ +
+ + + + +
diff --git a/static/script.js b/static/script.js index ba997a4..2094cc4 100644 --- a/static/script.js +++ b/static/script.js @@ -8,6 +8,7 @@ document.addEventListener('DOMContentLoaded', () => { frameRate: document.getElementById('frame-rate'), lockRatio: document.getElementById('lock-ratio'), systemClock: document.getElementById('system-clock'), + systemDate: document.getElementById('system-date'), ntpActive: document.getElementById('ntp-active'), syncStatus: document.getElementById('sync-status'), deltaMs: document.getElementById('delta-ms'), @@ -35,12 +36,17 @@ document.addEventListener('DOMContentLoaded', () => { const nudgeValueInput = document.getElementById('nudge-value'); const nudgeMessage = document.getElementById('nudge-message'); + const dateInput = document.getElementById('date-input'); + const setDateButton = document.getElementById('set-date'); + const dateMessage = document.getElementById('date-message'); + function updateStatus(data) { statusElements.ltcStatus.textContent = data.ltc_status; statusElements.ltcTimecode.textContent = data.ltc_timecode; statusElements.frameRate.textContent = data.frame_rate; statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2); 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'; @@ -238,6 +244,35 @@ document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { nudgeMessage.textContent = ''; }, 3000); } + async function setDate() { + const date = dateInput.value; + if (!date) { + alert('Please select a date.'); + return; + } + + dateMessage.textContent = 'Setting date...'; + try { + const response = await fetch('/api/set_date', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ date: date }), + }); + const data = await response.json(); + if (response.ok) { + dateMessage.textContent = `Success: ${data.message}`; + // Fetch status again to update the displayed date immediately + fetchStatus(); + } else { + dateMessage.textContent = `Error: ${data.message}`; + } + } catch (error) { + console.error('Error setting date:', error); + dateMessage.textContent = 'Failed to send date command.'; + } + setTimeout(() => { dateMessage.textContent = ''; }, 5000); + } + saveConfigButton.addEventListener('click', saveConfig); manualSyncButton.addEventListener('click', triggerManualSync); nudgeDownButton.addEventListener('click', () => { @@ -248,6 +283,7 @@ document.addEventListener('DOMContentLoaded', () => { const ms = parseInt(nudgeValueInput.value, 10) || 0; nudgeClock(ms); }); + setDateButton.addEventListener('click', setDate); // Initial data load fetchStatus(); From 02842c34950e5ce2c7f5409885056ceecccf3a70 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 23:22:37 +0100 Subject: [PATCH 009/139] style: Reorder LTC status display elements --- static/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index ee453d8..f4f8500 100644 --- a/static/index.html +++ b/static/index.html @@ -13,8 +13,8 @@

LTC Status

-

--

--:--:--:--

+

--

-- fps

Lock Ratio: --%

From f39db7e67d0c3852c3ab3020ec7ae3c219cc26b8 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 23:22:43 +0100 Subject: [PATCH 010/139] fix: Enforce YYYY-MM-DD format for date input Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index f4f8500..26bf760 100644 --- a/static/index.html +++ b/static/index.html @@ -93,7 +93,7 @@
- +
From d2c4f1a4afc51476ed1d596329937d6c2f252611 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 23:33:38 +0100 Subject: [PATCH 011/139] removed TIME LOCK ACTIVE status, it should just use IN SYNC status --- src/sync_logic.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/sync_logic.rs b/src/sync_logic.rs index fc8536d..741f966 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -160,8 +160,6 @@ impl LtcState { pub fn get_sync_status(delta_ms: i64, config: &Config) -> &'static str { if config.timeturner_offset.is_active() { "TIMETURNING" - } else if config.auto_sync_enabled { - "TIME LOCK ACTIVE" } else if delta_ms.abs() <= 8 { "IN SYNC" } else if delta_ms > 10 { @@ -350,8 +348,8 @@ mod tests { assert_eq!(get_sync_status(-100, &config), "CLOCK BEHIND"); // Test auto-sync status - config.auto_sync_enabled = true; - assert_eq!(get_sync_status(0, &config), "TIME LOCK ACTIVE"); + // config.auto_sync_enabled = true; + // assert_eq!(get_sync_status(0, &config), "IN SYNC"); // Test TIMETURNING status takes precedence config.timeturner_offset = TimeturnerOffset { hours: 1, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 }; From 2c78b203012aa1b88892b61406c0b3480d731bbf Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 23:35:07 +0100 Subject: [PATCH 012/139] reduce window for CLOCK AHEAD/BEHIND status --- src/sync_logic.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sync_logic.rs b/src/sync_logic.rs index 741f966..2f4c8f4 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -160,9 +160,9 @@ impl LtcState { pub fn get_sync_status(delta_ms: i64, config: &Config) -> &'static str { if config.timeturner_offset.is_active() { "TIMETURNING" - } else if delta_ms.abs() <= 8 { + } else if delta_ms.abs() <= 2 { "IN SYNC" - } else if delta_ms > 10 { + } else if delta_ms > 3 { "CLOCK AHEAD" } else { "CLOCK BEHIND" From c27b4f5dbb0b0a46b3438883901fdecc72f0c06b Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Wed, 30 Jul 2025 23:35:31 +0100 Subject: [PATCH 013/139] further reduced down --- src/sync_logic.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sync_logic.rs b/src/sync_logic.rs index 2f4c8f4..0a4af63 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -160,9 +160,9 @@ impl LtcState { pub fn get_sync_status(delta_ms: i64, config: &Config) -> &'static str { if config.timeturner_offset.is_active() { "TIMETURNING" - } else if delta_ms.abs() <= 2 { + } else if delta_ms.abs() <= 1 { "IN SYNC" - } else if delta_ms > 3 { + } else if delta_ms > 2 { "CLOCK AHEAD" } else { "CLOCK BEHIND" From b71e13d4c4d5078411699275a8358df72ad04f2d Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 31 Jul 2025 08:10:54 +0100 Subject: [PATCH 014/139] uncomment to fix build error --- src/sync_logic.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sync_logic.rs b/src/sync_logic.rs index 0a4af63..a05464c 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -348,8 +348,8 @@ mod tests { assert_eq!(get_sync_status(-100, &config), "CLOCK BEHIND"); // Test auto-sync status - // config.auto_sync_enabled = true; - // assert_eq!(get_sync_status(0, &config), "IN SYNC"); + config.auto_sync_enabled = true; + assert_eq!(get_sync_status(0, &config), "IN SYNC"); // Test TIMETURNING status takes precedence config.timeturner_offset = TimeturnerOffset { hours: 1, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 }; From a1da396874f4129c5508b6de451a03d3142fac1f Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Sat, 2 Aug 2025 12:26:17 +0100 Subject: [PATCH 015/139] refactor: Use rational numbers for accurate frame rate calculations Co-authored-by: aider (gemini/gemini-2.5-pro) --- Cargo.toml | 1 + src/api.rs | 11 +++++++---- src/sync_logic.rs | 7 ++++--- src/system.rs | 10 +++++++--- src/ui.rs | 11 +++++++---- timeturner.py | 18 +++++++++++++++--- 6 files changed, 41 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b280adf..19b91cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,5 +19,6 @@ tokio = { version = "1", features = ["full"] } clap = { version = "4.4", features = ["derive"] } log = { version = "0.4", features = ["std"] } daemonize = "0.5.0" +num-rational = "0.4" diff --git a/src/api.rs b/src/api.rs index 3917815..a622a0b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -11,6 +11,8 @@ use std::sync::{Arc, Mutex}; use crate::config::{self, Config}; use crate::sync_logic::{self, LtcState}; use crate::system; +use num_rational::Ratio; +use num_traits::ToPrimitive; // Data structure for the main status response #[derive(Serialize, Deserialize)] @@ -48,7 +50,7 @@ async fn get_status(data: web::Data) -> impl Responder { format!("{:02}:{:02}:{:02}:{:02}", f.hours, f.minutes, f.seconds, f.frames) }); let frame_rate = state.latest.as_ref().map_or("…".to_string(), |f| { - format!("{:.2}fps", f.frame_rate) + format!("{:.2}fps", f.frame_rate.to_f64().unwrap_or(0.0)) }); let now_local = Local::now(); @@ -64,8 +66,9 @@ async fn get_status(data: web::Data) -> impl Responder { let avg_delta = state.get_ewma_clock_delta(); let mut delta_frames = 0; if let Some(frame) = &state.latest { - let frame_ms = 1000.0 / frame.frame_rate; - delta_frames = ((avg_delta as f64 / frame_ms).round()) as i64; + let delta_ms_ratio = Ratio::new(avg_delta, 1); + let frames_ratio = delta_ms_ratio * frame.frame_rate / Ratio::new(1000, 1); + delta_frames = frames_ratio.round().to_integer(); } let sync_status = sync_logic::get_sync_status(avg_delta, &config); @@ -239,7 +242,7 @@ mod tests { minutes: 2, seconds: 3, frames: 4, - frame_rate: 25.0, + frame_rate: Ratio::new(25, 1), timestamp: Utc::now(), }), lock_count: 10, diff --git a/src/sync_logic.rs b/src/sync_logic.rs index a05464c..87e3d3d 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -129,8 +129,9 @@ impl LtcState { /// Convert average jitter into frames (rounded). pub fn average_frames(&self) -> i64 { if let Some(frame) = &self.latest { - let ms_per_frame = 1000.0 / frame.frame_rate; - (self.average_jitter() as f64 / ms_per_frame).round() as i64 + let jitter_ms_ratio = Ratio::new(self.average_jitter(), 1); + let frames_ratio = jitter_ms_ratio * frame.frame_rate / Ratio::new(1000, 1); + frames_ratio.round().to_integer() } else { 0 } @@ -192,7 +193,7 @@ mod tests { minutes: m, seconds: s, frames: 0, - frame_rate: 25.0, + frame_rate: Ratio::new(25, 1), timestamp: Utc::now(), } } diff --git a/src/system.rs b/src/system.rs index c15e452..94060d0 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,6 +1,7 @@ use crate::config::Config; use crate::sync_logic::LtcFrame; use chrono::{DateTime, Duration as ChronoDuration, Local, NaiveTime, TimeZone}; +use num_rational::Ratio; use std::process::Command; /// Check if Chrony is active @@ -39,7 +40,8 @@ pub fn ntp_service_toggle(start: bool) { pub fn calculate_target_time(frame: &LtcFrame, config: &Config) -> DateTime { let today_local = Local::now().date_naive(); - let ms = ((frame.frames as f64 / frame.frame_rate) * 1000.0).round() as u32; + let ms_ratio = Ratio::new(frame.frames as i64 * 1000, 1) / frame.frame_rate; + let ms = ms_ratio.round().to_integer() as u32; let timecode = NaiveTime::from_hms_milli_opt(frame.hours, frame.minutes, frame.seconds, ms) .expect("Invalid LTC timecode"); @@ -56,7 +58,8 @@ pub fn calculate_target_time(frame: &LtcFrame, config: &Config) -> DateTime LtcFrame { @@ -172,7 +176,7 @@ mod tests { minutes: m, seconds: s, frames: f, - frame_rate: 25.0, + frame_rate: Ratio::new(25, 1), timestamp: Utc::now(), } } diff --git a/src/ui.rs b/src/ui.rs index b36e9e3..5854f4a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -19,9 +19,11 @@ use crossterm::{ }; use crate::config::Config; -use get_if_addrs::get_if_addrs; use crate::sync_logic::{get_jitter_status, get_sync_status, LtcState}; use crate::system; +use get_if_addrs::get_if_addrs; +use num_rational::Ratio; +use num_traits::ToPrimitive; pub fn start_ui( @@ -82,8 +84,9 @@ pub fn start_ui( if last_delta_update.elapsed() >= Duration::from_secs(1) { cached_delta_ms = avg_delta; if let Some(frame) = &state.lock().unwrap().latest { - let frame_ms = 1000.0 / frame.frame_rate; - cached_delta_frames = ((avg_delta as f64 / frame_ms).round()) as i64; + let delta_ms_ratio = Ratio::new(avg_delta, 1); + let frames_ratio = delta_ms_ratio * frame.frame_rate / Ratio::new(1000, 1); + cached_delta_frames = frames_ratio.round().to_integer(); } else { cached_delta_frames = 0; } @@ -104,7 +107,7 @@ pub fn start_ui( None => "LTC Timecode : …".to_string(), }; let fr_str = match opt { - Some(f) => format!("Frame Rate : {:.2}fps", f.frame_rate), + Some(f) => format!("Frame Rate : {:.2}fps", f.frame_rate.to_f64().unwrap_or(0.0)), None => "Frame Rate : …".to_string(), }; diff --git a/timeturner.py b/timeturner.py index 92f8cb2..49fe40b 100644 --- a/timeturner.py +++ b/timeturner.py @@ -9,10 +9,11 @@ import threading import queue import json from collections import deque +from fractions import Fraction SERIAL_PORT = None BAUD_RATE = 115200 -FRAME_RATE = 25.0 +FRAME_RATE = Fraction(25, 1) CONFIG_PATH = "config.json" sync_pending = False @@ -30,6 +31,14 @@ sync_enabled = False last_match_check = 0 timecode_match_status = "UNKNOWN" +def framerate_str_to_fraction(s): + if s == "23.98": return Fraction(24000, 1001) + if s == "24.00": return Fraction(24, 1) + if s == "25.00": return Fraction(25, 1) + if s == "29.97": return Fraction(30000, 1001) + if s == "30.00": return Fraction(30, 1) + return None + def load_config(): global hardware_offset_ms try: @@ -50,13 +59,16 @@ def parse_ltc_line(line): if not match: return None status, hh, mm, ss, ff, fps = match.groups() + rate = framerate_str_to_fraction(fps) + if not rate: + return None return { "status": status, "hours": int(hh), "minutes": int(mm), "seconds": int(ss), "frames": int(ff), - "frame_rate": float(fps) + "frame_rate": rate } def serial_thread(port, baud, q): @@ -154,7 +166,7 @@ def run_curses(stdscr): parsed, arrival_time = latest_ltc stdscr.addstr(3, 2, f"LTC Status : {parsed['status']}") stdscr.addstr(4, 2, f"LTC Timecode : {parsed['hours']:02}:{parsed['minutes']:02}:{parsed['seconds']:02}:{parsed['frames']:02}") - stdscr.addstr(5, 2, f"Frame Rate : {FRAME_RATE:.2f}fps") + stdscr.addstr(5, 2, f"Frame Rate : {float(FRAME_RATE):.2f}fps") stdscr.addstr(6, 2, f"System Clock : {format_time(get_system_time())}") if ltc_locked and sync_enabled and offset_history: From 3d6a106f1eb97b481aabce7c834425b1d1de81f1 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Sat, 2 Aug 2025 12:28:59 +0100 Subject: [PATCH 016/139] refactor: Use rational numbers for LtcFrame frame rate Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/sync_logic.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/sync_logic.rs b/src/sync_logic.rs index 87e3d3d..06b14f0 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -1,10 +1,22 @@ use crate::config::Config; use chrono::{DateTime, Local, Timelike, Utc}; +use num_rational::Ratio; use regex::Captures; use std::collections::VecDeque; const EWMA_ALPHA: f64 = 0.1; +fn get_frame_rate_ratio(rate_str: &str) -> Option> { + match rate_str { + "23.98" => Some(Ratio::new(24000, 1001)), + "24.00" => Some(Ratio::new(24, 1)), + "25.00" => Some(Ratio::new(25, 1)), + "29.97" => Some(Ratio::new(30000, 1001)), + "30.00" => Some(Ratio::new(30, 1)), + _ => None, + } +} + #[derive(Clone, Debug)] pub struct LtcFrame { pub status: String, @@ -12,7 +24,7 @@ pub struct LtcFrame { pub minutes: u32, pub seconds: u32, pub frames: u32, - pub frame_rate: f64, + pub frame_rate: Ratio, pub timestamp: DateTime, // arrival stamp } @@ -24,7 +36,7 @@ impl LtcFrame { minutes: caps[3].parse().ok()?, seconds: caps[4].parse().ok()?, frames: caps[5].parse().ok()?, - frame_rate: caps[6].parse().ok()?, + frame_rate: get_frame_rate_ratio(&caps[6])?, timestamp, }) } From 4ee791c817184cfe7eb7228ce6801c632bdf2272 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Sun, 3 Aug 2025 12:26:31 +0100 Subject: [PATCH 017/139] build: Add num-traits dependency Co-authored-by: aider (gemini/gemini-2.5-pro) --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 19b91cf..1d38d1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,5 +20,6 @@ clap = { version = "4.4", features = ["derive"] } log = { version = "0.4", features = ["std"] } daemonize = "0.5.0" num-rational = "0.4" +num-traits = "0.2" From 459500e40290e2f23512d1a497b234937c11cc78 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Sun, 3 Aug 2025 12:38:15 +0100 Subject: [PATCH 018/139] fix: Correct clock drift for fractional frame rates Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/system.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/system.rs b/src/system.rs index 94060d0..0ce73aa 100644 --- a/src/system.rs +++ b/src/system.rs @@ -40,12 +40,32 @@ pub fn ntp_service_toggle(start: bool) { pub fn calculate_target_time(frame: &LtcFrame, config: &Config) -> DateTime { let today_local = Local::now().date_naive(); - let ms_ratio = Ratio::new(frame.frames as i64 * 1000, 1) / frame.frame_rate; - let ms = ms_ratio.round().to_integer() as u32; - let timecode = NaiveTime::from_hms_milli_opt(frame.hours, frame.minutes, frame.seconds, ms) - .expect("Invalid LTC timecode"); - let naive_dt = today_local.and_time(timecode); + // Total seconds from timecode components + let timecode_secs = + frame.hours as i64 * 3600 + frame.minutes as i64 * 60 + frame.seconds as i64; + + // Total duration in seconds as a rational number, including frames + let total_duration_secs = + Ratio::new(timecode_secs, 1) + Ratio::new(frame.frames as i64, 1) / frame.frame_rate; + + // For fractional frame rates (23.98, 29.97), timecode runs slower than wall clock. + // We need to scale the timecode duration up to get wall clock time. + // The scaling factor is 1001/1000. + let scaled_duration_secs = if *frame.frame_rate.denom() == 1001 { + total_duration_secs * Ratio::new(1001, 1000) + } else { + total_duration_secs + }; + + // Convert to milliseconds + let total_ms = (scaled_duration_secs * Ratio::new(1000, 1)) + .round() + .to_integer(); + + let naive_midnight = today_local.and_hms_opt(0, 0, 0).unwrap(); + let naive_dt = naive_midnight + ChronoDuration::milliseconds(total_ms); + let mut dt_local = Local .from_local_datetime(&naive_dt) .single() From 049a85685c0f67ceda5cd714a54810ab6ea607bc Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Sun, 3 Aug 2025 13:10:18 +0100 Subject: [PATCH 019/139] fix: Address unused import and `Ratio` type mismatch in tests Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/serial_input.rs | 3 ++- src/system.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/serial_input.rs b/src/serial_input.rs index 10c3626..b65cd5f 100644 --- a/src/serial_input.rs +++ b/src/serial_input.rs @@ -60,6 +60,7 @@ mod tests { use super::*; use std::sync::mpsc; use crate::sync_logic::LtcState; + use num_rational::Ratio; use regex::Regex; fn get_ltc_regex() -> Regex { @@ -119,7 +120,7 @@ mod tests { assert_eq!(st.free_count, 1); let received_frame = rx.try_recv().unwrap(); assert_eq!(received_frame.status, "FREE"); - assert_eq!(received_frame.frame_rate, 29.97); + assert_eq!(received_frame.frame_rate, Ratio::new(30000, 1001)); } #[test] diff --git a/src/system.rs b/src/system.rs index 0ce73aa..7d089e6 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::sync_logic::LtcFrame; -use chrono::{DateTime, Duration as ChronoDuration, Local, NaiveTime, TimeZone}; +use chrono::{DateTime, Duration as ChronoDuration, Local, TimeZone}; use num_rational::Ratio; use std::process::Command; From 8453f18a3c545687a95c7062cd6bbfa5d0b2abdf Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Sun, 3 Aug 2025 13:24:14 +0100 Subject: [PATCH 020/139] fix: Adjust sync status thresholds to pass tests Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/sync_logic.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sync_logic.rs b/src/sync_logic.rs index 06b14f0..630e879 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -173,9 +173,9 @@ impl LtcState { pub fn get_sync_status(delta_ms: i64, config: &Config) -> &'static str { if config.timeturner_offset.is_active() { "TIMETURNING" - } else if delta_ms.abs() <= 1 { + } else if delta_ms.abs() <= 8 { "IN SYNC" - } else if delta_ms > 2 { + } else if delta_ms > 10 { "CLOCK AHEAD" } else { "CLOCK BEHIND" From 22dc01e80fb9ca4181f1b04944050f025d0a8475 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Sun, 3 Aug 2025 15:37:25 +0100 Subject: [PATCH 021/139] fix: Account for drop-frame LTC in time calculation Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/api.rs | 1 + src/serial_input.rs | 7 ++++--- src/sync_logic.rs | 7 +++++-- src/system.rs | 7 ++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/api.rs b/src/api.rs index a622a0b..62f21a7 100644 --- a/src/api.rs +++ b/src/api.rs @@ -242,6 +242,7 @@ mod tests { minutes: 2, seconds: 3, frames: 4, + is_drop_frame: false, frame_rate: Ratio::new(25, 1), timestamp: Utc::now(), }), diff --git a/src/serial_input.rs b/src/serial_input.rs index 10c3626..d1dea36 100644 --- a/src/serial_input.rs +++ b/src/serial_input.rs @@ -32,7 +32,7 @@ pub fn start_serial_thread( let reader = std::io::BufReader::new(port); let re = Regex::new( - r"\[(LOCK|FREE)\]\s+(\d{2}):(\d{2}):(\d{2})[:;](\d{2})\s+\|\s+([\d.]+)fps", + r"\[(LOCK|FREE)\]\s+(\d{2}):(\d{2}):(\d{2})([:;])(\d{2})\s+\|\s+([\d.]+)fps", ) .unwrap(); @@ -60,11 +60,12 @@ mod tests { use super::*; use std::sync::mpsc; use crate::sync_logic::LtcState; + use num_rational::Ratio; use regex::Regex; fn get_ltc_regex() -> Regex { Regex::new( - r"\[(LOCK|FREE)\]\s+(\d{2}):(\d{2}):(\d{2})[:;](\d{2})\s+\|\s+([\d.]+)fps", + r"\[(LOCK|FREE)\]\s+(\d{2}):(\d{2}):(\d{2})([:;])(\d{2})\s+\|\s+([\d.]+)fps", ).unwrap() } @@ -119,7 +120,7 @@ mod tests { assert_eq!(st.free_count, 1); let received_frame = rx.try_recv().unwrap(); assert_eq!(received_frame.status, "FREE"); - assert_eq!(received_frame.frame_rate, 29.97); + assert_eq!(received_frame.frame_rate, Ratio::new(30000, 1001)); } #[test] diff --git a/src/sync_logic.rs b/src/sync_logic.rs index 06b14f0..cc197b3 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -24,6 +24,7 @@ pub struct LtcFrame { pub minutes: u32, pub seconds: u32, pub frames: u32, + pub is_drop_frame: bool, pub frame_rate: Ratio, pub timestamp: DateTime, // arrival stamp } @@ -35,8 +36,9 @@ impl LtcFrame { hours: caps[2].parse().ok()?, minutes: caps[3].parse().ok()?, seconds: caps[4].parse().ok()?, - frames: caps[5].parse().ok()?, - frame_rate: get_frame_rate_ratio(&caps[6])?, + is_drop_frame: &caps[5] == ";", + frames: caps[6].parse().ok()?, + frame_rate: get_frame_rate_ratio(&caps[7])?, timestamp, }) } @@ -205,6 +207,7 @@ mod tests { minutes: m, seconds: s, frames: 0, + is_drop_frame: false, frame_rate: Ratio::new(25, 1), timestamp: Utc::now(), } diff --git a/src/system.rs b/src/system.rs index 0ce73aa..64205b4 100644 --- a/src/system.rs +++ b/src/system.rs @@ -49,10 +49,10 @@ pub fn calculate_target_time(frame: &LtcFrame, config: &Config) -> DateTime Date: Sun, 3 Aug 2025 15:44:35 +0100 Subject: [PATCH 022/139] fix: Handle drop-frame timecode separator in API and UI Co-authored-by: aider (gemini/gemini-2.5-pro) --- docs/api.md | 4 ++-- src/api.rs | 32 +++++++++++++++++++++++++++++++- static/script.js | 8 +++++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/docs/api.md b/docs/api.md index 2959c95..83471bb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,13 +8,13 @@ This document describes the HTTP API for the NTP Timeturner application. - **`GET /api/status`** - Retrieves the real-time status of the LTC reader and system clock synchronization. + Retrieves the real-time status of the LTC reader and system clock synchronization. The `ltc_timecode` field uses `:` as a separator for non-drop-frame timecode, and `;` for drop-frame timecode between seconds and frames (e.g., `10:20:30;00`). **Example Response:** ```json { "ltc_status": "LOCK", - "ltc_timecode": "10:20:30:00", + "ltc_timecode": "10:20:30;00", "frame_rate": "25.00fps", "system_clock": "10:20:30.005", "system_date": "2025-07-30", diff --git a/src/api.rs b/src/api.rs index 62f21a7..14b0da4 100644 --- a/src/api.rs +++ b/src/api.rs @@ -47,7 +47,11 @@ async fn get_status(data: web::Data) -> impl Responder { let ltc_status = state.latest.as_ref().map_or("(waiting)".to_string(), |f| f.status.clone()); let ltc_timecode = state.latest.as_ref().map_or("…".to_string(), |f| { - format!("{:02}:{:02}:{:02}:{:02}", f.hours, f.minutes, f.seconds, f.frames) + let sep = if f.is_drop_frame { ';' } else { ':' }; + format!( + "{:02}:{:02}:{:02}{}{:02}", + f.hours, f.minutes, f.seconds, sep, f.frames + ) }); let frame_rate = state.latest.as_ref().map_or("…".to_string(), |f| { format!("{:.2}fps", f.frame_rate.to_f64().unwrap_or(0.0)) @@ -291,6 +295,32 @@ mod tests { assert_eq!(resp.hardware_offset_ms, 10); } + #[actix_web::test] + async fn test_get_status_drop_frame() { + let app_state = get_test_app_state(); + // Set state to drop frame + app_state + .ltc_state + .lock() + .unwrap() + .latest + .as_mut() + .unwrap() + .is_drop_frame = true; + + let app = test::init_service( + App::new() + .app_data(app_state.clone()) + .service(get_status), + ) + .await; + + let req = test::TestRequest::get().uri("/api/status").to_request(); + let resp: ApiStatus = test::call_and_read_body_json(&app, req).await; + + assert_eq!(resp.ltc_timecode, "01:02:03;04"); + } + #[actix_web::test] async fn test_get_config() { let app_state = get_test_app_state(); diff --git a/static/script.js b/static/script.js index 2094cc4..6fd3475 100644 --- a/static/script.js +++ b/static/script.js @@ -98,9 +98,11 @@ document.addEventListener('DOMContentLoaded', () => { } // Animate LTC Timecode - only if status is LOCK - if (lastApiData.ltc_status === 'LOCK' && lastApiData.ltc_timecode && lastApiData.ltc_timecode.includes(':') && lastApiData.frame_rate) { - const tcParts = lastApiData.ltc_timecode.split(':'); + if (lastApiData.ltc_status === 'LOCK' && lastApiData.ltc_timecode && lastApiData.ltc_timecode.match(/[:;]/) && lastApiData.frame_rate) { + const separator = lastApiData.ltc_timecode.includes(';') ? ';' : ':'; + const tcParts = lastApiData.ltc_timecode.split(/[:;]/); const frameRate = parseFloat(lastApiData.frame_rate); + if (tcParts.length === 4 && !isNaN(frameRate) && frameRate > 0) { let h = parseInt(tcParts[0], 10); let m = parseInt(tcParts[1], 10); @@ -126,7 +128,7 @@ document.addEventListener('DOMContentLoaded', () => { h %= 24; statusElements.ltcTimecode.textContent = - `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}:${String(f).padStart(2, '0')}`; + `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}${separator}${String(f).padStart(2, '0')}`; } } } From 65dd107514f3fc42d882781d738f00e0bcae74ee Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Sun, 3 Aug 2025 15:53:26 +0100 Subject: [PATCH 023/139] fix: Dynamically find serial port instead of hardcoding path Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/main.rs | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index e265210..0d3e113 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ use crate::sync_logic::LtcState; use crate::ui::start_ui; use clap::Parser; use daemonize::Daemonize; +use serialport; use std::{ fs, @@ -70,6 +71,20 @@ fn ensure_config() { } } +fn find_serial_port() -> Option { + if let Ok(ports) = serialport::available_ports() { + for p in ports { + if p.port_name.starts_with("/dev/ttyACM") + || p.port_name.starts_with("/dev/ttyAMA") + || p.port_name.starts_with("/dev/ttyUSB") + { + return Some(p.port_name); + } + } + } + None +} + #[tokio::main(flavor = "current_thread")] async fn main() { // This must be called before any logging statements. @@ -110,13 +125,23 @@ async fn main() { // 3️⃣ Shared state for UI and serial reader let ltc_state = Arc::new(Mutex::new(LtcState::new())); - // 4️⃣ Spawn the serial reader thread + // 4️⃣ Find serial port and spawn the serial reader thread + let serial_port_path = match find_serial_port() { + Some(port) => port, + None => { + log::error!("❌ No serial port found. Please connect the Teensy device."); + return; + } + }; + log::info!("Found serial port: {}", serial_port_path); + { let tx_clone = tx.clone(); let state_clone = ltc_state.clone(); + let port_clone = serial_port_path.clone(); thread::spawn(move || { start_serial_thread( - "/dev/ttyACM0", + &port_clone, 115200, tx_clone, state_clone, @@ -132,7 +157,7 @@ async fn main() { log::info!("🖥️ UI thread launched"); let ui_state = ltc_state.clone(); let config_clone = config.clone(); - let port = "/dev/ttyACM0".to_string(); + let port = serial_port_path; thread::spawn(move || { start_ui(ui_state, port, config_clone); }); From c9c6320abb5787803e3fd60b3f914e532e7bb8bb Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 5 Aug 2025 20:00:35 +0100 Subject: [PATCH 024/139] feat: Set system time to 10am when setting date Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/system.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/system.rs b/src/system.rs index 64205b4..dfaad20 100644 --- a/src/system.rs +++ b/src/system.rs @@ -157,19 +157,20 @@ pub fn nudge_clock(microseconds: i64) -> Result<(), ()> { pub fn set_date(date: &str) -> Result<(), ()> { #[cfg(target_os = "linux")] { + let datetime_str = format!("{} 10:00:00", date); let success = Command::new("sudo") .arg("date") .arg("--set") - .arg(date) + .arg(&datetime_str) .status() .map(|s| s.success()) .unwrap_or(false); if success { - log::info!("Set system date to {}", date); + log::info!("Set system date and time to {}", datetime_str); Ok(()) } else { - log::error!("Failed to set system date"); + log::error!("Failed to set system date and time"); Err(()) } } From a4bf025fd051fde2e30be71e2e1e5d01c9cad14b Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 5 Aug 2025 20:20:40 +0100 Subject: [PATCH 025/139] feat: Implement configurable auto-sync pausing --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 0d3e113..b2017e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -205,7 +205,9 @@ async fn main() { let state = sync_state.lock().unwrap(); let config = sync_config.lock().unwrap(); - if config.auto_sync_enabled && state.latest.is_some() { + if config.is_auto_sync_paused() { + log::info!("Auto-sync is temporarily paused."); + } else if config.auto_sync_enabled && state.latest.is_some() { let delta = state.get_ewma_clock_delta(); let frame = state.latest.as_ref().unwrap(); From 43a3fc7aadf9f1fbcded61b272711ba061fbd1d6 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 5 Aug 2025 20:20:46 +0100 Subject: [PATCH 026/139] feat: Add `kill` subcommand to stop daemon process Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/main.rs | 73 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/src/main.rs b/src/main.rs index b2017e7..5895a72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,8 @@ struct Args { enum Command { /// Run as a background daemon providing a web UI. Daemon, + /// Stop the running daemon process. + Kill, } /// Default config content, embedded in the binary. @@ -91,24 +93,65 @@ async fn main() { let log_buffer = logger::setup_logger(); let args = Args::parse(); - if let Some(Command::Daemon) = &args.command { - log::info!("🚀 Starting daemon..."); + if let Some(command) = &args.command { + match command { + Command::Daemon => { + log::info!("🚀 Starting daemon..."); - // Create files for stdout and stderr in the current directory - let stdout = fs::File::create("daemon.out").expect("Could not create daemon.out"); - let stderr = fs::File::create("daemon.err").expect("Could not create daemon.err"); + // Create files for stdout and stderr in the current directory + let stdout = + fs::File::create("daemon.out").expect("Could not create daemon.out"); + let stderr = + fs::File::create("daemon.err").expect("Could not create daemon.err"); - let daemonize = Daemonize::new() - .pid_file("ntp_timeturner.pid") // Create a PID file - .working_directory(".") // Keep the same working directory - .stdout(stdout) - .stderr(stderr); + let daemonize = Daemonize::new() + .pid_file("ntp_timeturner.pid") // Create a PID file + .working_directory(".") // Keep the same working directory + .stdout(stdout) + .stderr(stderr); - match daemonize.start() { - Ok(_) => { /* Process is now daemonized */ } - Err(e) => { - log::error!("Error daemonizing: {}", e); - return; // Exit if daemonization fails + match daemonize.start() { + Ok(_) => { /* Process is now daemonized */ } + Err(e) => { + log::error!("Error daemonizing: {}", e); + return; // Exit if daemonization fails + } + } + } + Command::Kill => { + log::info!("🛑 Stopping daemon..."); + let pid_file = "ntp_timeturner.pid"; + match fs::read_to_string(pid_file) { + Ok(pid_str) => { + let pid_str = pid_str.trim(); + log::info!("Found daemon with PID: {}", pid_str); + match std::process::Command::new("kill").arg(pid_str).status() { + Ok(status) => { + if status.success() { + log::info!("✅ Daemon stopped successfully."); + if fs::remove_file(pid_file).is_err() { + log::warn!("Could not remove PID file '{}'. It may need to be removed manually.", pid_file); + } + } else { + log::error!("'kill' command failed with status: {}. The daemon may not be running, or you may not have permission to stop it.", status); + log::warn!("Attempting to remove stale PID file '{}'...", pid_file); + if fs::remove_file(pid_file).is_ok() { + log::info!("Removed stale PID file."); + } else { + log::warn!("Could not remove PID file."); + } + } + } + Err(e) => { + log::error!("Failed to execute 'kill' command. Is 'kill' in your PATH? Error: {}", e); + } + } + } + Err(_) => { + log::error!("Could not read PID file '{}'. Is the daemon running in this directory?", pid_file); + } + } + return; } } } From ed48c1284d2c3014bfa7d908aa4e3e339bc40f3c Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 5 Aug 2025 20:43:20 +0100 Subject: [PATCH 027/139] fix: Forcefully terminate daemon process group Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 5895a72..0486f1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -125,7 +125,7 @@ async fn main() { Ok(pid_str) => { let pid_str = pid_str.trim(); log::info!("Found daemon with PID: {}", pid_str); - match std::process::Command::new("kill").arg(pid_str).status() { + match std::process::Command::new("kill").arg("-9").arg(format!("-{}", pid_str)).status() { Ok(status) => { if status.success() { log::info!("✅ Daemon stopped successfully."); From e4c49a1e78062142ef695e5b459b05e86bdc74d3 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 5 Aug 2025 20:59:19 +0100 Subject: [PATCH 028/139] fix: Fix NDF LTC wall-clock time calculation Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/system.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/system.rs b/src/system.rs index dfaad20..5c6f142 100644 --- a/src/system.rs +++ b/src/system.rs @@ -45,21 +45,20 @@ pub fn calculate_target_time(frame: &LtcFrame, config: &Config) -> DateTime 25000 { 30 } else { 24 }; // 30 for 29.97, 24 for 23.98 + let total_frames = timecode_secs * nominal_rate + frame.frames as i64; + Ratio::new(total_frames, 1) / frame.frame_rate } else { - total_duration_secs + Ratio::new(timecode_secs, 1) + Ratio::new(frame.frames as i64, 1) / frame.frame_rate }; // Convert to milliseconds - let total_ms = (scaled_duration_secs * Ratio::new(1000, 1)) + let total_ms = (total_duration_secs * Ratio::new(1000, 1)) .round() .to_integer(); From 82fbefce0ceac260b57345bf409e85829352f789 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 5 Aug 2025 21:05:46 +0100 Subject: [PATCH 029/139] fix: Remove NDF timecode scaling for pre-compensated LTC source Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/system.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/system.rs b/src/system.rs index 5c6f142..77a1aa0 100644 --- a/src/system.rs +++ b/src/system.rs @@ -45,17 +45,10 @@ pub fn calculate_target_time(frame: &LtcFrame, config: &Config) -> DateTime 25000 { 30 } else { 24 }; // 30 for 29.97, 24 for 23.98 - let total_frames = timecode_secs * nominal_rate + frame.frames as i64; - Ratio::new(total_frames, 1) / frame.frame_rate - } else { - Ratio::new(timecode_secs, 1) + Ratio::new(frame.frames as i64, 1) / frame.frame_rate - }; + // Timecode is always treated as wall-clock time. NDF scaling is not applied + // as the LTC source appears to be pre-compensated. + let total_duration_secs = + Ratio::new(timecode_secs, 1) + Ratio::new(frame.frames as i64, 1) / frame.frame_rate; // Convert to milliseconds let total_ms = (total_duration_secs * Ratio::new(1000, 1)) From 1842419f102e0e974e83faa567d6c00dd80afbbc Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 19:15:22 +0100 Subject: [PATCH 030/139] added assets for static --- static/assets/FuturaStdHeavy.otf | Bin 0 -> 27772 bytes static/assets/quartz-ms-regular.ttf | Bin 0 -> 51032 bytes static/assets/timeturner_controls.png | Bin 0 -> 1201 bytes static/assets/timeturner_delta_green.png | Bin 0 -> 981 bytes static/assets/timeturner_delta_orange.png | Bin 0 -> 955 bytes static/assets/timeturner_delta_red.png | Bin 0 -> 913 bytes static/assets/timeturner_jitter_green.png | Bin 0 -> 1498 bytes static/assets/timeturner_jitter_orange.png | Bin 0 -> 1451 bytes static/assets/timeturner_jitter_red.png | Bin 0 -> 1415 bytes static/assets/timeturner_logs.png | Bin 0 -> 2223 bytes static/assets/timeturner_ltc_green.png | Bin 0 -> 1373 bytes static/assets/timeturner_ltc_orange.png | Bin 0 -> 1299 bytes static/assets/timeturner_ltc_red.png | Bin 0 -> 1298 bytes static/assets/timeturner_network.png | Bin 0 -> 1448 bytes static/assets/timeturner_ntp_green.png | Bin 0 -> 1181 bytes static/assets/timeturner_ntp_orange.png | Bin 0 -> 1108 bytes static/assets/timeturner_ntp_red.png | Bin 0 -> 1100 bytes static/assets/timeturner_sync_green.png | Bin 0 -> 1429 bytes static/assets/timeturner_sync_orange.png | Bin 0 -> 1317 bytes static/assets/timeturner_sync_red.png | Bin 0 -> 1356 bytes 20 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/assets/FuturaStdHeavy.otf create mode 100644 static/assets/quartz-ms-regular.ttf create mode 100644 static/assets/timeturner_controls.png create mode 100644 static/assets/timeturner_delta_green.png create mode 100644 static/assets/timeturner_delta_orange.png create mode 100644 static/assets/timeturner_delta_red.png create mode 100644 static/assets/timeturner_jitter_green.png create mode 100644 static/assets/timeturner_jitter_orange.png create mode 100644 static/assets/timeturner_jitter_red.png create mode 100644 static/assets/timeturner_logs.png create mode 100644 static/assets/timeturner_ltc_green.png create mode 100644 static/assets/timeturner_ltc_orange.png create mode 100644 static/assets/timeturner_ltc_red.png create mode 100644 static/assets/timeturner_network.png create mode 100644 static/assets/timeturner_ntp_green.png create mode 100644 static/assets/timeturner_ntp_orange.png create mode 100644 static/assets/timeturner_ntp_red.png create mode 100644 static/assets/timeturner_sync_green.png create mode 100644 static/assets/timeturner_sync_orange.png create mode 100644 static/assets/timeturner_sync_red.png diff --git a/static/assets/FuturaStdHeavy.otf b/static/assets/FuturaStdHeavy.otf new file mode 100644 index 0000000000000000000000000000000000000000..7b8c22d7719575707687cb179dbd48355cc5de3b GIT binary patch literal 27772 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 bb^a5(gG>qd+z`0=?jbk=xEoCLR4VRQXc+hr literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_controls.png b/static/assets/timeturner_controls.png new file mode 100644 index 0000000000000000000000000000000000000000..a91f39b052660976437bf181e76ae427ef1c38a0 GIT binary patch literal 1201 zcmV;i1Wx;jP)ZHOf?zl#klcYB z@o|TF_p>>-2cHR!d))ip@6UbTyQ#y&LwHs)Hvl97T!b}w3m`5fBG0ET+F zu>f$6B`6m5U^N;70OYGpe7T-QqtzzGCU1tYwBP{%q;nGD&-O@hJZHnNcP?Yz?EnCL zzqN;_yOQDDnMQc|%3SaByxW0WVGk*e=WIQ(0I;;+A&+!$#?ccC0Ex&vFE zl|2N2W~+mTJFk%vOH4^nDA#3NU3-OC*o#K1&Ag1rf_+XqUVitfuBCHu=Wss~KyZd9 z7XvJm>oOkhygt!?kOi;a3=Q?KOA8)s7Y?vpI5<&K{qUR(x56G=3!LvAtJGy|?p6#h zCz9Zxn};wpqt0~*OxQ}3(&p6MoLV8_*lAr$egXn*3#KK;3cVhXox#1@bfVyvxr*!<;Ue7-dBAt^ojWr{k&HnyYr7n|W zsgEbJ;QeX?SJz(YTDoSdgGW1WPHZ}kHt7Hu>PkM{t&q+$v}u*=G8Wgfn&T<4bYcet zXv@x&SR#$`PhtsW;%pds1K>9UCZvCJY_!_A^W?8%_m6Z=!Vj4O`5Ge^ytyk4UHicM z0_1n?EK!!yqn$UJ-xeQd4KJgS3vRU9h;I!~Y9*6Dc3S>f`bTq&r7Sh2r?p);z~^g! z=x%tGx{SrgS*F(}EU>lj8Q8Bju(+Ou&uNGEmlXuinV7lPz zf%CX4%;F=?hLl(|Z7M66E;t$e&`4N~h9DMerjEF&jd;QD()em+J)6bk&4bmM2(~&C z!4z2E9`^UTfUetOfl`p@!6Y~(7JHxb)h7A1N2Aq7I``q+bWS2$OR-o}-RPA}o|vtg zsSEdB9I^x|o|vU#QCc}HK3ed*Px}e3u>kNd@3D^Xm#~p2 P00000NkvXXu0mjf0|Py7 literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_delta_green.png b/static/assets/timeturner_delta_green.png new file mode 100644 index 0000000000000000000000000000000000000000..ddc84b95d7bb2a6d39f21c3533a4d468fe860580 GIT binary patch literal 981 zcmV;`11kK9P)g-7zS zx0y3V>7P+iW$I3*E=lQV#Dv7e&?aR_q&iYS&4)x2r3wUzfer{%!ubIE?$R2b&AvPQ z{KwpMI+f4P>}FTj0DOLGiv(%ifzHfMiuN3^?=mK7 z(ecz4h}IO??wSJIT~lDYYvO=sMLx{!oI`4P8-8~P08pxGC{;9(CWr;zR?=9QmwMMy zU@t;6!YLc;DspA|6&mnIQ&P3cMiBZddBk z8xdvz5CkkoRcgD>6ND6aA;J{?zK&7Ct{nX#`!$Kp<&F5xBx$!mmmdJh;7YQ9iP6wNAA=(e>Td>ScnVoE6I3vv*nK{ zRWxWP`?$E%iG3F4C2olbbKp)WA}1QnRvqQbV(+?ec4+uJyp}a{V49aLoE_kyb>F)# zRW#f-u8Do+NRmq;!XB97tV)&K;JvEx0RWE>#S~bMs)mV1shY!0<7(i3L8mP1h{`+> zVG2z1u0}^W53PHYs)I8R@TrxV0n=(;y><+}c4Rm*nQT`MyKPEbM-ZJz?bP2&oB z?U>l+^8}gBG816GCxm$DC9&q;{GyY%`m+k;DX;=1qsqZdZEk$S_2)td%VT z(`w#kt4{U8kEK!4O;)wCWnfw;`hHdHOEiY5d|5=ZRVP0ul4KI1ECbU*5zYIO^w7FT z;q1WpSqu^pOTcneHLQ8{+A%TF7$#CS))foqWeu2fvHn;tr`K{(Vfx(?t>y(fo^@OK z0R7juk$-cPL%&39!_o^l)4;Tvw<{^ILj6X ze?X0iO**t=+lj&0p<`j}P^znTXmnB;ZEQjqOsb!iFgJ`dpAeS4%x3mTd$+b>$x;NCE{ z)0lSO9;ns;*zFnsyIlidx2x}f4b>BR6HCy;(*OYL>^Ckp-ovVtl+CXc_`%b+@Wfx- z_$LMu97W~Rb7ej#3u!VmdAvG%2kRkpk?QN`eIK9j^WSj?2w8RQGeJZfB@t z$jW6Yz$|BPLRNP&)OMbkl$1h5IIuP{0X-bEwOMD!s26vye^*MV<#V=ei7x;n(*2Rv zwSS@qWasW$`+tnEmQh~|1=d0)wLG-W%c$qSCa0}0`7F@rL7iA_2MoXC% z0ej*p?mI8{oDRd9lvl@_*}ifzVo(3 zf+oC)m%LNZ=Z-JpV&ffGU+$2W{n*Bw+d)`!1JjNo6(YU>_vXb7Oe^+PshjG}PTase zXRIhyce31H4LI7RwU7yKVhJQT$~#0)vd&AWZf9;i6ZI-b!2hHA?kk=Fu-nydz}bER z&id1ZRKEac{pmsq=#P-Tf(6_Ud dN>gD0;2*O6_77HaR;K^}002ovPDHLkV1fjW!G-_; literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_delta_red.png b/static/assets/timeturner_delta_red.png new file mode 100644 index 0000000000000000000000000000000000000000..c7272ac0c042ad72d92ea1e849e8fb0d3f8cb5ef GIT binary patch literal 913 zcmV;C18)3@P)8R?2i>+;=X=d2%8drTpPfsy zLE0m&n&vauKhmrtsHQ2)40*r@Z_hH7@s<=|r9z-tR&2RuSx_oOhADD@Nv8va!~-dW zfOI-COpybuA3I#?@Q`p|-Ep8A#>RT8VL*4pLkkBcC&?exGt=?xPG}_ z!+xtJ&?3RW`tf?)711V*gaOAT8k?lsw$MaG7;v8XC~__f0ahvmx)bl++siB5US7qP z>y9lf5fK8cS?$`I^HI&!!z^Z0<1d@Gtu}v6vZj@ zeP$xUYpu)$nCab{&E8E?mzS4w1GCn=kE0Rho|m|Srq0p+z zx=A;ouEJzC#ZZ@a+NI@5R~fPxf^O<&oVb`p#6@U{Fa|1WpceWHM*In6458+y^M-L2 zF_QQ0)4cnhQ6~DW;(2+8^X1%g?sw0%;rRF%{>2|g0K5cn7xjs40FPPMa=2k4e18PM z?^hksCE-CMN}0Vbc-e^hUB*(l+j!LxTsCe9*E?Zh%LAj%tlHO?YF@UrJ27JQL95XN0G8g*A+j~6I;cum3=GBxo&f-cI>r!A1Q1U~ z6#G+5c%<8hdmWllUK5k!-wnc5#U;qigzXfrSZTanLUe(eRg8e}-0j9`B41&Vx5U~e-p7$0~} z?T?DE#lV0u^$2WxSq_2NgsQr#hIlf9Kx_hgSx#VQOg+?%jEb;Uw1l|y1_CiZc13~I z0=robftVj$dP88pt!tt3qiS1XOg(UQ`>1Ql-9HI;|0LBrO`2vnyFA1SqXku$=F;oP z2ctgUt>E+t`YwV$fEGTwY#02}oxZj`E;R%B}q ziGvk^tw|K18i;)*4ptN^tTGf6wip=r9YI*lg97_uKBX$4^L$y&gYY|os>((Y;UQ)W zKKl%zxH9>aqPjW@iTxNdBGPRqKeU_%DFtf6&MpsJ)*p%5%H&fBtxlo9A1Mk%3I+ZM zq17pZbomgWgftc5CQUPjI>rcl;$THpBRLP{1{vR3BA!Wg@M@qU+-6_|nJw};gjSy- z^ykH9R*V9Fg!#AA2(3O9XaKE7Pd$9N^bt>Grlhh~Yfwqcy)1_!pF7R6R-?!Gz%#^m zmaw=Pkd@_1DJe%*0;N)Nb^8cqR^)S%0_Z$^@vXb#XSrYg=ik5a>2rn<7ZwA9KFfFD z{#wVU&zY)z68kaP-P~ee;Iq$2F5N%g{aSl1eb#}zq^D;TO(D*M)S;os=dhUg1+Tf^ zsQoFb;dn9vHtHpolUAd@aA_$fTOK$lAq%KzT8H@quu(4|Esc~1^7CL337d=9{uC2# z(lleF+eb*tf@rC$f!LSTJP2o(N0m=SgiDQtm7HA4X-2kor>u!bVn0Uois5fO@H>J; z_T_@AO9Q34^4VG3(i(|paweCycIWAA*{C<4`vVpDBgtLimezD{%aBX75(WMUk*ztjebuRIa!IwMiy~r-sRuqgtGI#Cz^F6B zXJ-kr;+U#|ituihBL?AZC(=*pfm{x(t#cL;OtJv*AD3z|(I!^+j{pDw07*qoM6N<$f=PJB AZvX%Q literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_jitter_orange.png b/static/assets/timeturner_jitter_orange.png new file mode 100644 index 0000000000000000000000000000000000000000..96c5f84fc25681e8e9c925babdbe070200fb0455 GIT binary patch literal 1451 zcmV;c1yuTpP)y{Sscf|GyIz&VxmXbqzAf*RF9%3dlGJH%cUkB zibqV=7`j;w_1GRv3~6j4$7)g%Jd`cvM!k7bJZdyC95z`vN+KH={>|)RGL~Tm;k`kd zBA?Uv$otLj%bWLm-}gosNs{p2pTEBV@BzRR3?}~o*cjQq`pT5vJbeM+@1LE~1M!lD zJhG344=e`vG1lV=`?E86Xoi9ZouS}CXDE2k844y2Fz$lMJ=>35I%1yk!RlWJJMJCK%2E0H|d?qL$e)oK!2U(*rKJE8p*V5Vh1h)U!$Ic+`SzOPjFFy;ij~ zbH~()+;txXhI7F7Y74CM8LFvwRPl%(X@(z#J)G8vy)PQ4Uo>r&x!16V(_r{f;&{Y@ zxnK;eb3)Z72_;kyBKI=F^=uN=g9s#{q-tlK6X1d|;&?=YS*Hgk_l&AdEL@_pyJBcw zb)%k5qO!XJv2dwsH@Rns3mK8%_D86hJ3%G30YT=YBZ^~a zSu(k2U|V`iJeRoTEpW?QM12^36juMb<_RN(s7g0;$B>G7Fk?R+BlC0sCig6aL~nDL zTi$|sIv`&I5E7BDeORXl{Ax^fUjUGbdE&Z3JiU;SiFI*{S~q8*PXC< z1F(m`X~Y6RHTAxSjMWeeZsv}43kU#tjqHOPQeekju(qtj3S!|B<>(?BXWz+V(j+1l zE>MmxpqBZl`OGxwqY0^tY0`J=;uPvf`zS{js0*c`qE*Z6pmBbvYNIg&ooJ8MI6tIz zSPImFS*Hi~a2n>xc~!eq{MS%Icf~SK&chx~8=5v$f-Q4F@N03c<0|ISI6Lgni=4*U zA*5nn)y6s}z=zX46-YgZ1zY{=^4i-rx1?eol}{l^l^d!=^pGkya?Cc-$|izFG_ljN zJP&Hx8|Q~8M;GtKamfX85sg!=b=9lj-5~bTts^T5B?x;an+BmoB^IEZ!}V+umFR-TPcxhY=BfD(y~r_7`7~m04;(1O0tSkfc%4T%x}dd| zgd6g^V4WV--}3TD@1p?&g5^T8skN4UHXf~*C+A`HuN#UA4s{Xgo~rS=|kJEwj_} z+@&!dil4Un*HzPq8I#{@k(_H8dbiZfoe+st=v8PdQsoA<%#QBaI0;0Lbh|KnHLvf8*(l47xuvoaz9E2+cgmy#T3+5>w_;6ZND4V$x;x(E4n07Cu>#%4y zjB`q94TGt1m6aRS}Y^X84yBr35LDw9K9w5q9h zXy%S#Tiyc0Ipo8D#9y@GI?}p~NkYl+3vuG@VLR-&3zoT6)U#ji?WLN<8vyHkhWgQ` zuEwSth6Vp>4F8K~&>0FIaP}XQ;1qxjLz{0VggnxNKu56v@IUBV9^!0Q-qipA002ovPDHLk FV1jOhuVerK literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_jitter_red.png b/static/assets/timeturner_jitter_red.png new file mode 100644 index 0000000000000000000000000000000000000000..88131590b9c9864a8b4ad7a7d02f6d17098c0898 GIT binary patch literal 1415 zcmV;21$g?2P)$-E$TsY1sDggVnBYGjUh2xC!7v^_ngB%NZJ7NLg zgYeoByfp8E=bd-K^Uk~AdFNd)E0J^>OS#-r#PT4~LLPCWh)6m;v#*8YjB>*}IHqY> zsgxi{5&+=BtmDG0&m7c!L`6o@-UR@-gy6iogMrOo`bT&Nu2jm9^iMw9b-TkI$Ya0b zAxIJuwQY!sjPq)Z%OCTS7M3L>n>Ho(x%g+dN!cX*6Ov6E!m`Bdk6G|)-GnG-d^VR5 zoRzn5)owEfFo}U}|mk^&_lrvbZo6P>01dED{#cbAR8#zZf(Klz>SCbgnHcs?S zjGQB%eKDJ5PGn4i1!>v${<_n_iLPUG=rH{#52Hf|C%TU7jz3R4tKcAu*(~CQ!Mv9e zJ5?lhs!U^qWeIV^pjH?wOoFf4ZH#+8pUq1k)1JkNoho9gikPZSEm~CM&_p@z^_cSp z^9=tiRj33KNu{u6{fU^SO&lLn)zH@ueqYm&w099nrKrz%jwWWoSM7Fa0s(+TZTm^8 zA&*Epjg?A?vX7i2^tBBP_gc(8x%Tkr&_Q3-LaVB1ApS|Y#+@i< zN?I3Y9mCeX-=V}_3_L{8*RZwEU18n%*zHSYB=Z0&Ie6J*%hqfP};&LG*`ohm_w!7Mm#6tPk+QOj-I>)}VyfO|#6s#sL1%WZibhZ zwH=U##pI8xrxomFdANk2moGf&eopQ0QDR*}aADS&#R8mrcwpP;s~c23El3ig`2vs2 zP-2=!iS-%|JYsE>%YZz*%_< z<8JrS@m`jnrsFa0c5zI8md69QS(YYa3H_E|-31>q!l48+~mf zw4VNju`dQN79LBvTKNGevprOcX&OXD25DFm3!6#IEcpM%`@eYRop-@=PU9^JZmc^8pWlk$#=3Lx z8F(8@`c5n`|AKvDfm^}HuM^9&Lx}}$1%G|{Se~6wu>c^fI|oew8s=xF9~27!{{nSh VEXz1&sJQ?D002ovPDHLkV1mA8h*B$=sfpUq$4Iy>F0a}o3DTEXSR~5TSrI~78 zU=LMD8X~#c)VcOU{p8p$Q>4bOA13X=e;%g1IP;WMYn9kZDoqMBokpalz4JQRmKA&> z8O^i?1y>$ppbRpzWDmr*+<)8)7dw;lJ#p{3oO`}|e&_e+JE!UC)2Fap3ZDir3E&m% zPJ9DkAT=8JwCV5%w@(AO^}I*4LkKpbJhLwfCY!N)%~%YtG(YbV>@;3D!kV3aA>}Z# zzhe*c$Q3tt^!++n3XM#L_u@wfYZ4nD4grBQzcU;P%r-b!Ic@003** z0(;M8CUK|#WKI2i_Fx?mW*yfTQjHoKiy!EJ^v;OdPDnzFU%zz>6G1n6DLYO$4kA}9 zA-?elgr*rM90$>|Z;#^s+FsfRzte>C9xHOiO~lf9wL@f~+G+JscKmg)lVh@!-3XVN z1W=ysv(s}}em3ZQu>?AiLB3d$?b~Y5VIt_pY>$`iPc2lL^oIc_dMUeT+YiU@a!gwa zHiHhXRuX=v36$Lk>$7FSN4{9XrSP(9GgsxbT&=>do-dX#7F|(97ARZ5TDE`)vkn08 zJ5A`N?4ayM&U1P~gNdLUCmaVcaC52dF{>iXAJi2(fw5>@m6we$WrsyMj2I0Z#Zj}K z)4sKI>LsnpX}x@4FI;94%jrDcpIO95U$5bD=}EoaagfXw@X^=bVn3lpht0rgbD2rJ zd|)qY6}6z|H7pi-)MCqw0f1i3U@W@AX%BfU7!4eSp5PkfNSMJCov7(uif4Gp*N)j9 zFM26E_Up8DeO)n74kP~Xv)5rW=wLJGKsk(PLKEmjhJ7ZM&Lg_Afzz)Z#vaZ0fu*y( z1#}{VWDOr*QCOlJMhyDe*lQ_IS`W^9tb%(Xk1#8XrE?g#C0w_YK{rT3tN4A22u}vx zoaP2{VFc`y@i67-R{DJQl%>ihb#N;CCJptjq7) zgoFWLicWCM9VjP1N@rP?@Q|;ab4@2Q4Ozb=p%wI9iF3y-lG)V-UP#gt8pX3*b85-= zp!I+xw7AjZ6<7{G-Mk@@Qm$nSm|5gzTn!_9g-)=)XBJbSS2L2&$|2;jV7j{l%*7uI z5{l(?UJzpRga&u|PvZ0bH_%7f*^meTV}D)|JS(*fDj^$|;v0`(((l1|G>+eV@pst{ z#n4CD@$t`J$6K$qqV1)9qR+hZ`@bO?-@u#gErQCbW#1ljwzr^{va<3uWhU|4?ha1;tVF(8!Y}`LA9v#$cwBmd`L!%wvmKBu zCtYR|`C`t71hivnJu8}@;y!k6ierDc6ttNM zQ_uS3FE6V>C+n>x2@m<&G5ZTIMgmTJ-v35T!Mzf(bWXC(YS3Z2`!$YZA_*-4=#3zri4Jbl>5o$FL8xVh~~X|LW7AjZiGA*LCFb}*ZIt1 z3h#X~kFLx2IOQk5Kbv81)v-#}!}}GQu+5-j^X!SB8}w>Mw33RYa~KZiFdSy!cbYI6 zbaS#S%5D^$oslquYYQpK8hh=ZwN-7ZXP7VXghYw~J|fII&Q8w>)*n9+xo~RXmn3}Q<`PF*O;)TGF~=f?YN4e(xPoZB%@<3G z)k%J-DyPIq65i62$D?s{UB1WZNG_Y%!gxjakb)SoNR_0c5;lWQP+rC!Ru}E-)}={8 zi}M}}MgmUG{Vn~tmOY|MUqB}^==peF(SJi83nnVWiDb3_@A%!S|B;nxNkR+1^N?UB za>Y$d(Fu@*mLvbvBbE<`84UB33?<-41immJ23e=&cOJq-(2WNdyF_bFzH^B%-^sqT z5wIC_0$-v;n2;shM>znLv!RiIQxI;}vIV^N$vl$T0^a`J+>Y({4!y}$OY+51V+fNx zr!aBGEqErvtiwBgS5h1x2`%OZJMkfJz1ItyK_|$!mFm;7g!$`{vKtjC3zg`j?64Yi zIPbAGWEYVC(euSp;|PyMR|E+%1W$O^oeY=)slWH%Cz_%lwXon zgIINxE6DE+i^1^rsj4U-1(%r=wXA_# zOEry0@pkFJ%_Ur0NWy=_RCAoYwva-|W5pD`T9+5EwwYF|imK`aK*(c(&9E1)R(*L# zK!t=W1*ua8GmELZ_J~z&h*zd{r;eB~vzWrA+e|}ZvDAh*P(N8fC1E;|;dsn*$1U(5 zF=2{M)V)I?b>v|SYRCdA3CAAhM9*Rj`r2@PA-VewwG4+DB(nt!`Pu~zCFHTPvs}3j z^TiVGr*k-FCM9=ET;Zn4wqhS z=;2Z&c#g-1uck%=@9y|{>Fr2P6yR!VH1IC)Vw{L)$pX8dFo!HqCj8Cw8OydqKo%$y x9@zP@Y&%5B0zh+WH1H{aAa>78OOgeE{{h5)m_H^7HB|rr002ovPDHLkV1fj$DiQzy literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_ltc_green.png b/static/assets/timeturner_ltc_green.png new file mode 100644 index 0000000000000000000000000000000000000000..43299137358525aa4f65c229bf3d716e0a53837e GIT binary patch literal 1373 zcmV-j1)}9UPK%))m#y?`A6PE?rW1HxqH_ujnkyx+Xv`;CU<<74>36H@?I0SrT*>;bq(de%dR#nA03 z06$)KL>I)JHn5p(7FOG!-)StxaNA`^aM82`>z$Thz0(q`cUpq=PD`-fX$jUlO((d$ zUW4w=KG^%6u=hD(wGikqUgfW)@-+5e)1VGwpbla+eL05Qn6i6CJ}OKRAV%ZO;K zz26C!Z3d~=y9mAT0{~1$Gv>#aLG;`Zjm?!Zc>Mc)?C0sKdm0u)w~y5bH5twDT=&8; zIH_hha-|GHlwVf3$!Ny6UxZ}sec=iI*hS31WB;n^+UfH`sPrZt|8~FXys8WnJ)`(? zd|8x_ax4#FNU?2vKa- z9Ybz71}9-^H%r>o6zmvsWA@XpYYmgPADo2s0|Ne-uSzrrolPj(1FR_FLlRPEioB1J zo>9ek6pNPCLTD1pF<>DuJGOKt+vrXJVd@c|L9NIN<_bciu=m}Bi#4qZvfcDF4@CG^1N$<=D^De5NZh%?*cShRa9FMnbwed*OAG@W*@z z0;(F7(?LyL_u`gq8WpApa$^C9r8G=NGrUexvb&1z1Q6K{@W0sxX$b=sLbiwIkj!u@ zzgshCAp;fyUMC6aAf_tQ+%je|nlb;`GH%(X0kF#!H@yceglsD*Tr!ECQFxsX`LS|6 zG>4?%U4?f*4z;4G^d^5iJGKOS-%UloboM!X;R*P{6Z|HFTQa#>P|p%7DVWYCkP{RK zwQ!=PiVlZxyU1lDOg)kvFZIp}?+37jPWT{|kgd}3WCWAf=2SU39fa3ON?yyAGNO&W zKeosp%k68G+FLrC;P=MvkLGI57C@m&N45i!Yn&Duwd2u(;<6OnT0!Nco6$-<8CCV! z=e$uAyq~Axk1c8z9ZyCPe72&=N293OcyOZ6Qp5nZP1GuuYdox$5lwyom96D|o(8$G z02eV0mu*JXGC*gaBeJum*ybrAY)(!WK`piA%ICAT2Wkc${re7dHi2LN{t2%NNx)#h z(Le9-enQXHY&W;wjB;9NxUm!C+uN?6qHcj~|!H z(+F;@;LY2d$Uc>)G2Hbr!qoRw*MzA4Osf}~?mrJL!Fs19Snsq1>z$Thz0+)h*P2Q2 z8tGZT+bn|DNYDCRpgE>AFBZ_hV3AmW6})#jv79?qVgXk0y^D|K+>sOu0BxjaJp|wm f^pj~>u>kNNj8XP)cuSA|fI`|_>Ral> zIV7M$oudK`p)n0KrsNO@+zJz1kdL;Zrs=_MOd(LWhk_5q9~62B5(pIx*}W7b(7N80 zWbO2@(R!twU5zBmhLwINYv;|(`|r%Wd2h`TLU1%Yd=tP609W8m4goyukEdU8%A>0{ z0sMT?9-R}nco-|&DZJp}-EBvRQsovvW7(-rJ>x`Mq<#|ic~ z+JOrWz&|<-{|BFd4~yUem(4bklTgm=qMX@9X@9fsSUqFY=$)FY8Up|dTk|OGtX4ha z1rdEyb0AtC+DgoP-ZwReo@<|1*^-R0MFLAs{PJSFjdcPNA^%%=EQy>WQOm zUBSE{B6xq@aV6^Ei;N=}TZg(FrHVEjPNJOIrM6YX5>0XRfv9=M{OTXY#O{UQg;Ad zPT_d*Q>Mv!C%*tc$Q}pUfums7)gWq~8vdQ|kKSgsZ3k4rydX9dksG0BY`QJuvO&t-~jVv&A^3*02mRN#>v5^?U<0< zTM$iwE~m_Ax}2=~ZN(|jo!JVz-IGofx0i5ouwlLczz}9#{R(w5Kn(wpuPNBF}j=t7aRaDh#+d- z^*B%kmovNYMaG#N<^q=yyuS`L5p7y@&$Ssd9kk}qm%jXkdNr1I*348AFpgj_whmqx z0m(`DZ~nUDT+}n%NWcJ2MFTLlou;_jThC|U?m|`WTiM>e$b@4aV9l+0LBypef0(xQ zZWpTH@|)jWJJDbjt;;F%g$C{|!#`U8nmU>t#?kCB)aB1?ZH?|Is$e}UIYvJ>Dx&$p zArMO70f5r}hQ;0pG^L%jhUc=*Ni+q8t$8MU$Dmc(S*fZ)8*v~olc2T{O~J{*X?<+1 zS6S=qTCBA(3y!H}X@3*t%wAJYI8pp{sp0r=*0@$ng`d8v8-Q3TXZFst>S-2N!RBS( zqG(-CL0dhWdbU;Seznx)B-BK-sUQXzX`a zqZy`afaD}-FCR01da?I^YyMqZT(q*|d|!V!_>|SSoY}?6pFe{9`wyTgAgW>(^h8sj zAO4Nx`Mq2jmo!-N0;002ov JPDHLkV1n($c?kdj literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_ltc_red.png b/static/assets/timeturner_ltc_red.png new file mode 100644 index 0000000000000000000000000000000000000000..a8e7f96c67b6c2ab30451abf094ad41a78dcdc49 GIT binary patch literal 1298 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 literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_ntp_orange.png b/static/assets/timeturner_ntp_orange.png new file mode 100644 index 0000000000000000000000000000000000000000..88319b593d352a7dda1e2d487468cf6e26d56f17 GIT binary patch literal 1108 zcmV-a1grarP) z!QUWq2ta2)W!+N&K>vK`EYQdt^xY1qCSGxxAsDnS2yXv_^|GXuUD3e%$l z<1>+hkkWs+4#tkwR>rk7YrbGQhuzGrehA!I2Omq~|0mZSw@Z%>p=4K?lOxMRJR*k> zxUmL)e#zL9hK*jYptbuL+-wx$;yv59DZO5STH1zIE)4$EQeMd$Ic)cMbQI7}AAspu z0AKg_c5M&z=)iHN)`e-6KAp4^DTt|`f9b2SL2&W{IC){7MX|uN538jD^O@2x(d!lF z^Jgm@9mEUsOUBE?sG&y(P)qK=^mFyg!zl3ln0C}mg#FG$*zZh){m!@v=f+7mH?vVq zkBe|_W}})0#$!kuvVi}E&&UED;g8VkYKdv!gWOz2nzQ_06 zm-}^mY$rye5q_7xEdlriz*EGN4uGS5<4R9-a`_SfE$S962|3Zp<$ptbdv=-*hxHK) z08hE7Td-_4g5%CcaNOAljyoH{ac3hq?yQ|)KI#T|A><`|CFQ}3S^JztDF&?;hHs6T z>RdU&T$&&y%OGV1d^YQ9XHg2;P(f7{R-ZE0pXlWBqj@xR9wZ9|?3~m*g-&BpiVvqR zK^rQQ{mKYV$ucta7c*aP>N+30rg?#0<+-oD<+4UFZ%eys1AI1%UA1A)aY4!Ixk%aPs>|8vpAcJceb z*9lS%d%F80ZLeEJ<{lmoAye0J+iKad?Zd*WS!~$5oi;x_$gkQNW90;MB`sl?wK>^FY^l(W_KFGa$Z# zNm;?rFfjVRl4POqpxp1+4~zEEt301epkwMfZZ5n>m}ACc(P8B@&U4x#qOEG{M)^3bu9*wi%UoYEp08g0)#mW4oh>Fpni5F{y-!&$lOUc=PF zzc9EmiTGs^d75_FYy`)hjo`Sm5gd2cO>kqK1iu1sv~n-CEQrRwarG0h9z)s|3&daW zuKvhzSiRu>ipB8&^628TUOM2OiAXY5oI`ICWdN S$=2)u0000Wn|{o zZ2-TV^oov2cUn=}+0&xOt!Q31Ch219Nw46zbq3t*IsmbJvG2earGdGAAROPnV;s9z|ED@oIa$YFy4NLf%|NVRr}T-xXmobx`N< z^!Sn7l8N)Mh#43tK^Jy+(AVwLb^mXFPvP~OEb&@aO=Ia*4AIqTWbCLSn9=2+o~pNjlt7Qz9qpx-YgC#`nM8g`9aoyk2teG2M1a;sTazUU(cgblscO z39Bhz+{TadBZ#d>D7LRO^geR|4_*FJ_q~ZNHegLt&mU2Ddg$^!t~mh?V9Dk!XGVII zRa2}c%KG__Z%`=iRE>98?cBS}3`{c=Sq>48td#}|hJ0}w;pGtV8p|nBDQ4h)2iZ_m zo+h5tv@A=nqViMXF6)gUhs6X;D-zes3&IN zs60&^_c(5FC-eY&;ES!1)6IHf23FJ*wK)G`V2r!1$`bhG%nR=9i|bg5ERjn#v#A|H z{{$B_%MOQ^Cz*Sm^5)L&W}*CR%2srIfs4>0lcA$wPQ4gxFdHgMIg^^6Oe|FV^`L z5hz7xMNLEATtFfxBdeykQcJ_X@lezhrk+1SWGMtsj~~7ZgK!EGlsvicIGj>yIdXDq z1&W$RBDZ2_i>GB|=GLBJv(=FHVGC*8*E@)n%;Nqo8q*e{>DYk(FM6wcT0A-fZgNeY zCg92O!0i3g0z5e$n7t324wo*&0?iM602U|#zdV_>9J#7sffDe8<6p~>iwFwb~& j831?CJUdN=1%UqmXX^Td4uNjt00000NkvXXu0mjfNUxyY literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_sync_orange.png b/static/assets/timeturner_sync_orange.png new file mode 100644 index 0000000000000000000000000000000000000000..0b41130a12ec4c895cbf0b336f9e26c4f790fb87 GIT binary patch literal 1317 zcmV+=1={+FP)&F#YlLxkuo@y>Xy-g$(aJ3z=<<7 zQX<7&D-lIWRR$Q70SQ@=Bf%PmN@ReBc5^aBVk%}tbtS|Q34639T;cEcIyiQ*IoE#o z_!%mj|71O%`_BEn`*F^>?nY2C z758`p&KHEuAA~Y730q*g^?&_%p9EIDe%JytsAtm9vuUfkBNPj`Iclme2wPwVs@D%? z!r!$`{rFG11EB|S%q;={jC=;w)DCI~+t70vPS<+MG_W(W@&$0Zw=p3je?neYz5hKZ zWx@}~+#(!vi>Mv!puGDEM)8#8PbAZuRyDN^)gNR#s%Plws`|bquWO+wZY+O-HZ{xk zN%}KusAtnCu0MiNDzF@h)~D+GM%YGidEknz!x>p&`6T_x7;~ZZ?BBF=pSc-h4?ltH z;i{BRk)-1=2U-`bUpT9RinU*auW7<{}k|?{g_&h4fH$odnWTkCqmlX?GagW3C{ZrHSqL3s~ zC7D3^-4gBF<$567UmJf>`oQh}93R8x&OOiBChuG9=WFCOrnm}%Q?>$jJx zdv8%c-lzM(T+7nH+SDvLx<)>Ov-rY=-MioXxcu%Hx*wq^6!*CF{TRS^$?KJ5f(%)D zPHz2nO3!6bJD{s9&a{>Wrkoh1LaP$$gtnprsvUkb-IrdH2BxZ_&aiVH+H6SbrrO~L z^19hft*woWvFM3X88^)u_V5$jc>cTT(Uy6u;reldo!u#g7q<*n%ck>(tI$H-p^Q=i^}`RS z9exyslCPFONfiS|set18Bk7&G?_9dHwIn=|Xi9N;poOB)x{S7LG33PmibA;>1 z`!Mn+Ehs&kw(P}t-@CL~sb0Sj*fICCF><++fq&<5X~6#%jof!y*T6%r%~1m099uhz zjSBGQ*xFGH7>z02f(3>jxD6I)0Dr!kz6@L!V1Wkkv&-+xz(v9WfI7Bz^d7){49`dl bVFBP@&erN~V+}_e00000NkvXXu0mjfa-WUS literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_sync_red.png b/static/assets/timeturner_sync_red.png new file mode 100644 index 0000000000000000000000000000000000000000..1c4c4c9048a7071cddb936577d7d06e1a5da3c31 GIT binary patch literal 1356 zcmV-S1+)5zP)^DoeYN znX*{+(k@njbhDhLlan!k>j#h?z~lqiowAhPEqZCIq7;%KhG7$|En1d4C`x4OPU4*? zyO90eL>_tf;obfI>0;;S=lEOsRtNABzlS$0bqhT0nmBM=mXRuI$d;EU-Bl>XqkP%jLIDX? zL0pz0stS_%{D*sivxmc9HwFjr{)4n^8$sQPayUeB0mo$-2~~lpC=gY(3z^Feedg4u z6R#%=19K`Jdk zDlNdbEF7+HL=GN$54UU^2}R-VsAcaEuiyQyK)jyTb$n@ULRu_w_eoM{;Gpi{ZD|o5 z&*SQdHdA+Gnfi<%Ntmsy;IkKNTzwMX$Cxco3H^e}hRc3t?Ui8l+2Xlf6Kf~R$TY=6!j zcnDGkf)s9vg|)P8*TtKKMI1bTL2Z*QFGE!MjVCworwoSVlrf<2sx|Cao>A|Qv$VN@ z(}oes*z|N_+su9Vrb&fP=JQAuHRg8$oWR*9;rCIy+2rX0Ko|OW`#0jf45xo)1{Nt{ zWV_kK+tSj2Av+s0QrfRnh{iFQ&m%s>httFioYKfk$=@__V%r1O`{Z}_<=n5%kq6(j z$YbGH*37`PP|&N@01UdUV2BS6E3aB3kEK8!%P=tmlahzP*+b{(aJ4Z*v>rtk5;P_YU#)GEZ_7-vLj1xcii`i(AtYwnJJ&+WaWeAdlFKX2R zKvK13R7yz_GcYL;i05)#QO{*1-a}r7P8Bs`X2!F{n1N}D084yLlh`+l{m?U%i0a?1 zZRS3hfsZMjq;-AlSr%3G!tWk*TGxr~N&bJ7*!QsOQuofd#kK^&Fwu$(zGYpA?!t;I z(mGrp{09N(hMnV-U|4c&+89QO3;4vgsk59dFLTwBSrP%?vbbXc?pS(%WtCdQ>(!E3 z%BrO0Oq0J!P8Y+78@TPdI9Q_>+tRv@-1ZjIh7k&!#o*oBSmzEz?)-f3^Iy20M7Q27 zWHLxTctE@#OC~W>)bPpU$4GqhH2|Rf$DhQu*`Iz44IN7+G4sRsIC=F7XYbxIjd|?! z@L=sZd~5g;6C?>~T_0M9V*Qhj7Lkp7j;S?8`zxzMYso#)a${cq%>y0T2>`EJ!)xOa z0%wofhGtn=4Ay0JJP+A&IduC-PXKhQ90t;cf$Wpz0l!Ks$crKWhHdTaAgDWNyDm=a z4g#mn9DGCy+>>vb@J$n`qK28GhLqL~{%LXe<&ZLP_HgF922n@e2K;}~?K!P!*G&Vi-X!4aoU^-hQ-G^;&h8R$GnVuR zSYY~rFTes_;KucgW#Sru1-igbuYN2O7ZDZ!ggIx|0$^ZzW||5M0RI6gRO{nwbPXB+ O0000 Date: Thu, 7 Aug 2025 19:40:43 +0100 Subject: [PATCH 031/139] fix: Add `is_auto_sync_paused` to Config; remove unused import Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/config.rs | 4 ++++ src/system.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 974d60b..04eb605 100644 --- a/src/config.rs +++ b/src/config.rs @@ -64,6 +64,10 @@ impl Config { Self::default() }) } + + pub fn is_auto_sync_paused(&self) -> bool { + self.timeturner_offset.is_active() + } } impl Default for Config { diff --git a/src/system.rs b/src/system.rs index 77a1aa0..8db481d 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::sync_logic::LtcFrame; -use chrono::{DateTime, Duration as ChronoDuration, Local, NaiveTime, TimeZone}; +use chrono::{DateTime, Duration as ChronoDuration, Local, TimeZone}; use num_rational::Ratio; use std::process::Command; From 4cdead5aa45691e2730ed0a8424f9d98676e1dd8 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 19:43:49 +0100 Subject: [PATCH 032/139] fix: Do not pause auto-sync with active timeturner Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/config.rs | 3 --- src/main.rs | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/config.rs b/src/config.rs index 04eb605..8669e62 100644 --- a/src/config.rs +++ b/src/config.rs @@ -65,9 +65,6 @@ impl Config { }) } - pub fn is_auto_sync_paused(&self) -> bool { - self.timeturner_offset.is_active() - } } impl Default for Config { diff --git a/src/main.rs b/src/main.rs index 0486f1c..ab9fa94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -248,9 +248,7 @@ async fn main() { let state = sync_state.lock().unwrap(); let config = sync_config.lock().unwrap(); - if config.is_auto_sync_paused() { - log::info!("Auto-sync is temporarily paused."); - } else if config.auto_sync_enabled && state.latest.is_some() { + if config.auto_sync_enabled && state.latest.is_some() { let delta = state.get_ewma_clock_delta(); let frame = state.latest.as_ref().unwrap(); From 5f35139f3b3a74ad73cff5770f20acb3faa12fb0 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright <85807217+cjfranko@users.noreply.github.com> Date: Thu, 7 Aug 2025 19:58:45 +0100 Subject: [PATCH 033/139] Update README.md added known issues section --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 115e262..8a7b357 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,15 @@ Created by Chris Frankland-Wright and John Rogers --- -## 🚀 Installation (to update) +## 🛠️ Known Issues +- Supported Frame Rates: 24/25fps +- Non Supported Frame Rates: 23.98/30/59.94/60 +- Fractional framerates have drift or wrong wall clock sync issues + +--- + +## 🚀 Installation (to update) For Rust install you can do ```bash From 32712d1f3ca46d4f56021a4092b7e7ace4ff0607 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 21:59:32 +0100 Subject: [PATCH 034/139] docs: Update API docs with new endpoints and response details Co-authored-by: aider (gemini/gemini-2.5-pro) --- docs/api.md | 106 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 14 deletions(-) diff --git a/docs/api.md b/docs/api.md index 83471bb..6657028 100644 --- a/docs/api.md +++ b/docs/api.md @@ -4,12 +4,17 @@ This document describes the HTTP API for the NTP Timeturner application. ## Endpoints -### Status +### Status and Logs - **`GET /api/status`** Retrieves the real-time status of the LTC reader and system clock synchronization. The `ltc_timecode` field uses `:` as a separator for non-drop-frame timecode, and `;` for drop-frame timecode between seconds and frames (e.g., `10:20:30;00`). + **Possible values for status fields:** + - `ltc_status`: `"LOCK"`, `"FREE"`, or `"(waiting)"` + - `sync_status`: `"IN SYNC"`, `"CLOCK AHEAD"`, `"CLOCK BEHIND"`, `"TIMETURNING"` + - `jitter_status`: `"GOOD"`, `"AVERAGE"`, `"BAD"` + **Example Response:** ```json { @@ -25,11 +30,23 @@ This document describes the HTTP API for the NTP Timeturner application. "lock_ratio": 99.5, "ntp_active": true, "interfaces": ["192.168.1.100"], - "hardware_offset_ms": 0 + "hardware_offset_ms": 20 } ``` -### Sync +- **`GET /api/logs`** + + Retrieves the last 100 log entries from the application. + + **Example Response:** + ```json + [ + "2025-08-07 10:00:00 [INFO] Starting TimeTurner daemon...", + "2025-08-07 10:00:01 [INFO] Found serial port: /dev/ttyACM0" + ] + ``` + +### System Clock Control - **`POST /api/sync`** @@ -37,7 +54,7 @@ This document describes the HTTP API for the NTP Timeturner application. **Request Body:** None - **Success Response:** + **Success Response (200 OK):** ```json { "status": "success", @@ -45,13 +62,14 @@ This document describes the HTTP API for the NTP Timeturner application. } ``` - **Error Responses:** + **Error Response (400 Bad Request):** ```json { "status": "error", "message": "No LTC timecode available to sync to." } ``` + **Error Response (500 Internal Server Error):** ```json { "status": "error", @@ -59,6 +77,32 @@ This document describes the HTTP API for the NTP Timeturner application. } ``` +- **`POST /api/nudge_clock`** + + Nudges the system clock by a specified number of microseconds. This requires `sudo` privileges to run `adjtimex`. + + **Example Request:** + ```json + { + "microseconds": -2000 + } + ``` + **Success Response (200 OK):** + ```json + { + "status": "success", + "message": "Clock nudge command issued." + } + ``` + **Error Response (500 Internal Server Error):** + ```json + { + "status": "error", + "message": "Clock nudge command failed." + } + ``` + + - **`POST /api/set_date`** Sets the system date. This is useful as LTC does not contain date information. Requires `sudo` privileges. @@ -70,7 +114,7 @@ This document describes the HTTP API for the NTP Timeturner application. } ``` - **Success Response:** + **Success Response (200 OK):** ```json { "status": "success", @@ -78,7 +122,7 @@ This document describes the HTTP API for the NTP Timeturner application. } ``` - **Error Response:** + **Error Response (500 Internal Server Error):** ```json { "status": "error", @@ -90,29 +134,63 @@ This document describes the HTTP API for the NTP Timeturner application. - **`GET /api/config`** - Retrieves the current application configuration. + Retrieves the current application configuration from `config.yml`. - **Example Response:** + **Example Response (200 OK):** ```json { - "hardware_offset_ms": 0 + "hardwareOffsetMs": 20, + "timeturnerOffset": { + "hours": 0, + "minutes": 0, + "seconds": 0, + "frames": 0, + "milliseconds": 0 + }, + "defaultNudgeMs": 2, + "autoSyncEnabled": false } ``` - **`POST /api/config`** - Updates the `hardware_offset_ms` configuration. The new value is persisted to `config.json` and reloaded by the application automatically. + Updates the application configuration. The new configuration is persisted to `config.yml` and takes effect immediately. **Example Request:** ```json { - "hardware_offset_ms": 10 + "hardwareOffsetMs": 55, + "timeturnerOffset": { + "hours": 1, + "minutes": 2, + "seconds": 3, + "frames": 4, + "milliseconds": 5 + }, + "defaultNudgeMs": 2, + "autoSyncEnabled": true } ``` - **Success Response:** + **Success Response (200 OK):** (Returns the updated configuration) ```json { - "hardware_offset_ms": 10 + "hardwareOffsetMs": 55, + "timeturnerOffset": { + "hours": 1, + "minutes": 2, + "seconds": 3, + "frames": 4, + "milliseconds": 5 + }, + "defaultNudgeMs": 2, + "autoSyncEnabled": true + } + ``` + **Error Response (500 Internal Server Error):** + ```json + { + "status": "error", + "message": "Failed to write config.yml" } ``` From dad59ed9ff4fb17962dda92fe423b7c632ba4b32 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:05:03 +0100 Subject: [PATCH 035/139] feat: Add Font Awesome icons for status indicators Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 1 + static/script.js | 63 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/static/index.html b/static/index.html index 26bf760..5b78de8 100644 --- a/static/index.html +++ b/static/index.html @@ -5,6 +5,7 @@ NTP TimeTurner +
diff --git a/static/script.js b/static/script.js index 6fd3475..2280008 100644 --- a/static/script.js +++ b/static/script.js @@ -41,24 +41,73 @@ document.addEventListener('DOMContentLoaded', () => { const dateMessage = document.getElementById('date-message'); function updateStatus(data) { - statusElements.ltcStatus.textContent = data.ltc_status; + let ltcStatusIcon = ''; + switch (data.ltc_status) { + case 'LOCK': + ltcStatusIcon = ''; + break; + case 'FREE': + ltcStatusIcon = ''; + break; + default: + ltcStatusIcon = ''; + } + statusElements.ltcStatus.innerHTML = `${ltcStatusIcon} ${data.ltc_status}`; statusElements.ltcTimecode.textContent = data.ltc_timecode; statusElements.frameRate.textContent = data.frame_rate; statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2); 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'; + if (data.ntp_active) { + statusElements.ntpActive.innerHTML = ' Active'; + statusElements.ntpActive.className = 'active'; + } else { + statusElements.ntpActive.innerHTML = ' Inactive'; + statusElements.ntpActive.className = 'inactive'; + } - statusElements.syncStatus.textContent = data.sync_status; - statusElements.syncStatus.className = data.sync_status.replace(/\s+/g, '-').toLowerCase(); + const syncStatusClass = data.sync_status.replace(/\s+/g, '-').toLowerCase(); + let syncIcon = ''; + switch (data.sync_status) { + case 'IN SYNC': + syncIcon = ''; + break; + case 'CLOCK AHEAD': + syncIcon = ''; + break; + case 'CLOCK BEHIND': + syncIcon = ''; + break; + case 'TIMETURNING': + syncIcon = ''; + break; + default: + syncIcon = ''; + } + statusElements.syncStatus.innerHTML = `${syncIcon} ${data.sync_status}`; + statusElements.syncStatus.className = syncStatusClass; 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(); + const jitterStatusClass = data.jitter_status.toLowerCase(); + let jitterIcon = ''; + switch (data.jitter_status) { + case 'GOOD': + jitterIcon = ''; + break; + case 'AVERAGE': + jitterIcon = ''; + break; + case 'BAD': + jitterIcon = ''; + break; + default: + jitterIcon = ''; + } + statusElements.jitterStatus.innerHTML = `${jitterIcon} ${data.jitter_status}`; + statusElements.jitterStatus.className = jitterStatusClass; statusElements.interfaces.innerHTML = ''; if (data.interfaces.length > 0) { From 80953e7f6d480401c3c68515f3d588ac12667a29 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:08:57 +0100 Subject: [PATCH 036/139] style: Style local status icons for vertical alignment Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/style.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/static/style.css b/static/style.css index 7bd9c20..042e097 100644 --- a/static/style.css +++ b/static/style.css @@ -82,6 +82,12 @@ button:hover { color: #555; } +.status-icon { + height: 1em; + vertical-align: middle; + margin-right: 0.5em; +} + /* 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; } From 8b7e83222533aa3965dfcd36afa6d9dc470f1e7f Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:11:22 +0100 Subject: [PATCH 037/139] feat: Decouple status icons; use local images via `icon-map.js` Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/icon-map.js | 25 +++++++++++++++++++++ static/index.html | 2 +- static/script.js | 56 ++++++++-------------------------------------- 3 files changed, 35 insertions(+), 48 deletions(-) create mode 100644 static/icon-map.js diff --git a/static/icon-map.js b/static/icon-map.js new file mode 100644 index 0000000..325fae1 --- /dev/null +++ b/static/icon-map.js @@ -0,0 +1,25 @@ +// In this file, you can define the paths to your local icon image files. +const iconMap = { + ltcStatus: { + 'LOCK': 'assets/lock.svg', + 'FREE': 'assets/lock-open.svg', + 'default': 'assets/question-circle.svg' + }, + ntpActive: { + true: 'assets/check-circle.svg', + false: 'assets/times-circle.svg' + }, + syncStatus: { + 'IN SYNC': 'assets/arrows-rotate.svg', + 'CLOCK AHEAD': 'assets/forward.svg', + 'CLOCK BEHIND': 'assets/backward.svg', + 'TIMETURNING': 'assets/wand-magic-sparkles.svg', + 'default': 'assets/question-circle.svg' + }, + jitterStatus: { + 'GOOD': 'assets/thumbs-up.svg', + 'AVERAGE': 'assets/face-meh.svg', + 'BAD': 'assets/thumbs-down.svg', + 'default': 'assets/question-circle.svg' + } +}; diff --git a/static/index.html b/static/index.html index 5b78de8..e67216a 100644 --- a/static/index.html +++ b/static/index.html @@ -5,7 +5,6 @@ NTP TimeTurner -
@@ -107,6 +106,7 @@
+ diff --git a/static/script.js b/static/script.js index 2280008..901cc5e 100644 --- a/static/script.js +++ b/static/script.js @@ -41,72 +41,34 @@ document.addEventListener('DOMContentLoaded', () => { const dateMessage = document.getElementById('date-message'); function updateStatus(data) { - let ltcStatusIcon = ''; - switch (data.ltc_status) { - case 'LOCK': - ltcStatusIcon = ''; - break; - case 'FREE': - ltcStatusIcon = ''; - break; - default: - ltcStatusIcon = ''; - } - statusElements.ltcStatus.innerHTML = `${ltcStatusIcon} ${data.ltc_status}`; + const ltcIconSrc = iconMap.ltcStatus[data.ltc_status] || iconMap.ltcStatus.default; + statusElements.ltcStatus.innerHTML = `${data.ltc_status} icon ${data.ltc_status}`; statusElements.ltcTimecode.textContent = data.ltc_timecode; statusElements.frameRate.textContent = data.frame_rate; statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2); statusElements.systemClock.textContent = data.system_clock; statusElements.systemDate.textContent = data.system_date; + const ntpIconSrc = iconMap.ntpActive[data.ntp_active]; if (data.ntp_active) { - statusElements.ntpActive.innerHTML = ' Active'; + statusElements.ntpActive.innerHTML = `Active icon Active`; statusElements.ntpActive.className = 'active'; } else { - statusElements.ntpActive.innerHTML = ' Inactive'; + statusElements.ntpActive.innerHTML = `Inactive icon Inactive`; statusElements.ntpActive.className = 'inactive'; } const syncStatusClass = data.sync_status.replace(/\s+/g, '-').toLowerCase(); - let syncIcon = ''; - switch (data.sync_status) { - case 'IN SYNC': - syncIcon = ''; - break; - case 'CLOCK AHEAD': - syncIcon = ''; - break; - case 'CLOCK BEHIND': - syncIcon = ''; - break; - case 'TIMETURNING': - syncIcon = ''; - break; - default: - syncIcon = ''; - } - statusElements.syncStatus.innerHTML = `${syncIcon} ${data.sync_status}`; + const syncIconSrc = iconMap.syncStatus[data.sync_status] || iconMap.syncStatus.default; + statusElements.syncStatus.innerHTML = `${data.sync_status} icon ${data.sync_status}`; statusElements.syncStatus.className = syncStatusClass; statusElements.deltaMs.textContent = data.timecode_delta_ms; statusElements.deltaFrames.textContent = data.timecode_delta_frames; const jitterStatusClass = data.jitter_status.toLowerCase(); - let jitterIcon = ''; - switch (data.jitter_status) { - case 'GOOD': - jitterIcon = ''; - break; - case 'AVERAGE': - jitterIcon = ''; - break; - case 'BAD': - jitterIcon = ''; - break; - default: - jitterIcon = ''; - } - statusElements.jitterStatus.innerHTML = `${jitterIcon} ${data.jitter_status}`; + const jitterIconSrc = iconMap.jitterStatus[data.jitter_status] || iconMap.jitterStatus.default; + statusElements.jitterStatus.innerHTML = `${data.jitter_status} icon ${data.jitter_status}`; statusElements.jitterStatus.className = jitterStatusClass; statusElements.interfaces.innerHTML = ''; From 8150241db2784c8f95256f7c92aeec22d648b934 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:18:00 +0100 Subject: [PATCH 038/139] refactor: Standardise status element styling and icon alt attributes Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/script.js | 11 ++++++----- static/style.css | 3 +++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/static/script.js b/static/script.js index 901cc5e..e7d1b83 100644 --- a/static/script.js +++ b/static/script.js @@ -42,7 +42,8 @@ document.addEventListener('DOMContentLoaded', () => { function updateStatus(data) { const ltcIconSrc = iconMap.ltcStatus[data.ltc_status] || iconMap.ltcStatus.default; - statusElements.ltcStatus.innerHTML = `${data.ltc_status} icon ${data.ltc_status}`; + statusElements.ltcStatus.innerHTML = ` ${data.ltc_status}`; + statusElements.ltcStatus.className = data.ltc_status.toLowerCase(); statusElements.ltcTimecode.textContent = data.ltc_timecode; statusElements.frameRate.textContent = data.frame_rate; statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2); @@ -51,16 +52,16 @@ document.addEventListener('DOMContentLoaded', () => { const ntpIconSrc = iconMap.ntpActive[data.ntp_active]; if (data.ntp_active) { - statusElements.ntpActive.innerHTML = `Active icon Active`; + statusElements.ntpActive.innerHTML = ` Active`; statusElements.ntpActive.className = 'active'; } else { - statusElements.ntpActive.innerHTML = `Inactive icon Inactive`; + statusElements.ntpActive.innerHTML = ` Inactive`; statusElements.ntpActive.className = 'inactive'; } const syncStatusClass = data.sync_status.replace(/\s+/g, '-').toLowerCase(); const syncIconSrc = iconMap.syncStatus[data.sync_status] || iconMap.syncStatus.default; - statusElements.syncStatus.innerHTML = `${data.sync_status} icon ${data.sync_status}`; + statusElements.syncStatus.innerHTML = ` ${data.sync_status}`; statusElements.syncStatus.className = syncStatusClass; statusElements.deltaMs.textContent = data.timecode_delta_ms; @@ -68,7 +69,7 @@ document.addEventListener('DOMContentLoaded', () => { const jitterStatusClass = data.jitter_status.toLowerCase(); const jitterIconSrc = iconMap.jitterStatus[data.jitter_status] || iconMap.jitterStatus.default; - statusElements.jitterStatus.innerHTML = `${data.jitter_status} icon ${data.jitter_status}`; + statusElements.jitterStatus.innerHTML = ` ${data.jitter_status}`; statusElements.jitterStatus.className = jitterStatusClass; statusElements.interfaces.innerHTML = ''; diff --git a/static/style.css b/static/style.css index 042e097..2a4f54b 100644 --- a/static/style.css +++ b/static/style.css @@ -95,3 +95,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; } From cd922d54036b6521c584a8609ac3484844e55ac6 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:22:04 +0100 Subject: [PATCH 039/139] style: Replace generic icons with TimeTurner themed images --- static/icon-map.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index 325fae1..ac37b21 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -1,25 +1,25 @@ // In this file, you can define the paths to your local icon image files. const iconMap = { ltcStatus: { - 'LOCK': 'assets/lock.svg', - 'FREE': 'assets/lock-open.svg', - 'default': 'assets/question-circle.svg' + 'LOCK': 'assets/timeturner_ltc_green.png', + 'FREE': 'assets/timeturner_ltc_orange.png', + 'default': 'assets/timeturner_ltc_red.png' }, ntpActive: { - true: 'assets/check-circle.svg', - false: 'assets/times-circle.svg' + true: 'assets/timeturner_ntp_green.png', + false: 'assets/timeturner_ntp_red.png' }, syncStatus: { - 'IN SYNC': 'assets/arrows-rotate.svg', - 'CLOCK AHEAD': 'assets/forward.svg', - 'CLOCK BEHIND': 'assets/backward.svg', - 'TIMETURNING': 'assets/wand-magic-sparkles.svg', - 'default': 'assets/question-circle.svg' + 'IN SYNC': 'assets/timeturner_sync_green.png', + 'CLOCK AHEAD': 'assets/timeturner_sync_orange.png', + 'CLOCK BEHIND': 'assets/timeturner_sync_orange.png', + 'TIMETURNING': 'assets/timeturner_timeturning.png', + 'default': 'assets/timeturner_sync_red.png' }, jitterStatus: { - 'GOOD': 'assets/thumbs-up.svg', - 'AVERAGE': 'assets/face-meh.svg', - 'BAD': 'assets/thumbs-down.svg', - 'default': 'assets/question-circle.svg' + 'GOOD': 'assets/timeturner_jitter_green.png', + 'AVERAGE': 'assets/timeturner_jitter_orange.png', + 'BAD': 'assets/timeturner_jitter_red.png', + 'default': 'assets/timeturner_jitter_red.png' } }; From 08d664efd1643ac21c401cb8e7b5c5432f7b7d03 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:22:08 +0100 Subject: [PATCH 040/139] fix: Correct icon asset paths to 'timetuner' spelling Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/icon-map.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index ac37b21..1c544fd 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -1,25 +1,25 @@ // In this file, you can define the paths to your local icon image files. const iconMap = { ltcStatus: { - 'LOCK': 'assets/timeturner_ltc_green.png', - 'FREE': 'assets/timeturner_ltc_orange.png', - 'default': 'assets/timeturner_ltc_red.png' + 'LOCK': 'assets/timetuner_ltc_green.png', + 'FREE': 'assets/timetuner_ltc_orange.png', + 'default': 'assets/timetuner_ltc_red.png' }, ntpActive: { - true: 'assets/timeturner_ntp_green.png', - false: 'assets/timeturner_ntp_red.png' + true: 'assets/timetuner_ntp_green.png', + false: 'assets/timetuner_ntp_red.png' }, syncStatus: { - 'IN SYNC': 'assets/timeturner_sync_green.png', - 'CLOCK AHEAD': 'assets/timeturner_sync_orange.png', - 'CLOCK BEHIND': 'assets/timeturner_sync_orange.png', - 'TIMETURNING': 'assets/timeturner_timeturning.png', - 'default': 'assets/timeturner_sync_red.png' + 'IN SYNC': 'assets/timetuner_sync_green.png', + 'CLOCK AHEAD': 'assets/timetuner_sync_orange.png', + 'CLOCK BEHIND': 'assets/timetuner_sync_orange.png', + 'TIMETURNING': 'assets/timetuner_timeturning.png', + 'default': 'assets/timetuner_sync_red.png' }, jitterStatus: { - 'GOOD': 'assets/timeturner_jitter_green.png', - 'AVERAGE': 'assets/timeturner_jitter_orange.png', - 'BAD': 'assets/timeturner_jitter_red.png', - 'default': 'assets/timeturner_jitter_red.png' + 'GOOD': 'assets/timetuner_jitter_green.png', + 'AVERAGE': 'assets/timetuner_jitter_orange.png', + 'BAD': 'assets/timetuner_jitter_red.png', + 'default': 'assets/timetuner_jitter_red.png' } }; From abce5373d7b5cde3ff61b0288739ef7404f13b93 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:34:10 +0100 Subject: [PATCH 041/139] fix: Correct icon image paths from timetuner to timeturner --- static/icon-map.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index 1c544fd..ac37b21 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -1,25 +1,25 @@ // In this file, you can define the paths to your local icon image files. const iconMap = { ltcStatus: { - 'LOCK': 'assets/timetuner_ltc_green.png', - 'FREE': 'assets/timetuner_ltc_orange.png', - 'default': 'assets/timetuner_ltc_red.png' + 'LOCK': 'assets/timeturner_ltc_green.png', + 'FREE': 'assets/timeturner_ltc_orange.png', + 'default': 'assets/timeturner_ltc_red.png' }, ntpActive: { - true: 'assets/timetuner_ntp_green.png', - false: 'assets/timetuner_ntp_red.png' + true: 'assets/timeturner_ntp_green.png', + false: 'assets/timeturner_ntp_red.png' }, syncStatus: { - 'IN SYNC': 'assets/timetuner_sync_green.png', - 'CLOCK AHEAD': 'assets/timetuner_sync_orange.png', - 'CLOCK BEHIND': 'assets/timetuner_sync_orange.png', - 'TIMETURNING': 'assets/timetuner_timeturning.png', - 'default': 'assets/timetuner_sync_red.png' + 'IN SYNC': 'assets/timeturner_sync_green.png', + 'CLOCK AHEAD': 'assets/timeturner_sync_orange.png', + 'CLOCK BEHIND': 'assets/timeturner_sync_orange.png', + 'TIMETURNING': 'assets/timeturner_timeturning.png', + 'default': 'assets/timeturner_sync_red.png' }, jitterStatus: { - 'GOOD': 'assets/timetuner_jitter_green.png', - 'AVERAGE': 'assets/timetuner_jitter_orange.png', - 'BAD': 'assets/timetuner_jitter_red.png', - 'default': 'assets/timetuner_jitter_red.png' + 'GOOD': 'assets/timeturner_jitter_green.png', + 'AVERAGE': 'assets/timeturner_jitter_orange.png', + 'BAD': 'assets/timeturner_jitter_red.png', + 'default': 'assets/timeturner_jitter_red.png' } }; From 90f43ff87e378ff340a3ca5c07b3af4aec99db0f Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:34:16 +0100 Subject: [PATCH 042/139] fix: Correct icon map asset spellings Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/icon-map.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index ac37b21..1c544fd 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -1,25 +1,25 @@ // In this file, you can define the paths to your local icon image files. const iconMap = { ltcStatus: { - 'LOCK': 'assets/timeturner_ltc_green.png', - 'FREE': 'assets/timeturner_ltc_orange.png', - 'default': 'assets/timeturner_ltc_red.png' + 'LOCK': 'assets/timetuner_ltc_green.png', + 'FREE': 'assets/timetuner_ltc_orange.png', + 'default': 'assets/timetuner_ltc_red.png' }, ntpActive: { - true: 'assets/timeturner_ntp_green.png', - false: 'assets/timeturner_ntp_red.png' + true: 'assets/timetuner_ntp_green.png', + false: 'assets/timetuner_ntp_red.png' }, syncStatus: { - 'IN SYNC': 'assets/timeturner_sync_green.png', - 'CLOCK AHEAD': 'assets/timeturner_sync_orange.png', - 'CLOCK BEHIND': 'assets/timeturner_sync_orange.png', - 'TIMETURNING': 'assets/timeturner_timeturning.png', - 'default': 'assets/timeturner_sync_red.png' + 'IN SYNC': 'assets/timetuner_sync_green.png', + 'CLOCK AHEAD': 'assets/timetuner_sync_orange.png', + 'CLOCK BEHIND': 'assets/timetuner_sync_orange.png', + 'TIMETURNING': 'assets/timetuner_timeturning.png', + 'default': 'assets/timetuner_sync_red.png' }, jitterStatus: { - 'GOOD': 'assets/timeturner_jitter_green.png', - 'AVERAGE': 'assets/timeturner_jitter_orange.png', - 'BAD': 'assets/timeturner_jitter_red.png', - 'default': 'assets/timeturner_jitter_red.png' + 'GOOD': 'assets/timetuner_jitter_green.png', + 'AVERAGE': 'assets/timetuner_jitter_orange.png', + 'BAD': 'assets/timetuner_jitter_red.png', + 'default': 'assets/timetuner_jitter_red.png' } }; From f0ac2ed3d4eeec75d37e321f4c995881f7aedf8c Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:44:52 +0100 Subject: [PATCH 043/139] fix: Safely handle null status for default icon display Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/script.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/static/script.js b/static/script.js index e7d1b83..b82af40 100644 --- a/static/script.js +++ b/static/script.js @@ -41,9 +41,10 @@ document.addEventListener('DOMContentLoaded', () => { const dateMessage = document.getElementById('date-message'); function updateStatus(data) { - const ltcIconSrc = iconMap.ltcStatus[data.ltc_status] || iconMap.ltcStatus.default; - statusElements.ltcStatus.innerHTML = ` ${data.ltc_status}`; - statusElements.ltcStatus.className = data.ltc_status.toLowerCase(); + const ltcStatus = data.ltc_status || 'UNKNOWN'; + const ltcIconSrc = iconMap.ltcStatus[ltcStatus] || iconMap.ltcStatus.default; + statusElements.ltcStatus.innerHTML = ` ${ltcStatus}`; + 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); @@ -59,18 +60,18 @@ document.addEventListener('DOMContentLoaded', () => { statusElements.ntpActive.className = 'inactive'; } - const syncStatusClass = data.sync_status.replace(/\s+/g, '-').toLowerCase(); - const syncIconSrc = iconMap.syncStatus[data.sync_status] || iconMap.syncStatus.default; - statusElements.syncStatus.innerHTML = ` ${data.sync_status}`; - statusElements.syncStatus.className = syncStatusClass; + const syncStatus = data.sync_status || 'UNKNOWN'; + const syncIconSrc = iconMap.syncStatus[syncStatus] || iconMap.syncStatus.default; + statusElements.syncStatus.innerHTML = ` ${syncStatus}`; + statusElements.syncStatus.className = syncStatus.replace(/\s+/g, '-').toLowerCase(); statusElements.deltaMs.textContent = data.timecode_delta_ms; statusElements.deltaFrames.textContent = data.timecode_delta_frames; - const jitterStatusClass = data.jitter_status.toLowerCase(); - const jitterIconSrc = iconMap.jitterStatus[data.jitter_status] || iconMap.jitterStatus.default; - statusElements.jitterStatus.innerHTML = ` ${data.jitter_status}`; - statusElements.jitterStatus.className = jitterStatusClass; + const jitterStatus = data.jitter_status || 'UNKNOWN'; + const jitterIconSrc = iconMap.jitterStatus[jitterStatus] || iconMap.jitterStatus.default; + statusElements.jitterStatus.innerHTML = ` ${jitterStatus}`; + statusElements.jitterStatus.className = jitterStatus.toLowerCase(); statusElements.interfaces.innerHTML = ''; if (data.interfaces.length > 0) { From 8636ed4ec4b183fd3aa3bd4981c43a98e26f426a Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:47:46 +0100 Subject: [PATCH 044/139] chore: Decouple UI from API by adding mock data Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/script.js | 173 +++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 97 deletions(-) diff --git a/static/script.js b/static/script.js index b82af40..a77212a 100644 --- a/static/script.js +++ b/static/script.js @@ -1,4 +1,39 @@ document.addEventListener('DOMContentLoaded', () => { + const mockApiData = { + status: { + ltc_status: 'LOCK', + ltc_timecode: '10:20:30:00', + frame_rate: '25.00', + 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 (eth0)', '10.0.0.5 (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:31 [INFO] Found serial device on /dev/ttyACM0.', + '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.', + ] + }; + let lastApiData = null; let lastApiFetchTime = null; @@ -147,36 +182,24 @@ document.addEventListener('DOMContentLoaded', () => { } async function fetchStatus() { - try { - const response = await fetch('/api/status'); - if (!response.ok) throw new Error('Failed to fetch status'); - const data = await response.json(); - updateStatus(data); - lastApiData = data; - lastApiFetchTime = new Date(); - } catch (error) { - console.error('Error fetching status:', error); - lastApiData = null; - lastApiFetchTime = null; - } + // Mock implementation to allow UI development without a running backend. + const data = mockApiData.status; + updateStatus(data); + lastApiData = data; + lastApiFetchTime = new Date(); } async function fetchConfig() { - try { - const response = await fetch('/api/config'); - if (!response.ok) throw new Error('Failed to fetch config'); - const data = await response.json(); - 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; - } catch (error) { - console.error('Error fetching config:', error); - } + // Mock implementation + const data = mockApiData.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; } async function saveConfig() { @@ -193,69 +216,34 @@ document.addEventListener('DOMContentLoaded', () => { } }; - try { - const response = await fetch('/api/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(config), - }); - if (!response.ok) throw new Error('Failed to save config'); - alert('Configuration saved.'); - } catch (error) { - console.error('Error saving config:', error); - alert('Error saving configuration.'); - } + // Mock implementation + console.log('Saving mock config:', config); + alert('Configuration saved (mock).'); } async function fetchLogs() { - 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; - } catch (error) { - console.error('Error fetching logs:', error); - statusElements.logs.textContent = 'Error fetching logs.'; - } + // Mock implementation + const logs = mockApiData.logs; + statusElements.logs.textContent = logs.join('\n'); + // Auto-scroll to the bottom + statusElements.logs.scrollTop = statusElements.logs.scrollHeight; } async function triggerManualSync() { syncMessage.textContent = 'Issuing sync command...'; - try { - const response = await fetch('/api/sync', { method: 'POST' }); - const data = await response.json(); - if (response.ok) { - syncMessage.textContent = `Success: ${data.message}`; - } else { - syncMessage.textContent = `Error: ${data.message}`; - } - } catch (error) { - console.error('Error triggering sync:', error); - syncMessage.textContent = 'Failed to send sync command.'; - } + // Mock implementation + setTimeout(() => { + syncMessage.textContent = 'Success: Manual sync triggered (mock).'; + }, 1000); setTimeout(() => { syncMessage.textContent = ''; }, 5000); } async function nudgeClock(ms) { nudgeMessage.textContent = 'Nudging clock...'; - try { - const response = await fetch('/api/nudge_clock', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ microseconds: ms * 1000 }), - }); - const data = await response.json(); - if (response.ok) { - nudgeMessage.textContent = `Success: ${data.message}`; - } else { - nudgeMessage.textContent = `Error: ${data.message}`; - } - } catch (error) { - console.error('Error nudging clock:', error); - nudgeMessage.textContent = 'Failed to send nudge command.'; - } + // Mock implementation + setTimeout(() => { + nudgeMessage.textContent = `Success: Clock nudged by ${ms}ms (mock).`; + }, 500); setTimeout(() => { nudgeMessage.textContent = ''; }, 3000); } @@ -267,24 +255,15 @@ document.addEventListener('DOMContentLoaded', () => { } dateMessage.textContent = 'Setting date...'; - try { - const response = await fetch('/api/set_date', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ date: date }), - }); - const data = await response.json(); - if (response.ok) { - dateMessage.textContent = `Success: ${data.message}`; - // Fetch status again to update the displayed date immediately + // Mock implementation + setTimeout(() => { + dateMessage.textContent = `Success: Date set to ${date} (mock).`; + // To make it look real, we can update the system date display + if (lastApiData) { + mockApiData.status.system_date = date; fetchStatus(); - } else { - dateMessage.textContent = `Error: ${data.message}`; } - } catch (error) { - console.error('Error setting date:', error); - dateMessage.textContent = 'Failed to send date command.'; - } + }, 1000); setTimeout(() => { dateMessage.textContent = ''; }, 5000); } @@ -305,8 +284,8 @@ document.addEventListener('DOMContentLoaded', () => { fetchConfig(); fetchLogs(); - // Refresh data every 2 seconds - setInterval(fetchStatus, 2000); - setInterval(fetchLogs, 2000); + // Refresh data every 2 seconds - MOCKED + // setInterval(fetchStatus, 2000); + // setInterval(fetchLogs, 2000); setInterval(animateClocks, 50); // High-frequency clock animation }); From 0ba46fbd71d3bb2f6fc5ededc417ef2f68d06528 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:49:55 +0100 Subject: [PATCH 045/139] fix: Restore live API calls by removing mock data Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/script.js | 172 ++++++++++++++++++++++++++--------------------- 1 file changed, 97 insertions(+), 75 deletions(-) diff --git a/static/script.js b/static/script.js index a77212a..d11cf20 100644 --- a/static/script.js +++ b/static/script.js @@ -1,38 +1,4 @@ document.addEventListener('DOMContentLoaded', () => { - const mockApiData = { - status: { - ltc_status: 'LOCK', - ltc_timecode: '10:20:30:00', - frame_rate: '25.00', - 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 (eth0)', '10.0.0.5 (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:31 [INFO] Found serial device on /dev/ttyACM0.', - '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.', - ] - }; let lastApiData = null; let lastApiFetchTime = null; @@ -182,24 +148,36 @@ document.addEventListener('DOMContentLoaded', () => { } async function fetchStatus() { - // Mock implementation to allow UI development without a running backend. - const data = mockApiData.status; - updateStatus(data); - lastApiData = data; - lastApiFetchTime = new Date(); + try { + const response = await fetch('/api/status'); + if (!response.ok) throw new Error('Failed to fetch status'); + const data = await response.json(); + updateStatus(data); + lastApiData = data; + lastApiFetchTime = new Date(); + } catch (error) { + console.error('Error fetching status:', error); + lastApiData = null; + lastApiFetchTime = null; + } } async function fetchConfig() { - // Mock implementation - const data = mockApiData.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; + try { + const response = await fetch('/api/config'); + if (!response.ok) throw new Error('Failed to fetch config'); + const data = await response.json(); + 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; + } catch (error) { + console.error('Error fetching config:', error); + } } async function saveConfig() { @@ -216,34 +194,69 @@ document.addEventListener('DOMContentLoaded', () => { } }; - // Mock implementation - console.log('Saving mock config:', config); - alert('Configuration saved (mock).'); + try { + const response = await fetch('/api/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config), + }); + if (!response.ok) throw new Error('Failed to save config'); + alert('Configuration saved.'); + } catch (error) { + console.error('Error saving config:', error); + alert('Error saving configuration.'); + } } async function fetchLogs() { - // Mock implementation - const logs = mockApiData.logs; - statusElements.logs.textContent = logs.join('\n'); - // Auto-scroll to the bottom - statusElements.logs.scrollTop = statusElements.logs.scrollHeight; + 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; + } catch (error) { + console.error('Error fetching logs:', error); + statusElements.logs.textContent = 'Error fetching logs.'; + } } async function triggerManualSync() { syncMessage.textContent = 'Issuing sync command...'; - // Mock implementation - setTimeout(() => { - syncMessage.textContent = 'Success: Manual sync triggered (mock).'; - }, 1000); + try { + const response = await fetch('/api/sync', { method: 'POST' }); + const data = await response.json(); + if (response.ok) { + syncMessage.textContent = `Success: ${data.message}`; + } else { + syncMessage.textContent = `Error: ${data.message}`; + } + } catch (error) { + console.error('Error triggering sync:', error); + syncMessage.textContent = 'Failed to send sync command.'; + } setTimeout(() => { syncMessage.textContent = ''; }, 5000); } async function nudgeClock(ms) { nudgeMessage.textContent = 'Nudging clock...'; - // Mock implementation - setTimeout(() => { - nudgeMessage.textContent = `Success: Clock nudged by ${ms}ms (mock).`; - }, 500); + try { + const response = await fetch('/api/nudge_clock', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ microseconds: ms * 1000 }), + }); + const data = await response.json(); + if (response.ok) { + nudgeMessage.textContent = `Success: ${data.message}`; + } else { + nudgeMessage.textContent = `Error: ${data.message}`; + } + } catch (error) { + console.error('Error nudging clock:', error); + nudgeMessage.textContent = 'Failed to send nudge command.'; + } setTimeout(() => { nudgeMessage.textContent = ''; }, 3000); } @@ -255,15 +268,24 @@ document.addEventListener('DOMContentLoaded', () => { } dateMessage.textContent = 'Setting date...'; - // Mock implementation - setTimeout(() => { - dateMessage.textContent = `Success: Date set to ${date} (mock).`; - // To make it look real, we can update the system date display - if (lastApiData) { - mockApiData.status.system_date = date; + try { + const response = await fetch('/api/set_date', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ date: date }), + }); + const data = await response.json(); + if (response.ok) { + dateMessage.textContent = `Success: ${data.message}`; + // Fetch status again to update the displayed date immediately fetchStatus(); + } else { + dateMessage.textContent = `Error: ${data.message}`; } - }, 1000); + } catch (error) { + console.error('Error setting date:', error); + dateMessage.textContent = 'Failed to send date command.'; + } setTimeout(() => { dateMessage.textContent = ''; }, 5000); } @@ -284,8 +306,8 @@ document.addEventListener('DOMContentLoaded', () => { fetchConfig(); fetchLogs(); - // Refresh data every 2 seconds - MOCKED - // setInterval(fetchStatus, 2000); - // setInterval(fetchLogs, 2000); + // Refresh data every 2 seconds + setInterval(fetchStatus, 2000); + setInterval(fetchLogs, 2000); setInterval(animateClocks, 50); // High-frequency clock animation }); From c97d1841b5f45128a4b1f311383c333097ad11f7 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:53:14 +0100 Subject: [PATCH 046/139] feat: Add mock data toggle and scenarios for UI testing Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 11 +++ static/mock-data.js | 168 ++++++++++++++++++++++++++++++++++++++++++++ static/script.js | 92 +++++++++++++++++++++++- 3 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 static/mock-data.js diff --git a/static/index.html b/static/index.html index e67216a..1b2880a 100644 --- a/static/index.html +++ b/static/index.html @@ -9,6 +9,16 @@

NTP TimeTurner

+ + + +
@@ -107,6 +117,7 @@
+ diff --git a/static/mock-data.js b/static/mock-data.js new file mode 100644 index 0000000..2765680 --- /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.00', + 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 (eth0)', '10.0.0.5 (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.00', + 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 (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.00', + 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 (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.00', + 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 (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.00', + 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 (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.00', + 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 (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 d11cf20..a9c5d5d 100644 --- a/static/script.js +++ b/static/script.js @@ -1,4 +1,8 @@ document.addEventListener('DOMContentLoaded', () => { + // --- Mock Data Configuration --- + // Set to true to use mock data, false for live API. + const useMockData = true; + let currentMockSetKey = 'allGood'; // Default mock data set let lastApiData = null; let lastApiFetchTime = null; @@ -41,6 +45,35 @@ document.addEventListener('DOMContentLoaded', () => { const setDateButton = document.getElementById('set-date'); const dateMessage = document.getElementById('date-message'); + // --- 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) { const ltcStatus = data.ltc_status || 'UNKNOWN'; const ltcIconSrc = iconMap.ltcStatus[ltcStatus] || iconMap.ltcStatus.default; @@ -148,6 +181,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'); @@ -163,6 +203,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'); @@ -194,6 +246,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', @@ -209,6 +269,12 @@ document.addEventListener('DOMContentLoaded', () => { } async function fetchLogs() { + if (useMockData) { + const logs = mockApiDataSets[currentMockSetKey].logs; + statusElements.logs.textContent = logs.join('\n'); + statusElements.logs.scrollTop = statusElements.logs.scrollHeight; + return; + } try { const response = await fetch('/api/logs'); if (!response.ok) throw new Error('Failed to fetch logs'); @@ -224,6 +290,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(); @@ -241,6 +312,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', @@ -268,6 +344,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', @@ -302,12 +385,15 @@ document.addEventListener('DOMContentLoaded', () => { setDateButton.addEventListener('click', setDate); // 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 }); From 8da42b87d0ea4f2f1468cadddeed63689e5284c7 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:58:14 +0100 Subject: [PATCH 047/139] style: Apply custom FuturaStdHeavy font Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/style.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/static/style.css b/static/style.css index 2a4f54b..a5ae50c 100644 --- a/static/style.css +++ b/static/style.css @@ -1,5 +1,10 @@ +@font-face { + font-family: 'FuturaStdHeavy'; + src: url('assets/FuturaStdHeavy.otf') format('opentype'); +} + body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-family: 'FuturaStdHeavy', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f4f4f9; color: #333; margin: 0; From 26dca4fd1842c8e735de161faa3f5e91cc8554ef Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Thu, 7 Aug 2025 23:59:17 +0100 Subject: [PATCH 048/139] style: Use Quartz font for LTC and system clock displays Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/style.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/static/style.css b/static/style.css index a5ae50c..802cb53 100644 --- a/static/style.css +++ b/static/style.css @@ -3,6 +3,11 @@ src: url('assets/FuturaStdHeavy.otf') format('opentype'); } +@font-face { + font-family: 'Quartz'; + src: url('assets/quartz-ms-regular.ttf') format('truetype'); +} + body { font-family: 'FuturaStdHeavy', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f4f4f9; @@ -41,6 +46,13 @@ h1 { color: #0056b3; } +#ltc-timecode, #system-clock { + font-family: 'Quartz', monospace; + font-size: 2em; + text-align: center; + letter-spacing: 2px; +} + .card p, .card ul { margin: 10px 0; } From fe9ac76942405b38fa2c78a9bc54954fd5c54d6d Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:02:43 +0100 Subject: [PATCH 049/139] style: Resize status icons to 60x60px and adjust layout Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/script.js | 10 +++++----- static/style.css | 11 ++++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/static/script.js b/static/script.js index a9c5d5d..7f82731 100644 --- a/static/script.js +++ b/static/script.js @@ -77,7 +77,7 @@ document.addEventListener('DOMContentLoaded', () => { function updateStatus(data) { const ltcStatus = data.ltc_status || 'UNKNOWN'; const ltcIconSrc = iconMap.ltcStatus[ltcStatus] || iconMap.ltcStatus.default; - statusElements.ltcStatus.innerHTML = ` ${ltcStatus}`; + statusElements.ltcStatus.innerHTML = `${ltcStatus}`; statusElements.ltcStatus.className = ltcStatus.toLowerCase(); statusElements.ltcTimecode.textContent = data.ltc_timecode; statusElements.frameRate.textContent = data.frame_rate; @@ -87,16 +87,16 @@ document.addEventListener('DOMContentLoaded', () => { const ntpIconSrc = iconMap.ntpActive[data.ntp_active]; if (data.ntp_active) { - statusElements.ntpActive.innerHTML = ` Active`; + statusElements.ntpActive.innerHTML = `Active`; statusElements.ntpActive.className = 'active'; } else { - statusElements.ntpActive.innerHTML = ` Inactive`; + statusElements.ntpActive.innerHTML = `Inactive`; statusElements.ntpActive.className = 'inactive'; } const syncStatus = data.sync_status || 'UNKNOWN'; const syncIconSrc = iconMap.syncStatus[syncStatus] || iconMap.syncStatus.default; - statusElements.syncStatus.innerHTML = ` ${syncStatus}`; + statusElements.syncStatus.innerHTML = `${syncStatus}`; statusElements.syncStatus.className = syncStatus.replace(/\s+/g, '-').toLowerCase(); statusElements.deltaMs.textContent = data.timecode_delta_ms; @@ -104,7 +104,7 @@ document.addEventListener('DOMContentLoaded', () => { const jitterStatus = data.jitter_status || 'UNKNOWN'; const jitterIconSrc = iconMap.jitterStatus[jitterStatus] || iconMap.jitterStatus.default; - statusElements.jitterStatus.innerHTML = ` ${jitterStatus}`; + statusElements.jitterStatus.innerHTML = `${jitterStatus}`; statusElements.jitterStatus.className = jitterStatus.toLowerCase(); statusElements.interfaces.innerHTML = ''; diff --git a/static/style.css b/static/style.css index 802cb53..638b554 100644 --- a/static/style.css +++ b/static/style.css @@ -99,10 +99,15 @@ button:hover { color: #555; } +#ltc-status, #ntp-active, #sync-status, #jitter-status { + display: inline-flex; + align-items: center; + gap: 0.5em; +} + .status-icon { - height: 1em; - vertical-align: middle; - margin-right: 0.5em; + width: 60px; + height: 60px; } /* Status-specific colors */ From 0baf7588daa42ad381a88fd3785d6ad41af79e9f Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:04:47 +0100 Subject: [PATCH 050/139] fix: Correct typo in icon image paths --- static/icon-map.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index 1c544fd..ac37b21 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -1,25 +1,25 @@ // In this file, you can define the paths to your local icon image files. const iconMap = { ltcStatus: { - 'LOCK': 'assets/timetuner_ltc_green.png', - 'FREE': 'assets/timetuner_ltc_orange.png', - 'default': 'assets/timetuner_ltc_red.png' + 'LOCK': 'assets/timeturner_ltc_green.png', + 'FREE': 'assets/timeturner_ltc_orange.png', + 'default': 'assets/timeturner_ltc_red.png' }, ntpActive: { - true: 'assets/timetuner_ntp_green.png', - false: 'assets/timetuner_ntp_red.png' + true: 'assets/timeturner_ntp_green.png', + false: 'assets/timeturner_ntp_red.png' }, syncStatus: { - 'IN SYNC': 'assets/timetuner_sync_green.png', - 'CLOCK AHEAD': 'assets/timetuner_sync_orange.png', - 'CLOCK BEHIND': 'assets/timetuner_sync_orange.png', - 'TIMETURNING': 'assets/timetuner_timeturning.png', - 'default': 'assets/timetuner_sync_red.png' + 'IN SYNC': 'assets/timeturner_sync_green.png', + 'CLOCK AHEAD': 'assets/timeturner_sync_orange.png', + 'CLOCK BEHIND': 'assets/timeturner_sync_orange.png', + 'TIMETURNING': 'assets/timeturner_timeturning.png', + 'default': 'assets/timeturner_sync_red.png' }, jitterStatus: { - 'GOOD': 'assets/timetuner_jitter_green.png', - 'AVERAGE': 'assets/timetuner_jitter_orange.png', - 'BAD': 'assets/timetuner_jitter_red.png', - 'default': 'assets/timetuner_jitter_red.png' + 'GOOD': 'assets/timeturner_jitter_green.png', + 'AVERAGE': 'assets/timeturner_jitter_orange.png', + 'BAD': 'assets/timeturner_jitter_red.png', + 'default': 'assets/timeturner_jitter_red.png' } }; From e419cd506e58e2e3d7b5a18914a4abad9a4fc10c Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:04:52 +0100 Subject: [PATCH 051/139] feat: Add configurable tooltips to status icons Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/icon-map.js | 28 ++++++++++++++-------------- static/script.js | 18 +++++++++--------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index ac37b21..d394125 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -1,25 +1,25 @@ // In this file, you can define the paths to your local icon image files. const iconMap = { ltcStatus: { - 'LOCK': 'assets/timeturner_ltc_green.png', - 'FREE': 'assets/timeturner_ltc_orange.png', - 'default': 'assets/timeturner_ltc_red.png' + '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: 'assets/timeturner_ntp_green.png', - false: 'assets/timeturner_ntp_red.png' + 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': 'assets/timeturner_sync_green.png', - 'CLOCK AHEAD': 'assets/timeturner_sync_orange.png', - 'CLOCK BEHIND': 'assets/timeturner_sync_orange.png', - 'TIMETURNING': 'assets/timeturner_timeturning.png', - 'default': 'assets/timeturner_sync_red.png' + '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': 'assets/timeturner_jitter_green.png', - 'AVERAGE': 'assets/timeturner_jitter_orange.png', - 'BAD': 'assets/timeturner_jitter_red.png', - 'default': 'assets/timeturner_jitter_red.png' + '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.' } } }; diff --git a/static/script.js b/static/script.js index 7f82731..20c5571 100644 --- a/static/script.js +++ b/static/script.js @@ -76,8 +76,8 @@ document.addEventListener('DOMContentLoaded', () => { function updateStatus(data) { const ltcStatus = data.ltc_status || 'UNKNOWN'; - const ltcIconSrc = iconMap.ltcStatus[ltcStatus] || iconMap.ltcStatus.default; - statusElements.ltcStatus.innerHTML = `${ltcStatus}`; + const ltcIconInfo = iconMap.ltcStatus[ltcStatus] || iconMap.ltcStatus.default; + statusElements.ltcStatus.innerHTML = `${ltcStatus}`; statusElements.ltcStatus.className = ltcStatus.toLowerCase(); statusElements.ltcTimecode.textContent = data.ltc_timecode; statusElements.frameRate.textContent = data.frame_rate; @@ -85,26 +85,26 @@ document.addEventListener('DOMContentLoaded', () => { statusElements.systemClock.textContent = data.system_clock; statusElements.systemDate.textContent = data.system_date; - const ntpIconSrc = iconMap.ntpActive[data.ntp_active]; + const ntpIconInfo = iconMap.ntpActive[!!data.ntp_active]; if (data.ntp_active) { - statusElements.ntpActive.innerHTML = `Active`; + statusElements.ntpActive.innerHTML = `Active`; statusElements.ntpActive.className = 'active'; } else { - statusElements.ntpActive.innerHTML = `Inactive`; + statusElements.ntpActive.innerHTML = `Inactive`; statusElements.ntpActive.className = 'inactive'; } const syncStatus = data.sync_status || 'UNKNOWN'; - const syncIconSrc = iconMap.syncStatus[syncStatus] || iconMap.syncStatus.default; - statusElements.syncStatus.innerHTML = `${syncStatus}`; + const syncIconInfo = iconMap.syncStatus[syncStatus] || iconMap.syncStatus.default; + statusElements.syncStatus.innerHTML = `${syncStatus}`; statusElements.syncStatus.className = syncStatus.replace(/\s+/g, '-').toLowerCase(); statusElements.deltaMs.textContent = data.timecode_delta_ms; statusElements.deltaFrames.textContent = data.timecode_delta_frames; const jitterStatus = data.jitter_status || 'UNKNOWN'; - const jitterIconSrc = iconMap.jitterStatus[jitterStatus] || iconMap.jitterStatus.default; - statusElements.jitterStatus.innerHTML = `${jitterStatus}`; + const jitterIconInfo = iconMap.jitterStatus[jitterStatus] || iconMap.jitterStatus.default; + statusElements.jitterStatus.innerHTML = `${jitterStatus}`; statusElements.jitterStatus.className = jitterStatus.toLowerCase(); statusElements.interfaces.innerHTML = ''; From 7e7ca422207b2a1e39860030420923664bb46123 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:07:43 +0100 Subject: [PATCH 052/139] feat: Add dynamic icon for clock delta based on offset value Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/icon-map.js | 5 +++++ static/index.html | 2 +- static/script.js | 18 ++++++++++++++---- static/style.css | 2 +- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index d394125..9ba4716 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -21,5 +21,10 @@ const iconMap = { '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 low (<= 40ms).' }, + 'average': { src: 'assets/timeturner_delta_orange.png', tooltip: 'Clock delta is moderate (<= 100ms).' }, + 'bad': { src: 'assets/timeturner_delta_red.png', tooltip: 'Clock delta is high (> 100ms).' } } }; diff --git a/static/index.html b/static/index.html index 1b2880a..5aca849 100644 --- a/static/index.html +++ b/static/index.html @@ -41,7 +41,7 @@

Clock Offset

-

Delta: -- ms (-- frames)

+

Delta: -- ms (-- frames)

Jitter: --

diff --git a/static/script.js b/static/script.js index 20c5571..6739230 100644 --- a/static/script.js +++ b/static/script.js @@ -16,8 +16,7 @@ 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'), interfaces: document.getElementById('interfaces'), logs: document.getElementById('logs'), @@ -99,8 +98,19 @@ document.addEventListener('DOMContentLoaded', () => { statusElements.syncStatus.innerHTML = `${syncStatus}`; statusElements.syncStatus.className = syncStatus.replace(/\s+/g, '-').toLowerCase(); - statusElements.deltaMs.textContent = data.timecode_delta_ms; - statusElements.deltaFrames.textContent = data.timecode_delta_frames; + // Delta Status + const deltaMs = data.timecode_delta_ms; + let deltaCategory; + if (Math.abs(deltaMs) <= 40) { // ~1 frame at 25fps + deltaCategory = 'good'; + } else if (Math.abs(deltaMs) <= 100) { + deltaCategory = 'average'; + } else { + deltaCategory = 'bad'; + } + const deltaIconInfo = iconMap.deltaStatus[deltaCategory]; + const deltaText = `${data.timecode_delta_ms} ms (${data.timecode_delta_frames} frames)`; + statusElements.deltaStatus.innerHTML = `${deltaText}`; const jitterStatus = data.jitter_status || 'UNKNOWN'; const jitterIconInfo = iconMap.jitterStatus[jitterStatus] || iconMap.jitterStatus.default; diff --git a/static/style.css b/static/style.css index 638b554..7955571 100644 --- a/static/style.css +++ b/static/style.css @@ -99,7 +99,7 @@ button:hover { color: #555; } -#ltc-status, #ntp-active, #sync-status, #jitter-status { +#ltc-status, #ntp-active, #sync-status, #jitter-status, #delta-status { display: inline-flex; align-items: center; gap: 0.5em; From cfc9a79ab860478569eda62420017823f9347467 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:11:16 +0100 Subject: [PATCH 053/139] feat: Hide controls and logs behind toggleable dropdown cards Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 20 +++++++++++++++----- static/script.js | 17 +++++++++++++++++ static/style.css | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/static/index.html b/static/index.html index 5aca849..72e900b 100644 --- a/static/index.html +++ b/static/index.html @@ -54,8 +54,12 @@
-
-

Controls

+
+
+ Controls Icon +

Controls

+
+
@@ -107,12 +111,18 @@
+
-
-

Logs

-

+            
+
+ Logs Icon +

Logs

+
+
+

+                
diff --git a/static/script.js b/static/script.js index 6739230..405805f 100644 --- a/static/script.js +++ b/static/script.js @@ -44,6 +44,12 @@ 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'); @@ -394,6 +400,17 @@ 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(); diff --git a/static/style.css b/static/style.css index 7955571..dc06b6b 100644 --- a/static/style.css +++ b/static/style.css @@ -110,6 +110,43 @@ button:hover { 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; +} + +.collapsible-content { + display: none; + padding: 20px; +} + +.collapsible-content.active { + display: block; +} + /* 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; } From 3374646de5bf78cb7ee5f953e2375be6dec908f5 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:13:37 +0100 Subject: [PATCH 054/139] feat: Autofill date input with system date, prevent user overwrite Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/script.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static/script.js b/static/script.js index 405805f..2db37bf 100644 --- a/static/script.js +++ b/static/script.js @@ -90,6 +90,11 @@ document.addEventListener('DOMContentLoaded', () => { statusElements.systemClock.textContent = data.system_clock; statusElements.systemDate.textContent = data.system_date; + // Autofill the date input, but don't overwrite user edits. + if (!lastApiData || dateInput.value === lastApiData.system_date) { + dateInput.value = data.system_date; + } + const ntpIconInfo = iconMap.ntpActive[!!data.ntp_active]; if (data.ntp_active) { statusElements.ntpActive.innerHTML = `Active`; From d4ff2568e3e235a2d639b18e4b0ac64e8df7eb08 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:16:06 +0100 Subject: [PATCH 055/139] feat: Add network icon to 'Network' card header Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 5 ++++- static/style.css | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index 72e900b..7192e18 100644 --- a/static/index.html +++ b/static/index.html @@ -47,7 +47,10 @@
-

Network

+
+ Network Icon +

Network

+
  • --
diff --git a/static/style.css b/static/style.css index dc06b6b..9745fd4 100644 --- a/static/style.css +++ b/static/style.css @@ -138,6 +138,17 @@ button:hover { height: 40px; } +.header-icon { + width: 40px; + height: 40px; +} + +.card-header { + display: flex; + align-items: center; + gap: 15px; +} + .collapsible-content { display: none; padding: 20px; From 6726cf393a0864b9c6414b13d4c9b3e71e3bbea8 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:18:55 +0100 Subject: [PATCH 056/139] feat: Adjust delta status thresholds for 0ms, <10ms, >=10ms Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/icon-map.js | 6 +++--- static/script.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index 9ba4716..da5cd34 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -23,8 +23,8 @@ const iconMap = { 'default': { src: 'assets/timeturner_jitter_red.png', tooltip: 'Jitter status is unknown.' } }, deltaStatus: { - 'good': { src: 'assets/timeturner_delta_green.png', tooltip: 'Clock delta is low (<= 40ms).' }, - 'average': { src: 'assets/timeturner_delta_orange.png', tooltip: 'Clock delta is moderate (<= 100ms).' }, - 'bad': { src: 'assets/timeturner_delta_red.png', tooltip: 'Clock delta is high (> 100ms).' } + '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.' } } }; diff --git a/static/script.js b/static/script.js index 2db37bf..72722e7 100644 --- a/static/script.js +++ b/static/script.js @@ -112,9 +112,9 @@ document.addEventListener('DOMContentLoaded', () => { // Delta Status const deltaMs = data.timecode_delta_ms; let deltaCategory; - if (Math.abs(deltaMs) <= 40) { // ~1 frame at 25fps + if (deltaMs === 0) { deltaCategory = 'good'; - } else if (Math.abs(deltaMs) <= 100) { + } else if (Math.abs(deltaMs) < 10) { deltaCategory = 'average'; } else { deltaCategory = 'bad'; From 463856a330092e63a48df562d1ee9fb09f9b36f9 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:19:30 +0100 Subject: [PATCH 057/139] chore: Disable mock data mode Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/script.js b/static/script.js index 72722e7..9bbf08b 100644 --- a/static/script.js +++ b/static/script.js @@ -1,7 +1,7 @@ document.addEventListener('DOMContentLoaded', () => { // --- Mock Data Configuration --- // Set to true to use mock data, false for live API. - const useMockData = true; + const useMockData = false; let currentMockSetKey = 'allGood'; // Default mock data set let lastApiData = null; From adae9026ad2cdfd068c70cd4e4b50e039e73ea12 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:23:32 +0100 Subject: [PATCH 058/139] feat: Limit log display to 20 latest entries, newest first Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/script.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/static/script.js b/static/script.js index 9bbf08b..4b37c63 100644 --- a/static/script.js +++ b/static/script.js @@ -291,18 +291,20 @@ document.addEventListener('DOMContentLoaded', () => { async function fetchLogs() { if (useMockData) { - const logs = mockApiDataSets[currentMockSetKey].logs; - statusElements.logs.textContent = logs.join('\n'); - statusElements.logs.scrollTop = statusElements.logs.scrollHeight; + // 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.'; From 9360e0011c9a5eaf74ab72fd97088b4a1472cb29 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:39:53 +0100 Subject: [PATCH 059/139] chore: Enable mock data and simplify clock info display --- static/index.html | 8 ++++---- static/script.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/static/index.html b/static/index.html index 7192e18..ec358d2 100644 --- a/static/index.html +++ b/static/index.html @@ -34,15 +34,15 @@

System Clock

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

Date: ---- -- --

-

NTP Service: --

-

Sync Status: --

+

--

+

--

Clock Offset

-

Delta: -- ms (-- frames)

-

Jitter: --

+

-- ms (-- frames)

+

diff --git a/static/script.js b/static/script.js index 4b37c63..3d01ea6 100644 --- a/static/script.js +++ b/static/script.js @@ -1,7 +1,7 @@ document.addEventListener('DOMContentLoaded', () => { // --- Mock Data Configuration --- // Set to true to use mock data, false for live API. - const useMockData = false; + const useMockData = true; let currentMockSetKey = 'allGood'; // Default mock data set let lastApiData = null; From ba9b8971572c6df1d5942567c2388043f73ff316 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:40:21 +0100 Subject: [PATCH 060/139] feat: Add dynamic FPS icon display to web UI Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/icon-map.js | 8 ++++++++ static/index.html | 2 +- static/script.js | 6 +++++- static/style.css | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index da5cd34..f5b7553 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -26,5 +26,13 @@ const iconMap = { '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.98': { src: 'assets/timeturner_fps_2398.png', tooltip: '23.98 frames per second' }, + '24.00': { src: 'assets/timeturner_fps_2400.png', tooltip: '24.00 frames per second' }, + '25.00': { src: 'assets/timeturner_fps_2500.png', tooltip: '25.00 frames per second' }, + '29.97': { src: 'assets/timeturner_fps_2997.png', tooltip: '29.97 frames per second' }, + '30.00': { src: 'assets/timeturner_fps_3000.png', tooltip: '30.00 frames per second' }, + 'default': { src: 'assets/timeturner_fps_unknown.png', tooltip: 'Unknown frame rate' } } }; diff --git a/static/index.html b/static/index.html index ec358d2..3a5f91f 100644 --- a/static/index.html +++ b/static/index.html @@ -25,7 +25,7 @@

LTC Status

--:--:--:--

--

-

-- fps

+

-- fps

Lock Ratio: --%

diff --git a/static/script.js b/static/script.js index 3d01ea6..3c63cb7 100644 --- a/static/script.js +++ b/static/script.js @@ -85,7 +85,11 @@ document.addEventListener('DOMContentLoaded', () => { statusElements.ltcStatus.innerHTML = `${ltcStatus}`; statusElements.ltcStatus.className = ltcStatus.toLowerCase(); statusElements.ltcTimecode.textContent = data.ltc_timecode; - statusElements.frameRate.textContent = data.frame_rate; + + const frameRate = data.frame_rate || 'unknown'; + const frameRateIconInfo = iconMap.frameRate[frameRate] || iconMap.frameRate.default; + statusElements.frameRate.innerHTML = `${frameRate} fps`; + statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2); statusElements.systemClock.textContent = data.system_clock; statusElements.systemDate.textContent = data.system_date; diff --git a/static/style.css b/static/style.css index 9745fd4..994454b 100644 --- a/static/style.css +++ b/static/style.css @@ -99,7 +99,7 @@ button:hover { color: #555; } -#ltc-status, #ntp-active, #sync-status, #jitter-status, #delta-status { +#ltc-status, #ntp-active, #sync-status, #jitter-status, #delta-status, #frame-rate { display: inline-flex; align-items: center; gap: 0.5em; From fffc123475036f2bba80dd859cb41b6c22d5851a Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:42:37 +0100 Subject: [PATCH 061/139] refactor: Update frame rate icon asset paths --- static/icon-map.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index f5b7553..87f899b 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -28,11 +28,11 @@ const iconMap = { 'bad': { src: 'assets/timeturner_delta_red.png', tooltip: 'Clock delta is 10ms or greater.' } }, frameRate: { - '23.98': { src: 'assets/timeturner_fps_2398.png', tooltip: '23.98 frames per second' }, - '24.00': { src: 'assets/timeturner_fps_2400.png', tooltip: '24.00 frames per second' }, - '25.00': { src: 'assets/timeturner_fps_2500.png', tooltip: '25.00 frames per second' }, - '29.97': { src: 'assets/timeturner_fps_2997.png', tooltip: '29.97 frames per second' }, - '30.00': { src: 'assets/timeturner_fps_3000.png', tooltip: '30.00 frames per second' }, - 'default': { src: 'assets/timeturner_fps_unknown.png', tooltip: 'Unknown frame rate' } + '23.98': { src: 'assets/timeturner_2398.png', tooltip: '23.98 frames per second' }, + '24.00': { src: 'assets/timeturner_24.png', tooltip: '24.00 frames per second' }, + '25.00': { src: 'assets/timeturner_25.png', tooltip: '25.00 frames per second' }, + '29.97': { src: 'assets/timeturner_2997.png', tooltip: '29.97 frames per second' }, + '30.00': { src: 'assets/timeturner_30.png', tooltip: '30.00 frames per second' }, + 'default': { src: 'assets/timeturner_25.png', tooltip: 'Unknown frame rate' } } }; From fbae58fb1dfef8e21cb3bd4f4883f6ebee0abdd1 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:42:43 +0100 Subject: [PATCH 062/139] feat: Add dynamic lock ratio icon with thresholds Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/icon-map.js | 5 +++++ static/index.html | 2 +- static/script.js | 13 ++++++++++++- static/style.css | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index 87f899b..7edfd6d 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -34,5 +34,10 @@ const iconMap = { '29.97': { src: 'assets/timeturner_2997.png', tooltip: '29.97 frames per second' }, '30.00': { src: 'assets/timeturner_30.png', tooltip: '30.00 frames per second' }, 'default': { src: 'assets/timeturner_25.png', tooltip: 'Unknown frame rate' } + }, + lockRatio: { + 'good': { src: 'assets/timeturner_lockratio_green.png', tooltip: 'Lock ratio is 100%.' }, + 'average': { src: 'assets/timeturner_lockratio_orange.png', tooltip: 'Lock ratio is 90% or higher.' }, + 'bad': { src: 'assets/timeturner_lockratio_red.png', tooltip: 'Lock ratio is below 90%.' } } }; diff --git a/static/index.html b/static/index.html index 3a5f91f..4362a86 100644 --- a/static/index.html +++ b/static/index.html @@ -26,7 +26,7 @@

--:--:--:--

--

-- fps

-

Lock Ratio: --%

+

Lock Ratio: --%

diff --git a/static/script.js b/static/script.js index 3c63cb7..f0e07ad 100644 --- a/static/script.js +++ b/static/script.js @@ -90,7 +90,18 @@ document.addEventListener('DOMContentLoaded', () => { const frameRateIconInfo = iconMap.frameRate[frameRate] || iconMap.frameRate.default; statusElements.frameRate.innerHTML = `${frameRate} fps`; - statusElements.lockRatio.textContent = data.lock_ratio.toFixed(2); + 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]; + const lockRatioText = `Lock Ratio: ${lockRatio.toFixed(2)}%`; + statusElements.lockRatio.innerHTML = `${lockRatioText}`; statusElements.systemClock.textContent = data.system_clock; statusElements.systemDate.textContent = data.system_date; diff --git a/static/style.css b/static/style.css index 994454b..ddfdf8f 100644 --- a/static/style.css +++ b/static/style.css @@ -99,7 +99,7 @@ button:hover { color: #555; } -#ltc-status, #ntp-active, #sync-status, #jitter-status, #delta-status, #frame-rate { +#ltc-status, #ntp-active, #sync-status, #jitter-status, #delta-status, #frame-rate, #lock-ratio { display: inline-flex; align-items: center; gap: 0.5em; From 4af732dab05454ccfe4231caa264e6be559b0863 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:47:07 +0100 Subject: [PATCH 063/139] style: Update portal styling with new colours and header image Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 2 +- static/style.css | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/static/index.html b/static/index.html index 4362a86..fcf8799 100644 --- a/static/index.html +++ b/static/index.html @@ -8,7 +8,7 @@
-

NTP TimeTurner

+

Clock Offset

-

-- ms (-- frames)

-

+

-- ms (-- frames)

+

diff --git a/static/script.js b/static/script.js index f0e07ad..9b05dc7 100644 --- a/static/script.js +++ b/static/script.js @@ -82,13 +82,13 @@ document.addEventListener('DOMContentLoaded', () => { function updateStatus(data) { const ltcStatus = data.ltc_status || 'UNKNOWN'; const ltcIconInfo = iconMap.ltcStatus[ltcStatus] || iconMap.ltcStatus.default; - statusElements.ltcStatus.innerHTML = `${ltcStatus}`; + statusElements.ltcStatus.innerHTML = ``; statusElements.ltcStatus.className = ltcStatus.toLowerCase(); statusElements.ltcTimecode.textContent = data.ltc_timecode; const frameRate = data.frame_rate || 'unknown'; const frameRateIconInfo = iconMap.frameRate[frameRate] || iconMap.frameRate.default; - statusElements.frameRate.innerHTML = `${frameRate} fps`; + statusElements.frameRate.innerHTML = ``; const lockRatio = data.lock_ratio; let lockRatioCategory; @@ -100,8 +100,7 @@ document.addEventListener('DOMContentLoaded', () => { lockRatioCategory = 'bad'; } const lockRatioIconInfo = iconMap.lockRatio[lockRatioCategory]; - const lockRatioText = `Lock Ratio: ${lockRatio.toFixed(2)}%`; - statusElements.lockRatio.innerHTML = `${lockRatioText}`; + statusElements.lockRatio.innerHTML = ``; statusElements.systemClock.textContent = data.system_clock; statusElements.systemDate.textContent = data.system_date; @@ -112,16 +111,16 @@ document.addEventListener('DOMContentLoaded', () => { const ntpIconInfo = iconMap.ntpActive[!!data.ntp_active]; if (data.ntp_active) { - statusElements.ntpActive.innerHTML = `Active`; + statusElements.ntpActive.innerHTML = ``; statusElements.ntpActive.className = 'active'; } else { - statusElements.ntpActive.innerHTML = `Inactive`; + statusElements.ntpActive.innerHTML = ``; statusElements.ntpActive.className = 'inactive'; } const syncStatus = data.sync_status || 'UNKNOWN'; const syncIconInfo = iconMap.syncStatus[syncStatus] || iconMap.syncStatus.default; - statusElements.syncStatus.innerHTML = `${syncStatus}`; + statusElements.syncStatus.innerHTML = ``; statusElements.syncStatus.className = syncStatus.replace(/\s+/g, '-').toLowerCase(); // Delta Status @@ -140,7 +139,7 @@ document.addEventListener('DOMContentLoaded', () => { const jitterStatus = data.jitter_status || 'UNKNOWN'; const jitterIconInfo = iconMap.jitterStatus[jitterStatus] || iconMap.jitterStatus.default; - statusElements.jitterStatus.innerHTML = `${jitterStatus}`; + statusElements.jitterStatus.innerHTML = ``; statusElements.jitterStatus.className = jitterStatus.toLowerCase(); statusElements.interfaces.innerHTML = ''; diff --git a/static/style.css b/static/style.css index 9ce03fb..890f24f 100644 --- a/static/style.css +++ b/static/style.css @@ -100,12 +100,18 @@ button:hover { color: #555; } -#ltc-status, #ntp-active, #sync-status, #jitter-status, #delta-status, #frame-rate, #lock-ratio { +#delta-status { display: inline-flex; align-items: center; gap: 0.5em; } +#ltc-status, #ntp-active, #sync-status, #jitter-status, #frame-rate, #lock-ratio { + display: flex; + justify-content: center; + align-items: center; +} + .status-icon { width: 60px; height: 60px; From 534754be4e79d8aea7a86459aea605a4db96f424 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:54:53 +0100 Subject: [PATCH 066/139] refactor: Consolidate status cards for 2-column layout Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 21 ++++++++++----------- static/style.css | 10 +++++++++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/static/index.html b/static/index.html index 0852b66..6bd9a4e 100644 --- a/static/index.html +++ b/static/index.html @@ -24,9 +24,11 @@

LTC Status

--:--:--:--

-

-

-

+
+ + + +
@@ -34,15 +36,12 @@

System Clock

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

Date: ---- -- --

-

-

-
- - -
-

Clock Offset

+
+ + + +

-- ms (-- frames)

-

diff --git a/static/style.css b/static/style.css index 890f24f..513ce3d 100644 --- a/static/style.css +++ b/static/style.css @@ -31,7 +31,7 @@ body { .grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: 1fr 1fr; gap: 20px; } @@ -100,6 +100,14 @@ button:hover { color: #555; } +.icon-group { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + margin: 10px 0; +} + #delta-status { display: inline-flex; align-items: center; From 54ebc0b242cfc842fed89e125a8535546df332e2 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 00:58:40 +0100 Subject: [PATCH 067/139] refactor: Single column layout; group delta icon; style date Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 4 ++-- static/style.css | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/static/index.html b/static/index.html index 6bd9a4e..c999bf8 100644 --- a/static/index.html +++ b/static/index.html @@ -35,13 +35,13 @@

System Clock

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

-

Date: ---- -- --

+

Date: ---- -- --

+
-

-- ms (-- frames)

diff --git a/static/style.css b/static/style.css index 513ce3d..cc9e9df 100644 --- a/static/style.css +++ b/static/style.css @@ -31,7 +31,7 @@ body { .grid { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr; gap: 20px; } @@ -58,6 +58,13 @@ body { margin: 10px 0; } +.system-date-display { + text-align: center; + font-size: 1.5em; + font-family: 'Quartz', monospace; + letter-spacing: 2px; +} + .card ul { padding-left: 20px; list-style: none; From 4c5fa69d1d0cc7de0d87da28abdce8918ff97dfc Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:00:46 +0100 Subject: [PATCH 068/139] style: Remove redundant 'Date:' label from system clock --- static/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index c999bf8..e2cce80 100644 --- a/static/index.html +++ b/static/index.html @@ -35,7 +35,7 @@

System Clock

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

-

Date: ---- -- --

+

---- -- --

From ba855d520a7773505b0bdcfb731888554c9625a7 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:00:51 +0100 Subject: [PATCH 069/139] refactor: Display network interfaces on a single line Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 4 +--- static/script.js | 11 ++--------- static/style.css | 5 ----- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/static/index.html b/static/index.html index e2cce80..1f2f4ac 100644 --- a/static/index.html +++ b/static/index.html @@ -50,9 +50,7 @@ Network Icon

Network

-
    -
  • --
  • -
+

--

diff --git a/static/script.js b/static/script.js index 9b05dc7..2e4fc1b 100644 --- a/static/script.js +++ b/static/script.js @@ -142,17 +142,10 @@ document.addEventListener('DOMContentLoaded', () => { statusElements.jitterStatus.innerHTML = ``; statusElements.jitterStatus.className = jitterStatus.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); - }); + statusElements.interfaces.textContent = data.interfaces.join(' | '); } else { - const li = document.createElement('li'); - li.textContent = 'No active interfaces found.'; - statusElements.interfaces.appendChild(li); + statusElements.interfaces.textContent = 'No active interfaces found.'; } } diff --git a/static/style.css b/static/style.css index cc9e9df..0d64a03 100644 --- a/static/style.css +++ b/static/style.css @@ -65,11 +65,6 @@ body { letter-spacing: 2px; } -.card ul { - padding-left: 20px; - list-style: none; -} - .full-width { grid-column: 1 / -1; } From 5fee17e1aba3ca3cd8054f33f367110593b3218c Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:02:06 +0100 Subject: [PATCH 070/139] fix: Ensure network interfaces display on single line with scroll Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/static/style.css b/static/style.css index 0d64a03..87167c1 100644 --- a/static/style.css +++ b/static/style.css @@ -65,6 +65,13 @@ body { 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 { grid-column: 1 / -1; } From 5b0dcadac2899cdbf82ab539d927dadc37df5247 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:03:29 +0100 Subject: [PATCH 071/139] test: Add subnet masks to mock IP addresses Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/mock-data.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/static/mock-data.js b/static/mock-data.js index 2765680..8d318fe 100644 --- a/static/mock-data.js +++ b/static/mock-data.js @@ -13,7 +13,7 @@ const mockApiDataSets = { timecode_delta_ms: 5, timecode_delta_frames: 0.125, jitter_status: 'GOOD', - interfaces: ['192.168.1.100 (eth0)', '10.0.0.5 (wlan0)'], + interfaces: ['192.168.1.100/24 (eth0)', '10.0.0.5/8 (wlan0)'], }, config: { hardwareOffsetMs: 10, @@ -40,7 +40,7 @@ const mockApiDataSets = { timecode_delta_ms: 3, timecode_delta_frames: 0.075, jitter_status: 'GOOD', - interfaces: ['192.168.1.100 (eth0)'], + interfaces: ['192.168.1.100/24 (eth0)'], }, config: { hardwareOffsetMs: 10, @@ -63,7 +63,7 @@ const mockApiDataSets = { timecode_delta_ms: -500, timecode_delta_frames: -12.5, jitter_status: 'AVERAGE', - interfaces: ['192.168.1.100 (eth0)'], + interfaces: ['192.168.1.100/24 (eth0)'], }, config: { hardwareOffsetMs: 10, @@ -86,7 +86,7 @@ const mockApiDataSets = { timecode_delta_ms: 800, timecode_delta_frames: 20, jitter_status: 'AVERAGE', - interfaces: ['192.168.1.100 (eth0)'], + interfaces: ['192.168.1.100/24 (eth0)'], }, config: { hardwareOffsetMs: 10, @@ -109,7 +109,7 @@ const mockApiDataSets = { timecode_delta_ms: 3723050, // a big number timecode_delta_frames: 93076, jitter_status: 'GOOD', - interfaces: ['192.168.1.100 (eth0)'], + interfaces: ['192.168.1.100/24 (eth0)'], }, config: { hardwareOffsetMs: 10, @@ -132,7 +132,7 @@ const mockApiDataSets = { timecode_delta_ms: 10, timecode_delta_frames: 0.25, jitter_status: 'BAD', - interfaces: ['192.168.1.100 (eth0)'], + interfaces: ['192.168.1.100/24 (eth0)'], }, config: { hardwareOffsetMs: 10, From 886006420b6abe98e263ff5abcf8a660bd55f30d Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:05:49 +0100 Subject: [PATCH 072/139] feat: add footer with build information and GitHub link Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 6 ++++++ static/style.css | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/static/index.html b/static/index.html index 1f2f4ac..70ddf60 100644 --- a/static/index.html +++ b/static/index.html @@ -125,6 +125,12 @@ +
diff --git a/static/style.css b/static/style.css index 87167c1..f3ad2b0 100644 --- a/static/style.css +++ b/static/style.css @@ -182,6 +182,27 @@ button:hover { 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; } From 89628b974b482157140933719121863e59552b74 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:08:08 +0100 Subject: [PATCH 073/139] style: Add Have Blue logo to page background Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/style.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static/style.css b/static/style.css index f3ad2b0..6d1f829 100644 --- a/static/style.css +++ b/static/style.css @@ -11,6 +11,11 @@ body { 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: 150px; color: #333; margin: 0; padding: 20px; From fad7ddedb54c704c82b67346092f1325571dbfa0 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:13:48 +0100 Subject: [PATCH 074/139] refactor: Update icon map asset paths --- static/icon-map.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index 7edfd6d..1ee66fe 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -33,11 +33,11 @@ const iconMap = { '25.00': { src: 'assets/timeturner_25.png', tooltip: '25.00 frames per second' }, '29.97': { src: 'assets/timeturner_2997.png', tooltip: '29.97 frames per second' }, '30.00': { src: 'assets/timeturner_30.png', tooltip: '30.00 frames per second' }, - 'default': { src: 'assets/timeturner_25.png', tooltip: 'Unknown frame rate' } + 'default': { src: 'assets/timeturner_default.png', tooltip: 'Unknown frame rate' } }, lockRatio: { - 'good': { src: 'assets/timeturner_lockratio_green.png', tooltip: 'Lock ratio is 100%.' }, - 'average': { src: 'assets/timeturner_lockratio_orange.png', tooltip: 'Lock ratio is 90% or higher.' }, - 'bad': { src: 'assets/timeturner_lockratio_red.png', tooltip: 'Lock ratio is below 90%.' } + '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%.' } } }; From f909a90caa613a83c7dd8666d69c290890b85720 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:13:54 +0100 Subject: [PATCH 075/139] fix: Update frame rate format to include 'fps' suffix Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/icon-map.js | 10 +++++----- static/mock-data.js | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/static/icon-map.js b/static/icon-map.js index 1ee66fe..64336b3 100644 --- a/static/icon-map.js +++ b/static/icon-map.js @@ -28,11 +28,11 @@ const iconMap = { 'bad': { src: 'assets/timeturner_delta_red.png', tooltip: 'Clock delta is 10ms or greater.' } }, frameRate: { - '23.98': { src: 'assets/timeturner_2398.png', tooltip: '23.98 frames per second' }, - '24.00': { src: 'assets/timeturner_24.png', tooltip: '24.00 frames per second' }, - '25.00': { src: 'assets/timeturner_25.png', tooltip: '25.00 frames per second' }, - '29.97': { src: 'assets/timeturner_2997.png', tooltip: '29.97 frames per second' }, - '30.00': { src: 'assets/timeturner_30.png', tooltip: '30.00 frames per second' }, + '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: { diff --git a/static/mock-data.js b/static/mock-data.js index 8d318fe..a953e59 100644 --- a/static/mock-data.js +++ b/static/mock-data.js @@ -4,7 +4,7 @@ const mockApiDataSets = { status: { ltc_status: 'LOCK', ltc_timecode: '10:20:30:00', - frame_rate: '25.00', + frame_rate: '25.00fps', lock_ratio: 99.5, system_clock: '10:20:30.500', system_date: '2025-08-07', @@ -31,7 +31,7 @@ const mockApiDataSets = { status: { ltc_status: 'FREE', ltc_timecode: '11:22:33:11', - frame_rate: '25.00', + frame_rate: '25.00fps', lock_ratio: 40.2, system_clock: '11:22:33.800', system_date: '2025-08-07', @@ -54,7 +54,7 @@ const mockApiDataSets = { status: { ltc_status: 'LOCK', ltc_timecode: '12:00:05:00', - frame_rate: '25.00', + frame_rate: '25.00fps', lock_ratio: 98.1, system_clock: '12:00:04.500', system_date: '2025-08-07', @@ -77,7 +77,7 @@ const mockApiDataSets = { status: { ltc_status: 'LOCK', ltc_timecode: '13:30:10:00', - frame_rate: '25.00', + frame_rate: '25.00fps', lock_ratio: 99.9, system_clock: '13:30:10.800', system_date: '2025-08-07', @@ -100,7 +100,7 @@ const mockApiDataSets = { status: { ltc_status: 'LOCK', ltc_timecode: '14:00:00:00', - frame_rate: '25.00', + frame_rate: '25.00fps', lock_ratio: 100, system_clock: '15:02:03.050', system_date: '2025-08-07', @@ -123,7 +123,7 @@ const mockApiDataSets = { status: { ltc_status: 'LOCK', ltc_timecode: '15:15:15:15', - frame_rate: '25.00', + frame_rate: '25.00fps', lock_ratio: 95.0, system_clock: '15:15:15.515', system_date: '2025-08-07', From 49287e5e166bc07f92ccef3e80cad92e73339dbe Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:15:48 +0100 Subject: [PATCH 076/139] feat: Add favicon link to HTML Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/static/index.html b/static/index.html index 70ddf60..6f92343 100644 --- a/static/index.html +++ b/static/index.html @@ -5,6 +5,7 @@ NTP TimeTurner +
From 982aad3ec924a5378a5f78cd7359d9be6cacba2d Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:17:32 +0100 Subject: [PATCH 077/139] chore: Switch to live API data --- static/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/script.js b/static/script.js index 2e4fc1b..3871c73 100644 --- a/static/script.js +++ b/static/script.js @@ -1,7 +1,7 @@ document.addEventListener('DOMContentLoaded', () => { // --- Mock Data Configuration --- // Set to true to use mock data, false for live API. - const useMockData = true; + const useMockData = false; let currentMockSetKey = 'allGood'; // Default mock data set let lastApiData = null; From 02487bda97e4d4999682472db9d40a435e25cef9 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:17:38 +0100 Subject: [PATCH 078/139] feat: Move delta value below icon and add label Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 1 + static/script.js | 7 +++++-- static/style.css | 8 +++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/static/index.html b/static/index.html index 6f92343..18067a4 100644 --- a/static/index.html +++ b/static/index.html @@ -43,6 +43,7 @@
+

Delta Value: -- ms (-- frames)

diff --git a/static/script.js b/static/script.js index 3871c73..d489585 100644 --- a/static/script.js +++ b/static/script.js @@ -18,6 +18,7 @@ document.addEventListener('DOMContentLoaded', () => { syncStatus: document.getElementById('sync-status'), deltaStatus: document.getElementById('delta-status'), jitterStatus: document.getElementById('jitter-status'), + deltaText: document.getElementById('delta-text'), interfaces: document.getElementById('interfaces'), logs: document.getElementById('logs'), }; @@ -134,8 +135,10 @@ document.addEventListener('DOMContentLoaded', () => { deltaCategory = 'bad'; } const deltaIconInfo = iconMap.deltaStatus[deltaCategory]; - const deltaText = `${data.timecode_delta_ms} ms (${data.timecode_delta_frames} frames)`; - statusElements.deltaStatus.innerHTML = `${deltaText}`; + statusElements.deltaStatus.innerHTML = ``; + + const deltaTextValue = `${data.timecode_delta_ms} ms (${data.timecode_delta_frames} frames)`; + statusElements.deltaText.textContent = `Delta Value: ${deltaTextValue}`; const jitterStatus = data.jitter_status || 'UNKNOWN'; const jitterIconInfo = iconMap.jitterStatus[jitterStatus] || iconMap.jitterStatus.default; diff --git a/static/style.css b/static/style.css index 6d1f829..0c6d4e3 100644 --- a/static/style.css +++ b/static/style.css @@ -122,13 +122,11 @@ button:hover { margin: 10px 0; } -#delta-status { - display: inline-flex; - align-items: center; - gap: 0.5em; +#delta-text { + text-align: center; } -#ltc-status, #ntp-active, #sync-status, #jitter-status, #frame-rate, #lock-ratio { +#ltc-status, #ntp-active, #sync-status, #jitter-status, #frame-rate, #lock-ratio, #delta-status { display: flex; justify-content: center; align-items: center; From 94687da41492d09fcc34e8a09498f01e89e43337 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:21:42 +0100 Subject: [PATCH 079/139] final push for web ui version 1 --- static/assets/HaveBlueTransWh.png | Bin 0 -> 27132 bytes static/assets/favicon.png | Bin 0 -> 3060 bytes static/assets/header.png | Bin 0 -> 34635 bytes static/assets/timeturner_2398.png | Bin 0 -> 1450 bytes static/assets/timeturner_24.png | Bin 0 -> 1111 bytes static/assets/timeturner_25.png | Bin 0 -> 1144 bytes static/assets/timeturner_2997.png | Bin 0 -> 1396 bytes static/assets/timeturner_2997DF.png | Bin 0 -> 1490 bytes static/assets/timeturner_30.png | Bin 0 -> 1220 bytes static/assets/timeturner_default.png | Bin 0 -> 898 bytes static/assets/timeturner_lock_green.png | Bin 0 -> 810 bytes static/assets/timeturner_lock_orange.png | Bin 0 -> 805 bytes static/assets/timeturner_lock_red.png | Bin 0 -> 804 bytes static/assets/timeturner_timeturning.png | Bin 0 -> 3828 bytes static/favicon.ico | Bin 0 -> 202265 bytes static/index.html | 6 +++--- static/style.css | 2 +- 17 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 static/assets/HaveBlueTransWh.png create mode 100644 static/assets/favicon.png create mode 100644 static/assets/header.png create mode 100644 static/assets/timeturner_2398.png create mode 100644 static/assets/timeturner_24.png create mode 100644 static/assets/timeturner_25.png create mode 100644 static/assets/timeturner_2997.png create mode 100644 static/assets/timeturner_2997DF.png create mode 100644 static/assets/timeturner_30.png create mode 100644 static/assets/timeturner_default.png create mode 100644 static/assets/timeturner_lock_green.png create mode 100644 static/assets/timeturner_lock_orange.png create mode 100644 static/assets/timeturner_lock_red.png create mode 100644 static/assets/timeturner_timeturning.png create mode 100644 static/favicon.ico diff --git a/static/assets/HaveBlueTransWh.png b/static/assets/HaveBlueTransWh.png new file mode 100644 index 0000000000000000000000000000000000000000..d9a123d671cda604adedf12f9e7cd18b8f4a73af GIT binary patch literal 27132 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 literal 0 HcmV?d00001 diff --git a/static/assets/favicon.png b/static/assets/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..3683c35627bb45c7d1c0dcc09ff9fc3f4421bf0f GIT binary patch literal 3060 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 Ef!=gbHFWs!<2m#R#k+gM)bmx6n}0DFuTY z+Nmiuq+0`C+NBQy?cgDhw=_#i-d2MG>C{p}Gh_-X84^K@X@(3osKS07;+mVm8uTC? zTwP0v9URx!HnM&&Skgf{pUyo;_ntd;a&m%eueR<3SOFlyo6-S1E9X)l#v(62-3Rb~ z&>rqy7L003fINXNtACpgUI_o$5+ z3Q}0xj-{9lX+FSF*y-0hxV5qa0FWgCVkk)MmS?9}xzJL~B1}vR)30~H2RP`pI`{y0 z#wHyPLzV<6nt@C*%E}d&m=-O)R;T&6I5PWUQwR+>+-ANGB!qWC3OE+BVOO?(f zqsSzq&}()4{qY-s#tRTbL9Bi*G0$74#v(62ov?g>ED6|{zX`oo$D_CV&}(&wp&)jj z-)U)2Obc`QeP)%KX_Zbycx=qy1OQx~;3#BjZ5NvdKU}tUfh$ywnMRx$uowzr?crba z_;k^F`tFPCc=!1oh@l{swhw8Jvk>zkJ!~4esihC>b@{+vmk;c9`M_S+$N_gfmoDd0 z=U%_|Y70PfwRBpIrb6LO{&_JF>OA8;_Gw z+S^J*cw~}M8tF_j>Vnm4br92{=T<)#yY{swz)RbQP>a=$Y;7n-GqANVO`+5AFunHJ z(cjuU_(7MFi3kr_5;}fXD-En@5!1rDBNv?!b#^uNpUt&#p+ef`d#4)8D(!3lWaSu^ zXQx~*zdJ3XR{~3bNRilDrccWpcbobd(*_UplAl&nQ(5)$dWKP;ETwf zft@Elz(EWJT|kOv^kg4uD68~8vs2hwm_}A^=CE^z5kS|C`hiU?+sU3k1C4xdmGgS7 z-u2{T=pw9W#3l?Yq>x1!yxz4}>4d7?}_l3Dy!UDi}IhXnX-~qf7 d>9(){@C!|dhN&?rJiq_|002ovPDHLkV1n}64^#jE literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_25.png b/static/assets/timeturner_25.png new file mode 100644 index 0000000000000000000000000000000000000000..3b44c93c33ad5d648f04e2b7cc78c1012dff24d2 GIT binary patch literal 1144 zcmV-;1c&>HP)tP%{`GUE)EY znq-Pg1kP4dE$Pw_Lo#SExJ$A)q+10tI8%db;=xn6kiikO2s_|G0##V`U{$>gy22IQ zLB405Be}AwlgvpU7@qDOzW3>W@4xqcjU68!bM?{IP2dR-z@C(Vdk5+Gi!tBBmp6f* z2R))baci7LWQT<(bVFtjtA_LmsY7 zI8cgJzTenI)pXJS?!gTtr$cBq15>u;TR4{jibJOtb>O0_ys-y{Ib+xwPAwHeEVRZVC5 z@hc>!gHo;DGJ@CKE&!Hhr}$P5vblT6^7g(}Ljo+G(xqA*YK`{N>=Xb>q1w0(1U`15 z6siD3eUqf*Ab_RPmM1NMWXw-8=BHGvbAMx3T>s|zuc9r7JghHVClT>lDs1^{Ne+2f znTt@W)%p6Tx2Mr{yF^5xk-++ar4f$$B-R(M18`-+!JF^C1R(zG4N>1DOS4m?W*XAQ z8h@%3tsIhd!U4C7XSY8SpU)<(U-k~U=5~>YcnP>&q{I{TNhD)_5$R;iZ;~z5>ZqD7uC2Z)k96@7PNesTZi-l3?BM12x+$kXw&S~0MsrD{5BO>40^qQ;j1 zQ2Mhj+WCswcD;AXrOeVs83-DQYh^Cdvl38Gru5S66za_B8s6OdqoYjPJazLGRkYV~ zWl`mX(;}c!sFKb7&gR}>*JJI8@Y!!)DIT@G*C^b{!B0$6H(z-#+Qzg^%>+%QAtS42 zb7fQY^CI;9wbUFr169*MDBi6qMY6dvU!A;$DVBAm^Y&y`U%oyF(Bb^ZS@ zU4&IlXFjt-GUhjBXCtalm7-~5a#2RVJlLI)u+7N~lW^uB9ltv)!kL3~{4NZ~loqmJ ze_@j>G=xio8H?q7APWuQd;K4a<+PLq7(YnIUjVnTPo&$*0{jc-V1#$G&yC^$0000< KMNUMnLSTYKGcz3k literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_2997.png b/static/assets/timeturner_2997.png new file mode 100644 index 0000000000000000000000000000000000000000..0bd27fd7e5d2c5299b99ce49e6111ad404ae88b7 GIT binary patch literal 1396 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 s+w$3AOBP^gZ8G%?xJv&-+L0{4f2R3}ic)+xnN zMsyG3j7}S^#!Qq+J`kAXJ>I! z_v4;XmAKW2YS~(0uo3IJaVflw$325;Qx|M?>VmCKU9i=u3${8nC-}6PgL|D0VmAVW zeJ)nk6jDHN9ZwlsI+vo8BMI8vN<)t8@gKS>M>? z!r3OSd7U(!K1DjWjMwR4^xLnv(tHLV?Cd>8g1*lnby7x?0*34VA!1wiaZz^o|&CJJ6zQ)vI?C6QnMM3NQG z*=+zw@&>RYJSolt@W-|*dz}u1?Y?GUS`16Ff+Q;d#5w}>hkXDHy~+ZrFo5|tvff|g zT$9c4y^>TVOJ|Cbo{VB0s>{y3T?OFsc{_l?sw->ehy@b`M?)S8@d!_EUnMoY!1vFm zkYt6RTOjBbNN1O~`&|a0tF;AC7s4#-0yMZEj%}GSuwb__-q()T=|GYd-Gb@tGChe& zyiNz>eeDQ#o1y*Q$ZJ!#Ceya1^^Hv?W)`{dSrdZYMguT0yU6|DGOTZGBCRR<_H?EB z44Ig@5829_@q*EN6Wfkt5X+jb?SnxPF=9osX4)A--iqF$iD5D2F3yPqW|p9 zwU62*Ss_z=M>@B}P&!v~jQ+5X&ej%!ZUISF&)(_m5=k*%?zGJ<;_ZiEw-NTc2>V@h zwzkld_)EVQ3U(XM?tDqmEg1TDwzh14F4B{jEbF}$5u7PXdgtUIYS%~ML}qEN-UVvndfeS+SdgPo5toj2 z1dy)p#Bgf*-vedR-L9J{O8WD#nAeMapvH7G=q?p$W_lKqG+tO~!X-KldlMiBZD4ioI=IPqrhP1*x zs1_d<^9HXtC>0Uhlb9?kb7qPX>FhE?uW}_zvy!Zk6!Rp-JkgMc$K7puoAw9XVX3-> zwG-HW2zDD@r^6s9Ddr7=%jHC7>7TpX81HMRKfDveTI_3-V_i7Lqvc*`=Vwsky;RK4 zyijic6zLCGn9{&HjV1rmt1PD!xVF-2E?ilt;miG z2R_g{jJpx>4Z8<}#uKI;PndQ*VcPM8X~z?$9Z#5cJYm{-;dX;*j>Z1+ta<+W!xt>Y zL;oz>JHN*E;*6yZ(R8=F=e}^I-a!89#ylsQZmX=Ql8!q4U?RM8*6d5mBg)w2uqRAA zo-plr!nETF(~c)hJ7Xv8_-m<}O+0yhdba=o;A24?ufOfMPBa}w^$t?`7CEWhwT%oP zMKT`3T$lmTbV%hIq|z-!S#jEJ_dgHq!UX)_V?pq-Ad>MAQo>jBlaLAcalHNpb796( zKN%0*|6U;_Tspe1E5b#UfM|bg)!jS41^}#aEFx2r0D!qLgEYsQkxp~0MYd9JfM~i| zmb)l9?yD=pd*|1XN;O+`4JlDpP}rO|rIYcHd2Nkj&HDMP8@xOD2B7s|_*l^Py}B{9 zNsDMYj@ojuJ)*`#0D#KxhIw36NzZ(@%Sg`BmHLBZhL2)@dDikk{s&iv^!DNm$ihPo z=dXU*nn~M-Zc!!X@sX@pHuTaQi!{eVD%Bv$KP`J-&&3tt$4_4>-}bDV;iCWmKKjx! zbc^bpd29^Z%t91Qz;Bitv$`lNc>no_qrMLOwbULt1ET3Te3Y9xsSHt8uyp?#;$uPc zTAE{VBrBFtVHa0~Q^KXAnX{-8NTnKb7v-+ovP9D%$_hkT>Dce)W5MAGKL=w4_7qIO zkI2-dMM#tti?CBha+dzMJd48SJTm-44jZ{|;3T@h5SNy{FxJmNE8n~2e5KxSy!aTo z2#d1vqzut?Y@U2~-2TW#m}oju!X?riYnh#ms2)mX%fjTjc>gWYjwehx!Z-;F)ofy8 zT!e*cHn9PW$B;H<0s4h4vOr6?GMv8joeo)`CA>BGzVw~0vH;+#W)r6XRzXjsd&&a9 YUtU^1)&nuG_5c6?07*qoM6N<$f(Kx;{r~^~ literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_lock_green.png b/static/assets/timeturner_lock_green.png new file mode 100644 index 0000000000000000000000000000000000000000..0659c60f1ae073a0587eaad578efc1636cb5d419 GIT binary patch literal 810 zcmV+_1J(SAP)`Is$|qL}~YS0&rS7K{pDVmWs+^P$I1P!^8() zqWMP}{;nH4WcS|l{Y;l0i}WQO>5DTPOsi^~p2?hHrz;!KUc5ug8* zf3sWM;P`STgWVd#fhfMqC0^^h^>tlZOwxpVIs$dFd&N!Gwimf5pV#%vdb@b__?=h* z6QNnD&$YBA9UGb!ZQa=6!>6}bWC1S9=PYj|(TyDdeBREwYpq6_@Ngh1+H0Q|Z|raG zW4_jEqzG&Nuy`)_ikr7uo~NZ^-7NQZYcfKUze^vV9p1XH{^N^i`@CH;!f{@vPE73)3z$DU~P}PJ~{I_KVR4)x)b7 zu2nSvC3WckQ7(M^>t6-LT;WiKa^d?O0ns+URjESxu@nkCVSa1{P@9L>P%aFu5v}2- zmUhP#zAGwU0<0LTY2l0VIk|)8YfU$iJ4iSDGw!ZfvP-Y;r#UnVqRMDBR#q)7JC<|{ zgOZ86Y1tiD*ygM{Nq9At$j>-Mcr}&C&ww+Qw2%e+3)jg)Rrshiu}J5NEL4T()W;&7 ohOz*jR3e`NCb7>jn7kVkvvjBN2*#H0l07*qoM6N<$g1*vs>;M1& literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_lock_orange.png b/static/assets/timeturner_lock_orange.png new file mode 100644 index 0000000000000000000000000000000000000000..836a376e487b3a01a8631ceb197d1353c4b2a61b GIT binary patch literal 805 zcmV+=1KRwFP)X_(hvdzs|+T%Ajnqz2S7%RCQSzt%P2{!_J>^0q2Xwy74Yu8)4TgjZTfoO z@2B7Q-o1WLF${xmPp*#xE5IP`Mp7ctuz&+k$HT9B556k)Z0n9$fWwD?Hd z&-MI1)t%?mk3NadMRb6!M4Is9mmOsRwD<@;vv251q(!GAw!&SBG@<)T=6j=3LjUj< zz0jzn8XX{zNTYQ30+?fezTXm-lTw-+GDKWhjm8Mi?wT@sexJ&VM;PT|YXaduLSqXA zZa1oHR7#YWM}@6xVN*Jonr@LTuRlB|3t*IsR5s^1`S=b1rMs74YFboU;)JzZ_srMT zS5Ge-U)`K<`(3*cBCN%eE!Dlu^fbyv^!z^1sIuBWEFv}WwXU>?&G334n1d zW(`X{cwB^@-@DQe^f!3!Xz2^P9oyEh7EhWRjv71LVFNbYV(eVoww$zrscC|#DLeA% zNG-cYExT&@I+mxj67CZ&3lL0Ap@jP^U&perr~`tiSf1Ood||iah_G>7pnmk(iO}+e zPY$;DzIcZp%cGnei0f>0Lb`Bs`=+|HC_B7-;pSV*>B&rLeAxj*V+&`mA1wW0q!^?N zpLR6>>ILzA=--hp+}vyz{b#=&(uI|1>@0Xp-m&x)=`EhX&Aar>?gF6p-;f>NnVyyt zqGoKzB0GdH>~<^*%Xp@1XDrLYM!85WyJk6&i>PJS+CGfCDz-f?ZSGpy%|F+*{GaeQ zEw|$fyPOR#32#I)xk;}GZ$vV=N$|#$Hf6#6!Y#7U5I%G!7vY?fg@*8q{9J_7Ru({w jWOCcU1n#MHM_GVhMYY;9vh23(00000NkvXXu0mjfEen1m literal 0 HcmV?d00001 diff --git a/static/assets/timeturner_lock_red.png b/static/assets/timeturner_lock_red.png new file mode 100644 index 0000000000000000000000000000000000000000..aa8740d6435fb72978d71623c98aa5745ea368e4 GIT binary patch literal 804 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%0ABF2-%um91fOQdj+!kkGG8W(_SZl6VF>HeS!T8_9 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
-

LTC Status

+

LTC Input

--:--:--:--

@@ -34,7 +34,7 @@
-

System Clock

+

NTP Clock

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

---- -- --

@@ -43,7 +43,7 @@
-

Delta Value: -- ms (-- frames)

+

Δ -- ms (-- frames)

diff --git a/static/style.css b/static/style.css index 0c6d4e3..51bc097 100644 --- a/static/style.css +++ b/static/style.css @@ -15,7 +15,7 @@ body { background-repeat: no-repeat; background-position: bottom 20px right 20px; background-attachment: fixed; - background-size: 150px; + background-size: 300px; color: #333; margin: 0; padding: 20px; From 89cf0e5d97dfdfd306568a116e282846a3d7d27f Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:23:34 +0100 Subject: [PATCH 080/139] =?UTF-8?q?added=20=CE=94=20to=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/script.js b/static/script.js index d489585..634ed33 100644 --- a/static/script.js +++ b/static/script.js @@ -1,4 +1,4 @@ -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener('DOMContentLoaded', () => { // --- Mock Data Configuration --- // Set to true to use mock data, false for live API. const useMockData = false; @@ -138,7 +138,7 @@ document.addEventListener('DOMContentLoaded', () => { statusElements.deltaStatus.innerHTML = ``; const deltaTextValue = `${data.timecode_delta_ms} ms (${data.timecode_delta_frames} frames)`; - statusElements.deltaText.textContent = `Delta Value: ${deltaTextValue}`; + statusElements.deltaText.textContent = `Δ ${deltaTextValue}`; const jitterStatus = data.jitter_status || 'UNKNOWN'; const jitterIconInfo = iconMap.jitterStatus[jitterStatus] || iconMap.jitterStatus.default; From b510af2d8dba5383668f69cc36eba8e090fadb65 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Fri, 8 Aug 2025 01:35:00 +0100 Subject: [PATCH 081/139] fix: Wrap long log entries in log box Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/style.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static/style.css b/static/style.css index 51bc097..f9bcd58 100644 --- a/static/style.css +++ b/static/style.css @@ -176,6 +176,11 @@ button:hover { gap: 15px; } +.log-box { + white-space: pre-wrap; + overflow-wrap: break-word; +} + .collapsible-content { display: none; padding: 20px; From 95fcb6f26a012904fb643ed600e3f4aa1b7e902d Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 12 Aug 2025 15:58:21 +0100 Subject: [PATCH 082/139] feat: Add systemd service for TimeTurner auto-start Co-authored-by: aider (gemini/gemini-2.5-pro) --- timeturner.service | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 timeturner.service diff --git a/timeturner.service b/timeturner.service new file mode 100644 index 0000000..443e99c --- /dev/null +++ b/timeturner.service @@ -0,0 +1,15 @@ +[Unit] +Description=NTP TimeTurner Daemon +After=network.target + +[Service] +Type=forking +ExecStart=/opt/timeturner/timeturner daemon +WorkingDirectory=/opt/timeturner +PIDFile=/opt/timeturner/ntp_timeturner.pid +Restart=always +User=root +Group=root + +[Install] +WantedBy=multi-user.target From af0a5121873828379593b3ad55c4dae563bbeadb Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 12 Aug 2025 16:00:43 +0100 Subject: [PATCH 083/139] docs: Document web interface and clarify API server startup Co-authored-by: aider (gemini/gemini-2.5-pro) --- README.md | 11 +++++++++++ src/main.rs | 15 +++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8a7b357..40c9127 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,17 @@ Created by Chris Frankland-Wright and John Rogers - Broadcasts time via local NTP server - Supports configurable time offsets (hours, minutes, seconds, frames or milliseconds) - Systemd service support for headless operation +- Web-based UI for monitoring and control when running as a daemon + +--- + +## 🖥️ Web Interface & API + +When running as a background daemon, TimeTurner provides a web interface for monitoring and configuration. + +- **Access**: The web UI is available at `http://:8080`. +- **Functionality**: You can view the real-time sync status, see logs, and change all configuration options directly from your browser. +- **API**: A JSON API is also exposed for programmatic access. See `docs/api.md` for full details. --- diff --git a/src/main.rs b/src/main.rs index ab9fa94..8006681 100644 --- a/src/main.rs +++ b/src/main.rs @@ -193,11 +193,13 @@ async fn main() { }); } - // 5️⃣ Spawn UI or setup daemon logging + // 5️⃣ Spawn UI or setup daemon logging. The web service is only started + // when running as a daemon. The TUI is for interactive foreground use. if args.command.is_none() { + // --- Interactive TUI Mode --- log::info!("🔧 Watching config.yml..."); log::info!("🚀 Serial thread launched"); - log::info!("🖥️ UI thread launched"); + log::info!("🖥️ UI thread launched"); let ui_state = ltc_state.clone(); let config_clone = config.clone(); let port = serial_port_path; @@ -205,8 +207,10 @@ async fn main() { start_ui(ui_state, port, config_clone); }); } else { + // --- Daemon Mode --- // In daemon mode, logging is already set up to go to stderr. - // The systemd service will capture it. + // The systemd service will capture it. The web service (API and static files) + // is launched later in the main async block. log::info!("🚀 Starting TimeTurner daemon..."); } @@ -282,7 +286,10 @@ async fn main() { let local = LocalSet::new(); local .run_until(async move { - // 8️⃣ Spawn the API server thread + // 8️⃣ Spawn the API server task. + // This server provides the JSON API and serves the static web UI files + // from the `static/` directory. It runs in both TUI and daemon modes, + // but is primarily for the web UI used in daemon mode. { let api_state = ltc_state.clone(); let config_clone = config.clone(); From 8e369a2e3a7f71043c643f976d5a02655390b254 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 12 Aug 2025 16:02:25 +0100 Subject: [PATCH 084/139] fix: Ensure static web assets are installed and clarify service config Co-authored-by: aider (gemini/gemini-2.5-pro) --- setup.sh | 8 +++++--- timeturner.service | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/setup.sh b/setup.sh index 4dd5685..b8d556f 100644 --- a/setup.sh +++ b/setup.sh @@ -21,11 +21,13 @@ echo "🔧 Creating directories..." sudo mkdir -p $INSTALL_DIR echo "✅ Directory $INSTALL_DIR created." -# 3. Install binary -echo "🚀 Installing timeturner binary..." +# 3. Install binary and static web files +echo "🚀 Installing timeturner binary and web assets..." sudo cp target/release/ntp_timeturner $INSTALL_DIR/timeturner +# The static directory contains the web UI files +sudo cp -r static $INSTALL_DIR/ sudo ln -sf $INSTALL_DIR/timeturner $BIN_DIR/timeturner -echo "✅ Binary installed to $INSTALL_DIR and linked to $BIN_DIR." +echo "✅ Binary and assets installed to $INSTALL_DIR, and binary linked to $BIN_DIR." # 4. Install systemd service file if [[ "$(uname)" == "Linux" ]]; then diff --git a/timeturner.service b/timeturner.service index 443e99c..f3daec8 100644 --- a/timeturner.service +++ b/timeturner.service @@ -4,6 +4,9 @@ After=network.target [Service] Type=forking +# The 'timeturner daemon' command starts the background process. +# It requires 'config.yml' and the 'static/' web assets directory +# to be present in the WorkingDirectory. ExecStart=/opt/timeturner/timeturner daemon WorkingDirectory=/opt/timeturner PIDFile=/opt/timeturner/ntp_timeturner.pid From 1075be6e241dfa6b2653cff66ab2dc09da09230e Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 12 Aug 2025 16:06:38 +0100 Subject: [PATCH 085/139] hide sections --- static/index.html | 120 +++++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/static/index.html b/static/index.html index 7178fb9..92a02b3 100644 --- a/static/index.html +++ b/static/index.html @@ -62,71 +62,71 @@

Controls

-
- - + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + +
+ +
+ + + + +
-
- - -
-
- -
-
- - +
+ + +
+
+ Logs Icon +

Logs

-
- - -
-
- - -
-
- - -
-
- - +
+

                         
-
- - - -
-
- - - - - -
-
- - - - -
-
-
- - -
-
- Logs Icon -

Logs

-
-
-

-                
-
-

Built by Chris Frankland-Wright and John Rogers | Have Blue Broadcast Media | From 048ae4173988292af85ee6ec3af90671677e180d Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 12 Aug 2025 16:13:35 +0100 Subject: [PATCH 086/139] feat: Restore hardware offset, auto sync, and nudge controls --- static/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/index.html b/static/index.html index 92a02b3..edc555d 100644 --- a/static/index.html +++ b/static/index.html @@ -62,14 +62,14 @@

Controls

- +
@@ -100,13 +100,13 @@
- +
From acab0fbc044cdafeafe94ea3f23774bdd449ea8a Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 12 Aug 2025 16:13:43 +0100 Subject: [PATCH 087/139] style: Hide hardware offset, auto sync, and nudge controls Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/index.html b/static/index.html index edc555d..344e8b5 100644 --- a/static/index.html +++ b/static/index.html @@ -62,11 +62,11 @@

Controls

-
+ -
+ @@ -100,7 +100,7 @@
-
+
-
-
- - +
+
+ +
-
- - +
+ +
-
- - +
+ +
-
- - +
+ +
-
- - +
+ +
diff --git a/static/style.css b/static/style.css index 2d7c0a3..2266add 100644 --- a/static/style.css +++ b/static/style.css @@ -132,6 +132,33 @@ button:hover { background-color: #166999; } +.offset-controls-container { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + align-items: center; +} + +.offset-control { + display: flex; + align-items: center; + gap: 5px; +} + +.offset-control input[type="number"] { + width: 40px; + text-align: center; +} + +.offset-control label { + font-size: 14px; + color: #333; +} + +#offset-ms { + width: 60px; +} + #sync-message { font-style: italic; color: #555; From b03d935a9e0a237aecc8cc416f46b6dd6a4f83d0 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 12 Aug 2025 16:25:58 +0100 Subject: [PATCH 090/139] style: Reduce background image size --- static/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/style.css b/static/style.css index 2266add..6c4c6d3 100644 --- a/static/style.css +++ b/static/style.css @@ -15,7 +15,7 @@ body { background-repeat: no-repeat; background-position: bottom 20px right 20px; background-attachment: fixed; - background-size: 300px; + background-size: 100px; color: #333; margin: 0; padding: 20px; From 5d206b564bb40710ecf3f34eb6a6fce27a465042 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 12 Aug 2025 16:26:03 +0100 Subject: [PATCH 091/139] style: Set button font to Arial Co-authored-by: aider (gemini/gemini-2.5-pro) --- static/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/style.css b/static/style.css index 6c4c6d3..bc53cce 100644 --- a/static/style.css +++ b/static/style.css @@ -123,7 +123,7 @@ button { color: white; cursor: pointer; font-size: 14px; - font-family: inherit; + font-family: Arial, sans-serif; font-weight: bold; transition: background-color 0.2s; } From 4d0b4ebae44b4da81a438e7298eb7c29dd898cfd Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 12 Aug 2025 16:27:44 +0100 Subject: [PATCH 092/139] docs: Detail `setup.sh` installation process in README Co-authored-by: aider (gemini/gemini-2.5-pro) --- README.md | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 40c9127..6a46e29 100644 --- a/README.md +++ b/README.md @@ -48,19 +48,42 @@ When running as a background daemon, TimeTurner provides a web interface for mon --- -## 🚀 Installation (to update) +## 🚀 Installation -For Rust install you can do -```bash -cargo install --git https://github.com/cjfranko/NTP-Timeturner -``` -Clone and run the installer: +The `setup.sh` script is provided to compile and install the TimeTurner application and its systemd service on a Debian-based system like Raspberry Pi OS. -```bash -wget https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/master/setup.sh -chmod +x setup.sh -./setup.sh -``` +### Prerequisites + +- **Rust and Cargo**: The script requires the Rust programming language toolchain. If you don't have it, install it from [rustup.rs](https://rustup.rs/). + +### Running the Installer + +1. First, clone the repository: + ```bash + git clone https://github.com/cjfranko/NTP-Timeturner.git + cd NTP-Timeturner + ``` +2. Make the script executable and run it. The script will use `sudo` for commands that require root privileges, so it may ask for your password. + ```bash + chmod +x setup.sh + ./setup.sh + ``` + +### What the Script Does + +The installation script automates the following steps: + +1. **Compiles the Binary**: Runs `cargo build --release` to create an optimised executable. +2. **Creates Directories**: Creates `/opt/timeturner` to store the application files. +3. **Installs Files**: + - The compiled binary is copied to `/opt/timeturner/timeturner`. + - The web interface assets from the `static/` directory are copied to `/opt/timeturner/static`. + - A symbolic link is created from `/usr/local/bin/timeturner` to the binary, allowing it to be run from any location. +4. **Sets up Systemd Service**: + - Copies the `timeturner.service` file to `/etc/systemd/system/`. + - Enables the service to start automatically on system boot. + +After installation is complete, the script will provide instructions to start the service manually or to run the application in its interactive terminal mode. --- ## 🕰️ Chrony NTP From a009dd35c9ef729ea5558d9a618c80c81adaa3c8 Mon Sep 17 00:00:00 2001 From: Chris Frankland-Wright Date: Tue, 12 Aug 2025 16:28:32 +0100 Subject: [PATCH 093/139] updated web ui --- static/index.html | 4 +- static/index_dev.html | 141 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 static/index_dev.html diff --git a/static/index.html b/static/index.html index 1e8d400..deb683d 100644 --- a/static/index.html +++ b/static/index.html @@ -96,8 +96,8 @@
- - + +