[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");
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();