[fix](trx-frontend-http): focus longest qso map paths

Let users click longest-QSO cards to isolate a single contact path on the map and click again to restore all visible contact paths. Also remove the extra inner panel styling from decode map tooltips so the popup renders as a single container.

Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Verification: git diff --check -- src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-14 17:01:42 +01:00
parent d5c3283b37
commit 86768c8e7f
2 changed files with 47 additions and 10 deletions
@@ -3796,6 +3796,7 @@ const MAP_QSO_SUMMARY_LIMIT = 5;
const stationMarkers = new Map(); const stationMarkers = new Map();
const locatorMarkers = new Map(); const locatorMarkers = new Map();
const decodeContactPaths = new Map(); const decodeContactPaths = new Map();
let selectedMapQsoKey = null;
const mapMarkers = new Set(); const mapMarkers = new Set();
const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, wspr: true }; const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, wspr: true };
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER }; const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
@@ -4490,11 +4491,22 @@ function ensureDecodeContactPathRendered(entry) {
if (typeof entry.line.bringToBack === "function") entry.line.bringToBack(); if (typeof entry.line.bringToBack === "function") entry.line.bringToBack();
} }
function syncDecodeContactPathVisibility() { function decodeContactPathBaseVisible(entry) {
for (const entry of decodeContactPaths.values()) { return mapDecodeContactPathsEnabled
const visible = mapDecodeContactPathsEnabled
&& decodeLocatorPathVisibility(entry.sourceGrid) && decodeLocatorPathVisibility(entry.sourceGrid)
&& decodeLocatorPathVisibility(entry.targetGrid); && decodeLocatorPathVisibility(entry.targetGrid);
}
function syncDecodeContactPathVisibility() {
if (selectedMapQsoKey) {
const selectedEntry = decodeContactPaths.get(selectedMapQsoKey);
if (!selectedEntry || !decodeContactPathBaseVisible(selectedEntry)) {
selectedMapQsoKey = null;
}
}
for (const entry of decodeContactPaths.values()) {
const visible = decodeContactPathBaseVisible(entry)
&& (!selectedMapQsoKey || entry.pathKey === selectedMapQsoKey);
if (!visible) { if (!visible) {
clearDecodeContactPathRender(entry); clearDecodeContactPathRender(entry);
continue; continue;
@@ -6073,6 +6085,7 @@ function rebuildDecodeContactPaths() {
const prev = decodeContactPaths.get(key); const prev = decodeContactPaths.get(key);
if (prev && prev.tsMs > msg.tsMs) continue; if (prev && prev.tsMs > msg.tsMs) continue;
decodeContactPaths.set(key, { decodeContactPaths.set(key, {
pathKey: key,
source: msg.source, source: msg.source,
target: msg.target, target: msg.target,
sourceGrid: msg.sourceGrid, sourceGrid: msg.sourceGrid,
@@ -6098,8 +6111,7 @@ function renderMapQsoSummary() {
const entries = Array.from(decodeContactPaths.values()) const entries = Array.from(decodeContactPaths.values())
.filter((entry) => entry .filter((entry) => entry
&& Number.isFinite(entry.distanceKm) && Number.isFinite(entry.distanceKm)
&& decodeLocatorPathVisibility(entry.sourceGrid) && decodeContactPathBaseVisible(entry))
&& decodeLocatorPathVisibility(entry.targetGrid))
.sort((a, b) => { .sort((a, b) => {
const distanceDelta = Number(b.distanceKm) - Number(a.distanceKm); const distanceDelta = Number(b.distanceKm) - Number(a.distanceKm);
if (Math.abs(distanceDelta) > 0.001) return distanceDelta; if (Math.abs(distanceDelta) > 0.001) return distanceDelta;
@@ -6107,6 +6119,10 @@ function renderMapQsoSummary() {
}) })
.slice(0, MAP_QSO_SUMMARY_LIMIT); .slice(0, MAP_QSO_SUMMARY_LIMIT);
if (selectedMapQsoKey && !entries.some((entry) => entry.pathKey === selectedMapQsoKey)) {
selectedMapQsoKey = null;
}
if (entries.length === 0) { if (entries.length === 0) {
const empty = document.createElement("div"); const empty = document.createElement("div");
empty.className = "map-qso-summary-empty"; empty.className = "map-qso-summary-empty";
@@ -6117,8 +6133,15 @@ function renderMapQsoSummary() {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
entries.forEach((entry, index) => { entries.forEach((entry, index) => {
const card = document.createElement("article"); const card = document.createElement("button");
card.type = "button";
card.className = "map-qso-card"; card.className = "map-qso-card";
card.classList.toggle("is-selected", entry.pathKey === selectedMapQsoKey);
card.setAttribute("aria-pressed", entry.pathKey === selectedMapQsoKey ? "true" : "false");
card.addEventListener("click", () => {
selectedMapQsoKey = selectedMapQsoKey === entry.pathKey ? null : entry.pathKey;
syncDecodeContactPathVisibility();
});
const head = document.createElement("div"); const head = document.createElement("div");
head.className = "map-qso-card-head"; head.className = "map-qso-card-head";
@@ -1329,6 +1329,8 @@ small { color: var(--text-muted); }
} }
.map-qso-card { .map-qso-card {
display: flex; display: flex;
appearance: none;
width: 100%;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
min-width: 0; min-width: 0;
@@ -1336,6 +1338,21 @@ small { color: var(--text-muted); }
border-radius: 0.75rem; border-radius: 0.75rem;
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent); border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
background: color-mix(in srgb, var(--card-bg) 78%, transparent); background: color-mix(in srgb, var(--card-bg) 78%, transparent);
text-align: left;
cursor: pointer;
transition: border-color 120ms ease, background 120ms ease, transform 120ms ease, box-shadow 120ms ease;
}
.map-qso-card:hover {
border-color: color-mix(in srgb, var(--accent-green) 38%, var(--border-light));
background: color-mix(in srgb, var(--card-bg) 70%, transparent);
}
.map-qso-card.is-selected {
border-color: color-mix(in srgb, var(--accent-green) 62%, var(--border-light));
background: color-mix(in srgb, var(--accent-green) 10%, var(--card-bg));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-green) 18%, transparent);
}
.map-qso-card:active {
transform: translateY(1px);
} }
.map-qso-card-head { .map-qso-card-head {
display: flex; display: flex;
@@ -1934,11 +1951,8 @@ body.map-fake-fullscreen-active {
max-width: 26rem; max-width: 26rem;
max-height: min(22rem, 60vh); max-height: min(22rem, 60vh);
overflow: auto; overflow: auto;
padding: 0.55rem 0.65rem; padding: 0.1rem 0.05rem 0.05rem;
border-radius: 0.65rem;
background: color-mix(in srgb, var(--card-bg) 84%, transparent);
color: var(--text); color: var(--text);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
} }
.decode-locator-tip-title { .decode-locator-tip-title {
color: var(--accent-yellow); color: var(--accent-yellow);