diff --git a/README.md b/README.md index 8a7b357..115e262 100644 --- a/README.md +++ b/README.md @@ -29,16 +29,9 @@ Created by Chris Frankland-Wright and John Rogers --- -## 🛠️ 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 cargo install --git https://github.com/cjfranko/NTP-Timeturner diff --git a/docs/api.md b/docs/api.md index 6657028..2959c95 100644 --- a/docs/api.md +++ b/docs/api.md @@ -4,22 +4,17 @@ This document describes the HTTP API for the NTP Timeturner application. ## Endpoints -### Status and Logs +### Status - **`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"` + Retrieves the real-time status of the LTC reader and system clock synchronization. **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", @@ -30,23 +25,11 @@ 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": 20 + "hardware_offset_ms": 0 } ``` -- **`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 +### Sync - **`POST /api/sync`** @@ -54,7 +37,7 @@ This document describes the HTTP API for the NTP Timeturner application. **Request Body:** None - **Success Response (200 OK):** + **Success Response:** ```json { "status": "success", @@ -62,14 +45,13 @@ This document describes the HTTP API for the NTP Timeturner application. } ``` - **Error Response (400 Bad Request):** + **Error Responses:** ```json { "status": "error", "message": "No LTC timecode available to sync to." } ``` - **Error Response (500 Internal Server Error):** ```json { "status": "error", @@ -77,32 +59,6 @@ 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. @@ -114,7 +70,7 @@ This document describes the HTTP API for the NTP Timeturner application. } ``` - **Success Response (200 OK):** + **Success Response:** ```json { "status": "success", @@ -122,7 +78,7 @@ This document describes the HTTP API for the NTP Timeturner application. } ``` - **Error Response (500 Internal Server Error):** + **Error Response:** ```json { "status": "error", @@ -134,63 +90,29 @@ This document describes the HTTP API for the NTP Timeturner application. - **`GET /api/config`** - Retrieves the current application configuration from `config.yml`. + Retrieves the current application configuration. - **Example Response (200 OK):** + **Example Response:** ```json { - "hardwareOffsetMs": 20, - "timeturnerOffset": { - "hours": 0, - "minutes": 0, - "seconds": 0, - "frames": 0, - "milliseconds": 0 - }, - "defaultNudgeMs": 2, - "autoSyncEnabled": false + "hardware_offset_ms": 0 } ``` - **`POST /api/config`** - Updates the application configuration. The new configuration is persisted to `config.yml` and takes effect immediately. + Updates the `hardware_offset_ms` configuration. The new value is persisted to `config.json` and reloaded by the application automatically. **Example Request:** ```json { - "hardwareOffsetMs": 55, - "timeturnerOffset": { - "hours": 1, - "minutes": 2, - "seconds": 3, - "frames": 4, - "milliseconds": 5 - }, - "defaultNudgeMs": 2, - "autoSyncEnabled": true + "hardware_offset_ms": 10 } ``` - **Success Response (200 OK):** (Returns the updated configuration) + **Success Response:** ```json { - "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" + "hardware_offset_ms": 10 } ``` diff --git a/src/api.rs b/src/api.rs index 14b0da4..a622a0b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -47,11 +47,7 @@ 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| { - let sep = if f.is_drop_frame { ';' } else { ':' }; - format!( - "{:02}:{:02}:{:02}{}{:02}", - f.hours, f.minutes, f.seconds, sep, f.frames - ) + 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.to_f64().unwrap_or(0.0)) @@ -246,7 +242,6 @@ mod tests { minutes: 2, seconds: 3, frames: 4, - is_drop_frame: false, frame_rate: Ratio::new(25, 1), timestamp: Utc::now(), }), @@ -295,32 +290,6 @@ 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/src/config.rs b/src/config.rs index 8669e62..974d60b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -64,7 +64,6 @@ impl Config { Self::default() }) } - } impl Default for Config { diff --git a/src/main.rs b/src/main.rs index ab9fa94..e265210 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,6 @@ use crate::sync_logic::LtcState; use crate::ui::start_ui; use clap::Parser; use daemonize::Daemonize; -use serialport; use std::{ fs, @@ -36,8 +35,6 @@ 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. @@ -73,85 +70,30 @@ 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. let log_buffer = logger::setup_logger(); let args = Args::parse(); - if let Some(command) = &args.command { - match command { - Command::Daemon => { - log::info!("🚀 Starting daemon..."); + if let Some(Command::Daemon) = &args.command { + 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 - } - } - } - 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("-9").arg(format!("-{}", 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; + match daemonize.start() { + Ok(_) => { /* Process is now daemonized */ } + Err(e) => { + log::error!("Error daemonizing: {}", e); + return; // Exit if daemonization fails } } } @@ -168,23 +110,13 @@ async fn main() { // 3️⃣ Shared state for UI and serial reader let ltc_state = Arc::new(Mutex::new(LtcState::new())); - // 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); - + // 4️⃣ Spawn the serial reader thread { let tx_clone = tx.clone(); let state_clone = ltc_state.clone(); - let port_clone = serial_port_path.clone(); thread::spawn(move || { start_serial_thread( - &port_clone, + "/dev/ttyACM0", 115200, tx_clone, state_clone, @@ -200,7 +132,7 @@ async fn main() { log::info!("🖥️ UI thread launched"); let ui_state = ltc_state.clone(); let config_clone = config.clone(); - let port = serial_port_path; + let port = "/dev/ttyACM0".to_string(); thread::spawn(move || { start_ui(ui_state, port, config_clone); }); diff --git a/src/serial_input.rs b/src/serial_input.rs index d1dea36..b65cd5f 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(); @@ -65,7 +65,7 @@ mod tests { 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() } diff --git a/src/sync_logic.rs b/src/sync_logic.rs index c6a3e80..630e879 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -24,7 +24,6 @@ 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 } @@ -36,9 +35,8 @@ impl LtcFrame { hours: caps[2].parse().ok()?, minutes: caps[3].parse().ok()?, seconds: caps[4].parse().ok()?, - is_drop_frame: &caps[5] == ";", - frames: caps[6].parse().ok()?, - frame_rate: get_frame_rate_ratio(&caps[7])?, + frames: caps[5].parse().ok()?, + frame_rate: get_frame_rate_ratio(&caps[6])?, timestamp, }) } @@ -207,7 +205,6 @@ 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 8db481d..7d089e6 100644 --- a/src/system.rs +++ b/src/system.rs @@ -45,13 +45,21 @@ pub fn calculate_target_time(frame: &LtcFrame, config: &Config) -> DateTime 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(&datetime_str) + .arg(date) .status() .map(|s| s.success()) .unwrap_or(false); if success { - log::info!("Set system date and time to {}", datetime_str); + log::info!("Set system date to {}", date); Ok(()) } else { - log::error!("Failed to set system date and time"); + log::error!("Failed to set system date"); Err(()) } } @@ -189,7 +196,6 @@ mod tests { minutes: m, seconds: s, frames: f, - is_drop_frame: false, frame_rate: Ratio::new(25, 1), timestamp: Utc::now(), } diff --git a/static/assets/FuturaStdHeavy.otf b/static/assets/FuturaStdHeavy.otf deleted file mode 100644 index 7b8c22d..0000000 Binary files a/static/assets/FuturaStdHeavy.otf and /dev/null differ diff --git a/static/assets/HaveBlueTransWh.png b/static/assets/HaveBlueTransWh.png deleted file mode 100644 index d9a123d..0000000 Binary files a/static/assets/HaveBlueTransWh.png and /dev/null differ diff --git a/static/assets/favicon.png b/static/assets/favicon.png deleted file mode 100644 index 3683c35..0000000 Binary files a/static/assets/favicon.png and /dev/null differ diff --git a/static/assets/header.png b/static/assets/header.png deleted file mode 100644 index f1677ed..0000000 Binary files a/static/assets/header.png and /dev/null differ diff --git a/static/assets/quartz-ms-regular.ttf b/static/assets/quartz-ms-regular.ttf deleted file mode 100644 index 15c7ce4..0000000 Binary files a/static/assets/quartz-ms-regular.ttf and /dev/null differ diff --git a/static/assets/timeturner_2398.png b/static/assets/timeturner_2398.png deleted file mode 100644 index 763bcba..0000000 Binary files a/static/assets/timeturner_2398.png and /dev/null differ diff --git a/static/assets/timeturner_24.png b/static/assets/timeturner_24.png deleted file mode 100644 index ffc75d0..0000000 Binary files a/static/assets/timeturner_24.png and /dev/null differ diff --git a/static/assets/timeturner_25.png b/static/assets/timeturner_25.png deleted file mode 100644 index 3b44c93..0000000 Binary files a/static/assets/timeturner_25.png and /dev/null differ diff --git a/static/assets/timeturner_2997.png b/static/assets/timeturner_2997.png deleted file mode 100644 index 0bd27fd..0000000 Binary files a/static/assets/timeturner_2997.png and /dev/null differ diff --git a/static/assets/timeturner_2997DF.png b/static/assets/timeturner_2997DF.png deleted file mode 100644 index bf03215..0000000 Binary files a/static/assets/timeturner_2997DF.png and /dev/null differ diff --git a/static/assets/timeturner_30.png b/static/assets/timeturner_30.png deleted file mode 100644 index 4ce0211..0000000 Binary files a/static/assets/timeturner_30.png and /dev/null differ diff --git a/static/assets/timeturner_controls.png b/static/assets/timeturner_controls.png deleted file mode 100644 index a91f39b..0000000 Binary files a/static/assets/timeturner_controls.png and /dev/null differ diff --git a/static/assets/timeturner_default.png b/static/assets/timeturner_default.png deleted file mode 100644 index 734aa8d..0000000 Binary files a/static/assets/timeturner_default.png and /dev/null differ diff --git a/static/assets/timeturner_delta_green.png b/static/assets/timeturner_delta_green.png deleted file mode 100644 index ddc84b9..0000000 Binary files a/static/assets/timeturner_delta_green.png and /dev/null differ diff --git a/static/assets/timeturner_delta_orange.png b/static/assets/timeturner_delta_orange.png deleted file mode 100644 index 64e9776..0000000 Binary files a/static/assets/timeturner_delta_orange.png and /dev/null differ diff --git a/static/assets/timeturner_delta_red.png b/static/assets/timeturner_delta_red.png deleted file mode 100644 index c7272ac..0000000 Binary files a/static/assets/timeturner_delta_red.png and /dev/null differ diff --git a/static/assets/timeturner_jitter_green.png b/static/assets/timeturner_jitter_green.png deleted file mode 100644 index 8cc64e3..0000000 Binary files a/static/assets/timeturner_jitter_green.png and /dev/null differ diff --git a/static/assets/timeturner_jitter_orange.png b/static/assets/timeturner_jitter_orange.png deleted file mode 100644 index 96c5f84..0000000 Binary files a/static/assets/timeturner_jitter_orange.png and /dev/null differ diff --git a/static/assets/timeturner_jitter_red.png b/static/assets/timeturner_jitter_red.png deleted file mode 100644 index 8813159..0000000 Binary files a/static/assets/timeturner_jitter_red.png and /dev/null differ diff --git a/static/assets/timeturner_lock_green.png b/static/assets/timeturner_lock_green.png deleted file mode 100644 index 0659c60..0000000 Binary files a/static/assets/timeturner_lock_green.png and /dev/null differ diff --git a/static/assets/timeturner_lock_orange.png b/static/assets/timeturner_lock_orange.png deleted file mode 100644 index 836a376..0000000 Binary files a/static/assets/timeturner_lock_orange.png and /dev/null differ diff --git a/static/assets/timeturner_lock_red.png b/static/assets/timeturner_lock_red.png deleted file mode 100644 index aa8740d..0000000 Binary files a/static/assets/timeturner_lock_red.png and /dev/null differ diff --git a/static/assets/timeturner_logs.png b/static/assets/timeturner_logs.png deleted file mode 100644 index 6bdd935..0000000 Binary files a/static/assets/timeturner_logs.png and /dev/null differ diff --git a/static/assets/timeturner_ltc_green.png b/static/assets/timeturner_ltc_green.png deleted file mode 100644 index 4329913..0000000 Binary files a/static/assets/timeturner_ltc_green.png and /dev/null differ diff --git a/static/assets/timeturner_ltc_orange.png b/static/assets/timeturner_ltc_orange.png deleted file mode 100644 index b060ac2..0000000 Binary files a/static/assets/timeturner_ltc_orange.png and /dev/null differ diff --git a/static/assets/timeturner_ltc_red.png b/static/assets/timeturner_ltc_red.png deleted file mode 100644 index a8e7f96..0000000 Binary files a/static/assets/timeturner_ltc_red.png and /dev/null differ diff --git a/static/assets/timeturner_network.png b/static/assets/timeturner_network.png deleted file mode 100644 index 06ec4b9..0000000 Binary files a/static/assets/timeturner_network.png and /dev/null differ diff --git a/static/assets/timeturner_ntp_green.png b/static/assets/timeturner_ntp_green.png deleted file mode 100644 index caf824d..0000000 Binary files a/static/assets/timeturner_ntp_green.png and /dev/null differ diff --git a/static/assets/timeturner_ntp_orange.png b/static/assets/timeturner_ntp_orange.png deleted file mode 100644 index 88319b5..0000000 Binary files a/static/assets/timeturner_ntp_orange.png and /dev/null differ diff --git a/static/assets/timeturner_ntp_red.png b/static/assets/timeturner_ntp_red.png deleted file mode 100644 index 16e66ee..0000000 Binary files a/static/assets/timeturner_ntp_red.png and /dev/null differ diff --git a/static/assets/timeturner_sync_green.png b/static/assets/timeturner_sync_green.png deleted file mode 100644 index 9b4988e..0000000 Binary files a/static/assets/timeturner_sync_green.png and /dev/null differ diff --git a/static/assets/timeturner_sync_orange.png b/static/assets/timeturner_sync_orange.png deleted file mode 100644 index 0b41130..0000000 Binary files a/static/assets/timeturner_sync_orange.png and /dev/null differ diff --git a/static/assets/timeturner_sync_red.png b/static/assets/timeturner_sync_red.png deleted file mode 100644 index 1c4c4c9..0000000 Binary files a/static/assets/timeturner_sync_red.png and /dev/null differ diff --git a/static/assets/timeturner_timeturning.png b/static/assets/timeturner_timeturning.png deleted file mode 100644 index fd3eaeb..0000000 Binary files a/static/assets/timeturner_timeturning.png and /dev/null differ diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index 83d6317..0000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/icon-map.js b/static/icon-map.js deleted file mode 100644 index 64336b3..0000000 --- a/static/icon-map.js +++ /dev/null @@ -1,43 +0,0 @@ -// In this file, you can define the paths to your local icon image files. -const iconMap = { - ltcStatus: { - 'LOCK': { src: 'assets/timeturner_ltc_green.png', tooltip: 'LTC signal is locked and stable.' }, - 'FREE': { src: 'assets/timeturner_ltc_orange.png', tooltip: 'LTC signal is in freewheel mode.' }, - 'default': { src: 'assets/timeturner_ltc_red.png', tooltip: 'LTC signal is not detected.' } - }, - ntpActive: { - true: { src: 'assets/timeturner_ntp_green.png', tooltip: 'NTP service is active.' }, - false: { src: 'assets/timeturner_ntp_red.png', tooltip: 'NTP service is inactive.' } - }, - syncStatus: { - 'IN SYNC': { src: 'assets/timeturner_sync_green.png', tooltip: 'System clock is in sync with LTC source.' }, - 'CLOCK AHEAD': { src: 'assets/timeturner_sync_orange.png', tooltip: 'System clock is ahead of the LTC source.' }, - 'CLOCK BEHIND': { src: 'assets/timeturner_sync_orange.png', tooltip: 'System clock is behind the LTC source.' }, - 'TIMETURNING': { src: 'assets/timeturner_timeturning.png', tooltip: 'Timeturner offset is active.' }, - 'default': { src: 'assets/timeturner_sync_red.png', tooltip: 'Sync status is unknown.' } - }, - jitterStatus: { - 'GOOD': { src: 'assets/timeturner_jitter_green.png', tooltip: 'Clock jitter is within acceptable limits.' }, - 'AVERAGE': { src: 'assets/timeturner_jitter_orange.png', tooltip: 'Clock jitter is moderate.' }, - 'BAD': { src: 'assets/timeturner_jitter_red.png', tooltip: 'Clock jitter is high and may affect accuracy.' }, - 'default': { src: 'assets/timeturner_jitter_red.png', tooltip: 'Jitter status is unknown.' } - }, - deltaStatus: { - 'good': { src: 'assets/timeturner_delta_green.png', tooltip: 'Clock delta is 0ms.' }, - 'average': { src: 'assets/timeturner_delta_orange.png', tooltip: 'Clock delta is less than 10ms.' }, - 'bad': { src: 'assets/timeturner_delta_red.png', tooltip: 'Clock delta is 10ms or greater.' } - }, - frameRate: { - '23.98fps': { src: 'assets/timeturner_2398.png', tooltip: '23.98 frames per second' }, - '24.00fps': { src: 'assets/timeturner_24.png', tooltip: '24.00 frames per second' }, - '25.00fps': { src: 'assets/timeturner_25.png', tooltip: '25.00 frames per second' }, - '29.97fps': { src: 'assets/timeturner_2997.png', tooltip: '29.97 frames per second' }, - '30.00fps': { src: 'assets/timeturner_30.png', tooltip: '30.00 frames per second' }, - 'default': { src: 'assets/timeturner_default.png', tooltip: 'Unknown frame rate' } - }, - lockRatio: { - 'good': { src: 'assets/timeturner_lock_green.png', tooltip: 'Lock ratio is 100%.' }, - 'average': { src: 'assets/timeturner_lock_orange.png', tooltip: 'Lock ratio is 90% or higher.' }, - 'bad': { src: 'assets/timeturner_lock_red.png', tooltip: 'Lock ratio is below 90%.' } - } -}; diff --git a/static/index.html b/static/index.html index 7178fb9..26bf760 100644 --- a/static/index.html +++ b/static/index.html @@ -5,63 +5,47 @@ NTP TimeTurner -
- - - - - +

NTP TimeTurner

-

LTC Input

+

LTC Status

--:--:--:--

-
- - - -
+

--

+

-- fps

+

Lock Ratio: --%

-

NTP Clock

+

System Clock

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

-

---- -- --

-
- - - - -
-

Δ -- ms (-- frames)

+

Date: ---- -- --

+

NTP Service: --

+

Sync Status: --

+
+ + +
+

Clock Offset

+

Delta: -- ms (-- frames)

+

Jitter: --

-
- Network Icon -

Network

-
-

--

+

Network

+
    +
  • --
  • +
-
-
- Controls Icon -

Controls

-
-
+
+

Controls

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

Logs

-
-
-

-                
+
+

Logs

+

             
-
- - diff --git a/static/mock-data.js b/static/mock-data.js deleted file mode 100644 index a953e59..0000000 --- a/static/mock-data.js +++ /dev/null @@ -1,168 +0,0 @@ -// This file contains mock data sets for UI development and testing without a live backend. -const mockApiDataSets = { - allGood: { - status: { - ltc_status: 'LOCK', - ltc_timecode: '10:20:30:00', - frame_rate: '25.00fps', - lock_ratio: 99.5, - system_clock: '10:20:30.500', - system_date: '2025-08-07', - ntp_active: true, - sync_status: 'IN SYNC', - timecode_delta_ms: 5, - timecode_delta_frames: 0.125, - jitter_status: 'GOOD', - interfaces: ['192.168.1.100/24 (eth0)', '10.0.0.5/8 (wlan0)'], - }, - config: { - hardwareOffsetMs: 10, - autoSyncEnabled: true, - defaultNudgeMs: 2, - timeturnerOffset: { hours: 1, minutes: 2, seconds: 3, frames: 4, milliseconds: 50 }, - }, - logs: [ - '2025-08-07 10:20:30 [INFO] Starting up...', - '2025-08-07 10:20:32 [INFO] LTC LOCK detected. Frame rate: 25.00fps.', - '2025-08-07 10:20:35 [INFO] Initial sync complete. Clock adjusted by -15ms.', - ] - }, - ltcFree: { - status: { - ltc_status: 'FREE', - ltc_timecode: '11:22:33:11', - frame_rate: '25.00fps', - lock_ratio: 40.2, - system_clock: '11:22:33.800', - system_date: '2025-08-07', - ntp_active: true, - sync_status: 'IN SYNC', - timecode_delta_ms: 3, - timecode_delta_frames: 0.075, - jitter_status: 'GOOD', - interfaces: ['192.168.1.100/24 (eth0)'], - }, - config: { - hardwareOffsetMs: 10, - autoSyncEnabled: true, - defaultNudgeMs: 2, - timeturnerOffset: { hours: 0, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 }, - }, - logs: [ '2025-08-07 11:22:30 [WARN] LTC signal lost, entering freewheel.' ] - }, - clockAhead: { - status: { - ltc_status: 'LOCK', - ltc_timecode: '12:00:05:00', - frame_rate: '25.00fps', - lock_ratio: 98.1, - system_clock: '12:00:04.500', - system_date: '2025-08-07', - ntp_active: true, - sync_status: 'CLOCK AHEAD', - timecode_delta_ms: -500, - timecode_delta_frames: -12.5, - jitter_status: 'AVERAGE', - interfaces: ['192.168.1.100/24 (eth0)'], - }, - config: { - hardwareOffsetMs: 10, - autoSyncEnabled: true, - defaultNudgeMs: 2, - timeturnerOffset: { hours: 0, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 }, - }, - logs: [ '2025-08-07 12:00:00 [WARN] System clock is ahead of LTC source by 500ms.' ] - }, - clockBehind: { - status: { - ltc_status: 'LOCK', - ltc_timecode: '13:30:10:00', - frame_rate: '25.00fps', - lock_ratio: 99.9, - system_clock: '13:30:10.800', - system_date: '2025-08-07', - ntp_active: true, - sync_status: 'CLOCK BEHIND', - timecode_delta_ms: 800, - timecode_delta_frames: 20, - jitter_status: 'AVERAGE', - interfaces: ['192.168.1.100/24 (eth0)'], - }, - config: { - hardwareOffsetMs: 10, - autoSyncEnabled: true, - defaultNudgeMs: 2, - timeturnerOffset: { hours: 0, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 }, - }, - logs: [ '2025-08-07 13:30:00 [WARN] System clock is behind LTC source by 800ms.' ] - }, - timeturning: { - status: { - ltc_status: 'LOCK', - ltc_timecode: '14:00:00:00', - frame_rate: '25.00fps', - lock_ratio: 100, - system_clock: '15:02:03.050', - system_date: '2025-08-07', - ntp_active: true, - sync_status: 'TIMETURNING', - timecode_delta_ms: 3723050, // a big number - timecode_delta_frames: 93076, - jitter_status: 'GOOD', - interfaces: ['192.168.1.100/24 (eth0)'], - }, - config: { - hardwareOffsetMs: 10, - autoSyncEnabled: false, - defaultNudgeMs: 2, - timeturnerOffset: { hours: 1, minutes: 2, seconds: 3, frames: 4, milliseconds: 50 }, - }, - logs: [ '2025-08-07 14:00:00 [INFO] Timeturner offset is active.' ] - }, - badJitter: { - status: { - ltc_status: 'LOCK', - ltc_timecode: '15:15:15:15', - frame_rate: '25.00fps', - lock_ratio: 95.0, - system_clock: '15:15:15.515', - system_date: '2025-08-07', - ntp_active: true, - sync_status: 'IN SYNC', - timecode_delta_ms: 10, - timecode_delta_frames: 0.25, - jitter_status: 'BAD', - interfaces: ['192.168.1.100/24 (eth0)'], - }, - config: { - hardwareOffsetMs: 10, - autoSyncEnabled: true, - defaultNudgeMs: 2, - timeturnerOffset: { hours: 0, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 }, - }, - logs: [ '2025-08-07 15:15:00 [ERROR] High jitter detected on LTC source.' ] - }, - ntpInactive: { - status: { - ltc_status: 'UNKNOWN', - ltc_timecode: '--:--:--:--', - frame_rate: '--', - lock_ratio: 0, - system_clock: '16:00:00.000', - system_date: '2025-08-07', - ntp_active: false, - sync_status: 'UNKNOWN', - timecode_delta_ms: 0, - timecode_delta_frames: 0, - jitter_status: 'UNKNOWN', - interfaces: [], - }, - config: { - hardwareOffsetMs: 0, - autoSyncEnabled: false, - defaultNudgeMs: 2, - timeturnerOffset: { hours: 0, minutes: 0, seconds: 0, frames: 0, milliseconds: 0 }, - }, - logs: [ '2025-08-07 16:00:00 [INFO] NTP service is inactive.' ] - } -}; diff --git a/static/script.js b/static/script.js index 634ed33..2094cc4 100644 --- a/static/script.js +++ b/static/script.js @@ -1,9 +1,4 @@ -document.addEventListener('DOMContentLoaded', () => { - // --- Mock Data Configuration --- - // Set to true to use mock data, false for live API. - const useMockData = false; - let currentMockSetKey = 'allGood'; // Default mock data set - +document.addEventListener('DOMContentLoaded', () => { let lastApiData = null; let lastApiFetchTime = null; @@ -16,9 +11,9 @@ systemDate: document.getElementById('system-date'), ntpActive: document.getElementById('ntp-active'), syncStatus: document.getElementById('sync-status'), - deltaStatus: document.getElementById('delta-status'), + deltaMs: document.getElementById('delta-ms'), + deltaFrames: document.getElementById('delta-frames'), jitterStatus: document.getElementById('jitter-status'), - deltaText: document.getElementById('delta-text'), interfaces: document.getElementById('interfaces'), logs: document.getElementById('logs'), }; @@ -45,110 +40,37 @@ const setDateButton = document.getElementById('set-date'); const dateMessage = document.getElementById('date-message'); - // --- Collapsible Sections --- - const controlsToggle = document.getElementById('controls-toggle'); - const controlsContent = document.getElementById('controls-content'); - const logsToggle = document.getElementById('logs-toggle'); - const logsContent = document.getElementById('logs-content'); - - // --- Mock Controls Setup --- - const mockControls = document.getElementById('mock-controls'); - const mockDataSelector = document.getElementById('mock-data-selector'); - - function setupMockControls() { - if (useMockData) { - mockControls.style.display = 'block'; - - // Populate dropdown - Object.keys(mockApiDataSets).forEach(key => { - const option = document.createElement('option'); - option.value = key; - option.textContent = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()); - mockDataSelector.appendChild(option); - }); - - mockDataSelector.value = currentMockSetKey; - - // Handle selection change - mockDataSelector.addEventListener('change', (event) => { - currentMockSetKey = event.target.value; - // Re-fetch all data from the new mock set - fetchStatus(); - fetchConfig(); - fetchLogs(); - }); - } - } - function updateStatus(data) { - const ltcStatus = data.ltc_status || 'UNKNOWN'; - const ltcIconInfo = iconMap.ltcStatus[ltcStatus] || iconMap.ltcStatus.default; - statusElements.ltcStatus.innerHTML = ``; - statusElements.ltcStatus.className = ltcStatus.toLowerCase(); + statusElements.ltcStatus.textContent = data.ltc_status; statusElements.ltcTimecode.textContent = data.ltc_timecode; - - const frameRate = data.frame_rate || 'unknown'; - const frameRateIconInfo = iconMap.frameRate[frameRate] || iconMap.frameRate.default; - statusElements.frameRate.innerHTML = ``; - - const lockRatio = data.lock_ratio; - let lockRatioCategory; - if (lockRatio === 100) { - lockRatioCategory = 'good'; - } else if (lockRatio >= 90) { - lockRatioCategory = 'average'; - } else { - lockRatioCategory = 'bad'; - } - const lockRatioIconInfo = iconMap.lockRatio[lockRatioCategory]; - statusElements.lockRatio.innerHTML = ``; + statusElements.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; - // Autofill the date input, but don't overwrite user edits. - if (!lastApiData || dateInput.value === lastApiData.system_date) { - dateInput.value = data.system_date; - } + statusElements.ntpActive.textContent = data.ntp_active ? 'Active' : 'Inactive'; + statusElements.ntpActive.className = data.ntp_active ? 'active' : 'inactive'; - const ntpIconInfo = iconMap.ntpActive[!!data.ntp_active]; - if (data.ntp_active) { - statusElements.ntpActive.innerHTML = ``; - statusElements.ntpActive.className = 'active'; - } else { - statusElements.ntpActive.innerHTML = ``; - statusElements.ntpActive.className = 'inactive'; - } + statusElements.syncStatus.textContent = data.sync_status; + statusElements.syncStatus.className = data.sync_status.replace(/\s+/g, '-').toLowerCase(); - const syncStatus = data.sync_status || 'UNKNOWN'; - const syncIconInfo = iconMap.syncStatus[syncStatus] || iconMap.syncStatus.default; - statusElements.syncStatus.innerHTML = ``; - statusElements.syncStatus.className = syncStatus.replace(/\s+/g, '-').toLowerCase(); + 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 (deltaMs === 0) { - deltaCategory = 'good'; - } else if (Math.abs(deltaMs) < 10) { - deltaCategory = 'average'; - } else { - deltaCategory = 'bad'; - } - const deltaIconInfo = iconMap.deltaStatus[deltaCategory]; - statusElements.deltaStatus.innerHTML = ``; - - const deltaTextValue = `${data.timecode_delta_ms} ms (${data.timecode_delta_frames} frames)`; - statusElements.deltaText.textContent = `Δ ${deltaTextValue}`; - - const jitterStatus = data.jitter_status || 'UNKNOWN'; - const jitterIconInfo = iconMap.jitterStatus[jitterStatus] || iconMap.jitterStatus.default; - statusElements.jitterStatus.innerHTML = ``; - statusElements.jitterStatus.className = jitterStatus.toLowerCase(); + statusElements.jitterStatus.textContent = data.jitter_status; + statusElements.jitterStatus.className = data.jitter_status.toLowerCase(); + statusElements.interfaces.innerHTML = ''; if (data.interfaces.length > 0) { - statusElements.interfaces.textContent = data.interfaces.join(' | '); + data.interfaces.forEach(ip => { + const li = document.createElement('li'); + li.textContent = ip; + statusElements.interfaces.appendChild(li); + }); } else { - statusElements.interfaces.textContent = 'No active interfaces found.'; + const li = document.createElement('li'); + li.textContent = 'No active interfaces found.'; + statusElements.interfaces.appendChild(li); } } @@ -176,11 +98,9 @@ } // Animate LTC Timecode - only if status is LOCK - 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(/[:;]/); + 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); @@ -206,19 +126,12 @@ h %= 24; statusElements.ltcTimecode.textContent = - `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}${separator}${String(f).padStart(2, '0')}`; + `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}:${String(f).padStart(2, '0')}`; } } } 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'); @@ -234,18 +147,6 @@ } 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'); @@ -277,14 +178,6 @@ } }; - 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', @@ -300,21 +193,13 @@ } async function fetchLogs() { - if (useMockData) { - // Use a copy to avoid mutating the original mock data array - const logs = mockApiDataSets[currentMockSetKey].logs.slice(); - // Show latest 20 logs, with the newest at the top. - logs.reverse(); - statusElements.logs.textContent = logs.slice(0, 20).join('\n'); - return; - } try { const response = await fetch('/api/logs'); if (!response.ok) throw new Error('Failed to fetch logs'); const logs = await response.json(); - // Show latest 20 logs, with the newest at the top. - logs.reverse(); - statusElements.logs.textContent = logs.slice(0, 20).join('\n'); + 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.'; @@ -323,11 +208,6 @@ 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(); @@ -345,11 +225,6 @@ 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', @@ -377,13 +252,6 @@ } 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', @@ -417,27 +285,13 @@ }); setDateButton.addEventListener('click', setDate); - // --- Collapsible Section Listeners --- - controlsToggle.addEventListener('click', () => { - const isActive = controlsContent.classList.toggle('active'); - controlsToggle.classList.toggle('active', isActive); - }); - - logsToggle.addEventListener('click', () => { - const isActive = logsContent.classList.toggle('active'); - logsToggle.classList.toggle('active', isActive); - }); - // Initial data load - setupMockControls(); fetchStatus(); fetchConfig(); fetchLogs(); - // Refresh data every 2 seconds if not using mock data - if (!useMockData) { - setInterval(fetchStatus, 2000); - setInterval(fetchLogs, 2000); - } + // Refresh data every 2 seconds + setInterval(fetchStatus, 2000); + setInterval(fetchLogs, 2000); setInterval(animateClocks, 50); // High-frequency clock animation }); diff --git a/static/style.css b/static/style.css index f9bcd58..7bd9c20 100644 --- a/static/style.css +++ b/static/style.css @@ -1,21 +1,6 @@ -@font-face { - font-family: 'FuturaStdHeavy'; - src: url('assets/FuturaStdHeavy.otf') format('opentype'); -} - -@font-face { - font-family: 'Quartz'; - src: url('assets/quartz-ms-regular.ttf') format('truetype'); -} - body { - font-family: 'FuturaStdHeavy', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - background-color: #221f1f; - background-image: url('assets/HaveBlueTransWh.png'); - background-repeat: no-repeat; - background-position: bottom 20px right 20px; - background-attachment: fixed; - background-size: 300px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background-color: #f4f4f9; color: #333; margin: 0; padding: 20px; @@ -28,20 +13,19 @@ body { max-width: 960px; } -.header-logo { - display: block; - margin: 0 auto 20px auto; - max-width: 60%; +h1 { + text-align: center; + color: #444; } .grid { display: grid; - grid-template-columns: 1fr; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } .card { - background: #c5ced6; + background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); @@ -49,32 +33,16 @@ body { .card h2 { margin-top: 0; - color: #1a7db6; -} - -#ltc-timecode, #system-clock { - font-family: 'Quartz', monospace; - font-size: 2em; - text-align: center; - letter-spacing: 2px; + color: #0056b3; } .card p, .card ul { margin: 10px 0; } -.system-date-display { - text-align: center; - font-size: 1.5em; - font-family: 'Quartz', monospace; - letter-spacing: 2px; -} - -#interfaces { - text-align: center; - white-space: nowrap; - overflow-x: auto; - padding-bottom: 5px; /* Add some space for the scrollbar if it appears */ +.card ul { + padding-left: 20px; + list-style: none; } .full-width { @@ -114,103 +82,6 @@ button:hover { color: #555; } -.icon-group { - display: flex; - justify-content: center; - align-items: center; - gap: 20px; - margin: 10px 0; -} - -#delta-text { - text-align: center; -} - -#ltc-status, #ntp-active, #sync-status, #jitter-status, #frame-rate, #lock-ratio, #delta-status { - display: flex; - justify-content: center; - align-items: center; -} - -.status-icon { - width: 60px; - height: 60px; -} - -.collapsible-card { - padding: 0; -} - -.collapsible-card .toggle-header { - display: flex; - align-items: center; - gap: 15px; - padding: 20px; - cursor: pointer; - border-radius: 8px; -} - -.collapsible-card .toggle-header.active { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - border-bottom: 1px solid #eee; -} - -.collapsible-card .toggle-header:hover { - background-color: #e9e9f3; -} - -.toggle-icon { - width: 40px; - height: 40px; -} - -.header-icon { - width: 40px; - height: 40px; -} - -.card-header { - display: flex; - align-items: center; - gap: 15px; -} - -.log-box { - white-space: pre-wrap; - overflow-wrap: break-word; -} - -.collapsible-content { - display: none; - padding: 20px; -} - -.collapsible-content.active { - display: block; -} - -footer { - text-align: center; - margin-top: 40px; - padding-top: 20px; - border-top: 1px solid #444; - color: #c5ced6; -} - -footer p { - margin: 0; -} - -footer a { - color: #1a7db6; - text-decoration: none; -} - -footer a:hover { - text-decoration: underline; -} - /* Status-specific colors */ #sync-status.in-sync, #jitter-status.good { font-weight: bold; color: #28a745; } #sync-status.clock-ahead, #sync-status.clock-behind, #jitter-status.average { font-weight: bold; color: #ffc107; } @@ -218,6 +89,3 @@ footer a: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; }