feat: Add mock data toggle and scenarios for UI testing

Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
Chris Frankland-Wright 2025-08-07 23:53:14 +01:00
parent 0ba46fbd71
commit c97d1841b5
3 changed files with 268 additions and 3 deletions

View file

@ -9,6 +9,16 @@
<body> <body>
<div class="container"> <div class="container">
<h1>NTP TimeTurner</h1> <h1>NTP TimeTurner</h1>
<!-- Mock Data Controls (hidden by default) -->
<div id="mock-controls" class="card full-width" style="display: none;">
<h2>Mock Data Controls</h2>
<div class="control-group">
<label for="mock-data-selector">Select Mock Data Scenario:</label>
<select id="mock-data-selector"></select>
</div>
</div>
<div class="grid"> <div class="grid">
<!-- LTC Status --> <!-- LTC Status -->
<div class="card"> <div class="card">
@ -107,6 +117,7 @@
</div> </div>
</div> </div>
<script src="icon-map.js"></script> <script src="icon-map.js"></script>
<script src="mock-data.js"></script>
<script src="script.js"></script> <script src="script.js"></script>
</body> </body>
</html> </html>

168
static/mock-data.js Normal file
View file

@ -0,0 +1,168 @@
// 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.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: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.00',
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 (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.00',
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 (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.00',
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 (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.00',
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 (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.00',
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 (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.' ]
}
};

View file

@ -1,4 +1,8 @@
document.addEventListener('DOMContentLoaded', () => { 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 lastApiData = null;
let lastApiFetchTime = null; let lastApiFetchTime = null;
@ -41,6 +45,35 @@ document.addEventListener('DOMContentLoaded', () => {
const setDateButton = document.getElementById('set-date'); const setDateButton = document.getElementById('set-date');
const dateMessage = document.getElementById('date-message'); 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) { function updateStatus(data) {
const ltcStatus = data.ltc_status || 'UNKNOWN'; const ltcStatus = data.ltc_status || 'UNKNOWN';
const ltcIconSrc = iconMap.ltcStatus[ltcStatus] || iconMap.ltcStatus.default; const ltcIconSrc = iconMap.ltcStatus[ltcStatus] || iconMap.ltcStatus.default;
@ -148,6 +181,13 @@ document.addEventListener('DOMContentLoaded', () => {
} }
async function fetchStatus() { async function fetchStatus() {
if (useMockData) {
const data = mockApiDataSets[currentMockSetKey].status;
updateStatus(data);
lastApiData = data;
lastApiFetchTime = new Date();
return;
}
try { try {
const response = await fetch('/api/status'); const response = await fetch('/api/status');
if (!response.ok) throw new Error('Failed to fetch status'); if (!response.ok) throw new Error('Failed to fetch status');
@ -163,6 +203,18 @@ document.addEventListener('DOMContentLoaded', () => {
} }
async function fetchConfig() { 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 { try {
const response = await fetch('/api/config'); const response = await fetch('/api/config');
if (!response.ok) throw new Error('Failed to fetch config'); if (!response.ok) throw new Error('Failed to fetch config');
@ -194,6 +246,14 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}; };
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 { try {
const response = await fetch('/api/config', { const response = await fetch('/api/config', {
method: 'POST', method: 'POST',
@ -209,6 +269,12 @@ document.addEventListener('DOMContentLoaded', () => {
} }
async function fetchLogs() { async function fetchLogs() {
if (useMockData) {
const logs = mockApiDataSets[currentMockSetKey].logs;
statusElements.logs.textContent = logs.join('\n');
statusElements.logs.scrollTop = statusElements.logs.scrollHeight;
return;
}
try { try {
const response = await fetch('/api/logs'); const response = await fetch('/api/logs');
if (!response.ok) throw new Error('Failed to fetch logs'); if (!response.ok) throw new Error('Failed to fetch logs');
@ -224,6 +290,11 @@ document.addEventListener('DOMContentLoaded', () => {
async function triggerManualSync() { async function triggerManualSync() {
syncMessage.textContent = 'Issuing sync command...'; syncMessage.textContent = 'Issuing sync command...';
if (useMockData) {
syncMessage.textContent = 'Success: Manual sync triggered (mock).';
setTimeout(() => { syncMessage.textContent = ''; }, 5000);
return;
}
try { try {
const response = await fetch('/api/sync', { method: 'POST' }); const response = await fetch('/api/sync', { method: 'POST' });
const data = await response.json(); const data = await response.json();
@ -241,6 +312,11 @@ document.addEventListener('DOMContentLoaded', () => {
async function nudgeClock(ms) { async function nudgeClock(ms) {
nudgeMessage.textContent = 'Nudging clock...'; nudgeMessage.textContent = 'Nudging clock...';
if (useMockData) {
nudgeMessage.textContent = `Success: Clock nudged by ${ms}ms (mock).`;
setTimeout(() => { nudgeMessage.textContent = ''; }, 3000);
return;
}
try { try {
const response = await fetch('/api/nudge_clock', { const response = await fetch('/api/nudge_clock', {
method: 'POST', method: 'POST',
@ -268,6 +344,13 @@ document.addEventListener('DOMContentLoaded', () => {
} }
dateMessage.textContent = 'Setting date...'; 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 { try {
const response = await fetch('/api/set_date', { const response = await fetch('/api/set_date', {
method: 'POST', method: 'POST',
@ -302,12 +385,15 @@ document.addEventListener('DOMContentLoaded', () => {
setDateButton.addEventListener('click', setDate); setDateButton.addEventListener('click', setDate);
// Initial data load // Initial data load
setupMockControls();
fetchStatus(); fetchStatus();
fetchConfig(); fetchConfig();
fetchLogs(); fetchLogs();
// Refresh data every 2 seconds // Refresh data every 2 seconds if not using mock data
setInterval(fetchStatus, 2000); if (!useMockData) {
setInterval(fetchLogs, 2000); setInterval(fetchStatus, 2000);
setInterval(fetchLogs, 2000);
}
setInterval(animateClocks, 50); // High-frequency clock animation setInterval(animateClocks, 50); // High-frequency clock animation
}); });