diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index a83cf363..e1d0609f 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -151,6 +151,7 @@ jobs:
node test-warmup-banner.js
node test-issue-1633-hide-1byte-hops.js
node test-issue-1668-m4-per-route.js
+ node test-a11y-axe-1668-selftest.js
- name: π‘οΈ Preflight XSS gate β actual --diff check (PR only)
# The fixture self-test above (test-preflight-xss-gate.js) only
@@ -362,6 +363,11 @@ jobs:
- name: Run Playwright E2E tests (fail-fast)
run: |
BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt
+ # M5 of #1668 β axe-core CI gate (color-contrast AA).
+ # Real browser run; fails on any net violation (raw β allowlist).
+ # Allowlist: tests/a11y-allowlist.yaml (0 entries at M5 baseline).
+ BASE_URL=http://localhost:13581 AXE_SCREENSHOT_DIR=/tmp/axe-1668 \
+ node test-a11y-axe-1668.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-filter-ux-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-channel-issue-1087-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-channel-issue-1111-e2e.js 2>&1 | tee -a e2e-output.txt
diff --git a/package-lock.json b/package-lock.json
index 2a938f20..5a3106af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "meshcore-analyzer",
- "version": "2.6.0",
+ "version": "0.0.0-use-git-tags",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meshcore-analyzer",
- "version": "2.6.0",
+ "version": "0.0.0-use-git-tags",
"license": "ISC",
"dependencies": {
"@michaelhart/meshcore-decoder": "^0.2.7",
@@ -16,11 +16,36 @@
"ws": "^8.19.0"
},
"devDependencies": {
+ "@axe-core/playwright": "^4.11.3",
+ "axe-core": "^4.12.1",
"nyc": "^18.0.0",
"playwright": "^1.58.2",
"supertest": "^7.2.2"
}
},
+ "node_modules/@axe-core/playwright": {
+ "version": "4.11.3",
+ "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.3.tgz",
+ "integrity": "sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "axe-core": "~4.11.4"
+ },
+ "peerDependencies": {
+ "playwright-core": ">= 1.0.0"
+ }
+ },
+ "node_modules/@axe-core/playwright/node_modules/axe-core": {
+ "version": "4.11.4",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz",
+ "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -566,6 +591,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/axe-core": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz",
+ "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
diff --git a/package.json b/package.json
index ce49e309..9f588656 100644
--- a/package.json
+++ b/package.json
@@ -20,8 +20,10 @@
"ws": "^8.19.0"
},
"devDependencies": {
+ "@axe-core/playwright": "^4.11.3",
+ "axe-core": "^4.12.1",
"nyc": "^18.0.0",
"playwright": "^1.58.2",
"supertest": "^7.2.2"
}
-}
\ No newline at end of file
+}
diff --git a/public/analytics.js b/public/analytics.js
index 4b6fa032..63e22c2c 100644
--- a/public/analytics.js
+++ b/public/analytics.js
@@ -502,7 +502,7 @@
'
' +
'cnt ' + cpct.toFixed(1) + '% ' +
' Β· ' +
- 'air ' + apct.toFixed(1) + '% ' +
+ ' air ' + apct.toFixed(1) + '% ' +
'
' +
'';
});
@@ -1364,7 +1364,7 @@
var roleColor = (window.ROLE_COLORS || {})[r.role] || '#6b7280';
return '' +
'' + esc(r.name) + ' ' +
- '' + esc(r.role || 'unknown') + ' ' +
+ '' + esc(r.role || 'unknown') + ' ' +
'' +
(statusIcon[r.status] || ' ') + ' ' + (statusLabel[r.status] || 'Unknown') + ' ' +
'' + r.hashSize + '-byte ' +
@@ -1458,19 +1458,19 @@
async function renderCollisionTab(el, data, collisionData) {
el.innerHTML = `
- Inconsistent Sizes
+ Inconsistent Sizes
|
- Hash Matrix
+ Hash Matrix
|
- Collision Risk
+ Collision Risk
|
- Check a prefix β
+ Check a prefix β
- Collisions actually observed in packet traffic β among repeaters grouped by their configured hash size. For theoretical address conflicts that would occur if all repeaters used a given hash size, see the Prefix Tool tab.
+ Collisions actually observed in packet traffic β among repeaters grouped by their configured hash size. For theoretical address conflicts that would occur if all repeaters used a given hash size, see the Prefix Tool tab.
-
Repeaters and room servers sending adverts with varying hash sizes in the last 7 days. Originally caused by a firmware bug where automatic adverts ignored the configured multibyte path setting, fixed in repeater v1.14.1 . Companion nodes are excluded.
+
Repeaters and room servers sending adverts with varying hash sizes in the last 7 days. Originally caused by a firmware bug where automatic adverts ignored the configured multibyte path setting, fixed in repeater v1.14.1 . Companion nodes are excluded.
@@ -1515,8 +1515,8 @@
}).join(' ');
const stripe = i % 2 === 1 ? 'background:var(--row-stripe)' : '';
return `
- ${esc(n.name || n.public_key.slice(0, 12))}
- ${n.role}
+ ${esc(n.name || n.public_key.slice(0, 12))}
+ ${n.role}
${prefix} (${n.hash_size || '?'}B)
${sizeBadges}
`;
@@ -1792,7 +1792,7 @@
if (bytes === 3) {
el.innerHTML = hashStatCardsHtml(totalNodes, stats.using_this_size || 0, '3-byte', 16777216, stats.unique_prefixes || 0, stats.collision_count || 0) +
`The 3-byte prefix space (16.7M values) is too large to visualize as a grid.${(stats.collision_count || 0) > 0 ? ' See collision details below.' : ''}
` +
- `βΉοΈ This tab only counts collisions among repeaters configured for this hash size. The Prefix Tool checks all repeaters regardless of configured hash size.
`;
+ `βΉοΈ This tab only counts collisions among repeaters configured for this hash size. The Prefix Tool checks all repeaters regardless of configured hash size.
`;
return;
}
@@ -1892,7 +1892,7 @@
for (const [twoHex, nodes] of Object.entries(info.two_byte_map || {}).sort()) {
const isCollision = nodes.length > 1;
dhtml += ``;
- dhtml += `
${twoHex}${isCollision ? '
COLLISION ' : ''} `;
+ dhtml += `
${twoHex}${isCollision ? '
COLLISION ' : ''} `;
dhtml += nodes.map(m => `
${esc(m.name || m.public_key.slice(0,12))} `).join(', ');
dhtml += `
`;
}
@@ -2228,7 +2228,7 @@
return `${esc(n.name || n.public_key.slice(0, 12))} `;
}
function claimedBadge(n) {
- return myKeys.has(n.public_key) ? ' MINE ' : '';
+ return myKeys.has(n.public_key) ? ' MINE ' : '';
}
// ROLE_COLORS from shared roles.js
@@ -2259,7 +2259,7 @@
${Object.entries(roleCounts).sort((a,b) => b[1]-a[1]).map(([role, count]) => {
const c = ROLE_COLORS[role] || '#6b7280';
- return `${role}: ${count} `;
+ return `${role}: ${count} `;
}).join('')}
@@ -2271,7 +2271,7 @@
const s = n.health.stats;
return `
${nodeLink(n)}
- ${n.role}
+ ${n.role}
${s.totalTransmissions || s.totalPackets || 0}
${s.avgSnr != null ? s.avgSnr.toFixed(1) + ' dB' : 'β'}
${n.health.observers?.length || 0}
@@ -2288,7 +2288,7 @@
${byPackets.slice(0, 15).map((n, i) => `
${i + 1}
${nodeLink(n)}${claimedBadge(n)}
- ${n.role}
+ ${n.role}
${n.health.stats.totalTransmissions || n.health.stats.totalPackets || 0}
${n.health.stats.packetsToday || 0}
@@ -2303,7 +2303,7 @@
${bySnr.slice(0, 15).map((n, i) => `
${i + 1}
${nodeLink(n)}${claimedBadge(n)}
- ${n.role}
+ ${n.role}
${n.health.stats.avgSnr.toFixed(1)} dB
${n.health.observers?.length || 0}
@@ -2318,7 +2318,7 @@
${byObservers.slice(0, 15).map((n, i) => `
${i + 1}
${nodeLink(n)}${claimedBadge(n)}
- ${n.role}
+ ${n.role}
${n.health.observers?.length || 0}
${n.health.stats.avgSnr != null ? n.health.stats.avgSnr.toFixed(1) + ' dB' : 'β'}
@@ -2332,7 +2332,7 @@
${byRecent.slice(0, 15).map(n => `
${nodeLink(n)}${claimedBadge(n)}
- ${n.role}
+ ${n.role}
${timeAgo(n.health.stats.lastHeard)}
${n.health.stats.packetsToday || 0}
@@ -3028,7 +3028,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
const rows = entries.map(([prefix, arr]) => {
const nodeLinks = arr.map(n => {
const label = esc(n.name || n.public_key.slice(0, 10));
- return `${label} `;
+ return `${label} `;
}).join(', ');
return `
${esc(prefix)}
@@ -3061,7 +3061,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
const initGenerate = hashParams.get('generate') || '';
const regionNote = regionLabel
- ? `Showing data for region: ${esc(regionLabel)} . Check all repeaters β
`
+ ? `Showing data for region: ${esc(regionLabel)} . Check all repeaters β
`
: '';
el.innerHTML = `
@@ -3104,7 +3104,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
+ style="background:none;border:none;color:var(--link-color);cursor:pointer;font-size:0.82em;padding:0">
Show ${opC} colliding slice${opC !== 1 ? 's' : ''} β
@@ -3115,7 +3115,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
+ style="background:none;border:none;color:var(--link-color);cursor:pointer;font-size:0.78em;padding:0">
Show ${theoC} would-collide slice${theoC !== 1 ? 's' : ''} (across all repeaters) β
@@ -3141,7 +3141,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
}).join('')}
-
βΉοΈ Theoretical vs observed: These are
theoretical address conflicts that would occur IF all repeaters used this hash size (would-collide-if-used). For collisions
actually observed in packet traffic , see the
Hash Issues tab.
+
βΉοΈ Theoretical vs observed: These are
theoretical address conflicts that would occur IF all repeaters used this hash size (would-collide-if-used). For collisions
actually observed in packet traffic , see the
Hash Issues tab.
Recommendation: ${rec} prefixes β ${recDetail}
@@ -3149,8 +3149,8 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
βΉοΈ About these numbers: The primary count is how many repeaters are
configured for each hash size (their advertised path hash byte length), matching the
-
Hash Stats tab. Address conflicts (would-collide-if-used) count colliding slices among repeaters configured for the same hash size β same definition the
-
Hash Issues tab uses, except Hash Issues counts collisions
actually observed in packet traffic rather than theoretical. The
theoretical line shows the math fact: how many distinct slices appear when every repeater pubkey is truncated to N bytes, regardless of configured hash size.
+
Hash Stats tab. Address conflicts (would-collide-if-used) count colliding slices among repeaters configured for the same hash size β same definition the
+
Hash Issues tab uses, except Hash Issues counts collisions
actually observed in packet traffic rather than theoretical. The
theoretical line shows the math fact: how many distinct slices appear when every repeater pubkey is truncated to N bytes, regardless of configured hash size.
@@ -3174,7 +3174,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
0x00 and 0xFF excluded as a first byte β the MeshCore firmware keygen routine re-rolls identities whose
pub_key[0] is
00 or
FF, so by convention you should not see those prefixes on real nodes (see
simple_repeater/main.cpp:83 ).
+ target="_blank" rel="noopener noreferrer" style="color:var(--link-color)">simple_repeater/main.cpp:83).
@@ -3193,7 +3193,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
@@ -3345,7 +3345,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
@@ -4356,7 +4356,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
'' + (r.warningCount || 0) + ' / ' +
'' + (r.criticalCount || 0) + ' / ' +
'' + (r.absurdCount || 0) + ' / ' +
- '' + (r.noClockCount || 0) + ' ';
+ '' + (r.noClockCount || 0) + ' ';
return '' +
'' +
'' + _rolesEmoji(r.role) + ' ' + esc(r.role) + ' ' +
@@ -4375,7 +4375,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
}).join('');
el.innerHTML =
'Distribution of node roles across the mesh, with per-role clock-skew posture.
' +
- '' +
+ '
' +
'' + total + ' nodes across ' + roles.length + ' roles' +
'
' +
'
' +
diff --git a/public/audio-lab.js b/public/audio-lab.js
index 5779e6e9..539cd576 100644
--- a/public/audio-lab.js
+++ b/public/audio-lab.js
@@ -53,10 +53,10 @@
.alab-btn { padding: 6px 14px; border: 1px solid var(--border); border-radius: 6px;
background: var(--surface-1); color: var(--text); cursor: pointer; font-size: 13px; }
.alab-btn:hover { background: var(--hover-bg); }
- .alab-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
+ .alab-btn.active { background: var(--accent-strong); color: var(--text-on-accent); border-color: var(--accent-strong); }
.alab-speed { padding: 4px 8px; font-size: 12px; border-radius: 4px; border: 1px solid var(--border);
background: var(--surface-1); color: var(--text-muted); cursor: pointer; }
- .alab-speed.active { background: var(--accent); color: #fff; border-color: var(--accent); }
+ .alab-speed.active { background: var(--accent-strong); color: var(--text-on-accent); border-color: var(--accent-strong); }
.alab-section { background: var(--surface-1); border: 1px solid var(--border);
border-radius: 8px; padding: 16px; margin-bottom: 16px; }
.alab-section h3 { margin: 0 0 12px 0; font-size: 14px; color: var(--text-muted); font-weight: 600; }
@@ -86,7 +86,7 @@
.map-why-inline { display: block; font-size: 10px; color: var(--text-muted); font-family: var(--mono); margin-top: 2px; }
.alab-note-play { background: none; border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
font-size: 10px; padding: 2px 6px; color: var(--text-muted); }
- .alab-note-play:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
+ .alab-note-play:hover { background: var(--accent-strong); color: var(--text-on-accent); border-color: var(--accent-strong); }
.alab-note-clickable { cursor: pointer; }
.alab-note-clickable:hover { background: var(--hover-bg); }
.alab-empty { text-align: center; padding: 60px 20px; color: var(--text-muted); font-size: 15px; }
@@ -514,7 +514,7 @@
let html = '';
for (const [type, pkts] of Object.entries(data.buckets)) {
const color = TYPE_COLORS[type] || TYPE_COLORS.UNKNOWN;
- html += `
+ html += `
${type} ${pkts.length}
`;
html += `
`;
pkts.forEach((p, i) => {
diff --git a/public/customize-v2.js b/public/customize-v2.js
index 0b5fcf6d..d2df7d5f 100644
--- a/public/customize-v2.js
+++ b/public/customize-v2.js
@@ -1123,7 +1123,7 @@
'.cust-tab{padding:8px 10px;cursor:pointer;border:none;background:none;color:var(--text-muted);font-size:12px;font-weight:500;border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap;flex:1;text-align:center}',
'.cust-tab-text{font-size:10px;display:block}',
'.cust-tab:hover{color:var(--text)}',
- '.cust-tab.active{color:var(--accent);border-bottom-color:var(--accent)}',
+ '.cust-tab.active{color:var(--link-color);border-bottom-color:var(--accent)}',
'.cust-tab .cv2-tab-badge{font-size:9px;background:var(--accent);color:#fff;border-radius:8px;padding:0 4px;margin-left:2px}',
'.cust-panel{display:none;padding:12px 16px}',
'.cust-panel.active{display:block}',
@@ -1137,7 +1137,7 @@
'.cust-hint{font-size:10px;color:var(--text-muted);margin-top:1px;line-height:1.2}',
'.cust-color-row input[type="color"]{width:40px;height:32px;border:1px solid var(--border);border-radius:6px;cursor:pointer;padding:2px;background:var(--input-bg)}',
'.cust-color-row .cust-hex{font-family:var(--mono);font-size:12px;color:var(--text-muted);min-width:70px}',
- '.cv2-override-dot{color:var(--accent);cursor:pointer;font-size:10px;margin-left:4px;vertical-align:middle;title:"Reset to server default"}',
+ '.cv2-override-dot{color:var(--link-color);cursor:pointer;font-size:10px;margin-left:4px;vertical-align:middle;title:"Reset to server default"}',
'.cv2-override-dot:hover{color:var(--status-red)}',
'.cust-node-dot{display:inline-block;width:16px;height:16px;border-radius:50%;vertical-align:middle}',
'.cust-preview-img{max-width:200px;max-height:60px;margin-top:6px;border-radius:6px;border:1px solid var(--border)}',
@@ -1153,7 +1153,7 @@
'.cust-list-btn:hover{background:var(--surface-3)}',
'.cust-list-btn.danger{color:#ef4444}',
'.cust-list-btn.danger:hover{background:#fef2f2}',
- '.cust-add-btn{display:inline-flex;align-items:center;gap:4px;padding:6px 14px;border:1px dashed var(--border);border-radius:6px;background:none;color:var(--accent);cursor:pointer;font-size:13px;margin-top:4px}',
+ '.cust-add-btn{display:inline-flex;align-items:center;gap:4px;padding:6px 14px;border:1px dashed var(--border);border-radius:6px;background:none;color:var(--link-color);cursor:pointer;font-size:13px;margin-top:4px}',
'.cust-add-btn:hover{background:var(--hover-bg)}',
'.cust-export-btns{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}',
'.cust-export-btns button{padding:6px 14px;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:500}',
@@ -2105,7 +2105,7 @@
'' +
'
Tools
' +
'
Server-side configuration helpers.
' +
- '
GeoFilter Builder β' +
+ '
GeoFilter Builder β' +
'
Draw a polygon on the map to generate a geo_filter block for config.json.
' +
'
';
}
diff --git a/public/customize.js b/public/customize.js
index 8d5427a4..fa203d90 100644
--- a/public/customize.js
+++ b/public/customize.js
@@ -653,7 +653,7 @@
font-size: 12px; font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; flex: 1; text-align: center; }
.cust-tab-text { font-size: 10px; display: block; }
.cust-tab:hover { color: var(--text); }
- .cust-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
+ .cust-tab.active { color: var(--link-color); border-bottom-color: var(--accent); }
.cust-panel { display: none; padding: 12px 16px; }
.cust-panel.active { display: block; }
.cust-field { margin-bottom: 12px; }
@@ -690,7 +690,7 @@
.cust-list-btn.danger { color: #ef4444; }
.cust-list-btn.danger:hover { background: #fef2f2; }
.cust-add-btn { display: inline-flex; align-items: center; gap: 4px; padding: 6px 14px; border: 1px dashed var(--border);
- border-radius: 6px; background: none; color: var(--accent); cursor: pointer; font-size: 13px; margin-top: 4px; }
+ border-radius: 6px; background: none; color: var(--link-color); cursor: pointer; font-size: 13px; margin-top: 4px; }
.cust-add-btn:hover { background: var(--hover-bg); }
.cust-export-area { width: 100%; min-height: 300px; font-family: var(--mono); font-size: 12px;
background: var(--surface-1); border: 1px solid var(--border); border-radius: 6px; padding: 12px;
diff --git a/public/home.js b/public/home.js
index 498dfae4..c1d9223b 100644
--- a/public/home.js
+++ b/public/home.js
@@ -148,7 +148,7 @@
`;
@@ -571,9 +571,9 @@
if (!html) {
const items = [
{ q: ' First: Join the Bay Area MeshCore Discord',
- a: 'The community Discord is the best place to get help and find local mesh enthusiasts.
Join the Discord β
Start with #intro-to-meshcore β it has detailed setup instructions.
' },
+ a: 'The community Discord is the best place to get help and find local mesh enthusiasts.
Join the Discord β
Start with #intro-to-meshcore β it has detailed setup instructions.
' },
{ q: ' Step 1: Connect via Bluetooth',
- a: 'Flash BLE companion firmware from MeshCore Flasher .
Screenless devices: default PIN 123456 Screen devices: random PIN shown on display If pairing fails: forget device, reboot, re-pair ' },
+ a: 'Flash BLE companion firmware from MeshCore Flasher .
Screenless devices: default PIN 123456 Screen devices: random PIN shown on display If pairing fails: forget device, reboot, re-pair ' },
{ q: ' Step 2: Set the right frequency preset',
a: 'US Recommended:
910.525 MHz Β· BW 62.5 kHz Β· SF 7 Β· CR 5
Select "US Recommended" in the app or flasher.
' },
{ q: ' Step 3: Advertise yourself',
@@ -581,7 +581,7 @@
{ q: ' Step 4: Check "Heard N repeats"',
a: '"Sent" = transmitted, no confirmation"Heard 0 repeats" = no repeater picked it up"Heard 1+ repeats" = you\'re on the mesh! ' },
{ q: ' Repeaters near you?',
- a: 'Check the network map to see active repeaters.
' }
+ a: 'Check the network map to see active repeaters.
' }
];
html = items.map(i => ``).join('');
}
diff --git a/public/live.js b/public/live.js
index af52cd59..e7f962dd 100644
--- a/public/live.js
+++ b/public/live.js
@@ -2497,7 +2497,7 @@
Adverts ${n.advert_count || 0}
${'default_scope' in n ? `Scope ${n.default_scope === null ? 'β '
: n.default_scope === '' ? 'unknown scope '
- : `${escapeHtml(n.default_scope)}`
+ : `${escapeHtml(n.default_scope)}`
} ` : ''}
${hasLoc ? `Location ${n.lat.toFixed(5)}, ${n.lon.toFixed(5)} ` : ''}
${stats.avgSnr != null ? `Avg SNR ${stats.avgSnr.toFixed(1)} dB ` : ''}
@@ -2509,7 +2509,7 @@
const regions = [...new Set(observers.map(o => o.iata).filter(Boolean))];
html += `Heard By${regions.length ? ' β Regions: ' + regions.join(', ') : ''}
` +
- observers.map(o => `
`).join('') +
+ observers.map(o => `
`).join('') +
'
';
}
@@ -2517,7 +2517,7 @@
html += `Recent Packets
` +
recent.slice(0, 10).map(p => `
`).join('') +
'
';
@@ -2526,8 +2526,8 @@
html += ``;
html += ``;
content.innerHTML = html;
@@ -2561,7 +2561,7 @@
const chain = filteredHops.map(h => {
const isThis = h.pubkey === n.public_key || (h.prefix && n.public_key.toLowerCase().startsWith(h.prefix.toLowerCase()));
const name = escapeHtml(h.name || h.prefix);
- if (isThis) return `${name} `;
+ if (isThis) return `${name} `;
return h.pubkey ? `${name} ` : name;
}).join(' β ');
return `${chain} (${p.count}Γ)
`;
@@ -2570,7 +2570,7 @@
pathEl.innerHTML = `Paths Through (${pathData.totalPaths}) ` +
`` +
renderPathList(pathData.paths.slice(0, COLLAPSE)) +
- (pathData.paths.length > COLLAPSE ? `Show all ${pathData.paths.length} paths ` : '') +
+ (pathData.paths.length > COLLAPSE ? `Show all ${pathData.paths.length} paths ` : '') +
'
';
const moreBtn = document.getElementById('showMorePaths');
if (moreBtn) moreBtn.addEventListener('click', () => {
diff --git a/public/map.js b/public/map.js
index bbb57542..10336be8 100644
--- a/public/map.js
+++ b/public/map.js
@@ -1702,7 +1702,7 @@
Packets
${packets}
- View Detail β
+ View Detail β
`;
}
@@ -1799,8 +1799,8 @@
${node.advert_count || 0}
`;
}
diff --git a/public/node-analytics.js b/public/node-analytics.js
index 35a6c627..7f209d5c 100644
--- a/public/node-analytics.js
+++ b/public/node-analytics.js
@@ -53,7 +53,7 @@
container.innerHTML = `