diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
index f555024..d6f8eab 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
@@ -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 = '';
+ for (const pkt of aprsBarFrames) {
+ const ts = pkt._ts ? `
${pkt._ts}` : "";
+ const call = `
${escapeMapHtml(pkt.srcCall)}`;
+ const dest = escapeMapHtml(pkt.destCall || "");
+ const info = escapeMapHtml((pkt.info || "").slice(0, 60));
+ const crc = pkt.crcOk ? "" : '
[CRC]';
+ html += `
${ts}${call}>${dest}: ${info}${crc}
`;
+ }
+ 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", () => {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
index c294d35..c628249 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
@@ -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;