Compare commits

..
Author SHA1 Message Date
sjgandClaude Opus 5 863a6d8fd4 [fix](trx-frontend-http): restore decode history from the stored records
CI / lint (pull_request) Successful in 2m21s
CI / test (pull_request) Successful in 8m19s
CI / frontend (pull_request) Successful in 3m1s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m16s
CI / test (push) Successful in 7m26s
CI / frontend (push) Successful in 2m11s
CI / reuse (push) Successful in 2s
Replay required every restored record to carry a string `type`, and
stored records do not have one: an AIS entry holds mmsi, lat, lon,
crc_ok and its decoder's own fields, nothing more.  The filter therefore
discarded all of them, and did it silently — the fetch returned its full
payload, the worker decoded it, and no error was logged, so the history
simply never appeared.

That field identifies live SSE frames, which do carry it, which is why
only replay was affected.  History arrives already grouped and the
group's kind is delivered alongside the messages, so `type` was never
needed to route them.  Require only that a record is an object.

Confirmed against a live server: the first restored group is AIS, and
its records expose their decoder fields with `type` undefined.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 18:39:13 +02:00
sjgandClaude Opus 5 b252717f6b [test](trx-frontend-http): serve a realistic decoder registry to the smoke test
CI / lint (pull_request) Successful in 2m18s
CI / test (pull_request) Successful in 7m25s
CI / frontend (pull_request) Successful in 2m10s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m17s
CI / test (push) Successful in 7m26s
CI / frontend (push) Successful in 2m12s
CI / reuse (push) Successful in 3s
The fixture answered /decoders with an empty list, which hid most of the
application from the only test that runs it in a browser.  The decoder
sub-tabs, their panels, the decode toggles and the bookmark decoder
checkboxes are all built from that registry, so the run exercised three
of thirteen sub-tabs and none of the decoder UI.  Finding this needed
route interception, because nothing in the suite could see it.

Serve eleven decoders covering the modes the real registry spans.  The
run now builds 13 sub-tabs and 11 bookmark decoder checkboxes — the same
checkboxes whose construction a recent fix changed without any test
reaching them — and still reports no runtime errors.

It also makes an existing fault observable: at 1100px the decoder
sub-tab bar hides 195px of itself with no scrollbar or fade, the same
silent truncation the top strip had.  No assertion for it here, since
that would fail until the truncation is fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 17:46:52 +02:00
sjgandClaude Opus 5 92697b11c5 [feat](trx-frontend-http): mark Tools active for its destinations
CI / test (pull_request) Successful in 8m9s
CI / test (push) Successful in 7m22s
CI / lint (pull_request) Successful in 2m15s
CI / frontend (pull_request) Successful in 3m1s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m16s
CI / frontend (push) Successful in 2m10s
CI / reuse (push) Successful in 2s
Grouping Statistics, Recorder, Settings and About behind Tools left the
tab strip looking identical on all four: the destination's own button
carries the active class, but the strip hides that button, so nothing
was marked.  The page titles named the page without saying how you got
there.

Mark the Tools button when the active destination is one the strip hides.
That state is read from the button's computed display rather than from a
second copy of the grouping, so the two cannot drift: whatever ui-core
puts in the menu lights up Tools, and a destination promoted back into
the strip stops doing so with no further change.

Tools already carries the tab class, so the existing active styling
applies unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 00:23:17 +02:00
3 changed files with 35 additions and 3 deletions
@@ -5592,6 +5592,8 @@ function navigateToTab(name, options = {}) {
_activeTab = name; _activeTab = name;
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active")); document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
btn.classList.add("active"); btn.classList.add("active");
const toolsBtn = document.getElementById("mobile-more-btn");
if (toolsBtn) toolsBtn.classList.toggle("active", getComputedStyle(btn).display === "none");
window.trxUi?.syncSelectedTab(document.querySelector(".tab-bar-nav"), btn); window.trxUi?.syncSelectedTab(document.querySelector(".tab-bar-nav"), btn);
document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none"); document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
const panel = document.getElementById(`tab-${name}`); const panel = document.getElementById(`tab-${name}`);
@@ -7422,7 +7424,7 @@ function connectDecode() {
return; return;
} }
if (data.type === "group") { if (data.type === "group") {
const messages = Array.isArray(data.messages) ? data.messages.filter((message) => isRecord2(message) && typeof message.type === "string") : []; const messages = Array.isArray(data.messages) ? data.messages.filter((message) => isRecord2(message)) : [];
enqueueDecodeHistoryGroup(typeof data.kind === "string" ? data.kind : "", messages); enqueueDecodeHistoryGroup(typeof data.kind === "string" ? data.kind : "", messages);
return; return;
} }
@@ -4600,6 +4600,12 @@ function navigateToTab(name: TabName, options: { updateHistory?: boolean; replac
_activeTab = name; _activeTab = name;
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active")); document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
btn.classList.add("active"); btn.classList.add("active");
// A destination the strip hides is reached through Tools, so mark that
// button instead — otherwise the strip looks identical on all four of them.
// Derived from what is actually hidden rather than from a second copy of the
// grouping, which would drift from the one ui-core installs.
const toolsBtn = document.getElementById("mobile-more-btn");
if (toolsBtn) toolsBtn.classList.toggle("active", getComputedStyle(btn).display === "none");
window.trxUi?.syncSelectedTab(document.querySelector(".tab-bar-nav"), btn); window.trxUi?.syncSelectedTab(document.querySelector(".tab-bar-nav"), btn);
document.querySelectorAll<HTMLElement>(".tab-panel").forEach((p) => p.style.display = "none"); document.querySelectorAll<HTMLElement>(".tab-panel").forEach((p) => p.style.display = "none");
const panel = document.getElementById(`tab-${name}`); const panel = document.getElementById(`tab-${name}`);
@@ -6360,7 +6366,12 @@ function connectDecode() {
} }
if (data.type === "group") { if (data.type === "group") {
const messages = Array.isArray(data.messages) const messages = Array.isArray(data.messages)
? data.messages.filter((message): message is DecodeMessage => isRecord(message) && typeof message.type === "string") // Stored records carry only decoder fields — an AIS entry has mmsi,
// lat, lon and so on, and no `type`. That field identifies live SSE
// frames; history is already grouped, and the group's kind is
// delivered alongside these messages, so requiring it here discarded
// every restored record silently.
? data.messages.filter((message): message is DecodeMessage => isRecord(message))
: []; : [];
enqueueDecodeHistoryGroup(typeof data.kind === "string" ? data.kind : "", messages); enqueueDecodeHistoryGroup(typeof data.kind === "string" ? data.kind : "", messages);
return; return;
@@ -30,9 +30,28 @@ const rigItems = ["rig-a", "rig-b"].map((remote) => ({
const rigsResponse = { rigs: rigItems, active_remote: "rig-a" }; const rigsResponse = { rigs: rigItems, active_remote: "rig-a" };
const selectedRigs = []; const selectedRigs = [];
// A realistic decoder registry. Serving an empty one hid most of the
// application from this test: the decoder sub-tabs, their panels, the decode
// toggles and the bookmark decoder checkboxes are all built from it, so with
// no decoders only three of thirteen sub-tabs existed and none of the decoder
// UI was ever constructed.
const decoderRegistry = [
{ id: "ft8", label: "FT8", activation: "toggle", active_modes: ["USB"] },
{ id: "ft4", label: "FT4", activation: "toggle", active_modes: ["USB"] },
{ id: "ft2", label: "FT2", activation: "toggle", active_modes: ["USB"] },
{ id: "wspr", label: "WSPR", activation: "toggle", active_modes: ["USB"] },
{ id: "cw", label: "CW", activation: "toggle", active_modes: ["CW", "CWR"] },
{ id: "sat", label: "SAT", activation: "toggle", active_modes: ["FM"] },
{ id: "wefax", label: "WEFAX", activation: "toggle", active_modes: ["USB"] },
{ id: "ais", label: "AIS", activation: "toggle", active_modes: ["FM"] },
{ id: "aprs", label: "APRS", activation: "toggle", active_modes: ["FM"] },
{ id: "hf-aprs", label: "HF APRS", activation: "toggle", active_modes: ["USB"] },
{ id: "vdes", label: "VDES", activation: "toggle", active_modes: ["FM"] },
].map((decoder) => ({ ...decoder, background_decode: false, bookmark_selectable: true }));
const jsonRoutes = new Map([ const jsonRoutes = new Map([
["/auth/session", { authenticated: true, role: "control", auth_disabled: true }], ["/auth/session", { authenticated: true, role: "control", auth_disabled: true }],
["/decoders", []], ["/decoders", decoderRegistry],
["/rigs", rigsResponse], ["/rigs", rigsResponse],
["/status", { ["/status", {
info: { info: {