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 f6907927..40e2d987 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-OZI7SINT.js";
+} from "./chunk-OPEIVJGD.js";
import {
hostCore,
hostState
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OZI7SINT.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OPEIVJGD.js
similarity index 84%
rename from src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OZI7SINT.js
rename to src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OPEIVJGD.js
index 6a6a5faf..c13bbc3c 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OZI7SINT.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OPEIVJGD.js
@@ -117,6 +117,9 @@ function aprsSymbolSprite(symbolTable, symbolCode) {
label: `Alternate APRS symbol \\${symbolCode} with overlay ${symbolTable}`
};
}
+function renderAprsSymbolSlot(packet, escapeHtml) {
+ return renderLocalAprsSymbol(packet, escapeHtml) || '';
+}
function renderLocalAprsSymbol(packet, escapeHtml) {
if (!packet.symbolTable || !packet.symbolCode) return "";
const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode);
@@ -221,7 +224,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)}` : "") + `
${renderAprsInfo(packet)}
` + (packet.info_bytes?.length ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
` : "") + `
` + (hasPosition ? `
` : "") + (hasPosition ? `
` : "") + `
QRZ `;
+ row.innerHTML = `${escapeAprsHtml(time)}` + (options.badge ? `${escapeAprsHtml(options.badge)}` : "") + renderAprsSymbolSlot(packet, escapeAprsHtml) + `${escapeAprsHtml(packet.srcCall ?? "")}${escapeAprsHtml(aprsCategoryLabel(category))}${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}` + (packet.crcOk ? "" : 'CRC') + (options.distance ? `${escapeAprsHtml(options.distance)}` : "") + `${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 3d504ec0..d267fa66 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-OZI7SINT.js";
+} from "./chunk-OPEIVJGD.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 dd456e9a..05298ff6 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-OZI7SINT.js";
+} from "./chunk-OPEIVJGD.js";
// src/map-core.ts
function mapEl(id) {
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 2da772a8..6642ae58 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
@@ -3153,6 +3153,8 @@ body.map-fake-fullscreen-active {
displays can swap in the @2x variants. */
.aprs-symbol { display: inline-block; width: 24px; height: 24px; background-repeat: no-repeat; background-size: 384px 144px; vertical-align: middle; margin-right: 0.3rem; }
.aprs-symbol-marker { margin-right: 0; }
+/* Holds the column open for a frame that carries no symbol. */
+.aprs-symbol-empty { background-image: none; }
.aprs-symbol-primary { background-image: url('/vendor/aprs-symbols-24-0.png'); }
.aprs-symbol-alternate { background-image: url('/vendor/aprs-symbols-24-1.png'); }
.aprs-symbol-overlaid { background-image: url('/vendor/aprs-symbols-24-2.png'), url('/vendor/aprs-symbols-24-1.png'); }
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 8ef8517f..8443bbcd 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
@@ -182,6 +182,13 @@ export function aprsSymbolSprite(
};
}
+/** An empty slot of the symbol's size, so a frame without one still lines up
+ * with the frames around it in the list. */
+export function renderAprsSymbolSlot(packet: AprsPacket, escapeHtml: (value: string) => string): string {
+ return renderLocalAprsSymbol(packet, escapeHtml)
+ || '';
+}
+
export function renderLocalAprsSymbol(packet: AprsPacket, escapeHtml: (value: string) => string): string {
if (!packet.symbolTable || !packet.symbolCode) return "";
const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode);
@@ -336,7 +343,7 @@ export function renderAprsPacketRow(packet: AprsPacket, options: AprsRowOptions
`` +
`${escapeAprsHtml(time)}` +
(options.badge ? `${escapeAprsHtml(options.badge)}` : "") +
- renderLocalAprsSymbol(packet, escapeAprsHtml) +
+ renderAprsSymbolSlot(packet, escapeAprsHtml) +
`${escapeAprsHtml(packet.srcCall ?? "")}` +
`` +
`${escapeAprsHtml(aprsCategoryLabel(category))}` +
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 f021d128..5d70a4ca 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
@@ -174,7 +174,11 @@ const APRS_FRAMES = [
{ packet_type: "position", info: "!5421.30N/01839.20E>Test beacon 73", lat: 54.35, lon: 18.65 },
].map((frame, index) => ({
src_call: `SP2SJG-${index}`, dest_call: "APRS", path: "WIDE1-1", crc_ok: true,
- symbol_table: "/", symbol_code: ">", rig_id: "rig-a", ts_ms: Date.now() - index * 1000,
+ // Only position reports carry a symbol here, which is the case the columns
+ // have to survive: a frame without one used to close the gap and shift
+ // everything after it left.
+ ...(frame.packet_type === "position" ? { symbol_table: "/", symbol_code: ">" } : {}),
+ rig_id: "rig-a", ts_ms: Date.now() - index * 1000,
...frame,
}));
@@ -224,6 +228,20 @@ try {
assert.match(summaryOf("Telemetry"), /^#005/, `telemetry summary: ${summaryOf("Telemetry")}`);
assert.match(summaryOf("Position"), /54\.3500, 18\.6500/, `position summary: ${summaryOf("Position")}`);
+ // Frames with and without a symbol line up: the slot is held open either way.
+ const columns = await aprs.page.evaluate(() =>
+ [...document.querySelectorAll("#aprs-packets .aprs-packet")].map((row) => ({
+ call: Math.round(row.querySelector(".aprs-call").getBoundingClientRect().x),
+ summary: Math.round(row.querySelector(".decode-line-summary").getBoundingClientRect().x),
+ symbol: !!row.querySelector(".aprs-symbol:not(.aprs-symbol-empty)"),
+ })));
+ assert.ok(columns.some((column) => column.symbol) && columns.some((column) => !column.symbol),
+ "the sample has to mix frames with and without a symbol to test this");
+ assert.equal(new Set(columns.map((column) => column.call)).size, 1,
+ `callsigns start at ${JSON.stringify(columns.map((column) => column.call))}`);
+ assert.equal(new Set(columns.map((column) => column.summary)).size, 1,
+ `summaries start at ${JSON.stringify(columns.map((column) => column.summary))}`);
+
// The frame as it arrived is still there, one click away.
await aprs.page.locator("#aprs-packets .aprs-packet", { hasText: "25 °C" })
.locator(".decode-line").first().click();