[feat](trx-frontend-http): add APRS bar overlay on overview strip

Show the last 5 received APRS frames as a compact overlay in the
bottom-left corner of the waterfall strip, styled similarly to the
RDS PS overlay (backdrop blur, pill border). Frames fade out by
recency via CSS sibling-selector opacity steps. Bar auto-hides when
empty and is cleared by the APRS clear button. Restored from
localStorage on page load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-01 14:37:17 +01:00
parent fce024b090
commit 62c5889619
3 changed files with 99 additions and 0 deletions
@@ -2,11 +2,15 @@
const aprsStatus = document.getElementById("aprs-status");
const aprsPacketsEl = document.getElementById("aprs-packets");
const aprsFilterInput = document.getElementById("aprs-filter");
const aprsBarOverlay = document.getElementById("aprs-bar-overlay");
const APRS_MAX_PACKETS = 100;
const APRS_BAR_MAX = 5;
let aprsFilterText = "";
// Persistent packet history
let aprsPacketHistory = loadSetting("aprsPackets", []);
// Ring buffer of last N packets for the overview bar
let aprsBarFrames = [];
function renderAprsInfo(pkt) {
const bytes = Array.isArray(pkt.info_bytes) ? pkt.info_bytes : null;
@@ -103,6 +107,25 @@ function applyAprsFilterToAll() {
rows.forEach((row) => applyAprsFilterToRow(row));
}
function updateAprsBar() {
if (!aprsBarOverlay) return;
if (aprsBarFrames.length === 0) {
aprsBarOverlay.style.display = "none";
return;
}
let html = '<div class="aprs-bar-header">APRS</div>';
for (const pkt of aprsBarFrames) {
const ts = pkt._ts ? `<span class="aprs-bar-time">${pkt._ts}</span>` : "";
const call = `<span class="aprs-bar-call">${escapeMapHtml(pkt.srcCall)}</span>`;
const dest = escapeMapHtml(pkt.destCall || "");
const info = escapeMapHtml((pkt.info || "").slice(0, 60));
const crc = pkt.crcOk ? "" : '<span class="aprs-bar-crc">[CRC]</span>';
html += `<div class="aprs-bar-frame">${ts}${call}>${dest}: ${info}${crc}</div>`;
}
aprsBarOverlay.innerHTML = html;
aprsBarOverlay.style.display = "flex";
}
function addAprsPacket(pkt) {
const tag = pkt.crcOk ? "[APRS]" : "[APRS-CRC-FAIL]";
console.log(tag, `${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: ${pkt.info}`, pkt);
@@ -115,6 +138,11 @@ function addAprsPacket(pkt) {
if (aprsPacketHistory.length > APRS_MAX_PACKETS) aprsPacketHistory.length = APRS_MAX_PACKETS;
saveSetting("aprsPackets", aprsPacketHistory);
// Update overview bar
aprsBarFrames.unshift(pkt);
if (aprsBarFrames.length > APRS_BAR_MAX) aprsBarFrames.length = APRS_BAR_MAX;
updateAprsBar();
const row = renderAprsRow(pkt);
if (pkt.lat != null && pkt.lon != null && window.aprsMapAddStation) {
window.aprsMapAddStation(pkt.srcCall, pkt.lat, pkt.lon, pkt.info, pkt.symbolTable, pkt.symbolCode);
@@ -129,6 +157,8 @@ document.getElementById("aprs-clear-btn").addEventListener("click", async () =>
aprsPacketsEl.innerHTML = "";
aprsPacketHistory = [];
saveSetting("aprsPackets", []);
aprsBarFrames = [];
updateAprsBar();
try { await postPath("/clear_aprs_decode"); } catch (e) { console.error("APRS clear failed", e); }
});
@@ -140,6 +170,9 @@ for (let i = aprsPacketHistory.length - 1; i >= 0; i--) {
window.aprsMapAddStation(pkt.srcCall, pkt.lat, pkt.lon, pkt.info, pkt.symbolTable, pkt.symbolCode);
}
}
// Pre-populate bar from history (most recent first)
aprsBarFrames = aprsPacketHistory.slice(0, APRS_BAR_MAX);
updateAprsBar();
if (aprsFilterInput) {
aprsFilterInput.addEventListener("input", () => {