document.addEventListener('DOMContentLoaded', () => { 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() { 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() { 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() { 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, } }; 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() { 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...'; 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...'; 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); } 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', () => { 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 setInterval(fetchStatus, 2000); setInterval(fetchLogs, 2000); setInterval(animateClocks, 50); // High-frequency clock animation });