mirror of
https://github.com/cjfranko/NTP-Timeturner.git
synced 2025-11-08 10:22:02 +00:00
feat: add web frontend to control the API
Co-authored-by: aider (gemini/gemini-2.5-pro-preview-05-06) <aider@aider.chat>
This commit is contained in:
parent
666ce4308f
commit
c48ef1cf3f
5 changed files with 279 additions and 0 deletions
|
|
@ -13,5 +13,6 @@ serde_json = "1.0.141"
|
|||
notify = "8.1.0"
|
||||
get_if_addrs = "0.5"
|
||||
actix-web = "4"
|
||||
actix-files = "0.6"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
use actix_files as fs;
|
||||
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
|
||||
use chrono::{Local, Timelike};
|
||||
use get_if_addrs::get_if_addrs;
|
||||
|
|
@ -158,6 +159,8 @@ pub async fn start_api_server(
|
|||
.service(manual_sync)
|
||||
.service(get_config)
|
||||
.service(update_config)
|
||||
// Serve frontend static files
|
||||
.service(fs::Files::new("/", "static/").index_file("index.html"))
|
||||
})
|
||||
.bind("0.0.0.0:8080")?
|
||||
.run()
|
||||
|
|
|
|||
62
static/index.html
Normal file
62
static/index.html
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NTP TimeTurner</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>NTP TimeTurner</h1>
|
||||
<div class="grid">
|
||||
<!-- LTC Status -->
|
||||
<div class="card">
|
||||
<h2>LTC Status</h2>
|
||||
<p id="ltc-status">--</p>
|
||||
<p id="ltc-timecode">--:--:--:--</p>
|
||||
<p id="frame-rate">-- fps</p>
|
||||
<p>Lock Ratio: <span id="lock-ratio">--</span>%</p>
|
||||
</div>
|
||||
|
||||
<!-- System Clock & Sync -->
|
||||
<div class="card">
|
||||
<h2>System Clock</h2>
|
||||
<p id="system-clock">--:--:--.---</p>
|
||||
<p>NTP Service: <span id="ntp-active">--</span></p>
|
||||
<p>Sync Status: <span id="sync-status">--</span></p>
|
||||
</div>
|
||||
|
||||
<!-- Delta & Jitter -->
|
||||
<div class="card">
|
||||
<h2>Clock Offset</h2>
|
||||
<p>Delta: <span id="delta-ms">--</span> ms (<span id="delta-frames">--</span> frames)</p>
|
||||
<p>Jitter: <span id="jitter-status">--</span></p>
|
||||
</div>
|
||||
|
||||
<!-- Network Interfaces -->
|
||||
<div class="card">
|
||||
<h2>Network</h2>
|
||||
<ul id="interfaces">
|
||||
<li>--</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="card full-width">
|
||||
<h2>Controls</h2>
|
||||
<div class="control-group">
|
||||
<label for="hw-offset">Hardware Offset (ms):</label>
|
||||
<input type="number" id="hw-offset" name="hw-offset">
|
||||
<button id="save-offset">Save</button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button id="manual-sync">Manual Sync</button>
|
||||
<span id="sync-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
123
static/script.js
Normal file
123
static/script.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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'),
|
||||
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'),
|
||||
};
|
||||
|
||||
const hwOffsetInput = document.getElementById('hw-offset');
|
||||
const saveOffsetButton = document.getElementById('save-offset');
|
||||
const manualSyncButton = document.getElementById('manual-sync');
|
||||
const syncMessage = document.getElementById('sync-message');
|
||||
|
||||
function updateStatus(data) {
|
||||
statusElements.ltcStatus.textContent = data.ltc_status;
|
||||
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.ntpActive.textContent = data.ntp_active ? 'Active' : 'Inactive';
|
||||
statusElements.ntpActive.className = data.ntp_active ? 'active' : 'inactive';
|
||||
|
||||
statusElements.syncStatus.textContent = data.sync_status;
|
||||
statusElements.syncStatus.className = data.sync_status.replace(/\s+/g, '-').toLowerCase();
|
||||
|
||||
statusElements.deltaMs.textContent = data.timecode_delta_ms;
|
||||
statusElements.deltaFrames.textContent = data.timecode_delta_frames;
|
||||
|
||||
statusElements.jitterStatus.textContent = data.jitter_status;
|
||||
statusElements.jitterStatus.className = data.jitter_status.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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error('Error fetching status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
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.hardware_offset_ms;
|
||||
} catch (error) {
|
||||
console.error('Error fetching config:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
const offset = parseInt(hwOffsetInput.value, 10);
|
||||
if (isNaN(offset)) {
|
||||
alert('Invalid hardware offset value.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hardware_offset_ms: offset }),
|
||||
});
|
||||
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 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);
|
||||
}
|
||||
|
||||
saveOffsetButton.addEventListener('click', saveConfig);
|
||||
manualSyncButton.addEventListener('click', triggerManualSync);
|
||||
|
||||
// Initial data load
|
||||
fetchStatus();
|
||||
fetchConfig();
|
||||
|
||||
// Refresh data every 2 seconds
|
||||
setInterval(fetchStatus, 2000);
|
||||
});
|
||||
90
static/style.css
Normal file
90
static/style.css
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: #f4f4f9;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-top: 0;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.card p, .card ul {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.card ul {
|
||||
padding-left: 20px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
#sync-message {
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Status-specific colors */
|
||||
#sync-status.in-sync, #jitter-status.good { font-weight: bold; color: #28a745; }
|
||||
#sync-status.clock-ahead, #sync-status.clock-behind, #jitter-status.average { font-weight: bold; color: #ffc107; }
|
||||
#jitter-status.bad { font-weight: bold; color: #dc3545; }
|
||||
#ntp-active.active { font-weight: bold; color: #28a745; }
|
||||
#ntp-active.inactive { font-weight: bold; color: #dc3545; }
|
||||
Loading…
Add table
Add a link
Reference in a new issue