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();