[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:
@@ -74,6 +74,7 @@
|
||||
<div class="overview-strip">
|
||||
<canvas id="overview-canvas" aria-hidden="true"></canvas>
|
||||
<div id="rds-ps-overlay" aria-live="polite" aria-label="RDS station name"></div>
|
||||
<div id="aprs-bar-overlay" aria-live="polite" aria-label="Recent APRS frames"></div>
|
||||
</div>
|
||||
<div id="spectrum-panel" style="display:none;">
|
||||
<div class="spectrum-wrap">
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -617,6 +617,71 @@ small { color: var(--text-muted); }
|
||||
color: var(--text-muted);
|
||||
background: color-mix(in srgb, var(--card-bg) 62%, transparent);
|
||||
}
|
||||
#aprs-bar-overlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 0.4rem;
|
||||
left: 0.4rem;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
color: var(--text-heading);
|
||||
padding: 0.28rem 0.72rem 0.32rem;
|
||||
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
|
||||
border-radius: 0.55rem;
|
||||
background: color-mix(in srgb, var(--card-bg) 52%, transparent);
|
||||
backdrop-filter: blur(14px) saturate(135%);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(135%);
|
||||
box-shadow:
|
||||
0 8px 18px color-mix(in srgb, #000000 16%, transparent),
|
||||
inset 0 1px 0 color-mix(in srgb, #ffffff 10%, transparent);
|
||||
max-width: min(92vw, 36rem);
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
.aprs-bar-header {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: clamp(0.52rem, 0.95vw, 0.66rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--accent-green);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.12rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.aprs-bar-frame {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: clamp(0.58rem, 1.05vw, 0.74rem);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.45;
|
||||
opacity: 1;
|
||||
}
|
||||
.aprs-bar-frame + .aprs-bar-frame {
|
||||
opacity: 0.72;
|
||||
}
|
||||
.aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame {
|
||||
opacity: 0.34;
|
||||
}
|
||||
.aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame {
|
||||
opacity: 0.2;
|
||||
}
|
||||
.aprs-bar-time {
|
||||
color: var(--text-muted);
|
||||
margin-right: 0.35em;
|
||||
}
|
||||
.aprs-bar-call {
|
||||
color: var(--accent-green);
|
||||
font-weight: 600;
|
||||
}
|
||||
.aprs-bar-crc {
|
||||
color: var(--accent-red);
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
.overview-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user