[feat](trx-frontend): rebuild APRS history viewer
Replace the APRS plugin log with a richer history view that adds summaries, filtering, pause/resume, duplicate collapsing, structured rows, row actions, and expandable details. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -443,10 +443,38 @@
|
||||
</div>
|
||||
<div id="subtab-aprs" class="sub-tab-panel" style="display:none;">
|
||||
<div class="aprs-controls">
|
||||
<button id="aprs-pause-btn" type="button">Pause</button>
|
||||
<button id="aprs-clear-btn" type="button">Clear</button>
|
||||
<input id="aprs-filter" class="ft8-filter" type="text" placeholder="Filter (e.g. SP2, beacon)" />
|
||||
<small id="aprs-status" style="color:var(--text-muted);">Waiting for server decode</small>
|
||||
</div>
|
||||
<div class="aprs-summary">
|
||||
<div class="aprs-summary-card">
|
||||
<span class="aprs-summary-label">Frames</span>
|
||||
<span id="aprs-total-count" class="aprs-summary-value">0 total</span>
|
||||
</div>
|
||||
<div class="aprs-summary-card">
|
||||
<span class="aprs-summary-label">Visible</span>
|
||||
<span id="aprs-visible-count" class="aprs-summary-value">0 shown</span>
|
||||
</div>
|
||||
<div class="aprs-summary-card">
|
||||
<span class="aprs-summary-label">Latest</span>
|
||||
<span id="aprs-latest-seen" class="aprs-summary-value">No packets yet</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aprs-filter-row">
|
||||
<button id="aprs-type-all" class="aprs-chip active" type="button">All</button>
|
||||
<button id="aprs-type-position" class="aprs-chip" type="button">Position</button>
|
||||
<button id="aprs-type-message" class="aprs-chip" type="button">Message</button>
|
||||
<button id="aprs-type-weather" class="aprs-chip" type="button">Weather</button>
|
||||
<button id="aprs-type-telemetry" class="aprs-chip" type="button">Telemetry</button>
|
||||
<button id="aprs-type-other" class="aprs-chip" type="button">Other</button>
|
||||
</div>
|
||||
<div class="aprs-filter-row">
|
||||
<button id="aprs-only-pos-btn" class="aprs-chip" type="button">Only Pos</button>
|
||||
<button id="aprs-hide-crc-btn" class="aprs-chip" type="button">Hide CRC</button>
|
||||
<button id="aprs-collapse-dup-btn" class="aprs-chip" type="button">Collapse Dupes</button>
|
||||
</div>
|
||||
<div id="aprs-packets"></div>
|
||||
</div>
|
||||
<div id="subtab-ft8" class="sub-tab-panel" style="display:none;">
|
||||
|
||||
@@ -3,10 +3,23 @@ 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 aprsPauseBtn = document.getElementById("aprs-pause-btn");
|
||||
const aprsOnlyPosBtn = document.getElementById("aprs-only-pos-btn");
|
||||
const aprsHideCrcBtn = document.getElementById("aprs-hide-crc-btn");
|
||||
const aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
|
||||
const aprsTotalCountEl = document.getElementById("aprs-total-count");
|
||||
const aprsVisibleCountEl = document.getElementById("aprs-visible-count");
|
||||
const aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
|
||||
const APRS_MAX_PACKETS = 100;
|
||||
const APRS_BAR_WINDOW_MS = 15 * 60 * 1000;
|
||||
let aprsFilterText = "";
|
||||
let aprsPacketHistory = [];
|
||||
let aprsPaused = false;
|
||||
let aprsBufferedWhilePaused = 0;
|
||||
let aprsOnlyPos = false;
|
||||
let aprsHideCrc = false;
|
||||
let aprsCollapseDup = false;
|
||||
let aprsTypeFilter = "all";
|
||||
|
||||
function renderAprsInfo(pkt) {
|
||||
const bytes = Array.isArray(pkt.info_bytes) ? pkt.info_bytes : null;
|
||||
@@ -47,12 +60,149 @@ function renderAprsInfo(pkt) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function renderAprsRow(pkt) {
|
||||
function aprsPacketCategory(pkt) {
|
||||
const type = String(pkt.type || "").toLowerCase();
|
||||
const info = String(pkt.info || "").toLowerCase();
|
||||
if (pkt.lat != null && pkt.lon != null || type.includes("position")) return "position";
|
||||
if (type.includes("message") || info.startsWith(":")) return "message";
|
||||
if (type.includes("weather") || info.startsWith("_")) return "weather";
|
||||
if (type.includes("telemetry") || info.startsWith("t#")) return "telemetry";
|
||||
return "other";
|
||||
}
|
||||
|
||||
function aprsCategoryLabel(category) {
|
||||
switch (category) {
|
||||
case "position": return "Position";
|
||||
case "message": return "Message";
|
||||
case "weather": return "Weather";
|
||||
case "telemetry": return "Telemetry";
|
||||
default: return "Other";
|
||||
}
|
||||
}
|
||||
|
||||
function aprsAgeText(tsMs) {
|
||||
if (!Number.isFinite(tsMs)) return "just now";
|
||||
const deltaMs = Math.max(0, Date.now() - tsMs);
|
||||
const seconds = Math.round(deltaMs / 1000);
|
||||
if (seconds < 5) return "just now";
|
||||
if (seconds < 60) return `${seconds}s ago`;
|
||||
const minutes = Math.round(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.round(minutes / 60);
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
|
||||
function aprsDistanceText(pkt) {
|
||||
if (serverLat == null || serverLon == null || pkt.lat == null || pkt.lon == null) return "";
|
||||
const distKm = haversineKm(serverLat, serverLon, pkt.lat, pkt.lon);
|
||||
if (!Number.isFinite(distKm)) return "";
|
||||
if (distKm < 1) return `${Math.round(distKm * 1000)} m from TRX`;
|
||||
return `${distKm.toFixed(1)} km from TRX`;
|
||||
}
|
||||
|
||||
function aprsPacketSignature(pkt) {
|
||||
return [
|
||||
pkt.srcCall || "",
|
||||
pkt.destCall || "",
|
||||
pkt.path || "",
|
||||
pkt.info || "",
|
||||
pkt.type || "",
|
||||
pkt.lat != null ? pkt.lat.toFixed(4) : "",
|
||||
pkt.lon != null ? pkt.lon.toFixed(4) : "",
|
||||
].join("|");
|
||||
}
|
||||
|
||||
function aprsHexBytes(bytes) {
|
||||
if (!Array.isArray(bytes) || bytes.length === 0) return "--";
|
||||
return bytes.map((b) => Number(b).toString(16).toUpperCase().padStart(2, "0")).join(" ");
|
||||
}
|
||||
|
||||
function aprsFilterMatch(pkt) {
|
||||
if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
|
||||
if (aprsHideCrc && !pkt.crcOk) return false;
|
||||
if (aprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== aprsTypeFilter) return false;
|
||||
if (!aprsFilterText) return true;
|
||||
const haystack = [
|
||||
pkt.srcCall,
|
||||
pkt.destCall,
|
||||
pkt.path,
|
||||
pkt.info,
|
||||
pkt.type,
|
||||
pkt.lat != null ? pkt.lat.toFixed(4) : "",
|
||||
pkt.lon != null ? pkt.lon.toFixed(4) : "",
|
||||
aprsPacketCategory(pkt),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toUpperCase();
|
||||
return haystack.includes(aprsFilterText);
|
||||
}
|
||||
|
||||
function aprsVisiblePackets() {
|
||||
const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
|
||||
return packets.filter(aprsFilterMatch);
|
||||
}
|
||||
|
||||
function collapseAprsDuplicates(packets) {
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const pkt of packets) {
|
||||
const key = aprsPacketSignature(pkt);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push(pkt);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function updateAprsSummary() {
|
||||
const visible = aprsVisiblePackets();
|
||||
if (aprsTotalCountEl) {
|
||||
aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
|
||||
}
|
||||
if (aprsVisibleCountEl) {
|
||||
let text = `${visible.length} shown`;
|
||||
if (aprsPaused && aprsBufferedWhilePaused > 0) {
|
||||
text += ` · ${aprsBufferedWhilePaused} buffered`;
|
||||
}
|
||||
aprsVisibleCountEl.textContent = text;
|
||||
}
|
||||
if (aprsLatestSeenEl) {
|
||||
const latest = aprsPacketHistory[0];
|
||||
if (!latest) {
|
||||
aprsLatestSeenEl.textContent = "No packets yet";
|
||||
} else {
|
||||
aprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateAprsChipState() {
|
||||
document.querySelectorAll("[id^='aprs-type-']").forEach((btn) => {
|
||||
btn.classList.toggle("active", btn.id === `aprs-type-${aprsTypeFilter}`);
|
||||
});
|
||||
aprsOnlyPosBtn?.classList.toggle("active", aprsOnlyPos);
|
||||
aprsHideCrcBtn?.classList.toggle("active", aprsHideCrc);
|
||||
aprsCollapseDupBtn?.classList.toggle("active", aprsCollapseDup);
|
||||
if (aprsPauseBtn) {
|
||||
aprsPauseBtn.textContent = aprsPaused ? "Resume" : "Pause";
|
||||
aprsPauseBtn.classList.toggle("active", aprsPaused);
|
||||
}
|
||||
}
|
||||
|
||||
function renderAprsRow(pkt, isFresh) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "aprs-packet";
|
||||
if (!pkt.crcOk) row.style.opacity = "0.5";
|
||||
if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
|
||||
if (isFresh) row.classList.add("aprs-packet-new");
|
||||
|
||||
const ts = pkt._ts || new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
const crcTag = pkt.crcOk ? "" : ' <span style="color:var(--accent-red);">[CRC]</span>';
|
||||
const age = aprsAgeText(pkt._tsMs);
|
||||
const category = aprsPacketCategory(pkt);
|
||||
const categoryLabel = aprsCategoryLabel(category);
|
||||
const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`;
|
||||
const pathBadge = pkt.path ? `<span class="aprs-badge">${escapeMapHtml(pkt.path)}</span>` : "";
|
||||
const crcBadge = pkt.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC Fail</span>';
|
||||
let symbolHtml = "";
|
||||
if (pkt.symbolTable && pkt.symbolCode) {
|
||||
const sheet = pkt.symbolTable === "/" ? 0 : 1;
|
||||
@@ -63,44 +213,93 @@ function renderAprsRow(pkt) {
|
||||
const bgY = -(row2 * 24);
|
||||
symbolHtml = `<span class="aprs-symbol" style="background-image:url('https://raw.githubusercontent.com/hessu/aprs-symbols/master/png/aprs-symbols-24-${sheet}.png');background-position:${bgX}px ${bgY}px"></span>`;
|
||||
}
|
||||
let posHtml = "";
|
||||
if (pkt.lat != null && pkt.lon != null) {
|
||||
const osmUrl = `https://www.openstreetmap.org/?mlat=${pkt.lat}&mlon=${pkt.lon}#map=15/${pkt.lat}/${pkt.lon}`;
|
||||
posHtml = ` <a class="aprs-pos" href="${osmUrl}" target="_blank">${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}</a>`;
|
||||
}
|
||||
const receiverHtml = pkt.receiver
|
||||
? `<span class="decode-rig-badge" style="--decode-rig-color:${pkt.receiver.color};">${pkt.receiver.label}</span> `
|
||||
const posLink = pkt.lat != null && pkt.lon != null
|
||||
? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${pkt.lat},${pkt.lon}">${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}</a>`
|
||||
: "";
|
||||
row.dataset.filterText = [
|
||||
pkt.receiver ? pkt.receiver.label : "",
|
||||
pkt.srcCall,
|
||||
pkt.destCall,
|
||||
pkt.path,
|
||||
pkt.info,
|
||||
pkt.type,
|
||||
pkt.lat != null ? pkt.lat.toFixed(4) : "",
|
||||
pkt.lon != null ? pkt.lon.toFixed(4) : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toUpperCase();
|
||||
row.innerHTML = `<span class="aprs-time">${ts}</span>${receiverHtml}${symbolHtml}<span class="aprs-call">${pkt.srcCall}</span>>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}: <span title="${pkt.type}">${renderAprsInfo(pkt)}</span>${posHtml}${crcTag}`;
|
||||
applyAprsFilterToRow(row);
|
||||
const distance = aprsDistanceText(pkt);
|
||||
const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
|
||||
|
||||
row.innerHTML =
|
||||
`<div class="aprs-row-head">` +
|
||||
`<span class="aprs-time">${ts}</span>` +
|
||||
symbolHtml +
|
||||
`<span class="aprs-call">${escapeMapHtml(pkt.srcCall)}</span>` +
|
||||
`<span>>${escapeMapHtml(pkt.destCall || "")}</span>` +
|
||||
`<span class="${categoryClass}">${escapeMapHtml(categoryLabel)}</span>` +
|
||||
pathBadge +
|
||||
crcBadge +
|
||||
`</div>` +
|
||||
`<div class="aprs-row-meta">` +
|
||||
`<span class="aprs-meta-text">${escapeMapHtml(age)}</span>` +
|
||||
(distance ? `<span class="aprs-meta-text">${escapeMapHtml(distance)}</span>` : "") +
|
||||
`<span class="aprs-meta-text">${escapeMapHtml(pkt.type || "--")}</span>` +
|
||||
`</div>` +
|
||||
`<div class="aprs-row-detail">` +
|
||||
`<span title="${escapeMapHtml(pkt.type || "")}">${renderAprsInfo(pkt)}</span>` +
|
||||
(posLink ? `<span>${posLink}</span>` : "") +
|
||||
`</div>` +
|
||||
`<div class="aprs-row-actions">` +
|
||||
(pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${pkt.lat},${pkt.lon}">Map</button>` : "") +
|
||||
(pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${pkt.lat},${pkt.lon}">Copy Coords</button>` : "") +
|
||||
`<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a>` +
|
||||
`</div>` +
|
||||
`<details class="aprs-details">` +
|
||||
`<summary>Details</summary>` +
|
||||
`<div class="aprs-details-grid">` +
|
||||
`<span class="aprs-detail-label">Source</span><span class="aprs-detail-value">${escapeMapHtml(pkt.srcCall || "--")}</span>` +
|
||||
`<span class="aprs-detail-label">Destination</span><span class="aprs-detail-value">${escapeMapHtml(pkt.destCall || "--")}</span>` +
|
||||
`<span class="aprs-detail-label">Type</span><span class="aprs-detail-value">${escapeMapHtml(pkt.type || "--")}</span>` +
|
||||
`<span class="aprs-detail-label">Path</span><span class="aprs-detail-value">${escapeMapHtml(pkt.path || "--")}</span>` +
|
||||
`<span class="aprs-detail-label">Age</span><span class="aprs-detail-value">${escapeMapHtml(age)}</span>` +
|
||||
`<span class="aprs-detail-label">CRC</span><span class="aprs-detail-value">${pkt.crcOk ? "OK" : "Failed"}</span>` +
|
||||
`<span class="aprs-detail-label">Position</span><span class="aprs-detail-value">${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}</span>` +
|
||||
`<span class="aprs-detail-label">Info</span><span class="aprs-detail-value">${escapeMapHtml(pkt.info || "--")}</span>` +
|
||||
`<span class="aprs-detail-label">Info Bytes</span><span class="aprs-detail-value">${escapeMapHtml(aprsHexBytes(pkt.info_bytes))}</span>` +
|
||||
`</div>` +
|
||||
`</details>`;
|
||||
|
||||
row.querySelectorAll("[data-aprs-map]").forEach((el) => {
|
||||
el.addEventListener("click", (evt) => {
|
||||
evt.preventDefault();
|
||||
const raw = String(el.dataset.aprsMap || "");
|
||||
const [lat, lon] = raw.split(",").map(Number);
|
||||
if (window.navigateToAprsMap && Number.isFinite(lat) && Number.isFinite(lon)) {
|
||||
window.navigateToAprsMap(lat, lon);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const copyBtn = row.querySelector("[data-aprs-copy]");
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener("click", async () => {
|
||||
const raw = String(copyBtn.dataset.aprsCopy || "");
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(raw);
|
||||
showHint("Coordinates copied", 1200);
|
||||
}
|
||||
} catch (_e) {
|
||||
showHint("Copy failed", 1500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function applyAprsFilterToRow(row) {
|
||||
if (!aprsFilterText) {
|
||||
row.style.display = "";
|
||||
function renderAprsHistory() {
|
||||
if (!aprsPacketsEl || aprsPaused) {
|
||||
updateAprsSummary();
|
||||
updateAprsChipState();
|
||||
return;
|
||||
}
|
||||
const message = row.dataset.filterText || "";
|
||||
row.style.display = message.includes(aprsFilterText) ? "" : "none";
|
||||
}
|
||||
|
||||
function applyAprsFilterToAll() {
|
||||
const rows = aprsPacketsEl.querySelectorAll(".aprs-packet");
|
||||
rows.forEach((row) => applyAprsFilterToRow(row));
|
||||
const visible = aprsVisiblePackets();
|
||||
aprsPacketsEl.innerHTML = "";
|
||||
for (let i = 0; i < visible.length; i++) {
|
||||
aprsPacketsEl.appendChild(renderAprsRow(visible[i], i === 0));
|
||||
}
|
||||
updateAprsSummary();
|
||||
updateAprsChipState();
|
||||
}
|
||||
|
||||
function updateAprsBar() {
|
||||
@@ -108,12 +307,13 @@ function updateAprsBar() {
|
||||
const isPkt = (document.getElementById("mode")?.value || "").toUpperCase() === "PKT";
|
||||
const cutoffMs = Date.now() - APRS_BAR_WINDOW_MS;
|
||||
const okFrames = aprsPacketHistory.filter((p) => p.crcOk && p._tsMs >= cutoffMs);
|
||||
if (!isPkt || okFrames.length === 0) {
|
||||
const frames = collapseAprsDuplicates(okFrames).slice(0, 8);
|
||||
if (!isPkt || frames.length === 0) {
|
||||
aprsBarOverlay.style.display = "none";
|
||||
return;
|
||||
}
|
||||
let html = '<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">APRS</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearAprsBar()" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();window.clearAprsBar();}" aria-label="Clear APRS overlay">Clear</span></span><span class="aprs-bar-window">Last 15 minutes</span></div>';
|
||||
for (const pkt of okFrames) {
|
||||
for (const pkt of frames) {
|
||||
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 || "");
|
||||
@@ -134,9 +334,11 @@ window.clearAprsBar = function() {
|
||||
};
|
||||
|
||||
window.resetAprsHistoryView = function() {
|
||||
aprsPacketsEl.innerHTML = "";
|
||||
if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
|
||||
aprsPacketHistory = [];
|
||||
aprsBufferedWhilePaused = 0;
|
||||
updateAprsBar();
|
||||
renderAprsHistory();
|
||||
if (window.clearMapMarkersByType) window.clearMapMarkersByType("aprs");
|
||||
};
|
||||
|
||||
@@ -144,7 +346,6 @@ 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);
|
||||
|
||||
// Stamp timestamp for persistence
|
||||
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
||||
pkt._tsMs = tsMs;
|
||||
pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
@@ -152,17 +353,20 @@ function addAprsPacket(pkt) {
|
||||
aprsPacketHistory.unshift(pkt);
|
||||
if (aprsPacketHistory.length > APRS_MAX_PACKETS) aprsPacketHistory.length = APRS_MAX_PACKETS;
|
||||
|
||||
// Update overview bar (CRC-failed frames excluded)
|
||||
if (pkt.crcOk) 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, pkt);
|
||||
}
|
||||
aprsPacketsEl.prepend(row);
|
||||
while (aprsPacketsEl.children.length > APRS_MAX_PACKETS) {
|
||||
aprsPacketsEl.removeChild(aprsPacketsEl.lastChild);
|
||||
|
||||
if (pkt.crcOk) updateAprsBar();
|
||||
|
||||
if (aprsPaused) {
|
||||
aprsBufferedWhilePaused += 1;
|
||||
updateAprsSummary();
|
||||
updateAprsChipState();
|
||||
return;
|
||||
}
|
||||
|
||||
renderAprsHistory();
|
||||
}
|
||||
|
||||
document.getElementById("aprs-clear-btn").addEventListener("click", async () => {
|
||||
@@ -174,16 +378,59 @@ document.getElementById("aprs-clear-btn").addEventListener("click", async () =>
|
||||
}
|
||||
});
|
||||
|
||||
if (aprsPauseBtn) {
|
||||
aprsPauseBtn.addEventListener("click", () => {
|
||||
aprsPaused = !aprsPaused;
|
||||
if (!aprsPaused) {
|
||||
aprsBufferedWhilePaused = 0;
|
||||
renderAprsHistory();
|
||||
} else {
|
||||
updateAprsSummary();
|
||||
updateAprsChipState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (aprsOnlyPosBtn) {
|
||||
aprsOnlyPosBtn.addEventListener("click", () => {
|
||||
aprsOnlyPos = !aprsOnlyPos;
|
||||
renderAprsHistory();
|
||||
});
|
||||
}
|
||||
|
||||
if (aprsHideCrcBtn) {
|
||||
aprsHideCrcBtn.addEventListener("click", () => {
|
||||
aprsHideCrc = !aprsHideCrc;
|
||||
renderAprsHistory();
|
||||
});
|
||||
}
|
||||
|
||||
if (aprsCollapseDupBtn) {
|
||||
aprsCollapseDupBtn.addEventListener("click", () => {
|
||||
aprsCollapseDup = !aprsCollapseDup;
|
||||
renderAprsHistory();
|
||||
});
|
||||
}
|
||||
|
||||
["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
|
||||
const btn = document.getElementById(`aprs-type-${type}`);
|
||||
if (!btn) return;
|
||||
btn.addEventListener("click", () => {
|
||||
aprsTypeFilter = type;
|
||||
renderAprsHistory();
|
||||
});
|
||||
});
|
||||
|
||||
if (aprsFilterInput) {
|
||||
aprsFilterInput.addEventListener("input", () => {
|
||||
aprsFilterText = aprsFilterInput.value.trim().toUpperCase();
|
||||
applyAprsFilterToAll();
|
||||
renderAprsHistory();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Server-side APRS decode handler ---
|
||||
window.onServerAprs = function(pkt) {
|
||||
aprsStatus.textContent = "Receiving";
|
||||
aprsStatus.textContent = aprsPaused ? "Paused" : "Receiving";
|
||||
addAprsPacket({
|
||||
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
|
||||
srcCall: pkt.src_call,
|
||||
@@ -199,3 +446,5 @@ window.onServerAprs = function(pkt) {
|
||||
symbolCode: pkt.symbol_code,
|
||||
});
|
||||
};
|
||||
|
||||
renderAprsHistory();
|
||||
|
||||
@@ -1088,6 +1088,64 @@ small { color: var(--text-muted); }
|
||||
.sub-tab:hover:not(.active) { color: var(--text); }
|
||||
#aprs-map { min-height: 150px; border-radius: 6px; }
|
||||
.aprs-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
|
||||
.aprs-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.aprs-summary-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.18rem;
|
||||
padding: 0.45rem 0.55rem;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--card-bg) 84%, transparent);
|
||||
}
|
||||
.aprs-summary-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.aprs-summary-value {
|
||||
color: var(--text);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.aprs-filter-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.aprs-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 1.9rem;
|
||||
padding: 0.18rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--filter-border);
|
||||
background: var(--filter-bg);
|
||||
color: var(--filter-fg);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.aprs-chip.active {
|
||||
color: var(--card-bg);
|
||||
background: var(--accent-green);
|
||||
border-color: var(--accent-green);
|
||||
}
|
||||
.aprs-chip:hover:not(.active) {
|
||||
border-color: var(--accent-green);
|
||||
color: var(--text);
|
||||
}
|
||||
.ais-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -1130,7 +1188,7 @@ small { color: var(--text-muted); }
|
||||
#ais-messages { max-height: 360px; overflow-y: auto; border: 1px solid var(--border-light); border-radius: 6px; background: var(--input-bg); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
#aprs-packets {
|
||||
flex: 1 1 auto;
|
||||
min-height: calc(100vh - 21rem);
|
||||
min-height: calc(100vh - 28rem);
|
||||
max-height: none;
|
||||
}
|
||||
#ais-messages {
|
||||
@@ -1138,8 +1196,128 @@ small { color: var(--text-muted); }
|
||||
min-height: calc(100vh - 24rem);
|
||||
max-height: none;
|
||||
}
|
||||
.aprs-packet { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--border); line-height: 1.4; }
|
||||
.aprs-packet { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; padding: 0.45rem 0.55rem; border-bottom: 1px solid var(--border); line-height: 1.35; }
|
||||
.aprs-packet:last-child { border-bottom: none; }
|
||||
.aprs-packet-new {
|
||||
animation: aprs-row-flash 1.2s ease;
|
||||
}
|
||||
.aprs-packet-crc {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.aprs-row-head,
|
||||
.aprs-row-meta,
|
||||
.aprs-row-detail,
|
||||
.aprs-row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.aprs-row-head + .aprs-row-meta,
|
||||
.aprs-row-meta + .aprs-row-detail,
|
||||
.aprs-row-detail + .aprs-row-actions {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.aprs-row-detail {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.aprs-row-actions {
|
||||
margin-top: 0.28rem;
|
||||
}
|
||||
.aprs-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 1.2rem;
|
||||
padding: 0.02rem 0.38rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-light) 78%, transparent);
|
||||
background: color-mix(in srgb, var(--card-bg) 72%, transparent);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.aprs-badge-type {
|
||||
color: var(--text);
|
||||
}
|
||||
.aprs-badge-type-position {
|
||||
color: #77d6a5;
|
||||
border-color: color-mix(in srgb, #77d6a5 42%, transparent);
|
||||
background: color-mix(in srgb, #77d6a5 12%, transparent);
|
||||
}
|
||||
.aprs-badge-type-message {
|
||||
color: #8ec8ff;
|
||||
border-color: color-mix(in srgb, #8ec8ff 42%, transparent);
|
||||
background: color-mix(in srgb, #8ec8ff 12%, transparent);
|
||||
}
|
||||
.aprs-badge-type-weather {
|
||||
color: #ffd77a;
|
||||
border-color: color-mix(in srgb, #ffd77a 42%, transparent);
|
||||
background: color-mix(in srgb, #ffd77a 14%, transparent);
|
||||
}
|
||||
.aprs-badge-type-telemetry {
|
||||
color: #d4a5ff;
|
||||
border-color: color-mix(in srgb, #d4a5ff 42%, transparent);
|
||||
background: color-mix(in srgb, #d4a5ff 12%, transparent);
|
||||
}
|
||||
.aprs-badge-crc {
|
||||
color: #ff9a9a;
|
||||
border-color: color-mix(in srgb, #ff9a9a 42%, transparent);
|
||||
background: color-mix(in srgb, #ff9a9a 10%, transparent);
|
||||
}
|
||||
.aprs-meta-text {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
.aprs-inline-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 1.7rem;
|
||||
padding: 0.08rem 0.42rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--filter-border);
|
||||
background: var(--filter-bg);
|
||||
color: var(--filter-fg);
|
||||
font: inherit;
|
||||
font-size: 0.74rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.aprs-inline-btn:hover {
|
||||
border-color: var(--accent-green);
|
||||
color: var(--text);
|
||||
}
|
||||
.aprs-details {
|
||||
width: 100%;
|
||||
}
|
||||
.aprs-details summary {
|
||||
cursor: pointer;
|
||||
color: var(--accent-green);
|
||||
font-size: 0.76rem;
|
||||
user-select: none;
|
||||
}
|
||||
.aprs-details[open] summary {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.aprs-details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.2rem 0.65rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.aprs-detail-label {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.aprs-detail-value {
|
||||
color: var(--text);
|
||||
word-break: break-word;
|
||||
}
|
||||
.ais-message { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; padding: 0.45rem 0.55rem; border-bottom: 1px solid var(--border); line-height: 1.35; }
|
||||
.ais-message:last-child { border-bottom: none; }
|
||||
.aprs-call { color: var(--accent-green); font-weight: 600; }
|
||||
@@ -1221,6 +1399,10 @@ small { color: var(--text-muted); }
|
||||
.aprs-bar-pos { background: none; border: none; padding: 0; margin-left: 0.4em; font-family: inherit; font-size: inherit; color: var(--accent-green); cursor: pointer; }
|
||||
.aprs-bar-pos:hover { text-decoration: underline; }
|
||||
.aprs-byte { color: var(--accent-yellow); background: rgba(255, 214, 0, 0.12); border: 1px solid rgba(255, 214, 0, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-size: 0.78em; }
|
||||
@keyframes aprs-row-flash {
|
||||
0% { background: color-mix(in srgb, var(--accent-green) 14%, transparent); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
.ft8-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
|
||||
.ft8-filter {
|
||||
flex: 1;
|
||||
@@ -1554,6 +1736,9 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
.ais-summary {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.aprs-summary {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
#subtab-ais {
|
||||
min-height: calc(100vh - 14rem);
|
||||
}
|
||||
@@ -1561,11 +1746,15 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
min-height: calc(100vh - 14rem);
|
||||
}
|
||||
#aprs-packets {
|
||||
min-height: calc(100vh - 19rem);
|
||||
min-height: calc(100vh - 26rem);
|
||||
}
|
||||
#ais-messages {
|
||||
min-height: calc(100vh - 22rem);
|
||||
}
|
||||
.aprs-details-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0.14rem;
|
||||
}
|
||||
.aprs-controls > button,
|
||||
.ft8-controls > button,
|
||||
.cw-controls > button {
|
||||
|
||||
Reference in New Issue
Block a user