[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 freqDirty = false;
let initialized = false; let initialized = false;
let lastEventAt = Date.now(); let lastEventAt = Date.now();
let aboutUptimeStart = null;
let es; let es;
let esHeartbeat; 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 reconnectTimer = null;
let overviewSignalSamples = []; let overviewSignalSamples = [];
let overviewSignalTimer = null; let overviewSignalTimer = null;
@@ -3194,20 +3214,23 @@ function render(update) {
} }
if (typeof update.clients === "number") lastClientCount = update.clients; if (typeof update.clients === "number") lastClientCount = update.clients;
// Populate About tab // Populate About tab — Server card
if (update.server_version) { if (update.server_version) {
document.getElementById("about-server-ver").textContent = `trx-server v${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; document.getElementById("about-server-addr").textContent = location.host;
if (update.server_callsign) { if (update.server_callsign) {
document.getElementById("about-server-call").textContent = update.server_callsign; document.getElementById("about-server-call").textContent = update.server_callsign;
} }
if (update.pskreporter_status) { if (Number.isFinite(serverLat) && Number.isFinite(serverLon)) {
document.getElementById("about-pskreporter").textContent = update.pskreporter_status; const grid = latLonToMaidenhead(serverLat, serverLon);
} document.getElementById("about-server-location").textContent = `${grid} (${serverLat.toFixed(4)}, ${serverLon.toFixed(4)})`;
if (update.aprs_is_status) {
document.getElementById("about-aprs-is").textContent = update.aprs_is_status;
} }
// About — Radio card
if (update.info) { if (update.info) {
const parts = [update.info.manufacturer, update.info.model, update.info.revision].filter(Boolean).join(" "); const parts = [update.info.manufacturer, update.info.model, update.info.revision].filter(Boolean).join(" ");
if (parts) document.getElementById("about-rig-info").textContent = parts; 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) { if (lastActiveRigId) {
document.getElementById("about-active-rig").textContent = lastActiveRigId; document.getElementById("about-active-rig").textContent = lastActiveRigId;
} }
if (Array.isArray(update.remotes)) { if (Array.isArray(update.remotes)) {
applyRigList(update.active_remote, 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") { if (typeof update.rigctl_clients === "number") {
document.getElementById("about-rigctl-clients").textContent = update.rigctl_clients; document.getElementById("about-rigctl-clients").textContent = update.rigctl_clients;
} }
if (typeof update.rigctl_addr === "string" && update.rigctl_addr.length > 0) { if (typeof update.rigctl_addr === "string" && update.rigctl_addr.length > 0) {
document.getElementById("about-rigctl-endpoint").textContent = update.rigctl_addr; 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(); powerHint.textContent = readyText();
lastLocked = update.status && update.status.lock === true; lastLocked = update.status && update.status.lock === true;
lockBtn.textContent = lastLocked ? "Unlock" : "Lock"; lockBtn.textContent = lastLocked ? "Unlock" : "Lock";
@@ -3313,6 +3374,7 @@ function connect() {
lastEventAt = Date.now(); lastEventAt = Date.now();
es.onopen = () => { es.onopen = () => {
setConnLostOverlay(false); setConnLostOverlay(false);
if (!aboutUptimeStart) aboutUptimeStart = Date.now();
pollFreshSnapshot(); pollFreshSnapshot();
refreshRigList(); refreshRigList();
}; };
@@ -1015,24 +1015,93 @@
</div> </div>
<div id="tab-about" class="tab-panel" style="display:none;"> <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> <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>
<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"> <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>Rig</td><td id="about-rig-info">--</td></tr>
<tr><td>Active rig</td><td id="about-active-rig">--</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>Available rigs</td><td id="about-rig-list">--</td></tr>
<tr><td>Rig connection</td><td id="about-rig-access">--</td></tr> <tr><td>Connection</td><td id="about-rig-access">--</td></tr>
<tr><td>Supported modes</td><td id="about-modes">--</td></tr> <tr><td>Modes</td><td id="about-modes">--</td></tr>
<tr><td>VFOs</td><td id="about-vfos">--</td></tr> <tr><td>VFOs</td><td id="about-vfos">--</td></tr>
<tr><td>Rigctl endpoint</td><td id="about-rigctl-endpoint">--</td></tr> </table>
<tr><td>Rigctl clients</td><td id="about-rigctl-clients">--</td></tr> </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>PSK Reporter</td><td id="about-pskreporter">--</td></tr>
<tr><td>APRS-IS</td><td id="about-aprs-is">--</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>Rigctl endpoint</td><td id="about-rigctl-endpoint">--</td></tr>
<tr><td>Connected clients</td><td id="about-clients">--</td></tr> <tr><td>Rigctl clients</td><td id="about-rigctl-clients">--</td></tr>
</table> </table>
</div> </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="footer">
<div class="copyright"> <div class="copyright">
Built by <a href="https://www.qrzcq.com/call/SP2SJG" target="_blank" rel="noopener">SP2SJG</a> from <a href="https://haxx.space" target="_blank" rel="noopener">haxx.space</a> · <span class="gh-link-wrap"><a class="gh-link" href="https://github.com/sgrams/trx-rs" target="_blank" rel="noopener" aria-label="Open trx-rs on GitHub"><svg class="gh-link-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0.2a8 8 0 0 0-2.53 15.59c0.4 0.07 0.55-0.17 0.55-0.39l-0.01-1.37c-2.23 0.49-2.7-0.95-2.7-0.95-0.36-0.91-0.89-1.15-0.89-1.15-0.73-0.49 0.06-0.48 0.06-0.48 0.8 0.06 1.22 0.82 1.22 0.82 0.72 1.22 1.88 0.87 2.34 0.67 0.07-0.51 0.28-0.86 0.5-1.06-1.78-0.2-3.64-0.89-3.64-3.95 0-0.87 0.31-1.58 0.81-2.14-0.08-0.2-0.35-1.02 0.08-2.12 0 0 0.67-0.21 2.2 0.82a7.56 7.56 0 0 1 4.01 0c1.53-1.03 2.2-0.82 2.2-0.82 0.43 1.1 0.16 1.92 0.08 2.12 0.51 0.56 0.81 1.27 0.81 2.14 0 3.07-1.87 3.75-3.66 3.95 0.29 0.25 0.54 0.73 0.54 1.48l-0.01 2.2c0 0.22 0.14 0.47 0.55 0.39A8 8 0 0 0 8 0.2Z"></path></svg><span>trx-rs on GitHub</span></a></span><span id="copyright-year"></span> Built by <a href="https://www.qrzcq.com/call/SP2SJG" target="_blank" rel="noopener">SP2SJG</a> from <a href="https://haxx.space" target="_blank" rel="noopener">haxx.space</a> · <span class="gh-link-wrap"><a class="gh-link" href="https://github.com/sgrams/trx-rs" target="_blank" rel="noopener" aria-label="Open trx-rs on GitHub"><svg class="gh-link-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0.2a8 8 0 0 0-2.53 15.59c0.4 0.07 0.55-0.17 0.55-0.39l-0.01-1.37c-2.23 0.49-2.7-0.95-2.7-0.95-0.36-0.91-0.89-1.15-0.89-1.15-0.73-0.49 0.06-0.48 0.06-0.48 0.8 0.06 1.22 0.82 1.22 0.82 0.72 1.22 1.88 0.87 2.34 0.67 0.07-0.51 0.28-0.86 0.5-1.06-1.78-0.2-3.64-0.89-3.64-3.95 0-0.87 0.31-1.58 0.81-2.14-0.08-0.2-0.35-1.02 0.08-2.12 0 0 0.67-0.21 2.2 0.82a7.56 7.56 0 0 1 4.01 0c1.53-1.03 2.2-0.82 2.2-0.82 0.43 1.1 0.16 1.92 0.08 2.12 0.51 0.56 0.81 1.27 0.81 2.14 0 3.07-1.87 3.75-3.66 3.95 0.29 0.25 0.54 0.73 0.54 1.48l-0.01 2.2c0 0.22 0.14 0.47 0.55 0.39A8 8 0 0 0 8 0.2Z"></path></svg><span>trx-rs on GitHub</span></a></span><span id="copyright-year"></span>
@@ -1217,10 +1217,16 @@ small { color: var(--text-muted); }
flex-shrink: 0; flex-shrink: 0;
} }
.tab-label { display: block; } .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 { 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 tr:last-child td { border-bottom: none; }
.about-table td:first-child { color: var(--text-muted); width: 40%; } .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 { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); color: var(--text); }
.plugin-item:last-child { border-bottom: none; } .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; } .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 { #rds-raw-copy-btn {
width: 100%; width: 100%;
} }
.about-grid {
grid-template-columns: 1fr;
}
.about-table, .about-table,
.about-table tbody, .about-table tbody,
.about-table tr, .about-table tr,
@@ -2857,8 +2866,8 @@ button:focus-visible, input:focus-visible, select:focus-visible {
box-sizing: border-box; box-sizing: border-box;
} }
.about-table tr { .about-table tr {
padding: 0.35rem 0; padding: 0.35rem 0.75rem;
border-bottom: 1px solid var(--border); border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
} }
.about-table tr:last-child { .about-table tr:last-child {
border-bottom: none; border-bottom: none;