From 6d25ecdc11b57148c7d55884f9cbee9733604e35 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Tue, 4 Aug 2026 23:24:09 +0200 Subject: [PATCH] [feat](trx-frontend-http): give the AIS list the same log shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A message was three stacked lines — time and name, then MMSI and route, then motion, distance, position and age — so a screen held eight of them. It is one line now: time, vessel, message type, and what the message says, opening in place for the MMSI, the channel frequency, the route, the age, the fix and a jump to the map. Twenty-two fit where eight did. What a message says depends on what it is. Position reports give the fix and the motion; the static and voyage reports that carry no fix give the callsign and where the vessel is bound. Both fall back to whatever fields are present rather than showing nothing. The row vocabulary the APRS list introduced is no longer APRS-specific — the classes are decode-line and decode-expanded now, shared by both, and identity sits in fixed columns so the summaries line up down the list instead of starting wherever the callsign happens to end. The three summary cards above the list go the way of the APRS ones. Signed-off-by: Stan Grams --- .../assets/web/generated/ais.js | 26 ++++++- .../assets/web/generated/aprs.js | 2 +- .../{chunk-YAWVT7YC.js => chunk-OZI7SINT.js} | 2 +- .../assets/web/generated/hf-aprs.js | 2 +- .../assets/web/generated/map-core.js | 2 +- .../trx-frontend-http/assets/web/index.html | 19 ++---- .../trx-frontend-http/assets/web/style.css | 47 +++++++++---- .../frontend/src/plugins/ais.ts | 67 ++++++++++++++----- .../frontend/src/plugins/aprs-shared.ts | 14 ++-- .../frontend/tests/decode-flow.mjs | 52 ++++++++++++-- 10 files changed, 172 insertions(+), 61 deletions(-) rename src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/{chunk-YAWVT7YC.js => chunk-OZI7SINT.js} (84%) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js index 521191e7..91f47f32 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js @@ -160,8 +160,18 @@ function updateAisSummary() { } } } +function aisSummaryText(msg) { + const parts = []; + if (msg.lat != null && msg.lon != null) parts.push(`${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}`); + const motion = aisMotionText(msg); + if (motion) parts.push(motion); + const route = aisRouteText(msg); + if (route && parts.length < 2) parts.push(route); + if (!parts.length) return route || "no position reported"; + return parts.join(" · "); +} function renderAisRow(msg) { - const row = document.createElement("div"); + const row = document.createElement("details"); row.className = "ais-message"; const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", @@ -174,7 +184,8 @@ function renderAisRow(msg) { const motion = aisMotionText(msg); const route = aisRouteText(msg); const distance = aisDistanceText(msg); - const pos = msg.lat != null && msg.lon != null ? `${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}` : ""; + const pos = msg.lat != null && msg.lon != null ? `${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)}` : ""; + const vesselUrl = aisWindow.buildAisVesselUrl?.(msg.mmsi) ?? null; row.dataset.filterText = [ name, msg.mmsi, @@ -185,7 +196,16 @@ function renderAisRow(msg) { msg.destination, aisTypeLabel(msg.message_type) ].filter(Boolean).join(" ").toUpperCase(); - row.innerHTML = `
${ts}${nameHtml}${escapeAisHtml(channel.label)}${escapeAisHtml(aisTypeLabel(msg.message_type))}
MMSI ${escapeAisHtml(String(msg.mmsi))}` + (route ? `${escapeAisHtml(route)}` : "") + `${escapeAisHtml(channel.freqText)}
` + (motion ? `${escapeAisHtml(motion)}` : `No motion data`) + (distance ? `${escapeAisHtml(distance)}` : "") + (pos ? `${pos}` : "") + `${escapeAisHtml(aisAgeText(msg._tsMs))}
`; + row.innerHTML = `${escapeAisHtml(ts)}${nameHtml}${escapeAisHtml(aisTypeLabel(msg.message_type))}${escapeAisHtml(aisSummaryText(msg))}${escapeAisHtml(channel.label)}` + (distance ? `${escapeAisHtml(distance)}` : "") + `
MMSI ${escapeAisHtml(String(msg.mmsi))}${escapeAisHtml(channel.freqText)}` + (route ? `${escapeAisHtml(route)}` : "") + (motion ? `${escapeAisHtml(motion)}` : "") + `${escapeAisHtml(aisAgeText(msg._tsMs))}` + (pos ? `${pos}` : "") + `
` + (msg.lat != null && msg.lon != null ? `` : "") + (vesselUrl ? `Vessel` : "") + `
`; + row.querySelectorAll("[data-ais-map]").forEach((element) => { + element.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + const [lat, lon] = (element.dataset.aisMap ?? "").split(",").map(Number); + if (lat == null || lon == null || !Number.isFinite(lat) || !Number.isFinite(lon)) return; + aisWindow.navigateToAprsMap?.(lat, lon); + }); + }); applyAisFilterToRow(row); return row; } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js index d8643464..f6907927 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js @@ -4,7 +4,7 @@ import { collapseAprsDuplicates, normalizeAprsPacket, renderAprsPacketRow -} from "./chunk-YAWVT7YC.js"; +} from "./chunk-OZI7SINT.js"; import { hostCore, hostState diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-YAWVT7YC.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OZI7SINT.js similarity index 84% rename from src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-YAWVT7YC.js rename to src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OZI7SINT.js index 3d8ea2e7..6a6a5faf 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-YAWVT7YC.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OZI7SINT.js @@ -221,7 +221,7 @@ function renderAprsPacketRow(packet, options = {}) { const summary = summarizeAprsPayload(packet); const hasPosition = packet.lat != null && packet.lon != null; const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(packet.srcCall || "")}`; - row.innerHTML = `${escapeAprsHtml(time)}` + (options.badge ? `${escapeAprsHtml(options.badge)}` : "") + renderLocalAprsSymbol(packet, escapeAprsHtml) + `${escapeAprsHtml(packet.srcCall ?? "")}${escapeAprsHtml(aprsCategoryLabel(category))}${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}` + (packet.crcOk ? "" : 'CRC') + (options.distance ? `${escapeAprsHtml(options.distance)}` : "") + `
>${escapeAprsHtml(packet.destCall || "--")}${escapeAprsHtml(packet.path || "no path")}${escapeAprsHtml(aprsAgeText(packet._tsMs))}CRC ${packet.crcOk ? "ok" : "failed"}` + (hasPosition ? `${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}` : "") + `
${renderAprsInfo(packet)}
` + (packet.info_bytes?.length ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
` : "") + `
` + (hasPosition ? `` : "") + (hasPosition ? `` : "") + `QRZ
`; + row.innerHTML = `${escapeAprsHtml(time)}` + (options.badge ? `${escapeAprsHtml(options.badge)}` : "") + renderLocalAprsSymbol(packet, escapeAprsHtml) + `${escapeAprsHtml(packet.srcCall ?? "")}${escapeAprsHtml(aprsCategoryLabel(category))}${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}` + (packet.crcOk ? "" : 'CRC') + (options.distance ? `${escapeAprsHtml(options.distance)}` : "") + `
>${escapeAprsHtml(packet.destCall || "--")}${escapeAprsHtml(packet.path || "no path")}${escapeAprsHtml(aprsAgeText(packet._tsMs))}CRC ${packet.crcOk ? "ok" : "failed"}` + (hasPosition ? `${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}` : "") + `
${renderAprsInfo(packet)}
` + (packet.info_bytes?.length ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
` : "") + `
` + (hasPosition ? `` : "") + (hasPosition ? `` : "") + `QRZ
`; row.querySelectorAll("[data-aprs-map]").forEach((element) => { element.addEventListener("click", (event) => { event.preventDefault(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js index 8ac8bbed..3d504ec0 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js @@ -4,7 +4,7 @@ import { collapseAprsDuplicates, normalizeAprsPacket, renderAprsPacketRow -} from "./chunk-YAWVT7YC.js"; +} from "./chunk-OZI7SINT.js"; import { hostCore, hostState diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js index 759ebf1b..dd456e9a 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js @@ -1,6 +1,6 @@ import { aprsSymbolSprite -} from "./chunk-YAWVT7YC.js"; +} from "./chunk-OZI7SINT.js"; // src/map-core.ts function mapEl(id) { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 600d963e..7b454294 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -680,19 +680,12 @@ SPDX-License-Identifier: GPL-2.0-or-later Waiting for server decode -
-
- Channels - A 161.975 MHz · B 162.025 MHz -
-
- Tracked - 0 vessels -
-
- Latest - No traffic yet -
+
+ + 0 vessels + No traffic yet + A 161.975 MHz · B 162.025 MHz +
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 7cda2aac..2da772a8 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -2844,12 +2844,12 @@ body.map-fake-fullscreen-active { background: var(--input-bg); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } -/* One line per frame, opening in place. A frame used to be a card five lines +/* One line per decode, opening in place — shared by the APRS and AIS lists. A frame used to be a card five lines tall — timestamp, meta, raw information field, three buttons, a Details panel repeating the row — so five of them filled the panel. */ .aprs-packet { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; border-bottom: 1px solid var(--border); line-height: 1.35; } .aprs-packet:last-child { border-bottom: none; } -.aprs-line { +.decode-line { display: flex; align-items: center; gap: 0.5rem; @@ -2858,47 +2858,66 @@ body.map-fake-fullscreen-active { list-style: none; white-space: nowrap; } -.aprs-line::-webkit-details-marker { display: none; } -.aprs-line:hover { +.decode-line::-webkit-details-marker { display: none; } +.decode-line:hover { background: color-mix(in srgb, var(--btn-bg) 45%, transparent); } -.aprs-packet[open] > .aprs-line { +.aprs-packet[open] > .decode-line { background: color-mix(in srgb, var(--accent-green) 8%, transparent); } +/* Fixed columns for what identifies a decode, so the summaries line up down + the list and the eye can run along one of them instead of hunting. Names + longer than the column ellipsise rather than pushing the rest along. */ +.decode-line .aprs-call, +.decode-line .ais-call { + flex: 0 0 auto; + width: 8.5rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.decode-line .aprs-badge-type { + flex: 0 0 auto; + min-width: 5.5rem; +} +.decode-line .ais-badge-type { + flex: 0 0 auto; + min-width: 9rem; +} /* The summary takes the leftover width and ellipsises: a frame is one line whatever its payload, so the column of times and callsigns stays readable. */ -.aprs-line-summary { +.decode-line-summary { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--text); } -.aprs-line-distance { +.decode-line-distance { flex: 0 0 auto; color: var(--text-muted); font-size: 0.75rem; } -.aprs-expanded { +.decode-expanded { display: flex; flex-direction: column; gap: 0.35rem; padding: 0.1rem 0.55rem 0.55rem 2.6rem; } -.aprs-expanded-meta { +.decode-expanded-meta { display: flex; flex-wrap: wrap; gap: 0.6rem; font-size: 0.75rem; color: var(--text-muted); } -.aprs-expanded-raw, -.aprs-expanded-bytes { +.decode-expanded-raw, +.decode-expanded-bytes { word-break: break-all; font-size: 0.78rem; color: var(--text); } -.aprs-expanded-bytes { +.decode-expanded-bytes { color: var(--text-muted); font-size: 0.72rem; } @@ -3025,7 +3044,9 @@ body.map-fake-fullscreen-active { color: var(--text); word-break: break-word; } -.ais-message { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; padding: 0.45rem 0.55rem; border-bottom: 1px solid var(--border); line-height: 1.35; } +.ais-message { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; border-bottom: 1px solid var(--border); line-height: 1.35; } +.ais-message > .decode-line:hover { background: color-mix(in srgb, var(--btn-bg) 45%, transparent); } +.ais-message[open] > .decode-line { background: color-mix(in srgb, var(--accent-red) 8%, transparent); } .ais-message:last-child { border-bottom: none; } .vdes-message { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; padding: 0.45rem 0.55rem; border-bottom: 1px solid var(--border); line-height: 1.35; } .vdes-message:last-child { border-bottom: none; } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts index 6a7034ed..7b6e15e3 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts @@ -27,6 +27,7 @@ interface AisMessage { } interface AisChannelInfo { label: string; badgeClass: string; freqText: string } interface AisBridge { + navigateToAprsMap?: (lat: number, lon: number) => void; getDecodeHistoryRetentionMs?: () => number; trxScheduleUiFrameJob?: (key: string, job: () => void) => void; buildAisVesselUrl?: (mmsi: number | null | undefined) => string | null; @@ -212,8 +213,21 @@ function updateAisSummary() { } } +/** What the message says, in one line: where the vessel is and what it is + * doing, or — for the static reports that carry no fix — where it is going. */ +function aisSummaryText(msg: AisMessage): string { + const parts: string[] = []; + if (msg.lat != null && msg.lon != null) parts.push(`${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}`); + const motion = aisMotionText(msg); + if (motion) parts.push(motion); + const route = aisRouteText(msg); + if (route && parts.length < 2) parts.push(route); + if (!parts.length) return route || "no position reported"; + return parts.join(" · "); +} + function renderAisRow(msg: AisMessage): HTMLElement { - const row = document.createElement("div"); + const row = document.createElement("details"); row.className = "ais-message"; const ts = msg._ts || new Date().toLocaleTimeString([], { hour: "2-digit", @@ -227,8 +241,9 @@ function renderAisRow(msg: AisMessage): HTMLElement { const route = aisRouteText(msg); const distance = aisDistanceText(msg); const pos = msg.lat != null && msg.lon != null - ? `${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}` + ? `${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)}` : ""; + const vesselUrl = aisWindow.buildAisVesselUrl?.(msg.mmsi) ?? null; row.dataset.filterText = [ name, msg.mmsi, @@ -243,23 +258,43 @@ function renderAisRow(msg: AisMessage): HTMLElement { .join(" ") .toUpperCase(); row.innerHTML = - `
` + - `${ts}` + + `` + + `${escapeAisHtml(ts)}` + `${nameHtml}` + - `${escapeAisHtml(channel.label)}` + `${escapeAisHtml(aisTypeLabel(msg.message_type))}` + - `
` + - `
` + - `MMSI ${escapeAisHtml(String(msg.mmsi))}` + - (route ? `${escapeAisHtml(route)}` : "") + - `${escapeAisHtml(channel.freqText)}` + - `
` + - `
` + - (motion ? `${escapeAisHtml(motion)}` : `No motion data`) + - (distance ? `${escapeAisHtml(distance)}` : "") + - (pos ? `${pos}` : "") + - `${escapeAisHtml(aisAgeText(msg._tsMs))}` + + `${escapeAisHtml(aisSummaryText(msg))}` + + `${escapeAisHtml(channel.label)}` + + (distance ? `${escapeAisHtml(distance)}` : "") + + `` + + `
` + + `
` + + `MMSI ${escapeAisHtml(String(msg.mmsi))}` + + `${escapeAisHtml(channel.freqText)}` + + (route ? `${escapeAisHtml(route)}` : "") + + (motion ? `${escapeAisHtml(motion)}` : "") + + `${escapeAisHtml(aisAgeText(msg._tsMs))}` + + (pos ? `${pos}` : "") + + `
` + + `
` + + (msg.lat != null && msg.lon != null + ? `` + : "") + + (vesselUrl + ? `Vessel` + : "") + + `
` + `
`; + + row.querySelectorAll("[data-ais-map]").forEach((element) => { + element.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + const [lat, lon] = (element.dataset.aisMap ?? "").split(",").map(Number); + if (lat == null || lon == null || !Number.isFinite(lat) || !Number.isFinite(lon)) return; + aisWindow.navigateToAprsMap?.(lat, lon); + }); + }); + applyAisFilterToRow(row); return row; } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts index cf7c00d2..8ef8517f 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts @@ -333,19 +333,19 @@ export function renderAprsPacketRow(packet: AprsPacket, options: AprsRowOptions const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(packet.srcCall || "")}`; row.innerHTML = - `` + + `` + `${escapeAprsHtml(time)}` + (options.badge ? `${escapeAprsHtml(options.badge)}` : "") + renderLocalAprsSymbol(packet, escapeAprsHtml) + `${escapeAprsHtml(packet.srcCall ?? "")}` + `` + `${escapeAprsHtml(aprsCategoryLabel(category))}` + - `${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}` + + `${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}` + (packet.crcOk ? "" : 'CRC') + - (options.distance ? `${escapeAprsHtml(options.distance)}` : "") + + (options.distance ? `${escapeAprsHtml(options.distance)}` : "") + `` + - `
` + - `
` + + `
` + + `
` + `>${escapeAprsHtml(packet.destCall || "--")}` + `${escapeAprsHtml(packet.path || "no path")}` + `${escapeAprsHtml(aprsAgeText(packet._tsMs))}` + @@ -355,9 +355,9 @@ export function renderAprsPacketRow(packet: AprsPacket, options: AprsRowOptions + `${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}` : "") + `
` + - `
${renderAprsInfo(packet)}
` + + `
${renderAprsInfo(packet)}
` + (packet.info_bytes?.length - ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
` + ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
` : "") + `
` + (hasPosition ? `` : "") + diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs index ee21064b..f021d128 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs @@ -178,7 +178,20 @@ const APRS_FRAMES = [ ...frame, })); -const aprsFixture = await startWebFixture({ spectrum: true, history: { aprs: APRS_FRAMES } }); +// The AIS list is the same shape: a line per message, saying where the vessel +// is and what it is doing, with the identifiers behind it. +const AIS_MESSAGES = [ + { message_type: 1, mmsi: 244660001, vessel_name: "NEDERLAND", channel: "A", + lat: 54.35, lon: 18.65, sog_knots: 8.2, cog_deg: 91.4 }, + { message_type: 5, mmsi: 244660002, vessel_name: "STENA SPIRIT", channel: "B", + callsign: "PBTX", destination: "GDANSK" }, +].map((message, index) => ({ rig_id: "rig-a", ts_ms: Date.now() - index * 1000, ...message })); + +const aprsFixture = await startWebFixture({ + spectrum: true, + mode: "AIS", + history: { aprs: APRS_FRAMES, ais: AIS_MESSAGES }, +}); const aprs = await startBrowser(chromium); try { @@ -194,7 +207,7 @@ try { tag: row.tagName, height: Math.round(row.getBoundingClientRect().height), type: row.querySelector(".aprs-badge-type")?.textContent?.trim() ?? "", - summary: row.querySelector(".aprs-line-summary")?.textContent?.trim() ?? "", + summary: row.querySelector(".decode-line-summary")?.textContent?.trim() ?? "", }))); // Newest first, so find each by the type it carries rather than by position. const summaryOf = (type) => rows.find((row) => row.type === type)?.summary ?? ""; @@ -213,20 +226,49 @@ try { // The frame as it arrived is still there, one click away. await aprs.page.locator("#aprs-packets .aprs-packet", { hasText: "25 °C" }) - .locator(".aprs-line").first().click(); + .locator(".decode-line").first().click(); await aprs.page.waitForTimeout(200); const expanded = await aprs.page.evaluate(() => { const row = document.querySelector("#aprs-packets .aprs-packet[open]"); return { open: row.hasAttribute("open"), - raw: row.querySelector(".aprs-expanded-raw")?.textContent?.trim() ?? "", - meta: row.querySelector(".aprs-expanded-meta")?.textContent ?? "", + raw: row.querySelector(".decode-expanded-raw")?.textContent?.trim() ?? "", + meta: row.querySelector(".decode-expanded-meta")?.textContent ?? "", }; }); assert.equal(expanded.open, true, "the frame did not open"); assert.match(expanded.raw, /^_10090556c220s004g005t077/, `raw frame: ${expanded.raw}`); assert.match(expanded.meta, /WIDE1-1/, `expanded meta: ${expanded.meta}`); + // AIS, on the same row. + await aprs.page.locator('.sub-tab[data-subtab="ais"]').click(); + await aprs.page.waitForTimeout(300); + const aisRows = await aprs.page.evaluate(() => + [...document.querySelectorAll("#ais-messages .ais-message")].map((row) => ({ + tag: row.tagName, + height: Math.round(row.getBoundingClientRect().height), + name: row.querySelector(".ais-call")?.textContent?.trim() ?? "", + summary: row.querySelector(".decode-line-summary")?.textContent?.trim() ?? "", + }))); + assert.equal(aisRows.length, AIS_MESSAGES.length, `rendered ${aisRows.length} messages`); + for (const row of aisRows) { + assert.equal(row.tag, "DETAILS", "an AIS message is not expandable in place"); + assert.ok(row.height < 44, `an AIS message is ${row.height}px tall`); + } + const positionRow = aisRows.find((row) => row.name === "NEDERLAND"); + const staticRow = aisRows.find((row) => row.name === "STENA SPIRIT"); + assert.match(positionRow?.summary ?? "", /54\.3500, 18\.6500/, `position: ${positionRow?.summary}`); + assert.match(positionRow?.summary ?? "", /8\.2 kn/, `position: ${positionRow?.summary}`); + // A static report carries no fix, so it says where the vessel is going. + assert.match(staticRow?.summary ?? "", /PBTX -> GDANSK/, `static: ${staticRow?.summary}`); + + await aprs.page.locator("#ais-messages .ais-message .decode-line").first().click(); + await aprs.page.waitForTimeout(200); + const aisExpanded = await aprs.page.evaluate(() => + document.querySelector("#ais-messages .ais-message[open] .decode-expanded-meta")?.textContent ?? ""); + assert.match(aisExpanded, /MMSI 2446600/, `expanded AIS: ${aisExpanded}`); + assert.match(aisExpanded, /MHz/, `expanded AIS: ${aisExpanded}`); + assert.deepEqual(aprs.runtimeErrors, []); } finally { await aprs.browser.close();