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; const statusElements = { ltcStatus: document.getElementById('ltc-status'), ltcTimecode: document.getElementById('ltc-timecode'), 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'), deltaFrames: document.getElementById('delta-frames'), jitterStatus: document.getElementById('jitter-status'), interfaces: document.getElementById('interfaces'), logs: document.getElementById('logs'), }; const hwOffsetInput = document.getElementById('hw-offset'); const autoSyncCheckbox = document.getElementById('auto-sync-enabled'); const offsetInputs = { h: document.getElementById('offset-h'), m: document.getElementById('offset-m'), s: document.getElementById('offset-s'), f: document.getElementById('offset-f'), ms: document.getElementById('offset-ms'), }; const saveConfigButton = document.getElementById('save-config'); const manualSyncButton = document.getElementById('manual-sync'); const syncMessage = document.getElementById('sync-message'); const nudgeDownButton = document.getElementById('nudge-down'); const nudgeUpButton = document.getElementById('nudge-up'); 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) { 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); 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.className = 'active'; } else { 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.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}`; 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); }); } else { const li = document.createElement('li'); li.textContent = 'No active interfaces found.'; statusElements.interfaces.appendChild(li); } } 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.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); 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')}${separator}${String(f).padStart(2, '0')}`; } } } async function fetchStatus() { // Mock implementation to allow UI development without a running backend. const data = mockApiData.status; updateStatus(data); lastApiData = data; lastApiFetchTime = new Date(); } 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; } async function saveConfig() { const config = { hardwareOffsetMs: parseInt(hwOffsetInput.value, 10) || 0, autoSyncEnabled: autoSyncCheckbox.checked, defaultNudgeMs: parseInt(nudgeValueInput.value, 10) || 0, timeturnerOffset: { 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, milliseconds: parseInt(offsetInputs.ms.value, 10) || 0, } }; // Mock implementation console.log('Saving mock config:', config); alert('Configuration saved (mock).'); } 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; } async function triggerManualSync() { syncMessage.textContent = 'Issuing 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...'; // Mock implementation setTimeout(() => { nudgeMessage.textContent = `Success: Clock nudged by ${ms}ms (mock).`; }, 500); setTimeout(() => { nudgeMessage.textContent = ''; }, 3000); } async function setDate() { const date = dateInput.value; if (!date) { alert('Please select a date.'); return; } 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; fetchStatus(); } }, 1000); setTimeout(() => { dateMessage.textContent = ''; }, 5000); } saveConfigButton.addEventListener('click', saveConfig); manualSyncButton.addEventListener('click', triggerManualSync); nudgeDownButton.addEventListener('click', () => { const ms = parseInt(nudgeValueInput.value, 10) || 0; nudgeClock(-ms); }); nudgeUpButton.addEventListener('click', () => { const ms = parseInt(nudgeValueInput.value, 10) || 0; nudgeClock(ms); }); setDateButton.addEventListener('click', setDate); // Initial data load fetchStatus(); fetchConfig(); fetchLogs(); // Refresh data every 2 seconds - MOCKED // setInterval(fetchStatus, 2000); // setInterval(fetchLogs, 2000); setInterval(animateClocks, 50); // High-frequency clock animation });