From 4740b38ad45e3b0b205a9e1956c7a499e129b9f7 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Tue, 10 Mar 2026 19:24:34 +0100 Subject: [PATCH] [fix](trx-frontend-http): AIS map markers and tracks follow theme accent color Read --accent-red CSS variable at draw time so markers, track lines, and TrackSymbol icons automatically match the active color scheme. Add refreshAisMarkerColors() called on theme toggle and style picker changes to repaint existing markers without a page reload. Also buffer live SSE decode messages until the /decode/history fetch settles to eliminate the history-appears-after-reload race condition. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/app.js | 71 ++++++++++++++----- 1 file changed, 55 insertions(+), 16 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 4fed3e0..e7b0a7c 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 @@ -664,6 +664,7 @@ if (themeToggleBtn) { setTheme(currentTheme() === "dark" ? "light" : "dark"); updateMapBaseLayerForTheme(currentTheme()); syncLocatorMarkerStyles(); + refreshAisMarkerColors(); scheduleOverviewDraw(); if (typeof scheduleSpectrumDraw === "function" && lastSpectrumData) scheduleSpectrumDraw(); }); @@ -674,6 +675,7 @@ if (headerStylePickSelect) { setStyle(headerStylePickSelect.value); updateMapBaseLayerForTheme(currentTheme()); syncLocatorMarkerStyles(); + refreshAisMarkerColors(); }); } @@ -4981,7 +4983,7 @@ function ensureAisTrack(mmsi, entry) { return; } const track = L.polyline(entry.trackPoints, { - color: "#ff7559", + color: getAisAccentColor(), weight: 2, opacity: 0.68, lineCap: "round", @@ -5011,13 +5013,18 @@ function syncSelectedAisTrackVisibility() { }); } +function getAisAccentColor() { + return getComputedStyle(document.documentElement).getPropertyValue("--accent-red").trim() || "#ff7559"; +} + function aisMarkerOptionsFromMessage(msg) { + const color = getAisAccentColor(); return { heading: msg?.heading_deg, course: msg?.cog_deg, speed: msg?.sog_knots, - color: "#ff7559", - outline: "#6b2118", + color, + outline: "#00000055", size: 22, }; } @@ -5026,10 +5033,11 @@ function createAisMarker(lat, lon, msg) { if (typeof L !== "undefined" && typeof L.trxAisTrackSymbol === "function") { return L.trxAisTrackSymbol([lat, lon], aisMarkerOptionsFromMessage(msg)); } + const color = getAisAccentColor(); return L.circleMarker([lat, lon], { radius: 6, - color: "#e2553d", - fillColor: "#ff7559", + color, + fillColor: color, fillOpacity: 0.82, }); } @@ -5041,17 +5049,33 @@ function updateAisMarker(marker, msg, popupHtml) { marker.setAisState(aisMarkerOptionsFromMessage(msg)); } if (typeof marker.setStyle === "function" && typeof marker.setAisState !== "function") { - const hasHeading = Number.isFinite(msg?.heading_deg) || Number.isFinite(msg?.cog_deg); + const color = getAisAccentColor(); marker.setStyle({ - radius: hasHeading ? 6.5 : 6, - color: hasHeading ? "#c8412f" : "#e2553d", - fillColor: hasHeading ? "#ff6f4d" : "#ff7559", + radius: 6, + color, + fillColor: color, fillOpacity: 0.84, }); } marker.setPopupContent(popupHtml); } +function refreshAisMarkerColors() { + const color = getAisAccentColor(); + aisMarkers.forEach((entry) => { + if (entry.marker) { + if (typeof entry.marker.setAisState === "function") { + entry.marker.setAisState(aisMarkerOptionsFromMessage(entry.msg || {})); + } else if (typeof entry.marker.setStyle === "function") { + entry.marker.setStyle({ color, fillColor: color }); + } + } + if (entry.track && typeof entry.track.setStyle === "function") { + entry.track.setStyle({ color }); + } + }); +} + window.aisMapAddVessel = function(msg) { if (msg == null || msg.lat == null || msg.lon == null || !Number.isFinite(msg.mmsi)) return; if (!aprsMap) initAprsMap(); @@ -6154,9 +6178,20 @@ function connectDecode() { if (window.resetFt8HistoryView) window.resetFt8HistoryView(); if (window.resetWsprHistoryView) window.resetWsprHistoryView(); - // Open the live SSE stream first so real-time messages are never blocked by - // history replay. History is fetched separately via a plain HTTP request and - // drained in the background using the existing chunked helper. + // Buffer live messages until history fetch settles so history always appears + // before any live updates, regardless of network ordering. + let historySettled = false; + const liveBuffer = []; + function flushLiveBuffer() { + historySettled = true; + for (const msg of liveBuffer) { + try { dispatchDecodeMessage(msg); } catch (_) {} + } + liveBuffer.length = 0; + } + // Safety valve: if the history fetch hangs, unblock after 8 s. + const historyTimeout = setTimeout(() => { if (!historySettled) flushLiveBuffer(); }, 8000); + decodeSource = new EventSource("/decode"); decodeSource.onopen = () => { decodeConnected = true; @@ -6164,7 +6199,9 @@ function connectDecode() { }; decodeSource.onmessage = (evt) => { try { - dispatchDecodeMessage(JSON.parse(evt.data)); + const msg = JSON.parse(evt.data); + if (historySettled) dispatchDecodeMessage(msg); + else liveBuffer.push(msg); } catch (e) { /* ignore parse errors */ } }; decodeSource.onerror = () => { @@ -6181,13 +6218,15 @@ function connectDecode() { } }; - // Fetch history in parallel — does not block the live SSE stream. + // Fetch history in parallel; drain it first, then flush buffered live msgs. fetch("/decode/history").then((resp) => { - if (!resp.ok) return; + if (!resp.ok) return null; return resp.json(); }).then((msgs) => { + clearTimeout(historyTimeout); if (Array.isArray(msgs)) drainDecodeHistory(msgs, 0); - }).catch(() => { /* history unavailable, ignore */ }); + flushLiveBuffer(); + }).catch(() => { clearTimeout(historyTimeout); flushLiveBuffer(); }); } if (document.readyState === "complete") { connectDecode();