Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"invalidateApiCache": "readonly",
"isFavorite": "readonly",
"isPathHashSizeWarn": "readonly",
"isUnscopedFlood": "readonly",
"isTransportRoute": "readonly",
"makeColumnsResizable": "readonly",
"makeRoleMarkerSVG": "readonly",
Expand All @@ -266,6 +267,7 @@
"pullReconnect": "readonly",
"qrcode": "readonly",
"registerPage": "readonly",
"renderNoScope": "readonly",
"renderPathHashSize": "readonly",
"renderVersionCard": "readonly",
"renderSkewBadge": "readonly",
Expand Down
28 changes: 28 additions & 0 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

function routeTypeName(n) { return ROUTE_TYPES[n] || 'UNKNOWN'; }
function payloadTypeName(n) { return PAYLOAD_TYPES[n] || 'UNKNOWN'; }
function payloadTypeColor(n) { return PAYLOAD_COLORS[n] || 'unknown'; }

Check warning on line 11 in public/app.js

View workflow job for this annotation

GitHub Actions / ✅ JavaScript Unit Tests & Lint

'payloadTypeColor' is defined but never used. Allowed unused vars must match /^_/u
function isTransportRoute(rt) { return rt === 0 || rt === 3; }
/** Byte offset of path_len in raw_hex: 5 for transport routes (4 bytes of next/last hop codes precede it), 1 otherwise. */
function getPathLenOffset(routeType) { return isTransportRoute(routeType) ? 5 : 1; }
Expand Down Expand Up @@ -40,7 +40,7 @@
return size <= 3 ? size : null;
}
/** Sender-selected path-hash width in this frame, or null when not encoded. */
function senderPathHashSize(rawHex) {

Check warning on line 43 in public/app.js

View workflow job for this annotation

GitHub Actions / ✅ JavaScript Unit Tests & Lint

'senderPathHashSize' is defined but never used. Allowed unused vars must match /^_/u
if (typeof rawHex !== 'string' || !/^[0-9a-f]{2}/i.test(rawHex)) return null;
const header = parseInt(rawHex.slice(0, 2), 16);
const route = header & 0x03;
Expand Down Expand Up @@ -102,6 +102,34 @@
'<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-warning"/></svg>' +
label + rec + '</span>';
}
/**
* #480: the ONE decision for "this flood carries no region scope". Decided
* from the transmission's route type only: FLOOD (1) is unscoped,
* TRANSPORT_FLOOD (0) is scoped whether or not its name is known (an unknown
* name has scope_name = '' and is NOT "no scope"). DIRECT (2) and
* TRANSPORT_DIRECT (3) are not flood scope, so they get no marker.
*/
function isUnscopedFlood(routeType) {
return routeType === 1 || routeType === '1';
}
/**
* #480: the ONE markup for the "no scope" recommendation, shared by the
* Channels message meta line and the packet-detail Scope row. Same amber
* --path-hash-warn recommendation style as #353, a warning (not error) icon.
* The explanation states only the firmware behaviour (companion default flood
* scope; repeater flood.max.unscoped / region denyf, firmware
* docs/cli_commands.md) and never attributes a low heard-count to it. Returns
* '' unless the route type is an unscoped FLOOD.
*/
function renderNoScope(routeType) {
if (!isUnscopedFlood(routeType)) return '';
const tip = 'Sent without a scope. The message floods outside your region, and repeaters that limit ' +
'unscoped traffic (flood.max.unscoped / region denyf) may stop it. ' +
'Recommended: set a default scope (region) in the companion app.';
return '<span class="no-scope path-hash-warn" title="' + escapeHtml(tip) + '">' +
'<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-warning"/></svg>' +
'no scope<span class="sr-only"> — sent without a scope. Recommended: set a default scope (region) in the companion app</span></span>';
}
/**
* scopeName is optional (callers that don't pass it get the original
* unscoped "T" badge). Pass a packet's scope_name to also surface the
Expand Down
5 changes: 5 additions & 0 deletions public/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -2840,6 +2840,11 @@
const isTransportRoute = msg.routeType === 0 || msg.routeType === 3;
if (msg.scope) meta.push(`scope: ${escapeHtml(msg.scope)}`);
else if (isTransportRoute) meta.push('scope: unknown');
else if (typeof renderNoScope === 'function') {
// #480: no scope on a plain FLOOD (decided from the route type, not the name).
const noScopeHtml = renderNoScope(msg.routeType);
if (noScopeHtml) meta.push(noScopeHtml);
}
// msg.area (set server-side from the message's own path[0]
// entry-point repeater, not from the scope string) is where the
// SENDER physically was — distinct from the scope-linked area
Expand Down
14 changes: 13 additions & 1 deletion public/packets.js
Original file line number Diff line number Diff line change
Expand Up @@ -3768,6 +3768,17 @@
return inner ? '<dt>Hash Size</dt><dd>' + inner + '</dd>' : '';
}

// #480: Scope row. A named/unknown scope keeps its existing display; an
// unscoped FLOOD GRP_TXT/GRP_DATA gets the shared "no scope" marker.
function scopeDetailHtml(pkt) {
if (pkt.scope_name != null) {
return '<dt>Scope</dt><dd>' + (pkt.scope_name !== '' ? escapeHtml(pkt.scope_name) : '<span style="color:var(--text-muted)">unknown scope</span>') + '</dd>';
}
if (pkt.payload_type !== 5 && pkt.payload_type !== 6) return '';
const marker = renderNoScope(pkt.route_type);
return marker ? '<dt>Scope</dt><dd>' + marker + '</dd>' : '';
}

async function renderDetail(panel, data, chosenObsId) {
const pkt = data.packet;
const observations = data.observations || [];
Expand Down Expand Up @@ -4004,7 +4015,7 @@
${locationHtml ? `<dt>Location</dt><dd>${locationHtml}</dd>` : ''}
<dt>SNR / RSSI</dt><dd>${snr != null ? snr + ' dB' : '—'} / ${rssi != null ? rssi + ' dBm' : '—'}</dd>
<dt>Route Type</dt><dd>${routeTypeName(pkt.route_type)}</dd>
${pkt.scope_name != null ? `<dt>Scope</dt><dd>${pkt.scope_name !== '' ? escapeHtml(pkt.scope_name) : '<span style="color:var(--text-muted)">unknown scope</span>'}</dd>` : ''}
${scopeDetailHtml(pkt)}
${hashSizeDetailHtml(hashSize)}
<dt>Propagation</dt><dd>${propagationHtml}</dd>
${transportCodesRow}
Expand Down Expand Up @@ -4689,6 +4700,7 @@
getDetailPreview,
buildDetailMessageHtml,
hashSizeDetailHtml,
scopeDetailHtml,
sortGroupChildren,
getPathHopCount,
renderDecodedPacket,
Expand Down
3 changes: 3 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2036,6 +2036,9 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; }
* public/app.js). The icon + sr-only text carry it without colour. */
.path-hash-warn { color: var(--path-hash-warn); font-weight: 600; }
.path-hash-warn .ph-icon { margin-right: 3px; vertical-align: -0.125em; }
/* #480 — "no scope" marker (renderNoScope in app.js): reuses the amber
* --path-hash-warn recommendation colour; stays compact on most messages. */
.no-scope { white-space: nowrap; }
.ch-path-hash-badge--warn { border-color: var(--path-hash-warn); align-items: center; }
.node-path-hash-badge { background: var(--surface-2); border: 1px solid var(--path-hash-warn); font-size: 10px; }
.ch-analyze-link { color: var(--link-color); text-decoration: none; margin-left: 8px; }
Expand Down
8 changes: 8 additions & 0 deletions scripts/e2e-shards.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
{
"version": 1,
"suites": [
{
"file": "test-issue-480-no-scope-e2e.js",
"shard": 2,
"env": { "BASE_URL": "$BASE_URL" },
"output": "e2e",
"meanSeconds": 0,
"measurementNote": "New suite; no CI duration measured yet. Assigned to the lowest current measured shard total."
},
{
"file": "test-perf-ws-history-e2e.js",
"shard": 2,
Expand Down
1 change: 1 addition & 0 deletions test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ run test-issue-1562-observers-summary.js
run test-issue-1509-nav-active-bg.js
run test-issue-1509-detect-preset.js
run test-issue-353-path-hash-warn.js
run test-issue-480-no-scope.js
run test-live.js
run test-issue-220-live-filter-readiness.js
run test-coverage-gate.js
Expand Down
2 changes: 1 addition & 1 deletion test-issue-353-path-hash-warn-e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ async function assertContrast(page, selector, where) {
const size = page.locator('dl.detail-meta .detail-hash-size');
await size.waitFor();
assert((await size.textContent()) === '2 bytes', 'text ' + await size.textContent());
assert(await page.locator('dl.detail-meta .path-hash-warn').count() === 0, '2-byte packet warns');
assert(await page.locator('dl.detail-meta .detail-hash-size.path-hash-warn').count() === 0, '2-byte packet warns');
});

await step(`${v.name}: node detail shows the 1-byte badge, not on a 2-byte node`, async () => {
Expand Down
Loading
Loading