[feat](trx-frontend-http): rework About page with grouped cards and new info

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-26 20:07:33 +01:00
parent 5be4019c04
commit 25338710ee
3 changed files with 169 additions and 29 deletions
@@ -1037,8 +1037,28 @@ let lastUnsupportedFreqPopupAt = 0;
let freqDirty = false;
let initialized = false;
let lastEventAt = Date.now();
let aboutUptimeStart = null;
let es;
let esHeartbeat;
function formatUptime(ms) {
const s = Math.floor(ms / 1000);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
const parts = [];
if (d > 0) parts.push(`${d}d`);
if (h > 0 || d > 0) parts.push(`${h}h`);
parts.push(`${m}m`);
parts.push(`${sec}s`);
return parts.join(" ");
}
setInterval(() => {
if (!aboutUptimeStart) return;
const el = document.getElementById("about-uptime");
if (el) el.textContent = formatUptime(Date.now() - aboutUptimeStart);
}, 1000);
let reconnectTimer = null;
let overviewSignalSamples = [];
let overviewSignalTimer = null;
@@ -3194,20 +3214,23 @@ function render(update) {
}
if (typeof update.clients === "number") lastClientCount = update.clients;
// Populate About tab
// Populate About tab — Server card
if (update.server_version) {
document.getElementById("about-server-ver").textContent = `trx-server v${update.server_version}`;
}
if (update.server_build_date) {
document.getElementById("about-server-build-date").textContent = update.server_build_date;
}
document.getElementById("about-server-addr").textContent = location.host;
if (update.server_callsign) {
document.getElementById("about-server-call").textContent = update.server_callsign;
}
if (update.pskreporter_status) {
document.getElementById("about-pskreporter").textContent = update.pskreporter_status;
}
if (update.aprs_is_status) {
document.getElementById("about-aprs-is").textContent = update.aprs_is_status;
if (Number.isFinite(serverLat) && Number.isFinite(serverLon)) {
const grid = latLonToMaidenhead(serverLat, serverLon);
document.getElementById("about-server-location").textContent = `${grid} (${serverLat.toFixed(4)}, ${serverLon.toFixed(4)})`;
}
// About — Radio card
if (update.info) {
const parts = [update.info.manufacturer, update.info.model, update.info.revision].filter(Boolean).join(" ");
if (parts) document.getElementById("about-rig-info").textContent = parts;
@@ -3233,21 +3256,59 @@ function render(update) {
}
}
}
if (typeof update.clients === "number") {
document.getElementById("about-clients").textContent = update.clients;
}
if (lastActiveRigId) {
document.getElementById("about-active-rig").textContent = lastActiveRigId;
}
if (Array.isArray(update.remotes)) {
applyRigList(update.active_remote, update.remotes);
}
// About — Audio card
if (streamInfo) {
document.getElementById("about-audio-codec").textContent = "Opus";
document.getElementById("about-audio-samplerate").textContent = `${(streamInfo.sample_rate || 48000).toLocaleString()} Hz`;
document.getElementById("about-audio-channels").textContent = (streamInfo.channels || 1) === 1 ? "Mono" : "Stereo";
if (streamInfo.frame_duration_ms) {
document.getElementById("about-audio-frame").textContent = `${streamInfo.frame_duration_ms} ms`;
}
}
document.getElementById("about-audio-rx").textContent = rxActive ? "Active" : "Off";
// About — Decoders card
const decMap = [
["about-dec-ft8", update.ft8_decode_enabled],
["about-dec-ft4", update.ft4_decode_enabled],
["about-dec-ft2", update.ft2_decode_enabled],
["about-dec-wspr", update.wspr_decode_enabled],
["about-dec-cw", update.cw_decode_enabled],
["about-dec-aprs", update.aprs_decode_enabled || update.hf_aprs_decode_enabled],
];
for (const [id, enabled] of decMap) {
const el = document.getElementById(id);
if (el) {
el.textContent = enabled ? "Active" : "Off";
el.className = enabled ? "about-status-on" : "about-status-off";
}
}
// About — Integrations card
if (update.pskreporter_status) {
document.getElementById("about-pskreporter").textContent = update.pskreporter_status;
}
if (update.aprs_is_status) {
document.getElementById("about-aprs-is").textContent = update.aprs_is_status;
}
if (typeof update.rigctl_clients === "number") {
document.getElementById("about-rigctl-clients").textContent = update.rigctl_clients;
}
if (typeof update.rigctl_addr === "string" && update.rigctl_addr.length > 0) {
document.getElementById("about-rigctl-endpoint").textContent = update.rigctl_addr;
}
// About — Clients card
if (typeof update.clients === "number") {
document.getElementById("about-clients").textContent = update.clients;
}
powerHint.textContent = readyText();
lastLocked = update.status && update.status.lock === true;
lockBtn.textContent = lastLocked ? "Unlock" : "Lock";
@@ -3313,6 +3374,7 @@ function connect() {
lastEventAt = Date.now();
es.onopen = () => {
setConnLostOverlay(false);
if (!aboutUptimeStart) aboutUptimeStart = Date.now();
pollFreshSnapshot();
refreshRigList();
};
@@ -1015,23 +1015,92 @@
</div>
<div id="tab-about" class="tab-panel" style="display:none;">
<div id="auth-badge" style="display:none; margin-bottom: 1rem; padding: 0.5rem; background: var(--bg-secondary); border-radius: 0.25rem; color: var(--text-muted); font-size: 0.85rem;">Authenticated as: <strong id="auth-role-badge">--</strong></div>
<table class="about-table">
<tr><td>Server version</td><td id="about-server-ver">--</td></tr>
<tr><td>Server address</td><td id="about-server-addr">--</td></tr>
<tr><td>Server callsign</td><td id="about-server-call">--</td></tr>
<tr><td>Rig</td><td id="about-rig-info">--</td></tr>
<tr><td>Active rig</td><td id="about-active-rig">--</td></tr>
<tr><td>Available rigs</td><td id="about-rig-list">--</td></tr>
<tr><td>Rig connection</td><td id="about-rig-access">--</td></tr>
<tr><td>Supported modes</td><td id="about-modes">--</td></tr>
<tr><td>VFOs</td><td id="about-vfos">--</td></tr>
<tr><td>Rigctl endpoint</td><td id="about-rigctl-endpoint">--</td></tr>
<tr><td>Rigctl clients</td><td id="about-rigctl-clients">--</td></tr>
<tr><td>PSK Reporter</td><td id="about-pskreporter">--</td></tr>
<tr><td>APRS-IS</td><td id="about-aprs-is">--</td></tr>
<tr><td>Client version</td><td>{pkg} v{ver}</td></tr>
<tr><td>Connected clients</td><td id="about-clients">--</td></tr>
</table>
<div class="about-grid">
<!-- Server -->
<div class="about-card">
<div class="about-card-title">
<svg class="about-card-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="12" height="5" rx="1"/><rect x="2" y="9" width="12" height="5" rx="1"/><circle cx="5" cy="4.5" r="0.7" fill="currentColor" stroke="none"/><circle cx="5" cy="11.5" r="0.7" fill="currentColor" stroke="none"/></svg>
Server
</div>
<table class="about-table">
<tr><td>Version</td><td id="about-server-ver">--</td></tr>
<tr><td>Build date</td><td id="about-server-build-date">--</td></tr>
<tr><td>Address</td><td id="about-server-addr">--</td></tr>
<tr><td>Callsign</td><td id="about-server-call">--</td></tr>
<tr><td>Location</td><td id="about-server-location">--</td></tr>
<tr><td>Uptime</td><td id="about-uptime">--</td></tr>
</table>
</div>
<!-- Radio -->
<div class="about-card">
<div class="about-card-title">
<svg class="about-card-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2 5h12v8H2z"/><path d="M4 5V3l8 -1v3"/><circle cx="11" cy="9" r="2"/><path d="M4 8h4"/><path d="M4 10h3"/></svg>
Radio
</div>
<table class="about-table">
<tr><td>Rig</td><td id="about-rig-info">--</td></tr>
<tr><td>Active rig</td><td id="about-active-rig">--</td></tr>
<tr><td>Available rigs</td><td id="about-rig-list">--</td></tr>
<tr><td>Connection</td><td id="about-rig-access">--</td></tr>
<tr><td>Modes</td><td id="about-modes">--</td></tr>
<tr><td>VFOs</td><td id="about-vfos">--</td></tr>
</table>
</div>
<!-- Audio -->
<div class="about-card">
<div class="about-card-title">
<svg class="about-card-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5v6"/><path d="M6 3v10"/><path d="M9 6v4"/><path d="M12 4v8"/></svg>
Audio
</div>
<table class="about-table">
<tr><td>Codec</td><td id="about-audio-codec">--</td></tr>
<tr><td>Sample rate</td><td id="about-audio-samplerate">--</td></tr>
<tr><td>Channels</td><td id="about-audio-channels">--</td></tr>
<tr><td>Frame duration</td><td id="about-audio-frame">--</td></tr>
<tr><td>RX status</td><td id="about-audio-rx">Off</td></tr>
</table>
</div>
<!-- Decoders -->
<div class="about-card">
<div class="about-card-title">
<svg class="about-card-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12 L5 4 L8 10 L10 6 L14 12"/></svg>
Decoders
</div>
<table class="about-table">
<tr><td>FT8</td><td id="about-dec-ft8" class="about-status-off">Off</td></tr>
<tr><td>FT4</td><td id="about-dec-ft4" class="about-status-off">Off</td></tr>
<tr><td>FT2</td><td id="about-dec-ft2" class="about-status-off">Off</td></tr>
<tr><td>WSPR</td><td id="about-dec-wspr" class="about-status-off">Off</td></tr>
<tr><td>CW</td><td id="about-dec-cw" class="about-status-off">Off</td></tr>
<tr><td>APRS</td><td id="about-dec-aprs" class="about-status-off">Off</td></tr>
</table>
</div>
<!-- Integrations -->
<div class="about-card">
<div class="about-card-title">
<svg class="about-card-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2"/><path d="M8 2v2"/><path d="M8 12v2"/><path d="M2 8h2"/><path d="M12 8h2"/><path d="M3.8 3.8l1.4 1.4"/><path d="M10.8 10.8l1.4 1.4"/><path d="M3.8 12.2l1.4-1.4"/><path d="M10.8 5.2l1.4-1.4"/></svg>
Integrations
</div>
<table class="about-table">
<tr><td>PSK Reporter</td><td id="about-pskreporter">--</td></tr>
<tr><td>APRS-IS</td><td id="about-aprs-is">--</td></tr>
<tr><td>Rigctl endpoint</td><td id="about-rigctl-endpoint">--</td></tr>
<tr><td>Rigctl clients</td><td id="about-rigctl-clients">--</td></tr>
</table>
</div>
<!-- Clients -->
<div class="about-card">
<div class="about-card-title">
<svg class="about-card-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="5" r="3"/><path d="M2 14c0-3 2.7-5 6-5s6 2 6 5"/></svg>
Clients
</div>
<table class="about-table">
<tr><td>HTTP clients</td><td id="about-clients">--</td></tr>
<tr><td>Client version</td><td>{pkg} v{ver}</td></tr>
<tr><td>Client build</td><td>{client_build_date}</td></tr>
</table>
</div>
</div>
</div>
<div class="footer">
<div class="copyright">
@@ -1217,10 +1217,16 @@ small { color: var(--text-muted); }
flex-shrink: 0;
}
.tab-label { display: block; }
.about-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
.about-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0; overflow: hidden; }
.about-card-title { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.75rem; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-heading); border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--card-bg) 50%, var(--bg)); }
.about-card-icon { width: 15px; height: 15px; flex-shrink: 0; }
.about-table { width: 100%; border-collapse: collapse; }
.about-table td { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); }
.about-table td { padding: 0.4rem 0.75rem; border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent); font-size: 0.85rem; }
.about-table tr:last-child td { border-bottom: none; }
.about-table td:first-child { color: var(--text-muted); width: 40%; }
.about-status-on { color: #00d17f; }
.about-status-off { color: var(--text-muted); }
.plugin-item { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); color: var(--text); }
.plugin-item:last-child { border-bottom: none; }
.footer { display: flex; justify-content: space-between; align-items: baseline; margin-top: auto; padding-top: 1rem; flex-shrink: 0; }
@@ -2848,6 +2854,9 @@ button:focus-visible, input:focus-visible, select:focus-visible {
#rds-raw-copy-btn {
width: 100%;
}
.about-grid {
grid-template-columns: 1fr;
}
.about-table,
.about-table tbody,
.about-table tr,
@@ -2857,8 +2866,8 @@ button:focus-visible, input:focus-visible, select:focus-visible {
box-sizing: border-box;
}
.about-table tr {
padding: 0.35rem 0;
border-bottom: 1px solid var(--border);
padding: 0.35rem 0.75rem;
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.about-table tr:last-child {
border-bottom: none;