diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
index cee6801..8b6d78a 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
@@ -443,10 +443,38 @@
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 e6bea63..7f1ef77 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
@@ -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 ? "" : '
[CRC]';
+ 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 ? `
${escapeMapHtml(pkt.path)}` : "";
+ const crcBadge = pkt.crcOk ? "" : '
CRC Fail';
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 = `
`;
}
- 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 = `
${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}`;
- }
- const receiverHtml = pkt.receiver
- ? `
${pkt.receiver.label} `
+ const posLink = pkt.lat != null && pkt.lon != null
+ ? `
${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}`
: "";
- 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 = `
${ts}${receiverHtml}${symbolHtml}
${pkt.srcCall}>${pkt.destCall}${pkt.path ? "," + pkt.path : ""}:
${renderAprsInfo(pkt)}${posHtml}${crcTag}`;
- applyAprsFilterToRow(row);
+ const distance = aprsDistanceText(pkt);
+ const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
+
+ row.innerHTML =
+ `
` +
+ `${ts}` +
+ symbolHtml +
+ `${escapeMapHtml(pkt.srcCall)}` +
+ `>${escapeMapHtml(pkt.destCall || "")}` +
+ `${escapeMapHtml(categoryLabel)}` +
+ pathBadge +
+ crcBadge +
+ `
` +
+ `
` +
+ `${escapeMapHtml(age)}` +
+ (distance ? `${escapeMapHtml(distance)}` : "") +
+ `${escapeMapHtml(pkt.type || "--")}` +
+ `
` +
+ `
` +
+ `${renderAprsInfo(pkt)}` +
+ (posLink ? `${posLink}` : "") +
+ `
` +
+ `
` +
+ (pkt.lat != null && pkt.lon != null ? `
` : "") +
+ (pkt.lat != null && pkt.lon != null ? `
` : "") +
+ `
QRZ` +
+ `
` +
+ `
` +
+ `Details
` +
+ `` +
+ `Source${escapeMapHtml(pkt.srcCall || "--")}` +
+ `Destination${escapeMapHtml(pkt.destCall || "--")}` +
+ `Type${escapeMapHtml(pkt.type || "--")}` +
+ `Path${escapeMapHtml(pkt.path || "--")}` +
+ `Age${escapeMapHtml(age)}` +
+ `CRC${pkt.crcOk ? "OK" : "Failed"}` +
+ `Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}` +
+ `Info${escapeMapHtml(pkt.info || "--")}` +
+ `Info Bytes${escapeMapHtml(aprsHexBytes(pkt.info_bytes))}` +
+ `
` +
+ ` `;
+
+ 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 = '';
- for (const pkt of okFrames) {
+ for (const pkt of frames) {
const ts = pkt._ts ? `
${pkt._ts}` : "";
const call = `
${escapeMapHtml(pkt.srcCall)}`;
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();
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 604e133..432ca29 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
@@ -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 {