mirror of
https://github.com/cjfranko/NTP-Timeturner.git
synced 2025-11-08 18:32:02 +00:00
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:
parent
0ba46fbd71
commit
c97d1841b5
3 changed files with 268 additions and 3 deletions
|
|
@ -9,6 +9,16 @@
|
|||
<body>
|
||||
<div class="container">
|
||||
<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">
|
||||
<!-- LTC Status -->
|
||||
<div class="card">
|
||||
|
|
@ -107,6 +117,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<script src="icon-map.js"></script>
|
||||
<script src="mock-data.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
168
static/mock-data.js
Normal file
168
static/mock-data.js
Normal 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.' ]
|
||||
}
|
||||
};
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
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;
|
||||
|
|
@ -41,6 +45,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
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;
|
||||
|
|
@ -148,6 +181,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
|
||||
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');
|
||||
|
|
@ -163,6 +203,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
|
||||
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');
|
||||
|
|
@ -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 {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
|
|
@ -209,6 +269,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
|
||||
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');
|
||||
|
|
@ -224,6 +290,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
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();
|
||||
|
|
@ -241,6 +312,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
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',
|
||||
|
|
@ -268,6 +344,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
|
||||
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',
|
||||
|
|
@ -302,12 +385,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
setDateButton.addEventListener('click', setDate);
|
||||
|
||||
// Initial data load
|
||||
setupMockControls();
|
||||
fetchStatus();
|
||||
fetchConfig();
|
||||
fetchLogs();
|
||||
|
||||
// Refresh data every 2 seconds
|
||||
setInterval(fetchStatus, 2000);
|
||||
setInterval(fetchLogs, 2000);
|
||||
// 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
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue