[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 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-10 19:24:34 +01:00
parent ce1ca48384
commit 4740b38ad4
@@ -664,6 +664,7 @@ if (themeToggleBtn) {
setTheme(currentTheme() === "dark" ? "light" : "dark"); setTheme(currentTheme() === "dark" ? "light" : "dark");
updateMapBaseLayerForTheme(currentTheme()); updateMapBaseLayerForTheme(currentTheme());
syncLocatorMarkerStyles(); syncLocatorMarkerStyles();
refreshAisMarkerColors();
scheduleOverviewDraw(); scheduleOverviewDraw();
if (typeof scheduleSpectrumDraw === "function" && lastSpectrumData) scheduleSpectrumDraw(); if (typeof scheduleSpectrumDraw === "function" && lastSpectrumData) scheduleSpectrumDraw();
}); });
@@ -674,6 +675,7 @@ if (headerStylePickSelect) {
setStyle(headerStylePickSelect.value); setStyle(headerStylePickSelect.value);
updateMapBaseLayerForTheme(currentTheme()); updateMapBaseLayerForTheme(currentTheme());
syncLocatorMarkerStyles(); syncLocatorMarkerStyles();
refreshAisMarkerColors();
}); });
} }
@@ -4981,7 +4983,7 @@ function ensureAisTrack(mmsi, entry) {
return; return;
} }
const track = L.polyline(entry.trackPoints, { const track = L.polyline(entry.trackPoints, {
color: "#ff7559", color: getAisAccentColor(),
weight: 2, weight: 2,
opacity: 0.68, opacity: 0.68,
lineCap: "round", lineCap: "round",
@@ -5011,13 +5013,18 @@ function syncSelectedAisTrackVisibility() {
}); });
} }
function getAisAccentColor() {
return getComputedStyle(document.documentElement).getPropertyValue("--accent-red").trim() || "#ff7559";
}
function aisMarkerOptionsFromMessage(msg) { function aisMarkerOptionsFromMessage(msg) {
const color = getAisAccentColor();
return { return {
heading: msg?.heading_deg, heading: msg?.heading_deg,
course: msg?.cog_deg, course: msg?.cog_deg,
speed: msg?.sog_knots, speed: msg?.sog_knots,
color: "#ff7559", color,
outline: "#6b2118", outline: "#00000055",
size: 22, size: 22,
}; };
} }
@@ -5026,10 +5033,11 @@ function createAisMarker(lat, lon, msg) {
if (typeof L !== "undefined" && typeof L.trxAisTrackSymbol === "function") { if (typeof L !== "undefined" && typeof L.trxAisTrackSymbol === "function") {
return L.trxAisTrackSymbol([lat, lon], aisMarkerOptionsFromMessage(msg)); return L.trxAisTrackSymbol([lat, lon], aisMarkerOptionsFromMessage(msg));
} }
const color = getAisAccentColor();
return L.circleMarker([lat, lon], { return L.circleMarker([lat, lon], {
radius: 6, radius: 6,
color: "#e2553d", color,
fillColor: "#ff7559", fillColor: color,
fillOpacity: 0.82, fillOpacity: 0.82,
}); });
} }
@@ -5041,17 +5049,33 @@ function updateAisMarker(marker, msg, popupHtml) {
marker.setAisState(aisMarkerOptionsFromMessage(msg)); marker.setAisState(aisMarkerOptionsFromMessage(msg));
} }
if (typeof marker.setStyle === "function" && typeof marker.setAisState !== "function") { 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({ marker.setStyle({
radius: hasHeading ? 6.5 : 6, radius: 6,
color: hasHeading ? "#c8412f" : "#e2553d", color,
fillColor: hasHeading ? "#ff6f4d" : "#ff7559", fillColor: color,
fillOpacity: 0.84, fillOpacity: 0.84,
}); });
} }
marker.setPopupContent(popupHtml); 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) { window.aisMapAddVessel = function(msg) {
if (msg == null || msg.lat == null || msg.lon == null || !Number.isFinite(msg.mmsi)) return; if (msg == null || msg.lat == null || msg.lon == null || !Number.isFinite(msg.mmsi)) return;
if (!aprsMap) initAprsMap(); if (!aprsMap) initAprsMap();
@@ -6154,9 +6178,20 @@ function connectDecode() {
if (window.resetFt8HistoryView) window.resetFt8HistoryView(); if (window.resetFt8HistoryView) window.resetFt8HistoryView();
if (window.resetWsprHistoryView) window.resetWsprHistoryView(); if (window.resetWsprHistoryView) window.resetWsprHistoryView();
// Open the live SSE stream first so real-time messages are never blocked by // Buffer live messages until history fetch settles so history always appears
// history replay. History is fetched separately via a plain HTTP request and // before any live updates, regardless of network ordering.
// drained in the background using the existing chunked helper. 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 = new EventSource("/decode");
decodeSource.onopen = () => { decodeSource.onopen = () => {
decodeConnected = true; decodeConnected = true;
@@ -6164,7 +6199,9 @@ function connectDecode() {
}; };
decodeSource.onmessage = (evt) => { decodeSource.onmessage = (evt) => {
try { 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 */ } } catch (e) { /* ignore parse errors */ }
}; };
decodeSource.onerror = () => { 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) => { fetch("/decode/history").then((resp) => {
if (!resp.ok) return; if (!resp.ok) return null;
return resp.json(); return resp.json();
}).then((msgs) => { }).then((msgs) => {
clearTimeout(historyTimeout);
if (Array.isArray(msgs)) drainDecodeHistory(msgs, 0); if (Array.isArray(msgs)) drainDecodeHistory(msgs, 0);
}).catch(() => { /* history unavailable, ignore */ }); flushLiveBuffer();
}).catch(() => { clearTimeout(historyTimeout); flushLiveBuffer(); });
} }
if (document.readyState === "complete") { if (document.readyState === "complete") {
connectDecode(); connectDecode();