diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e2ff43fe..0136a254 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -398,6 +398,8 @@ jobs: CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1510-live-nav-pin-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-live-fullscreen-1572-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1599-replay-freeze-e2e.js 2>&1 | tee -a e2e-output.txt + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m1-icons-e2e.js 2>&1 | tee -a e2e-output.txt + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m2-icons-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1224-channels-mobile-ux-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1367-channels-chat-app-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js 2>&1 | tee -a e2e-output.txt diff --git a/public/analytics.js b/public/analytics.js index 3e2bae36..d9cab35b 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -98,7 +98,7 @@ app.innerHTML = `
-

πŸ“Š Mesh Analytics

+

Mesh Analytics

Deep dive into your mesh network data

@@ -369,7 +369,7 @@
-

πŸ“ˆ Packets / Hour

+

Packets / Hour

${(() => { const pph = rf.packetsPerHour; const counts = pph.map(h => h.count); @@ -394,18 +394,18 @@
-

πŸ“¦ Payload Type Mix

+

Payload Type Mix

${renderPayloadPie(rf.payloadTypes)}
-

πŸ”— Hop Count Distribution

+

Hop Count Distribution

${barChart(topo.hopDistribution.map(h=>h.count), topo.hopDistribution.map(h=>h.hops), ['#3b82f6'])}
-

πŸ“‘ Relay Airtime Share

+

Relay Airtime Share

Score = payload bytes Γ— distinct repeaters that forwarded the packet. Counts relay re-transmissions; originator TX excluded. Not comparable across meshes.

${renderRelayAirtimeDumbbell(d.airtimeData)}
@@ -427,7 +427,7 @@ var widget = document.createElement('div'); widget.className = 'analytics-row'; widget.innerHTML = '
' + - '

πŸ” Neighbor Affinity Graph

' + + '

Neighbor Affinity Graph

' + '
' + '
' + s.totalEdges + '
Total Edges
' + '
' + s.totalNodes + '
Total Nodes
' + @@ -525,7 +525,7 @@ el.innerHTML = `
-

πŸ“Ά SNR Distribution

+

SNR Distribution

Signal-to-Noise Ratio (higher = cleaner signal)

${snrHist.svg}
@@ -537,7 +537,7 @@
-

πŸ“‘ RSSI Distribution

+

RSSI Distribution

Received Signal Strength (closer to 0 = stronger)

${rssiHist.svg}
@@ -552,7 +552,7 @@
-

🎯 SNR vs RSSI Scatter

+

SNR vs RSSI Scatter

Each dot = one packet. Cluster position reveals link quality.

${renderScatter(rf.scatterData)}
@@ -560,17 +560,17 @@
-

πŸ“Š SNR by Payload Type

+

SNR by Payload Type

${renderSNRByType(rf.snrByType)}
-

πŸ“ˆ Signal Quality Over Time

+

Signal Quality Over Time

${renderSignalTimeline(rf.signalOverTime)}
-

πŸ“ Packet Size Distribution

+

Packet Size Distribution

Raw packet length in bytes

${histogram(rf.packetSizes, 25, '#8b5cf6').svg}
@@ -693,7 +693,7 @@ el.innerHTML = `
-

πŸ”— Hop Count Distribution

+

Hop Count Distribution

Number of repeater hops per packet

${barChart(topo.hopDistribution.map(h=>h.count), topo.hopDistribution.map(h=>h.hops), ['#3b82f6'])}
@@ -704,7 +704,7 @@
-

πŸ•ΈοΈ Top Repeaters

+

Top Repeaters

Nodes appearing most in packet paths

${renderRepeaterTable(topo.topRepeaters)}
@@ -712,25 +712,25 @@
-

🀝 Repeater Pair Heatmap

+

Repeater Pair Heatmap

Which repeaters frequently appear together in paths

${renderPairTable(topo.topPairs)}
-

πŸ“Š Hops vs SNR

+

Hops vs SNR

Does more hops = worse signal?

${renderHopsSNR(topo.hopsVsSnr)}
-

πŸ† Best Path to Each Node

+

Best Path to Each Node

Shortest hop distance seen across all observers

${renderBestPath(topo.bestPathList)}
-

🌐 Per-Observer Reachability

+

Per-Observer Reachability

Nodes at each hop distance, from each observer's perspective

${topo.observers.length > 1 ? `
${topo.observers.map((o, i) => ``).join('')} @@ -740,7 +740,7 @@
${topo.multiObsNodes.length ? `
-

πŸ”€ Cross-Observer Comparison

+

Cross-Observer Comparison

Nodes seen by multiple observers β€” hop distance varies by vantage point

${renderCrossObserver(topo.multiObsNodes)}
` : ''} @@ -832,7 +832,7 @@ function renderAllObserversReach(perObserverReach) { let html = ''; for (const [obsId, data] of Object.entries(perObserverReach)) { - html += `

πŸ“‘ ${esc(data.observer_name)}

`; + html += `

${esc(data.observer_name)}

`; html += renderPerObserverReach(perObserverReach, obsId); } return html || '
No data
'; @@ -947,7 +947,7 @@ '' + c.messages + '' + '' + c.senders + '' + '' + timeAgo(c.lastActivity) + '' + - '' + (c.encrypted ? (c.group === 'mine' ? 'πŸ”‘' : 'πŸ”’') : 'βœ…') + '' + + '' + (c.encrypted ? (c.group === 'mine' ? '' : '') : '') + '' + ''; } @@ -975,8 +975,8 @@ // Empty-name encrypted rows would otherwise leak through with an // empty in the row; force the placeholder rendering. copy.displayName = !isNaN(hashNum) - ? 'πŸ”’ Encrypted (0x' + hashNum.toString(16).toUpperCase().padStart(2, '0') + ')' - : 'πŸ”’ Encrypted'; + ? 'πŸ”’ Encrypted (0x' + hashNum.toString(16).toUpperCase().padStart(2, '0') + ')' // EMOJI-OK: displayName data field consumed by tests + : 'πŸ”’ Encrypted'; // EMOJI-OK: displayName data field consumed by tests copy.group = 'encrypted'; } else { // Server gave us a real name (rainbow table hit) for an encrypted ch. @@ -1024,9 +1024,9 @@ (groups[g] || (groups[g] = [])).push(sorted[gi]); } var sections = [ - { key: 'mine', label: 'πŸ”‘ My Channels' }, - { key: 'network', label: 'πŸ“» Network' }, - { key: 'encrypted', label: 'πŸ”’ Encrypted' }, + { key: 'mine', label: ' My Channels' }, + { key: 'network', label: ' Network' }, + { key: 'encrypted', label: ' Encrypted' }, ]; for (var si = 0; si < sections.length; si++) { var rows = groups[sections[si].key] || []; @@ -1111,7 +1111,7 @@ el.innerHTML = '
' + - '

πŸ“» Channel Activity

' + + '

Channel Activity

' + '

' + ch.activeChannels + ' active channels, ' + ch.decryptable + ' decryptable

' + '' + channelTheadHtml(_channelSortState.col, _channelSortState.dir) + @@ -1122,16 +1122,16 @@ '' + '
' + '
' + - '

πŸ’¬ Messages / Hour by Channel

' + + '

Messages / Hour by Channel

' + timelineHtml + '
' + '
' + - '

πŸ—£οΈ Top Senders

' + + '

Top Senders

' + topSendersHtml + '
' + '
' + '
' + - '

πŸ“Š Message Length Distribution

' + + '

Message Length Distribution

' + histoHtml + '
'; @@ -1275,7 +1275,7 @@ })() : ''}
-

πŸ“ˆ Hash Size Over Time

+

Hash Size Over Time

${renderHashTimeline(data.hourly)}
@@ -1309,7 +1309,7 @@ var capByPubkey = {}; (caps || []).forEach(function(c) { capByPubkey[c.pubkey] = c; }); - var statusIcon = { confirmed: 'βœ…', suspected: '⚠️', unknown: '❓' }; + var statusIcon = { confirmed: '', suspected: '', unknown: '' }; var statusLabel = { confirmed: 'Confirmed', suspected: 'Suspected', unknown: 'Unknown' }; var statusColor = { confirmed: 'var(--success, #22c55e)', suspected: 'var(--warning, #eab308)', unknown: 'var(--text-muted, #888)' }; @@ -1345,7 +1345,7 @@ '' + '' + '' + + (statusIcon[r.status] || '') + ' ' + (statusLabel[r.status] || 'Unknown') + '' + '' + '' + '' + @@ -1370,9 +1370,9 @@ '' + '
' + '' + - '' + - '' + - '' + + '' + + '' + + '' + '
' + '' + '
' + buildTableContent(rows, 'all') + '
' + @@ -1437,25 +1437,25 @@ async function renderCollisionTab(el, data, collisionData) { el.innerHTML = `

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.

-

⚠️ Inconsistent Hash Sizes

↑ top
+

Inconsistent Hash Sizes

↑ top

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.

Loading…
-

πŸ”’ Hash Usage Matrix

+

Hash Usage Matrix

↑ top
@@ -1470,7 +1470,7 @@
-

πŸ’₯ Collision Risk

↑ top
+

Collision Risk

↑ top
Loading…
`; @@ -1480,7 +1480,7 @@ const ihEl = document.getElementById('inconsistentHashList'); if (ihEl) { if (!inconsistent.length) { - ihEl.innerHTML = '
βœ… No inconsistencies detected β€” all nodes are reporting consistent hash sizes.
'; + ihEl.innerHTML = '
No inconsistencies detected β€” all nodes are reporting consistent hash sizes.
'; } else { ihEl.innerHTML = `
' + esc(r.name) + '' + esc(r.role || 'unknown') + '' + - (statusIcon[r.status] || '❓') + ' ' + (statusLabel[r.status] || 'Unknown') + '' + r.hashSize + '-byte' + r.packets + '' + (r.lastSeen ? timeAgo(r.lastSeen) : 'β€”') + '
@@ -1519,8 +1519,8 @@ const matrixTitle = document.getElementById('hashMatrixTitle'); const matrixDesc = document.getElementById('hashMatrixDesc'); const riskTitle = document.getElementById('collisionRiskTitle'); - if (matrixTitle) matrixTitle.textContent = bytes === 3 ? 'πŸ”’ Hash Usage Matrix' : `πŸ”’ ${bytes}-Byte Hash Usage Matrix`; - if (riskTitle) riskTitle.textContent = `πŸ’₯ ${bytes}-Byte Collision Risk`; + if (matrixTitle) matrixTitle.innerHTML = bytes === 3 ? ' Hash Usage Matrix' : ` ${bytes}-Byte Hash Usage Matrix`; + if (riskTitle) riskTitle.innerHTML = ` ${bytes}-Byte Collision Risk`; if (matrixDesc) { if (bytes === 1) matrixDesc.textContent = 'Cells include the first byte of all repeaters β€” including those using 2- or 3-byte prefixes β€” so this reflects real conflicts in the 1-byte hash space. Click a cell to see the nodes.'; else if (bytes === 2) matrixDesc.textContent = 'Each cell = first-byte group. Color shows worst 2-byte collision within. Click a cell to see the breakdown.'; @@ -1663,7 +1663,7 @@
0 ? 'onclick="document.getElementById(\'collisionRiskSection\')?.scrollIntoView({behavior:\'smooth\',block:\'start\'})"' : ''} ${collisionCount > 0 ? 'title="Click to see collision details"' : ''}>
Prefix collisions
-
${collisionCount}${collisionCount > 0 ? ' β–Ό' : ''}
+
${collisionCount}${collisionCount > 0 ? ' ' : ''}
`; } @@ -1855,7 +1855,7 @@ const groupNodes = info.group_nodes || []; let dhtml = `0x${hex}__ β€” ${groupNodes.length} node${groupNodes.length !== 1 ? 's' : ''} in group`; if ((info.collision_count || 0) === 0) { - dhtml += `
βœ… No 2-byte collisions in this group
`; + dhtml += `
No 2-byte collisions in this group
`; dhtml += `
${groupNodes.map(m => { const prefix = m.public_key.slice(0,4).toUpperCase(); return ``; @@ -1884,8 +1884,8 @@ if (!collisions.length) { const cleanMsg = bytes === 3 - ? 'βœ… No 3-byte prefix collisions detected β€” all repeaters have unique 3-byte prefixes.' - : `βœ… No ${bytes}-byte collisions detected`; + ? ' No 3-byte prefix collisions detected β€” all repeaters have unique 3-byte prefixes.' + : ` No ${bytes}-byte collisions detected`; el.innerHTML = `
${cleanMsg}
`; return; } @@ -1904,16 +1904,16 @@
${collisions.map(c => { let badge, tooltip; if (c.classification === 'local') { - badge = `🏘️ Local`; + badge = ` Local`; tooltip = 'Nodes close enough for direct RF β€” probably genuine prefix collision'; } else if (c.classification === 'regional') { - badge = `⚑ Regional`; + badge = ` Regional`; tooltip = 'At edge of 915MHz range β€” could indicate atmospheric ducting or hilltop-to-hilltop links'; } else if (c.classification === 'distant') { - badge = `🌐 Distant`; + badge = ` Distant`; tooltip = 'Beyond typical LoRa range β€” likely internet bridging, MQTT gateway, or separate mesh networks sharing prefix'; } else { - badge = '❓ Unknown'; + badge = ' Unknown'; tooltip = 'Not enough coordinate data to classify'; } const nodes = c.nodes || []; @@ -1933,9 +1933,9 @@ }).join('')}
NodeRoleCurrent HashSizes Seen
- 🏘️ Local <${t50}: true prefix collision, same mesh area   - ⚑ Regional ${t50}–${t200}: edge of LoRa range, possible atmospheric propagation   - 🌐 Distant >${t200}: beyond 915MHz range β€” internet bridge, MQTT gateway, or separate networks + Local <${t50}: true prefix collision, same mesh area   + Regional ${t50}–${t200}: edge of LoRa range, possible atmospheric propagation   + Distant >${t200}: beyond 915MHz range β€” internet bridge, MQTT gateway, or separate networks
`; } async function renderSubpaths(el) { @@ -1967,7 +1967,7 @@ const prefixDisplay = rawHops.join(' β†’ '); return ` ${i + 1} - ${routeDisplay}${hasSelfLoop ? ' πŸ”„' : ''}
${esc(prefixDisplay)} + ${routeDisplay}${hasSelfLoop ? ' ' : ''}
${esc(prefixDisplay)} ${s.count.toLocaleString()} ${s.pct}%
@@ -1979,7 +1979,7 @@ el.innerHTML = `
-

πŸ›€οΈ Route Pattern Analysis

+

Route Pattern Analysis

Click a route to see details. Most common subpaths β€” reveals backbone routes, bottlenecks, and preferred relay chains.

${nodesWithLoc.length >= 2 ? `
-
πŸ“ Hop Distances
+
Hop Distances
${(() => { const dists = []; let total = 0; @@ -2084,7 +2084,7 @@ ${hasMap ? '
' : ''}
-
πŸ“‘ Observer Receive Signal
+
Observer Receive Signal

Last hop β†’ observer only, not between nodes in the route

${data.signal.avgSnr != null ? `
Avg SNR: ${data.signal.avgSnr} dB Β· Avg RSSI: ${data.signal.avgRssi} dBm Β· ${data.signal.samples} samples
` @@ -2092,7 +2092,7 @@
-
πŸ• Activity by Hour (UTC)
+
Activity by Hour (UTC)
${data.hourDistribution.map((c, h) => `
`).join('')}
@@ -2107,13 +2107,13 @@ ${data.observers.length ? `
-
πŸ‘οΈ Observers
+
Observers
${data.observers.map(o => `
${esc(o.name)}: ${o.count}
`).join('')}
` : ''} ${data.parentPaths.length ? `
-
πŸ”— Full Paths Containing This Route
+
Full Paths Containing This Route
${data.parentPaths.map(p => `
${esc(p.path)} Γ—${p.count}
`).join('')}
@@ -2187,26 +2187,26 @@ 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 el.innerHTML = `
-

πŸ” Network Status

+

Network Status

${active}
-
🟒 Active
+
Active
${degraded}
-
🟑 Degraded
+
Degraded
${silent}
-
πŸ”΄ Silent
+
Silent
${totalNodes}
@@ -2214,7 +2214,7 @@
-

πŸ“Š Role Breakdown

+

Role Breakdown

${Object.entries(roleCounts).sort((a,b) => b[1]-a[1]).map(([role, count]) => { const c = ROLE_COLORS[role] || '#6b7280'; @@ -2240,7 +2240,7 @@ ` : ''} -

πŸ† Most Active Nodes

+

Most Active Nodes

@@ -2250,12 +2250,12 @@ - + `).join('')}
#NodeRoleTotal PacketsPackets TodayAnalytics
${n.role} ${n.health.stats.totalTransmissions || n.health.stats.totalPackets || 0} ${n.health.stats.packetsToday || 0}πŸ“Š
-

πŸ“Ά Best Signal Quality

+

Best Signal Quality

@@ -2265,12 +2265,12 @@ - + `).join('')}
#NodeRoleAvg SNRObserversAnalytics
${n.role} ${n.health.stats.avgSnr.toFixed(1)} dB ${n.health.observers?.length || 0}πŸ“Š
-

πŸ‘€ Most Observed Nodes

+

Most Observed Nodes

@@ -2280,7 +2280,7 @@ - + `).join('')}
#NodeRoleObserversAvg SNRAnalytics
${n.role} ${n.health.observers?.length || 0} ${n.health.stats.avgSnr != null ? n.health.stats.avgSnr.toFixed(1) + ' dB' : 'β€”'}πŸ“Š
@@ -2294,7 +2294,7 @@ ${n.role} ${timeAgo(n.health.stats.lastHeard)} ${n.health.stats.packetsToday || 0} - πŸ“Š + `).join('')} @@ -2340,7 +2340,7 @@ } // Top hops leaderboard - html += `

πŸ† Top 20 Longest Hops

`; + html += `

Top 20 Longest Hops

#FromToDistance (${distUnitLabel})TypeObsBest SNRMedian SNRPacket
`; const top20 = data.topHops.slice(0, 20); top20.forEach((h, i) => { const fromLink = h.fromPk ? `${esc(h.fromName)}` : esc(h.fromName || '?'); @@ -2349,7 +2349,7 @@ const medianSnr = h.medianSnr != null ? Number(h.medianSnr).toFixed(1) + ' dB' : 'β€”'; const obs = h.obsCount != null ? h.obsCount : 1; const pktLink = h.hash ? `${esc(h.hash.slice(0, 12))}…` : 'β€”'; - const mapBtn = h.fromPk && h.toPk ? `` : ''; + const mapBtn = h.fromPk && h.toPk ? `` : ''; const tsTitle = h.timestamp ? `Best observation: ${h.timestamp}` : ''; html += ``; }); @@ -2357,7 +2357,7 @@ // Top paths if (data.topPaths.length) { - html += `

πŸ›€οΈ Top 10 Longest Multi-Hop Paths

#FromToDistance (${distUnitLabel})TypeObsBest SNRMedian SNRPacket
${i+1}${fromLink}${toLink}${formatDistance(h.dist)}${esc(h.type)}${obs}${bestSnr}${medianSnr}${pktLink}${mapBtn}
`; + html += `

Top 10 Longest Multi-Hop Paths

#Total Distance (${distUnitLabel})HopsRoutePacket
`; data.topPaths.slice(0, 10).forEach((p, i) => { const route = p.hops.map(h => esc(h.fromName)).concat(esc(p.hops[p.hops.length-1].toName)).join(' β†’ '); const pktLink = p.hash ? `${esc(p.hash.slice(0, 12))}…` : 'β€”'; @@ -2365,7 +2365,7 @@ const pathPks = []; p.hops.forEach(h => { if (h.fromPk && !pathPks.includes(h.fromPk)) pathPks.push(h.fromPk); }); if (p.hops.length && p.hops[p.hops.length-1].toPk) { const last = p.hops[p.hops.length-1].toPk; if (!pathPks.includes(last)) pathPks.push(last); } - const mapBtn = pathPks.length >= 2 ? `` : ''; + const mapBtn = pathPks.length >= 2 ? `` : ''; html += ``; }); html += `
#Total Distance (${distUnitLabel})HopsRoutePacket
${i+1}${formatDistance(p.totalDist)}${p.hopCount}${route}${pktLink}${mapBtn}
`; @@ -2418,7 +2418,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData = async function renderNeighborGraphTab(el) { el.innerHTML = `
-

πŸ•ΈοΈ Neighbor Graph

+

Neighbor Graph

- πŸ“‹ Text-based neighbor list (accessible alternative) + Text-based neighbor list (accessible alternative)
`; @@ -2596,13 +2596,13 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData = var neighbors = (adj[n.pubkey] || []).map(function(nb) { var peer = nodeMap[nb.pk]; var name = peer ? (peer.name || nb.pk.slice(0, 8)) : nb.pk.slice(0, 8); - var conf = nb.ambiguous ? ' ⚠' : (nb.score >= 0.5 ? ' ●' : ' β—‹'); + var conf = nb.ambiguous ? ' ' + '' : (nb.score >= 0.5 ? ' ' + '' : ' ' + ''); return esc(name) + conf; }).join(', '); html += '' + esc(n.name || n.pubkey.slice(0, 12)) + '' + esc(n.role || 'unknown') + '' + (neighbors || 'none') + ''; }); html += ''; - html += '

● = high confidence (score β‰₯ 0.5), β—‹ = low confidence, ⚠ = ambiguous/unresolved

'; + html += '

' + '' + ' = high confidence (score β‰₯ 0.5), ' + '' + ' = low confidence, ' + '' + ' = ambiguous/unresolved

'; listEl.innerHTML = html; } @@ -3052,8 +3052,8 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData = // distinguish theoretical/would-collide-if-used from packet- // traffic-observed collisions shown on the Hash Issues tab. const opLine = opC === 0 - ? `βœ… No address conflicts among configured repeaters` - : `⚠️ ${opC} address conflict${opC !== 1 ? 's' : ''} among configured repeaters (would-collide-if-used)`; + ? ` No address conflicts among configured repeaters` + : ` ${opC} address conflict${opC !== 1 ? 's' : ''} among configured repeaters (would-collide-if-used)`; // #1306: expandable WHICH-collides toggles (op + theoretical) const opEntries = collEntries.operational[b]; const theoEntries = collEntries.theoretical[b]; @@ -3130,7 +3130,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =

Generate Available Prefix

Find a prefix with zero current collisions.

- + 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). @@ -3150,7 +3150,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =

- πŸ“– New to multi-byte prefixes? + New to multi-byte prefixes? Read the MeshCore FAQ on multi-byte support β†’ @@ -3168,9 +3168,9 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData = } function severityBadge(count) { - if (count === 0) return 'βœ… Unique'; - if (count <= 2) return `⚠️ ${count} collision${count !== 1 ? 's' : ''}`; - return `πŸ”΄ ${count} collisions`; + if (count === 0) return ' Unique'; + if (count <= 2) return ` ${count} collision${count !== 1 ? 's' : ''}`; + return ` ${count} collisions`; } // --- Checker --- @@ -3202,7 +3202,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData = if (typeof PrefixReserved !== 'undefined' && PrefixReserved && PrefixReserved.isReservedPrefix(input)) { html += `
- ⚠️ Firmware avoids this first byte + Firmware avoids this first byte
${input.slice(0,2)} as the first byte of a node pubkey is avoided by the MeshCore firmware keygen convention (the standard repeater re-rolls identities whose pub_key[0] is 00 or FF). You generally shouldn't see this on real nodes.
@@ -3298,7 +3298,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
${prefix} - βœ… No existing nodes use this prefix + No existing nodes use this prefix
${available.toLocaleString()} of ${totalSpace.toLocaleString()} ${b}-byte prefixes are available.
@@ -3627,7 +3627,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData = container.innerHTML = `

${esc(name)}

- +
diff --git a/public/app.js b/public/app.js index bf6836be..324f842a 100644 --- a/public/app.js +++ b/public/app.js @@ -424,9 +424,13 @@ function toggleFavorite(pubkey) { localStorage.setItem(FAV_KEY, JSON.stringify(favs)); return idx < 0; // true if now favorited } +function favStarIconHtml(on) { + var id = on ? 'ph-star-fill' : 'ph-star'; + return ''; +} function favStar(pubkey, cls) { const on = isFavorite(pubkey); - return ''; + return ''; } function bindFavStars(container, onToggle) { container.querySelectorAll('.fav-star').forEach(btn => { @@ -434,8 +438,9 @@ function bindFavStars(container, onToggle) { e.stopPropagation(); const pk = btn.dataset.fav; const nowOn = toggleFavorite(pk); - btn.textContent = nowOn ? 'β˜…' : 'β˜†'; + btn.innerHTML = favStarIconHtml(nowOn); btn.classList.toggle('on', nowOn); + btn.setAttribute('aria-pressed', nowOn ? 'true' : 'false'); btn.title = nowOn ? 'Remove from favorites' : 'Add to favorites'; if (onToggle) onToggle(pk, nowOn); }); diff --git a/public/audio-lab.js b/public/audio-lab.js index 6857de1f..cee3c48d 100644 --- a/public/audio-lab.js +++ b/public/audio-lab.js @@ -200,7 +200,7 @@ document.getElementById('alabDetail').innerHTML = `
-

πŸ“¦ Packet Data

+

Packet Data

Type
${m.typeName}
Total Size
${m.totalSize} bytes
@@ -215,7 +215,7 @@
-

🎡 Sound Mapping

+

Sound Mapping

@@ -261,7 +261,7 @@
-

🎹 Note Sequence

+

Note Sequence

Instrument
${m.notes.map((n, i) => { @@ -283,7 +283,7 @@
-

πŸ“Š Byte Visualizer

+

Byte Visualizer

`; @@ -437,7 +437,7 @@
- + Speed: diff --git a/public/filter-ux.js b/public/filter-ux.js index 12816d14..5bb71619 100644 --- a/public/filter-ux.js +++ b/public/filter-ux.js @@ -351,7 +351,9 @@ // ── Saved filters dropdown ───────────────────────────────────────────── function _renderSavedDropdown(container, input) { - var btn = _h('button', { type: 'button', class: 'fux-saved-trigger', id: 'filterSavedTrigger', title: 'Saved filters' }, 'β˜… Saved β–Ύ'); + var savedIconHtml = ''; + var caretIconHtml = ''; + var btn = _h('button', { type: 'button', class: 'fux-saved-trigger', id: 'filterSavedTrigger', title: 'Saved filters' }, savedIconHtml + ' Saved ' + caretIconHtml); var menu = _h('div', { class: 'fux-saved-menu hidden', id: 'filterSavedMenu', role: 'menu' }); container.appendChild(btn); container.appendChild(menu); diff --git a/public/icons/phosphor-sprite.svg b/public/icons/phosphor-sprite.svg index 67c5c3da..c1d31db6 100644 --- a/public/icons/phosphor-sprite.svg +++ b/public/icons/phosphor-sprite.svg @@ -1,36 +1,84 @@ \ No newline at end of file + + diff --git a/public/live.js b/public/live.js index 61b0ca4f..ea43b8c6 100644 --- a/public/live.js +++ b/public/live.js @@ -73,7 +73,7 @@ function setObserverIataMap(m) { observerIataMap = m || {}; } // #1189 R2 mesh-operator fix: live feed must show the observer's IATA pill - // alongside the existing πŸ‘ N badge so operators on /live can tell SAME- + // alongside the existing observation-count badge so operators on /live can tell SAME- // region from CROSS-region reception at a glance (same affordance as the // /packets table). Mirrors `obsIataBadge` in public/packets.js β€” kept as a // local helper for now (live.js and packets.js are separate IIFEs with no @@ -153,8 +153,8 @@ }; const PAYLOAD_ICONS = { - ADVERT: 'πŸ“‘', GRP_TXT: 'πŸ’¬', TXT_MSG: 'βœ‰οΈ', ACK: 'βœ“', - REQUEST: '❓', RESPONSE: 'πŸ“¨', TRACE: 'πŸ”', PATH: 'πŸ›€οΈ' + ADVERT: '', GRP_TXT: '', TXT_MSG: '', ACK: '', + REQUEST: '', RESPONSE: '', TRACE: '', PATH: '' }; /* ---- Panel Corner Positioning (#608 M0) ---- */ @@ -264,7 +264,7 @@ const iso = d && isFinite(d.getTime()) ? d.toISOString() : null; const f = formatTimestampWithTooltip(iso, getTimestampMode()); const warn = f.isFuture - ? ' ⚠️' + ? ' ' : ''; return `${escapeHtml(f.text)}${warn}`; } @@ -1083,7 +1083,7 @@
+ aria-label="Show live stats">
- +
@@ -1493,13 +1493,13 @@
-
- +
- + @@ -2175,7 +2175,7 @@ - + `; if (isExpanded && p._children) { @@ -2740,31 +2740,31 @@ if (decoded.type === 'CHAN' && decoded.text) { const ch = decoded.channel ? `${escapeHtml(decoded.channel)} ` : ''; const t = decoded.text.length > 80 ? decoded.text.slice(0, 80) + '…' : decoded.text; - return `${ch}πŸ’¬ ${escapeHtml(t)}`; + return `${ch} ${escapeHtml(t)}`; } // Advertisements β€” show node name and role if (decoded.type === 'ADVERT' && decoded.name) { - const role = decoded.flags?.repeater ? 'πŸ“‘' : decoded.flags?.room ? '🏠' : decoded.flags?.sensor ? '🌑' : 'πŸ“»'; + const role = decoded.flags?.repeater ? '' : decoded.flags?.room ? '' : decoded.flags?.sensor ? '' : ''; return `${role} ${escapeHtml(decoded.name)}`; } // Undecrypted channel messages β€” show channel hash and decryption status if (decoded.type === 'GRP_TXT' && decoded.channelHash != null) { const hashHex = decoded.channelHashHex || decoded.channelHash.toString(16).padStart(2, '0').toUpperCase(); const statusLabel = decoded.decryptionStatus === 'no_key' ? 'no key' : 'decryption failed'; - return `πŸ”’ Ch 0x${hashHex} (${statusLabel})`; + return ` Ch 0x${hashHex} (${statusLabel})`; } // Direct messages - if (decoded.type === 'TXT_MSG') return `βœ‰οΈ ${decoded.srcHash?.slice(0,8) || '?'} β†’ ${decoded.destHash?.slice(0,8) || '?'}`; + if (decoded.type === 'TXT_MSG') return ` ${decoded.srcHash?.slice(0,8) || '?'} β†’ ${decoded.destHash?.slice(0,8) || '?'}`; // Path updates - if (decoded.type === 'PATH') return `πŸ”€ ${decoded.srcHash?.slice(0,8) || '?'} β†’ ${decoded.destHash?.slice(0,8) || '?'}`; + if (decoded.type === 'PATH') return ` ${decoded.srcHash?.slice(0,8) || '?'} β†’ ${decoded.destHash?.slice(0,8) || '?'}`; // Requests/responses (encrypted) - if (decoded.type === 'REQ' || decoded.type === 'RESPONSE') return `πŸ”’ ${decoded.srcHash?.slice(0,8) || '?'} β†’ ${decoded.destHash?.slice(0,8) || '?'}`; + if (decoded.type === 'REQ' || decoded.type === 'RESPONSE') return ` ${decoded.srcHash?.slice(0,8) || '?'} β†’ ${decoded.destHash?.slice(0,8) || '?'}`; // Anonymous requests - if (decoded.type === 'ANON_REQ') return `πŸ”’ anon β†’ ${decoded.destHash?.slice(0,8) || '?'}`; + if (decoded.type === 'ANON_REQ') return ` anon β†’ ${decoded.destHash?.slice(0,8) || '?'}`; // Companion bridge text if (decoded.text) return escapeHtml(decoded.text.length > 80 ? decoded.text.slice(0, 80) + '…' : decoded.text); // Bare adverts with just pubkey - if (decoded.public_key) return `πŸ“‘ ${decoded.public_key.slice(0, 16)}…`; + if (decoded.public_key) return ` ${decoded.public_key.slice(0, 16)}…`; return ''; } @@ -2823,7 +2823,7 @@ sheet = document.createElement('div'); sheet.id = 'mobileDetailSheet'; sheet.className = 'mobile-detail-sheet'; - sheet.innerHTML = '
'; + sheet.innerHTML = '
'; document.body.appendChild(sheet); sheet.querySelector('#mobileSheetClose').addEventListener('click', () => { sheet.classList.remove('open'); @@ -2975,7 +2975,7 @@ const hashHex = decoded.channelHashHex || decoded.channelHash.toString(16).padStart(2, '0').toUpperCase(); const statusLabel = decoded.decryptionStatus === 'no_key' ? 'no key' : 'decryption failed'; messageHtml = `
-
πŸ”’ Channel Hash: 0x${hashHex} (${statusLabel})
+
Channel Hash: 0x${hashHex} (${statusLabel})
`; } @@ -3012,7 +3012,7 @@ const nodeName = decoded.name || ''; locationHtml = `${decoded.lat.toFixed(5)}, ${decoded.lon.toFixed(5)}`; if (nodeName) locationHtml = `${escapeHtml(nodeName)} β€” ${locationHtml}`; - if (locationNodeKey) locationHtml += ` πŸ“map`; + if (locationNodeKey) locationHtml += ` map`; } else { // Try to resolve sender node location from nodes list const senderKey = decoded.pubKey || decoded.srcPubKey; @@ -3024,7 +3024,7 @@ locationNodeKey = nodeData.node.public_key; locationHtml = `${nodeData.node.lat.toFixed(5)}, ${nodeData.node.lon.toFixed(5)}`; if (nodeData.node.name) locationHtml = `${escapeHtml(nodeData.node.name)} β€” ${locationHtml}`; - locationHtml += ` πŸ“map`; + locationHtml += ` map`; } else if (senderName && !senderKey) { // Search by name const searchData = await api(`/nodes/search?q=${encodeURIComponent(senderName)}`, { ttl: 30000 }).catch(() => null); @@ -3033,7 +3033,7 @@ locationNodeKey = match.public_key; locationHtml = `${match.lat.toFixed(5)}, ${match.lon.toFixed(5)}`; locationHtml = `${escapeHtml(match.name)} β€” ${locationHtml}`; - locationHtml += ` πŸ“map`; + locationHtml += ` map`; } } } catch {} @@ -3041,7 +3041,7 @@ } const anomalyBanner = decoded.anomaly - ? `
⚠️ Anomaly: ${escapeHtml(decoded.anomaly)}
` + ? `
Anomaly: ${escapeHtml(decoded.anomaly)}
` : ''; // Hop count display: use pathHops length (= effective observation's path_json). @@ -3109,9 +3109,9 @@ ${effectivePkt.direction ? `
Direction
${escapeHtml(effectivePkt.direction)}
` : ''}
- - ${pathHops.length ? `` : ''} - ${pkt.hash ? `πŸ” Trace` : ''} + + ${pathHops.length ? `` : ''} + ${pkt.hash ? ` Trace` : ''}
@@ -3180,8 +3180,8 @@ const obsParam = selectedObservationId ? `?obs=${selectedObservationId}` : ''; const url = pktHash ? `${location.origin}/#/packets/${pktHash}${obsParam}` : `${location.origin}/#/packets/${copyLinkBtn.dataset.packetId}${obsParam}`; window.copyToClipboard(url, () => { - copyLinkBtn.textContent = 'βœ… Copied!'; - setTimeout(() => { copyLinkBtn.textContent = 'πŸ”— Copy Link'; }, 1500); + copyLinkBtn.innerHTML = ' Copied!'; + setTimeout(() => { copyLinkBtn.innerHTML = ' Copy Link'; }, 1500); }); }); } @@ -3405,7 +3405,7 @@ } if (decoded.anomaly) { - rows += ``; + rows += ``; } return `
#Payload IndexByte→ MIDI→ FreqDuration (why)Gap (why)
${isSingle ? '' : (isExpanded ? 'β–Ό' : 'β–Ά')}${isSingle ? '' : (isExpanded ? '' : '')} ${groupRegion ? `${groupRegion}` : 'β€”'} ${renderTimestampCell(p.latest)} ${truncate(p.hash || 'β€”', 8)}${p.payload_type != null ? `${groupTypeName}${transportBadge(p.route_type)}` : 'β€”'} ${isSingle ? escapeHtml(truncate(obsNameOnly(headerObserverId), 16)) + obsIataBadge(p) : escapeHtml(truncate(obsNameOnly(headerObserverId), 10)) + groupedObserverIataBadgesHtml(p)} ${groupPathStr}${p.observation_count > 1 ? 'πŸ‘ ' + p.observation_count + '' : (isSingle ? '' : p.count)}${p.observation_count > 1 ? ' ' + p.observation_count + '' : (isSingle ? '' : p.count)} ${getDetailPreview(getParsedDecoded(p))}
⚠️ Anomaly${escapeHtml(decoded.anomaly)}
Anomaly${escapeHtml(decoded.anomaly)}
@@ -3428,7 +3428,7 @@ const overlay = document.createElement('div'); overlay.className = 'modal-overlay byop-overlay'; overlay.innerHTML = '`; @@ -230,9 +230,9 @@ function renderVersionCard(health) { // SQLite perf (separate from existing SQLite block β€” focused on WAL + cache hit) (#1120) if (sqliteStats) { const walMB = sqliteStats.walSizeMB || 0; - const walFlag = walMB > 100 ? ' ⚠️' : ''; + const walFlag = walMB > 100 ? ' ' : ''; const hitRate = (sqliteStats.cacheHitRate || 0) * 100; - const hitFlag = hitRate > 0 && hitRate < 90 ? ' ⚠️' : ''; + const hitFlag = hitRate > 0 && hitRate < 90 ? ' ' : ''; html += `

SQLite (WAL + Cache Hit)

${walMB.toFixed(1)}MB${walFlag}
WAL Size
${(sqliteStats.pageCount || 0).toLocaleString()}
Page Count
diff --git a/public/roles.js b/public/roles.js index 6f48fbbd..2a425e57 100644 --- a/public/roles.js +++ b/public/roles.js @@ -771,7 +771,7 @@ if (!severity) return ''; var cls = 'skew-badge skew-badge--' + severity; if (severity === 'no_clock') { - return '🚫 No Clock'; + return ' No Clock'; } if (severity === 'bimodal_clock' && cs) { var badPct = cs.goodFraction != null ? Math.round((1 - cs.goodFraction) * 100) : '?'; diff --git a/public/style.css b/public/style.css index e1f3e34c..2e698157 100644 --- a/public/style.css +++ b/public/style.css @@ -2277,13 +2277,16 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; } *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } -/* Favorite stars */ +/* Favorite stars β€” Phosphor sprite, 44Γ—44 hit target (WCAG 2.5.5) */ .fav-star { - background: none; border: none; cursor: pointer; font-size: 1.1rem; - color: var(--text-muted); padding: 2px 4px; line-height: 1; + background: none; border: none; cursor: pointer; + color: var(--text-muted); padding: 0; line-height: 1; transition: color .15s, transform .15s; + min-width: 44px; min-height: 44px; + display: inline-flex; align-items: center; justify-content: center; } -.fav-star:hover { transform: scale(1.2); } +.fav-star .ph-icon { width: 22px; height: 22px; } +.fav-star:hover { transform: scale(1.1); } .fav-star.on { color: var(--status-yellow); } /* BYOP Decode Modal */ diff --git a/public/table-sort.js b/public/table-sort.js index 5a2aed26..01b6eacd 100644 --- a/public/table-sort.js +++ b/public/table-sort.js @@ -157,7 +157,8 @@ window.TableSort = (function() { th.setAttribute('aria-sort', state.direction === 'asc' ? 'ascending' : 'descending'); var span = document.createElement('span'); span.className = 'sort-arrow'; - span.textContent = state.direction === 'asc' ? ' β–²' : ' β–Ό'; + // #1648 M2 β€” Phosphor caret-up/caret-down via sprite (no emoji). + span.innerHTML = ' '; th.appendChild(span); } else { th.classList.remove('sort-active'); diff --git a/public/traces.js b/public/traces.js index e0221d43..e45c2837 100644 --- a/public/traces.js +++ b/public/traces.js @@ -18,7 +18,7 @@ app.innerHTML = `
${k}${v.toLocaleString()}${rateStr}${baseStr}${flag}