[feat](trx-frontend-http): add weakest decoded signal stats and clickable tiles

Add weakest decoded signal panel showing top 5 weakest SNR signals. Make all
stat tiles (longest decode, strongest signal, weakest signal) clickable to
highlight the corresponding locator on the map.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-21 08:46:40 +01:00
parent 3877de3c4f
commit e6c34bb695
2 changed files with 132 additions and 0 deletions
@@ -4810,6 +4810,7 @@ function syncDecodeContactPathVisibility() {
} }
renderMapQsoSummary(); renderMapQsoSummary();
renderMapSignalSummary(); renderMapSignalSummary();
renderMapWeakSignalSummary();
updateMapPathsAnimationClass(); updateMapPathsAnimationClass();
} }
@@ -6437,6 +6438,9 @@ function renderMapQsoSummary() {
card.addEventListener("click", () => { card.addEventListener("click", () => {
selectedMapQsoKey = selectedMapQsoKey === entry.pathKey ? null : entry.pathKey; selectedMapQsoKey = selectedMapQsoKey === entry.pathKey ? null : entry.pathKey;
syncDecodeContactPathVisibility(); syncDecodeContactPathVisibility();
if (selectedMapQsoKey && entry.sourceGrid) {
navigateToMapLocator(entry.sourceGrid, entry.sourceType);
}
}); });
const head = document.createElement("div"); const head = document.createElement("div");
@@ -6548,6 +6552,125 @@ function renderMapSignalSummary() {
const card = document.createElement("button"); const card = document.createElement("button");
card.type = "button"; card.type = "button";
card.className = "map-qso-card"; card.className = "map-qso-card";
if (entry.grid) {
card.addEventListener("click", () => {
navigateToMapLocator(entry.grid, entry.sourceType);
});
}
const head = document.createElement("div");
head.className = "map-qso-card-head";
const rank = document.createElement("span");
rank.className = "map-qso-card-rank";
rank.textContent = `#${index + 1}`;
head.appendChild(rank);
const snr = document.createElement("span");
snr.className = "map-qso-card-distance";
snr.textContent = `${entry.snrDb >= 0 ? "+" : ""}${entry.snrDb.toFixed(0)} dB`;
head.appendChild(snr);
const body = document.createElement("div");
body.className = "map-qso-card-body";
const pair = document.createElement("div");
pair.className = "map-qso-card-pair";
pair.textContent = entry.station;
body.appendChild(pair);
const meta = document.createElement("div");
meta.className = "map-qso-card-meta";
const sourceType = document.createElement("span");
sourceType.className = "map-qso-card-pill";
sourceType.textContent = String(entry.sourceType || "ft8").toUpperCase();
meta.appendChild(sourceType);
if (entry.bandLabel) {
const band = document.createElement("span");
band.className = "map-qso-card-pill map-qso-card-band";
band.style.setProperty("--band-color", locatorBandChipColor(entry.bandLabel));
band.textContent = entry.bandLabel;
meta.appendChild(band);
}
const ageText = formatTimeAgo(Number(entry.tsMs));
if (ageText) {
const age = document.createElement("span");
age.className = "map-qso-card-pill";
age.textContent = ageText;
meta.appendChild(age);
}
body.appendChild(meta);
const grids = document.createElement("div");
grids.className = "map-qso-card-grids";
grids.textContent = entry.grid || "--";
body.appendChild(grids);
card.appendChild(head);
card.appendChild(body);
fragment.appendChild(card);
});
listEl.replaceChildren(fragment);
}
function renderMapWeakSignalSummary() {
const listEl = document.getElementById("map-weak-signal-summary-list");
if (!listEl) return;
const worstByStation = new Map();
for (const entry of locatorMarkers.values()) {
if (!entry || (entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr")) continue;
if (!(entry.stationDetails instanceof Map)) continue;
for (const detail of entry.stationDetails.values()) {
if (!Number.isFinite(detail?.snr_db)) continue;
const station = String(detail?.source || detail?.station || "").trim().toUpperCase();
if (!station) continue;
const snrDb = Number(detail.snr_db);
const tsMs = Number.isFinite(detail?.ts_ms) ? Number(detail.ts_ms) : 0;
const prev = worstByStation.get(station);
if (!prev || snrDb < prev.snrDb || (snrDb === prev.snrDb && tsMs > prev.tsMs)) {
worstByStation.set(station, {
station,
snrDb,
tsMs,
grid: entry.grid,
sourceType: entry.sourceType,
bandLabel: bandForHz(Number(detail?.freq_hz))?.label || null,
});
}
}
}
const entries = Array.from(worstByStation.values())
.sort((a, b) => {
const delta = a.snrDb - b.snrDb;
if (Math.abs(delta) > 0.001) return delta;
return b.tsMs - a.tsMs;
})
.slice(0, MAP_QSO_SUMMARY_LIMIT);
if (entries.length === 0) {
const empty = document.createElement("div");
empty.className = "map-qso-summary-empty";
empty.textContent = "No decoded signals with SNR data in the current map history.";
listEl.replaceChildren(empty);
return;
}
const fragment = document.createDocumentFragment();
entries.forEach((entry, index) => {
const card = document.createElement("button");
card.type = "button";
card.className = "map-qso-card";
if (entry.grid) {
card.addEventListener("click", () => {
navigateToMapLocator(entry.grid, entry.sourceType);
});
}
const head = document.createElement("div"); const head = document.createElement("div");
head.className = "map-qso-card-head"; head.className = "map-qso-card-head";
@@ -792,6 +792,15 @@
</div> </div>
<div id="map-signal-summary-list" class="map-qso-summary-list"></div> <div id="map-signal-summary-list" class="map-qso-summary-list"></div>
</section> </section>
<section class="map-qso-summary" aria-labelledby="map-weak-signal-summary-title">
<div class="map-qso-summary-head">
<div>
<div id="map-weak-signal-summary-title" class="map-qso-summary-title">Weakest decoded signal</div>
<div class="map-qso-summary-subtitle">Top 5 weakest signals in the current map history</div>
</div>
</div>
<div id="map-weak-signal-summary-list" class="map-qso-summary-list"></div>
</section>
</div> </div>
<div id="tab-settings" class="tab-panel" style="display:none;"> <div id="tab-settings" class="tab-panel" style="display:none;">
<div class="sub-tab-bar"> <div class="sub-tab-bar">