[feat](trx-frontend-http): give the AIS list the same log shape
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 <sjg@haxx.space>
This commit is contained in:
@@ -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 ? `<a class="ais-pos-link" href="javascript:void(0)" onclick="window.navigateToAprsMap(${msg.lat},${msg.lon})">${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}</a>` : "";
|
||||
const pos = msg.lat != null && msg.lon != null ? `<a class="ais-pos-link" href="javascript:void(0)" data-ais-map="${msg.lat},${msg.lon}">${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)}</a>` : "";
|
||||
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 = `<div class="ais-row-head"><span class="ais-time">${ts}</span><span class="ais-call">${nameHtml}</span><span class="${channel.badgeClass}">${escapeAisHtml(channel.label)}</span><span class="ais-badge ais-badge-type">${escapeAisHtml(aisTypeLabel(msg.message_type))}</span></div><div class="ais-row-meta"><span>MMSI ${escapeAisHtml(String(msg.mmsi))}</span>` + (route ? `<span class="ais-meta-text">${escapeAisHtml(route)}</span>` : "") + `<span class="ais-meta-text">${escapeAisHtml(channel.freqText)}</span></div><div class="ais-row-detail">` + (motion ? `<span>${escapeAisHtml(motion)}</span>` : `<span>No motion data</span>`) + (distance ? `<span>${escapeAisHtml(distance)}</span>` : "") + (pos ? `<span>${pos}</span>` : "") + `<span>${escapeAisHtml(aisAgeText(msg._tsMs))}</span></div>`;
|
||||
row.innerHTML = `<summary class="decode-line"><span class="ais-time">${escapeAisHtml(ts)}</span><span class="ais-call">${nameHtml}</span><span class="ais-badge ais-badge-type">${escapeAisHtml(aisTypeLabel(msg.message_type))}</span><span class="decode-line-summary">${escapeAisHtml(aisSummaryText(msg))}</span><span class="${channel.badgeClass}">${escapeAisHtml(channel.label)}</span>` + (distance ? `<span class="decode-line-distance">${escapeAisHtml(distance)}</span>` : "") + `</summary><div class="decode-expanded"><div class="decode-expanded-meta"><span>MMSI ${escapeAisHtml(String(msg.mmsi))}</span><span>${escapeAisHtml(channel.freqText)}</span>` + (route ? `<span>${escapeAisHtml(route)}</span>` : "") + (motion ? `<span>${escapeAisHtml(motion)}</span>` : "") + `<span>${escapeAisHtml(aisAgeText(msg._tsMs))}</span>` + (pos ? `<span>${pos}</span>` : "") + `</div><div class="aprs-row-actions">` + (msg.lat != null && msg.lon != null ? `<button class="aprs-inline-btn" type="button" data-ais-map="${msg.lat},${msg.lon}">Map</button>` : "") + (vesselUrl ? `<a class="aprs-inline-btn" href="${escapeAisHtml(vesselUrl)}" target="_blank" rel="noopener">Vessel</a>` : "") + `</div></div>`;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
collapseAprsDuplicates,
|
||||
normalizeAprsPacket,
|
||||
renderAprsPacketRow
|
||||
} from "./chunk-YAWVT7YC.js";
|
||||
} from "./chunk-OZI7SINT.js";
|
||||
import {
|
||||
hostCore,
|
||||
hostState
|
||||
|
||||
+1
-1
@@ -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 = `<summary class="aprs-line"><span class="aprs-time">${escapeAprsHtml(time)}</span>` + (options.badge ? `<span class="aprs-badge aprs-badge-band">${escapeAprsHtml(options.badge)}</span>` : "") + renderLocalAprsSymbol(packet, escapeAprsHtml) + `<span class="aprs-call">${escapeAprsHtml(packet.srcCall ?? "")}</span><span class="aprs-badge aprs-badge-type aprs-badge-type-${category}">${escapeAprsHtml(aprsCategoryLabel(category))}</span><span class="aprs-line-summary">${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}</span>` + (packet.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC</span>') + (options.distance ? `<span class="aprs-line-distance">${escapeAprsHtml(options.distance)}</span>` : "") + `</summary><div class="aprs-expanded"><div class="aprs-expanded-meta"><span>>${escapeAprsHtml(packet.destCall || "--")}</span><span>${escapeAprsHtml(packet.path || "no path")}</span><span>${escapeAprsHtml(aprsAgeText(packet._tsMs))}</span><span>CRC ${packet.crcOk ? "ok" : "failed"}</span>` + (hasPosition ? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${packet.lat},${packet.lon}">${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}</a>` : "") + `</div><div class="aprs-expanded-raw">${renderAprsInfo(packet)}</div>` + (packet.info_bytes?.length ? `<div class="aprs-expanded-bytes">${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}</div>` : "") + `<div class="aprs-row-actions">` + (hasPosition ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${packet.lat},${packet.lon}">Map</button>` : "") + (hasPosition ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${packet.lat},${packet.lon}">Copy Coords</button>` : "") + `<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a></div></div>`;
|
||||
row.innerHTML = `<summary class="decode-line"><span class="aprs-time">${escapeAprsHtml(time)}</span>` + (options.badge ? `<span class="aprs-badge aprs-badge-band">${escapeAprsHtml(options.badge)}</span>` : "") + renderLocalAprsSymbol(packet, escapeAprsHtml) + `<span class="aprs-call">${escapeAprsHtml(packet.srcCall ?? "")}</span><span class="aprs-badge aprs-badge-type aprs-badge-type-${category}">${escapeAprsHtml(aprsCategoryLabel(category))}</span><span class="decode-line-summary">${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}</span>` + (packet.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC</span>') + (options.distance ? `<span class="decode-line-distance">${escapeAprsHtml(options.distance)}</span>` : "") + `</summary><div class="decode-expanded"><div class="decode-expanded-meta"><span>>${escapeAprsHtml(packet.destCall || "--")}</span><span>${escapeAprsHtml(packet.path || "no path")}</span><span>${escapeAprsHtml(aprsAgeText(packet._tsMs))}</span><span>CRC ${packet.crcOk ? "ok" : "failed"}</span>` + (hasPosition ? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${packet.lat},${packet.lon}">${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}</a>` : "") + `</div><div class="decode-expanded-raw">${renderAprsInfo(packet)}</div>` + (packet.info_bytes?.length ? `<div class="decode-expanded-bytes">${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}</div>` : "") + `<div class="aprs-row-actions">` + (hasPosition ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${packet.lat},${packet.lon}">Map</button>` : "") + (hasPosition ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${packet.lat},${packet.lon}">Copy Coords</button>` : "") + `<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a></div></div>`;
|
||||
row.querySelectorAll("[data-aprs-map]").forEach((element) => {
|
||||
element.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
collapseAprsDuplicates,
|
||||
normalizeAprsPacket,
|
||||
renderAprsPacketRow
|
||||
} from "./chunk-YAWVT7YC.js";
|
||||
} from "./chunk-OZI7SINT.js";
|
||||
import {
|
||||
hostCore,
|
||||
hostState
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
aprsSymbolSprite
|
||||
} from "./chunk-YAWVT7YC.js";
|
||||
} from "./chunk-OZI7SINT.js";
|
||||
|
||||
// src/map-core.ts
|
||||
function mapEl(id) {
|
||||
|
||||
@@ -680,19 +680,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<input id="ais-filter" class="ft8-filter" type="text" placeholder="Filter (e.g. MMSI, vessel, A)" />
|
||||
<small id="ais-status" style="color:var(--text-muted);">Waiting for server decode</small>
|
||||
</div>
|
||||
<div class="ais-summary">
|
||||
<div class="ais-summary-card">
|
||||
<span class="ais-summary-label">Channels</span>
|
||||
<span id="ais-channel-summary" class="ais-summary-value">A 161.975 MHz · B 162.025 MHz</span>
|
||||
</div>
|
||||
<div class="ais-summary-card">
|
||||
<span class="ais-summary-label">Tracked</span>
|
||||
<span id="ais-vessel-count" class="ais-summary-value">0 vessels</span>
|
||||
</div>
|
||||
<div class="ais-summary-card">
|
||||
<span class="ais-summary-label">Latest</span>
|
||||
<span id="ais-latest-seen" class="ais-summary-value">No traffic yet</span>
|
||||
</div>
|
||||
<div class="aprs-filter-row">
|
||||
<span class="aprs-counts">
|
||||
<span id="ais-vessel-count" class="aprs-counts-value">0 vessels</span>
|
||||
<span id="ais-latest-seen" class="aprs-counts-value">No traffic yet</span>
|
||||
<span id="ais-channel-summary" class="aprs-counts-value">A 161.975 MHz · B 162.025 MHz</span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="ais-messages"></div>
|
||||
</div>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user