Merge pull request #32 from cjfranko/updated-web-ui-setup
Some checks failed
Build for Raspberry Pi / Build for aarch64 (push) Failing after 20s

Updated web UI setup
This commit is contained in:
Chris Frankland-Wright 2025-08-12 16:48:40 +01:00 committed by GitHub
commit c2b1aedaba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 335 additions and 83 deletions

View file

@ -26,6 +26,17 @@ Created by Chris Frankland-Wright and John Rogers
- Broadcasts time via local NTP server - Broadcasts time via local NTP server
- Supports configurable time offsets (hours, minutes, seconds, frames or milliseconds) - Supports configurable time offsets (hours, minutes, seconds, frames or milliseconds)
- Systemd service support for headless operation - Systemd service support for headless operation
- Web-based UI for monitoring and control when running as a daemon
---
## 🖥️ Web Interface & API
When running as a background daemon, TimeTurner provides a web interface for monitoring and configuration.
- **Access**: The web UI is available at `http://<raspberry_pi_ip>:8080`.
- **Functionality**: You can view the real-time sync status, see logs, and change all configuration options directly from your browser.
- **API**: A JSON API is also exposed for programmatic access. See `docs/api.md` for full details.
--- ---
@ -37,19 +48,42 @@ Created by Chris Frankland-Wright and John Rogers
--- ---
## 🚀 Installation (to update) ## 🚀 Installation
For Rust install you can do The `setup.sh` script is provided to compile and install the TimeTurner application and its systemd service on a Debian-based system like Raspberry Pi OS.
```bash
cargo install --git https://github.com/cjfranko/NTP-Timeturner
```
Clone and run the installer:
```bash ### Prerequisites
wget https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/master/setup.sh
chmod +x setup.sh - **Rust and Cargo**: The script requires the Rust programming language toolchain. If you don't have it, install it from [rustup.rs](https://rustup.rs/).
./setup.sh
``` ### Running the Installer
1. First, clone the repository:
```bash
git clone https://github.com/cjfranko/NTP-Timeturner.git
cd NTP-Timeturner
```
2. Make the script executable and run it. The script will use `sudo` for commands that require root privileges, so it may ask for your password.
```bash
chmod +x setup.sh
./setup.sh
```
### What the Script Does
The installation script automates the following steps:
1. **Compiles the Binary**: Runs `cargo build --release` to create an optimised executable.
2. **Creates Directories**: Creates `/opt/timeturner` to store the application files.
3. **Installs Files**:
- The compiled binary is copied to `/opt/timeturner/timeturner`.
- The web interface assets from the `static/` directory are copied to `/opt/timeturner/static`.
- A symbolic link is created from `/usr/local/bin/timeturner` to the binary, allowing it to be run from any location.
4. **Sets up Systemd Service**:
- Copies the `timeturner.service` file to `/etc/systemd/system/`.
- Enables the service to start automatically on system boot.
After installation is complete, the script will provide instructions to start the service manually or to run the application in its interactive terminal mode.
--- ---
## 🕰️ Chrony NTP ## 🕰️ Chrony NTP

View file

@ -21,11 +21,13 @@ echo "🔧 Creating directories..."
sudo mkdir -p $INSTALL_DIR sudo mkdir -p $INSTALL_DIR
echo "✅ Directory $INSTALL_DIR created." echo "✅ Directory $INSTALL_DIR created."
# 3. Install binary # 3. Install binary and static web files
echo "🚀 Installing timeturner binary..." echo "🚀 Installing timeturner binary and web assets..."
sudo cp target/release/ntp_timeturner $INSTALL_DIR/timeturner sudo cp target/release/ntp_timeturner $INSTALL_DIR/timeturner
# The static directory contains the web UI files
sudo cp -r static $INSTALL_DIR/
sudo ln -sf $INSTALL_DIR/timeturner $BIN_DIR/timeturner sudo ln -sf $INSTALL_DIR/timeturner $BIN_DIR/timeturner
echo "✅ Binary installed to $INSTALL_DIR and linked to $BIN_DIR." echo "✅ Binary and assets installed to $INSTALL_DIR, and binary linked to $BIN_DIR."
# 4. Install systemd service file # 4. Install systemd service file
if [[ "$(uname)" == "Linux" ]]; then if [[ "$(uname)" == "Linux" ]]; then

View file

@ -193,11 +193,13 @@ async fn main() {
}); });
} }
// 5⃣ Spawn UI or setup daemon logging // 5⃣ Spawn UI or setup daemon logging. The web service is only started
// when running as a daemon. The TUI is for interactive foreground use.
if args.command.is_none() { if args.command.is_none() {
// --- Interactive TUI Mode ---
log::info!("🔧 Watching config.yml..."); log::info!("🔧 Watching config.yml...");
log::info!("🚀 Serial thread launched"); log::info!("🚀 Serial thread launched");
log::info!("🖥️ UI thread launched"); log::info!("🖥️ UI thread launched");
let ui_state = ltc_state.clone(); let ui_state = ltc_state.clone();
let config_clone = config.clone(); let config_clone = config.clone();
let port = serial_port_path; let port = serial_port_path;
@ -205,8 +207,10 @@ async fn main() {
start_ui(ui_state, port, config_clone); start_ui(ui_state, port, config_clone);
}); });
} else { } else {
// --- Daemon Mode ---
// In daemon mode, logging is already set up to go to stderr. // In daemon mode, logging is already set up to go to stderr.
// The systemd service will capture it. // The systemd service will capture it. The web service (API and static files)
// is launched later in the main async block.
log::info!("🚀 Starting TimeTurner daemon..."); log::info!("🚀 Starting TimeTurner daemon...");
} }
@ -282,7 +286,10 @@ async fn main() {
let local = LocalSet::new(); let local = LocalSet::new();
local local
.run_until(async move { .run_until(async move {
// 8⃣ Spawn the API server thread // 8⃣ Spawn the API server task.
// This server provides the JSON API and serves the static web UI files
// from the `static/` directory. It runs in both TUI and daemon modes,
// but is primarily for the web UI used in daemon mode.
{ {
let api_state = ltc_state.clone(); let api_state = ltc_state.clone();
let config_clone = config.clone(); let config_clone = config.clone();

View file

@ -62,71 +62,71 @@
<h2>Controls</h2> <h2>Controls</h2>
</div> </div>
<div class="collapsible-content" id="controls-content"> <div class="collapsible-content" id="controls-content">
<div class="control-group"> <div class="control-group" style="display: none;">
<label for="hw-offset">Hardware Offset (ms):</label> <label for="hw-offset">Hardware Offset (ms):</label>
<input type="number" id="hw-offset" name="hw-offset"> <input type="number" id="hw-offset" name="hw-offset">
</div>
<div class="control-group" style="display: none;">
<input type="checkbox" id="auto-sync-enabled" name="auto-sync-enabled" style="vertical-align: middle;">
<label for="auto-sync-enabled" style="vertical-align: middle;">Enable Auto Sync</label>
</div>
<div class="control-group">
<label>Timeturner Offset</label>
<div class="offset-controls-container">
<div class="offset-control">
<input type="number" id="offset-h" min="-99" max="99">
<label for="offset-h">hr</label>
</div>
<div class="offset-control">
<input type="number" id="offset-m" min="-99" max="99">
<label for="offset-m">min</label>
</div>
<div class="offset-control">
<input type="number" id="offset-s" min="-99" max="99">
<label for="offset-s">sec</label>
</div>
<div class="offset-control">
<input type="number" id="offset-f" min="-99" max="99">
<label for="offset-f">fr</label>
</div>
<div class="offset-control">
<input type="number" id="offset-ms">
<label for="offset-ms">ms</label>
</div>
</div>
</div>
<div class="control-group">
<button id="save-config">Save Timeturner Config</button>
<button id="manual-sync">Send Manual Sync</button>
<span id="sync-message"></span>
</div>
<div class="control-group" style="display: none;">
<label>Nudge Clock (ms):</label>
<button id="nudge-down">-</button>
<input type="number" id="nudge-value" style="width: 60px;">
<button id="nudge-up">+</button>
<span id="nudge-message"></span>
</div>
<div class="control-group">
<label for="date-input">Set System Date:</label>
<input type="text" id="date-input" placeholder="YYYY-MM-DD" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
<button id="set-date">Set Date</button>
<span id="date-message"></span>
</div>
</div> </div>
<div class="control-group"> </div>
<input type="checkbox" id="auto-sync-enabled" name="auto-sync-enabled" style="vertical-align: middle;">
<label for="auto-sync-enabled" style="vertical-align: middle;">Enable Auto Sync</label> <!-- Logs -->
</div> <div class="card full-width collapsible-card">
<div class="control-group"> <div class="toggle-header" id="logs-toggle">
<label>Timeturner Offset</label> <img src="assets/timeturner_logs.png" class="toggle-icon" alt="Logs Icon">
<div style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start;"> <h2>Logs</h2>
<div style="display: flex; flex-direction: column;">
<label for="offset-h">Hours</label>
<input type="number" id="offset-h" style="width: 60px;">
</div> </div>
<div style="display: flex; flex-direction: column;"> <div class="collapsible-content" id="logs-content">
<label for="offset-m">Minutes</label> <pre id="logs" class="log-box"></pre>
<input type="number" id="offset-m" style="width: 60px;">
</div>
<div style="display: flex; flex-direction: column;">
<label for="offset-s">Seconds</label>
<input type="number" id="offset-s" style="width: 60px;">
</div>
<div style="display: flex; flex-direction: column;">
<label for="offset-f">Frames</label>
<input type="number" id="offset-f" style="width: 60px;">
</div>
<div style="display: flex; flex-direction: column;">
<label for="offset-ms">Milliseconds</label>
<input type="number" id="offset-ms" style="width: 60px;">
</div> </div>
</div> </div>
</div> </div>
<div class="control-group">
<button id="save-config">Save Config</button>
<button id="manual-sync">Manual Sync</button>
<span id="sync-message"></span>
</div>
<div class="control-group">
<label>Nudge Clock (ms):</label>
<button id="nudge-down">-</button>
<input type="number" id="nudge-value" style="width: 60px;">
<button id="nudge-up">+</button>
<span id="nudge-message"></span>
</div>
<div class="control-group">
<label for="date-input">Set System Date:</label>
<input type="text" id="date-input" placeholder="YYYY-MM-DD" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
<button id="set-date">Set Date</button>
<span id="date-message"></span>
</div>
</div>
</div>
<!-- Logs -->
<div class="card full-width collapsible-card">
<div class="toggle-header" id="logs-toggle">
<img src="assets/timeturner_logs.png" class="toggle-icon" alt="Logs Icon">
<h2>Logs</h2>
</div>
<div class="collapsible-content" id="logs-content">
<pre id="logs" class="log-box"></pre>
</div>
</div>
</div>
<footer> <footer>
<p> <p>
Built by Chris Frankland-Wright and John Rogers | Have Blue Broadcast Media | Built by Chris Frankland-Wright and John Rogers | Have Blue Broadcast Media |

141
static/index_dev.html Normal file
View file

@ -0,0 +1,141 @@
<!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">
<link rel="icon" href="favicon.ico" type="image/x-icon">
</head>
<body>
<div class="container">
<img src="assets/header.png" alt="NTP Timeturner" class="header-logo">
<!-- 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">
<h2>LTC Input</h2>
<p id="ltc-timecode">--:--:--:--</p>
<div class="icon-group">
<span id="ltc-status"></span>
<span id="frame-rate"></span>
<span id="lock-ratio"></span>
</div>
</div>
<!-- System Clock & Sync -->
<div class="card">
<h2>NTP Clock</h2>
<p id="system-clock">--:--:--.---</p>
<p class="system-date-display"><span id="system-date">---- -- --</span></p>
<div class="icon-group">
<span id="ntp-active"></span>
<span id="sync-status"></span>
<span id="jitter-status"></span>
<span id="delta-status"></span>
</div>
<p id="delta-text">Δ -- ms (-- frames)</p>
</div>
<!-- Network Interfaces -->
<div class="card">
<div class="card-header">
<img src="assets/timeturner_network.png" class="header-icon" alt="Network Icon">
<h2>Network</h2>
</div>
<p id="interfaces">--</p>
</div>
<!-- Controls -->
<div class="card full-width collapsible-card">
<div class="toggle-header" id="controls-toggle">
<img src="assets/timeturner_controls.png" class="toggle-icon" alt="Controls Icon">
<h2>Controls</h2>
</div>
<div class="collapsible-content" id="controls-content">
<div class="control-group">
<label for="hw-offset">Hardware Offset (ms):</label>
<input type="number" id="hw-offset" name="hw-offset">
</div>
<div class="control-group">
<input type="checkbox" id="auto-sync-enabled" name="auto-sync-enabled" style="vertical-align: middle;">
<label for="auto-sync-enabled" style="vertical-align: middle;">Enable Auto Sync</label>
</div>
<div class="control-group">
<label>Timeturner Offset</label>
<div style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start;">
<div style="display: flex; flex-direction: column;">
<label for="offset-h">Hours</label>
<input type="number" id="offset-h" style="width: 60px;">
</div>
<div style="display: flex; flex-direction: column;">
<label for="offset-m">Minutes</label>
<input type="number" id="offset-m" style="width: 60px;">
</div>
<div style="display: flex; flex-direction: column;">
<label for="offset-s">Seconds</label>
<input type="number" id="offset-s" style="width: 60px;">
</div>
<div style="display: flex; flex-direction: column;">
<label for="offset-f">Frames</label>
<input type="number" id="offset-f" style="width: 60px;">
</div>
<div style="display: flex; flex-direction: column;">
<label for="offset-ms">Milliseconds</label>
<input type="number" id="offset-ms" style="width: 60px;">
</div>
</div>
</div>
<div class="control-group">
<button id="save-config">Save Config</button>
<button id="manual-sync">Manual Sync</button>
<span id="sync-message"></span>
</div>
<div class="control-group">
<label>Nudge Clock (ms):</label>
<button id="nudge-down">-</button>
<input type="number" id="nudge-value" style="width: 60px;">
<button id="nudge-up">+</button>
<span id="nudge-message"></span>
</div>
<div class="control-group">
<label for="date-input">Set System Date:</label>
<input type="text" id="date-input" placeholder="YYYY-MM-DD" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
<button id="set-date">Set Date</button>
<span id="date-message"></span>
</div>
</div>
</div>
<!-- Logs -->
<div class="card full-width collapsible-card">
<div class="toggle-header" id="logs-toggle">
<img src="assets/timeturner_logs.png" class="toggle-icon" alt="Logs Icon">
<h2>Logs</h2>
</div>
<div class="collapsible-content" id="logs-content">
<pre id="logs" class="log-box"></pre>
</div>
</div>
</div>
<footer>
<p>
Built by Chris Frankland-Wright and John Rogers | Have Blue Broadcast Media |
<a href="https://github.com/cjfranko/NTP-Timeturner" target="_blank" rel="noopener noreferrer">https://github.com/cjfranko/NTP-Timeturner</a>
</p>
</footer>
</div>
<script src="icon-map.js"></script>
<script src="mock-data.js"></script>
<script src="script.js"></script>
</body>
</html>

View file

@ -15,7 +15,7 @@ body {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: bottom 20px right 20px; background-position: bottom 20px right 20px;
background-attachment: fixed; background-attachment: fixed;
background-size: 300px; background-size: 100px;
color: #333; color: #333;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
@ -88,25 +88,75 @@ body {
gap: 10px; gap: 10px;
} }
input[type="number"] { input[type="number"],
input[type="text"] {
padding: 8px; padding: 8px;
border: 1px solid #ccc; border: 1px solid #9fb3c8;
border-radius: 4px; border-radius: 4px;
background-color: #f0f4f8;
font-family: inherit;
font-size: 14px;
color: #333;
transition: border-color 0.2s, box-shadow 0.2s;
}
input[type="number"]:focus,
input[type="text"]:focus {
outline: none;
border-color: #1a7db6;
box-shadow: 0 0 0 2px rgba(26, 125, 182, 0.2);
}
input[type="number"] {
width: 80px; width: 80px;
} }
input[type="text"] {
width: auto;
}
button { button {
padding: 8px 15px; padding: 8px 15px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
background-color: #007bff; background-color: #1a7db6;
color: white; color: white;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
font-family: Arial, sans-serif;
font-weight: bold;
transition: background-color 0.2s;
} }
button:hover { button:hover {
background-color: #0056b3; background-color: #166999;
}
.offset-controls-container {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
align-items: center;
}
.offset-control {
display: flex;
align-items: center;
gap: 5px;
}
.offset-control input[type="number"] {
width: 40px;
text-align: center;
}
.offset-control label {
font-size: 14px;
color: #333;
}
#offset-ms {
width: 60px;
} }
#sync-message { #sync-message {

18
timeturner.service Normal file
View file

@ -0,0 +1,18 @@
[Unit]
Description=NTP TimeTurner Daemon
After=network.target
[Service]
Type=forking
# The 'timeturner daemon' command starts the background process.
# It requires 'config.yml' and the 'static/' web assets directory
# to be present in the WorkingDirectory.
ExecStart=/opt/timeturner/timeturner daemon
WorkingDirectory=/opt/timeturner
PIDFile=/opt/timeturner/ntp_timeturner.pid
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target