From e2a9e4d610b1a312b8686a11d4309a684e7230b8 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 1 Mar 2026 18:00:22 +0100 Subject: [PATCH] [feat](trx-frontend): rich popup for receiver/owner marker on map Replace plain-text receiver marker popup with a styled info card matching the APRS popup layout. Shows callsign, trx-server version and build date, owner callsign (when different), QTH coordinates, and all configured rigs with manufacturer/model; active rig is badged. Rig data (manufacturer, model, display_name, active state) is stored in serverRigs/serverActiveRigId on each /rigs refresh. Popup content is rebuilt live on popupopen so it always reflects current state. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/app.js | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 9518158..138dde4 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -639,6 +639,8 @@ async function refreshRigList() { displayNames[r.rig_id] = r.rig_id; } }); + serverRigs = rigs; + serverActiveRigId = data.active_rig_id || null; applyRigList(data.active_rig_id, rigIds, displayNames); } catch (e) { // Non-fatal: SSE/status path still drives main UI. @@ -1245,6 +1247,8 @@ let serverVersion = null; let serverBuildDate = null; let serverCallsign = null; let ownerCallsign = null; +let serverRigs = []; +let serverActiveRigId = null; let serverLat = null; let serverLon = null; let initialMapZoom = 10; @@ -2442,17 +2446,21 @@ function initAprsMap() { updateMapBaseLayerForTheme(currentTheme()); if (hasLocation) { - const popupText = serverCallsign ? serverCallsign : "Receiver"; aprsMapReceiverMarker = L.circleMarker([serverLat, serverLon], { radius: 8, color: "#3388ff", fillColor: "#3388ff", fillOpacity: 0.8 - }).addTo(aprsMap).bindPopup(popupText); + }).addTo(aprsMap).bindPopup(""); } - // Rebuild APRS popup content on open so age and distance are always fresh - // and draw an animated radio path from receiver to the station + // Rebuild popup content on open (keeps age/distance/rig list fresh) aprsMap.on("popupopen", function(e) { const marker = e.popup._source; if (aprsRadioPath) { aprsRadioPath.remove(); aprsRadioPath = null; } + + if (marker === aprsMapReceiverMarker) { + e.popup.setContent(buildReceiverPopupHtml()); + return; + } + if (!marker || !marker._aprsCall) return; const entry = stationMarkers.get(marker._aprsCall); if (!entry) return; @@ -2572,6 +2580,33 @@ function formatTimeAgo(tsMs) { return remMins > 0 ? `${hrs}h ${remMins}min ago` : `${hrs}h ago`; } +function buildReceiverPopupHtml() { + const call = serverCallsign || ownerCallsign || "Receiver"; + let meta = ""; + if (serverVersion) { + meta = `trx-server v${escapeMapHtml(serverVersion)}`; + if (serverBuildDate) meta += ` · ${escapeMapHtml(serverBuildDate)}`; + } + let rows = ""; + if (ownerCallsign && ownerCallsign !== serverCallsign) { + rows += `Owner${escapeMapHtml(ownerCallsign)}`; + } + if (serverLat != null && serverLon != null) { + rows += `QTH${serverLat.toFixed(5)}, ${serverLon.toFixed(5)}`; + } + for (const rig of serverRigs) { + const name = rig.display_name || `${rig.manufacturer} ${rig.model}`.trim(); + const active = rig.rig_id === serverActiveRigId + ? ` active` : ""; + rows += `Rig${escapeMapHtml(name)}${active}`; + } + return `
` + + `
${escapeMapHtml(call)}
` + + (meta ? `
${meta}
` : "") + + (rows ? `${rows}
` : "") + + `
`; +} + function buildAprsPopupHtml(call, lat, lon, info, pkt) { const age = pkt?._tsMs ? formatTimeAgo(pkt._tsMs) : (pkt?._ts || null); const distKm = (serverLat != null && serverLon != null)