NTP-Timeturner/static/script.js
Chris Frankland-Wright fe9ac76942 style: Resize status icons to 60x60px and adjust layout
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-08 00:02:43 +01:00

399 lines
17 KiB
JavaScript

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;
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');
// --- 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;
statusElements.ltcStatus.innerHTML = `<img src="${ltcIconSrc}" class="status-icon" alt=""><span>${ltcStatus}</span>`;
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 = `<img src="${ntpIconSrc}" class="status-icon" alt=""><span>Active</span>`;
statusElements.ntpActive.className = 'active';
} else {
statusElements.ntpActive.innerHTML = `<img src="${ntpIconSrc}" class="status-icon" alt=""><span>Inactive</span>`;
statusElements.ntpActive.className = 'inactive';
}
const syncStatus = data.sync_status || 'UNKNOWN';
const syncIconSrc = iconMap.syncStatus[syncStatus] || iconMap.syncStatus.default;
statusElements.syncStatus.innerHTML = `<img src="${syncIconSrc}" class="status-icon" alt=""><span>${syncStatus}</span>`;
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 = `<img src="${jitterIconSrc}" class="status-icon" alt=""><span>${jitterStatus}</span>`;
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() {
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');
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() {
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');
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,
}
};
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',
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() {
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');
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...';
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();
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...';
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',
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...';
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',
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
setupMockControls();
fetchStatus();
fetchConfig();
fetchLogs();
// 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
});