From f04f1b8e7710a7b6318e874aba36f6ddebc3cb86 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Date: Sat, 28 Mar 2026 02:42:01 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20accessibility=20=E2=80=94=20chart=20labe?= =?UTF-8?q?ls,=20table=20scope,=20form=20labels=20(#210,=20#211,=20#212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #210: Add role="img" aria-label to 9 Chart.js canvases in node-analytics.js and observer-detail.js with descriptive labels. #211: Add scope="col" to all elements across analytics.js, audio-lab.js, compare.js, node-analytics.js, nodes.js, observer-detail.js, observers.js, and packets.js (40+ headers). #212: Add aria-label to packet filter input and time window select in packets.js. Add for/id associations to all customize.js inputs: branding, theme colors, node/type colors, heatmap sliders, onboarding fields, and export controls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- public/analytics.js | 43 +++++++++++++++++++------------ public/audio-lab.js | 2 +- public/compare.js | 17 +++++++++--- public/customize.js | 48 +++++++++++++++++----------------- public/index.html | 54 +++++++++++++++++++-------------------- public/node-analytics.js | 16 ++++++------ public/nodes.js | 12 ++++----- public/observer-detail.js | 21 ++++++++++----- public/observers.js | 16 +++++++++--- public/packets.js | 9 ++++--- 10 files changed, 138 insertions(+), 100 deletions(-) diff --git a/public/analytics.js b/public/analytics.js index fb4310e3..3af4b85a 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -177,6 +177,15 @@ tbl.id = tbl.id || `analytics-tbl-${tab}-${i}`; if (typeof makeColumnsResizable === 'function') makeColumnsResizable('#' + tbl.id, `meshcore-analytics-${tab}-${i}-col-widths`); }); + // #206 β€” Wrap analytics tables in scroll containers on mobile + el.querySelectorAll('.analytics-table').forEach(tbl => { + if (!tbl.parentElement.classList.contains('analytics-table-scroll')) { + const wrapper = document.createElement('div'); + wrapper.className = 'analytics-table-scroll'; + tbl.parentElement.insertBefore(wrapper, tbl); + wrapper.appendChild(tbl); + } + }); }); // Deep-link scroll to section within tab const sectionId = new URLSearchParams((location.hash.split('?')[1] || '')).get('section'); @@ -395,7 +404,7 @@ function renderSNRByType(snrByType) { if (!snrByType.length) return '
No data
'; - let html = ''; + let html = '
TypePacketsAvg SNRMinMaxDistribution
'; snrByType.forEach(t => { const barPct = Math.max(((t.avg - (-12)) / 27) * 100, 2); const color = t.avg > 6 ? statusGreen() : t.avg > 0 ? statusYellow() : statusRed(); @@ -535,7 +544,7 @@ function renderPairTable(pairs) { if (!pairs.length) return '
Not enough multi-hop data
'; - let html = '
TypePacketsAvg SNRMinMaxDistribution
'; + let html = '
Node ANode BCo-appearances
'; pairs.slice(0, 12).forEach(p => { html += ` @@ -598,7 +607,7 @@ function renderCrossObserver(nodes) { if (!nodes.length) return '
No nodes seen by multiple observers
'; let html = `
Node ANode BCo-appearances
${p.nameA ? `${esc(p.nameA)}` : `${p.hopA}`}
- `; + `; nodes.forEach(n => { const name = n.name ? `${esc(n.name)}` @@ -719,7 +728,7 @@ var ths = ''; for (var i = 0; i < cols.length; i++) { var c = cols[i]; - ths += ''; } return '' + ths + ''; @@ -880,7 +889,7 @@

Nodes advertising with 2+ byte hash paths

${data.multiByteNodes.length ? `
NodeObserversHop Distances
NodeObserversHop Distances
' + + ths += '' + c.label + channelSortArrow(c.key, activeCol, dir) + '
- + ${data.multiByteNodes.map(n => ` @@ -896,7 +905,7 @@

Top Path Hops

NodeHash SizeAdvertsLast Seen
NodeHash SizeAdvertsLast Seen
${esc(n.name)}
- + ${data.topHops.map(h => { const link = h.pubkey ? `#/nodes/${encodeURIComponent(h.pubkey)}` : `#/packets?search=${h.hex}`; @@ -952,7 +961,7 @@ ihEl.innerHTML = '
βœ… No inconsistencies detected β€” all nodes are reporting consistent hash sizes.
'; } else { ihEl.innerHTML = `
HopNodeBytesAppearances
HopNodeBytesAppearances
- + ${inconsistent.map((n, i) => { const roleColor = window.ROLE_COLORS?.[n.role] || '#6b7280'; const prefix = n.hash_size ? n.public_key.slice(0, n.hash_size * 2).toUpperCase() : '?'; @@ -1123,7 +1132,7 @@ collisions.sort((a, b) => classOrder[a.classification] - classOrder[b.classification] || b.count - a.count); el.innerHTML = `
NodeRoleCurrent HashSizes Seen
NodeRoleCurrent HashSizes Seen
- + ${collisions.map(c => { let badge, tooltip; if (c.classification === 'local') { @@ -1179,7 +1188,7 @@ return `

${title}

From ${data.totalPaths.toLocaleString()} paths with 2+ hops

HopAppearancesMax DistanceAssessmentColliding Nodes
HopAppearancesMax DistanceAssessmentColliding Nodes
- + ${data.subpaths.map((s, i) => { const barW = Math.max(2, Math.round(s.count / maxCount * 100)); @@ -1434,7 +1443,7 @@ ${myKeys.size ? `

⭐ My Claimed Nodes

#RouteOccurrences% of pathsFrequency#RouteOccurrences% of pathsFrequency
- + ${enriched.filter(n => myKeys.has(n.public_key)).map(n => { const s = n.health.stats; @@ -1452,7 +1461,7 @@

πŸ† Most Active Nodes

NodeRolePacketsAvg SNRObserversLast Heard
NodeRolePacketsAvg SNRObserversLast Heard
- + ${byPackets.slice(0, 15).map((n, i) => ` @@ -1467,7 +1476,7 @@

πŸ“Ά Best Signal Quality

#NodeRoleTotal PacketsPackets TodayAnalytics
#NodeRoleTotal PacketsPackets TodayAnalytics
${i + 1}
- + ${bySnr.slice(0, 15).map((n, i) => ` @@ -1482,7 +1491,7 @@

πŸ‘€ Most Observed Nodes

#NodeRoleAvg SNRObserversAnalytics
#NodeRoleAvg SNRObserversAnalytics
${i + 1}
- + ${byObservers.slice(0, 15).map((n, i) => ` @@ -1497,7 +1506,7 @@

⏰ Recently Active

#NodeRoleObserversAvg SNRAnalytics
#NodeRoleObserversAvg SNRAnalytics
${i + 1}
- + ${byRecent.slice(0, 15).map(n => ` @@ -1529,7 +1538,7 @@ // Category stats const cats = data.catStats; - html += `

Distance by Link Type

NodeRoleLast HeardPackets TodayAnalytics
NodeRoleLast HeardPackets TodayAnalytics
${nodeLink(n)}${claimedBadge(n)}
`; + html += `

Distance by Link Type

TypeCountAvg (km)Median (km)Min (km)Max (km)
`; for (const [cat, st] of Object.entries(cats)) { if (!st.count) continue; html += ``; @@ -1549,7 +1558,7 @@ } // Top hops leaderboard - html += `

πŸ† Top 20 Longest Hops

TypeCountAvg (km)Median (km)Min (km)Max (km)
${esc(cat)}${st.count.toLocaleString()}${st.avg}${st.median}${st.min}${st.max}
`; + html += `

πŸ† Top 20 Longest Hops

#FromToDistance (km)TypeSNRPacket
`; const top20 = data.topHops.slice(0, 20); top20.forEach((h, i) => { const fromLink = h.fromPk ? `${esc(h.fromName)}` : esc(h.fromName || '?'); @@ -1563,7 +1572,7 @@ // Top paths if (data.topPaths.length) { - html += `

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

#FromToDistance (km)TypeSNRPacket
`; + html += `

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

#Total Distance (km)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))}…` : 'β€”'; diff --git a/public/audio-lab.js b/public/audio-lab.js index 781b9f29..6de036dc 100644 --- a/public/audio-lab.js +++ b/public/audio-lab.js @@ -263,7 +263,7 @@

🎹 Note Sequence

#Total Distance (km)HopsRoutePacket
- + ${m.notes.map((n, i) => { const durWhy = `byte ${n.byte} β†’ map(0...255 β†’ 50...400ms) Γ— tempo`; const gapWhy = i < m.notes.length - 1 diff --git a/public/compare.js b/public/compare.js index 5b086a1a..2471c3c9 100644 --- a/public/compare.js +++ b/public/compare.js @@ -57,6 +57,15 @@ if (typeof window !== 'undefined') window.comparePacketSets = comparePacketSets; '
' + ''; + // #209 β€” Keyboard accessibility for compare table rows + app.addEventListener('keydown', function (e) { + var row = e.target.closest('tr[data-action="navigate"]'); + if (!row) return; + if (e.key !== 'Enter' && e.key !== ' ') return; + e.preventDefault(); + location.hash = row.dataset.value; + }); + loadObservers(); } @@ -316,9 +325,9 @@ if (typeof window !== 'undefined') window.comparePacketSets = comparePacketSets; el.innerHTML = (hashes.length > displayLimit ? '
Showing first ' + displayLimit + ' of ' + hashes.length.toLocaleString() + ' packets.
' : '') + - '
#Payload IndexByte→ MIDI→ FreqDuration (why)Gap (why)
#Payload IndexByte→ MIDI→ FreqDuration (why)Gap (why)
' + + '
' + '' + - '' + + '' + '' + '' + displayed.map(function (h) { var p = mapA.get(h) || mapB.get(h); @@ -332,7 +341,7 @@ if (typeof window !== 'undefined') window.comparePacketSets = comparePacketSets; } else { obsLabel = nameB; } - return '' + + return '' + '' + '' + '' + @@ -340,7 +349,7 @@ if (typeof window !== 'undefined') window.comparePacketSets = comparePacketSets; ''; }).join('') + '' + - '
HashTimeTypeObserverHashTimeTypeObserver
' + escapeHtml(h.substring(0, 12)) + '' + timeAgo(p.timestamp || p.first_seen) + '' + escapeHtml(typeName) + '
'; + ''; } registerPage('compare', { init: init, destroy: destroy }); diff --git a/public/customize.js b/public/customize.js index da637add..7b0eb17f 100644 --- a/public/customize.js +++ b/public/customize.js @@ -642,21 +642,21 @@ var b = state.branding; var logoPreview = b.logoUrl ? 'Logo preview' : ''; return '
' + - '
' + - '
' + - '
' + logoPreview + '
' + - '
' + + '
' + + '
' + + '
' + logoPreview + '
' + + '
' + '
'; } function renderColorRow(key, val, def, dataAttr) { var isFont = key === 'font' || key === 'mono'; var inputHtml = isFont - ? '' - : '' + + ? '' + : '' + '' + val + ''; return '
' + - '
' + + '
' + '
' + (THEME_HINTS[key] || '') + '
' + inputHtml + (val !== def ? '' : '') + @@ -708,9 +708,9 @@ var val = state.nodeColors[key]; var def = DEFAULTS.nodeColors[key]; rows += '
' + - '
' + + '
' + '
' + (NODE_HINTS[key] || '') + '
' + - '' + + '' + '' + '' + val + '' + (val !== def ? '' : '') + @@ -721,9 +721,9 @@ var tval = state.typeColors[tkey]; var tdef = DEFAULTS.typeColors[tkey]; typeRows += '
' + - '
' + + '
' + '
' + (TYPE_HINTS[tkey] || '') + '
' + - '' + + '' + '' + '' + tval + '' + (tval !== tdef ? '' : '') + @@ -742,13 +742,13 @@ '
' + '

Heatmap Opacity

' + '
' + - '
' + + '
' + '
Heatmap overlay on the Nodes β†’ Map page (0–100%)
' + '' + '' + heatPct + '%' + '
' + '
' + - '
' + + '
' + '
Heatmap overlay on the Live page (0–100%)
' + '' + '' + liveHeatPct + '%' + @@ -761,13 +761,13 @@ var stepsHtml = h.steps.map(function (s, i) { return '
' + '
' + - '' + - '' + + '' + + '' + '' + '' + '' + '
' + - '' + + '' + '
Markdown: **bold** *italic* `code` [text](url) - list
' + '
'; }).join(''); @@ -775,10 +775,10 @@ var checkHtml = h.checklist.map(function (c, i) { return '
' + '
' + - '' + + '' + '' + '
' + - '' + + '' + '
Markdown: **bold** *italic* `code` [text](url) - list
' + '
'; }).join(''); @@ -786,16 +786,16 @@ var linksHtml = h.footerLinks.map(function (l, i) { return '
' + '
' + - '' + + '' + '' + '
' + - '' + + '' + '
'; }).join(''); return '
' + - '
' + - '
' + + '
' + + '
' + '

Steps

' + stepsHtml + '' + '

FAQ / Checklist

' + checkHtml + @@ -870,11 +870,11 @@ '
' + '' + '' + - '' + + '' + '' + '
' + '
Raw JSON' + - '' + + '' + '
' + '
'; } diff --git a/public/index.html b/public/index.html index 532b59e1..2eb6269b 100644 --- a/public/index.html +++ b/public/index.html @@ -22,9 +22,9 @@ - - - + + + @@ -81,29 +81,29 @@
- - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/node-analytics.js b/public/node-analytics.js index db87bc22..9537661f 100644 --- a/public/node-analytics.js +++ b/public/node-analytics.js @@ -102,27 +102,27 @@

Activity Timeline

Packet count per time bucket β€” shows when this node is most active
- +

SNR Trend

Signal-to-noise ratio over time β€” higher is better reception
- +

Packet Types

Breakdown of advert, position, text, and other packet types
- +

Observer Coverage

Which stations hear this node and how often
- +

Hop Distribution

How many repeater hops packets take β€” 0 means direct
- +

Uptime Heatmap

@@ -132,14 +132,14 @@ ${data.peerInteractions.length ? `

Peer Interactions

Nodes this device has exchanged messages with
- - +
PeerMessagesLast Contact
+ ${data.peerInteractions.map(p => ``).join('')} -
PeerMessagesLast Contact
${escapeHtml(p.peer_name)} ${p.messageCount} ${timeAgo(p.lastContact)}
+
` : ''}
`; diff --git a/public/nodes.js b/public/nodes.js index 92afb104..94d58d8c 100644 --- a/public/nodes.js +++ b/public/nodes.js @@ -287,7 +287,7 @@ ${(() => { const regions = [...new Set(observers.map(o => o.iata).filter(Boolean))]; return regions.length ? `
Regions: ${regions.map(r => '' + escapeHtml(r) + '').join(' ')}
` : ''; })()}

Heard By (${observers.length} observer${observers.length > 1 ? 's' : ''})

- + ${observers.map(o => ` @@ -577,11 +577,11 @@
ObserverRegionPacketsAvg SNRAvg RSSI
ObserverRegionPacketsAvg SNRAvg RSSI
${escapeHtml(o.observer_name || o.observer_id)}
- - - - - + + + + +
Name${sortArrow('name')}Public Key${sortArrow('public_key')}Role${sortArrow('role')}Last Seen${sortArrow('last_seen')}Adverts${sortArrow('advert_count')}Name${sortArrow('name')}Public Key${sortArrow('public_key')}Role${sortArrow('role')}Last Seen${sortArrow('last_seen')}Adverts${sortArrow('advert_count')}
`; diff --git a/public/observer-detail.js b/public/observer-detail.js index 9fc9fa3f..e9f57873 100644 --- a/public/observer-detail.js +++ b/public/observer-detail.js @@ -157,19 +157,19 @@

Packets Over Time

- +

Packet Types

-
+

Unique Nodes Heard

- +

SNR Distribution

- +
@@ -299,12 +299,12 @@ const el = document.getElementById('obsRecentPackets'); if (!el || !packets.length) { if (el) el.innerHTML = '
No recent packets.
'; return; } el.innerHTML = ` - + ${packets.map(p => { const decoded = typeof p.decoded_json === 'string' ? JSON.parse(p.decoded_json) : (p.decoded_json || {}); const hops = typeof p.path_json === 'string' ? JSON.parse(p.path_json) : (p.path_json || []); const typeName = PAYLOAD_LABELS[p.payload_type] || 'Type ' + p.payload_type; - return ` + return ` @@ -314,6 +314,15 @@ `; }).join('')}
TimeTypeHashSNRRSSIHops
TimeTypeHashSNRRSSIHops
${timeAgo(p.timestamp)} ${typeName} ${(p.hash || '').substring(0, 10)}
`; + + // #209 β€” Keyboard accessibility for recent packet rows + el.addEventListener('keydown', function (e) { + var row = e.target.closest('tr[data-action="navigate"]'); + if (!row) return; + if (e.key !== 'Enter' && e.key !== ' ') return; + e.preventDefault(); + location.hash = row.dataset.value; + }); } registerPage('observer-detail', { init, destroy }); diff --git a/public/observers.js b/public/observers.js index de65e972..943f1456 100644 --- a/public/observers.js +++ b/public/observers.js @@ -25,6 +25,16 @@ app.addEventListener('click', function (e) { var btn = e.target.closest('[data-action]'); if (btn && btn.dataset.action === 'obs-refresh') loadObservers(); + var row = e.target.closest('tr[data-action="navigate"]'); + if (row) location.hash = row.dataset.value; + }); + // #209 β€” Keyboard accessibility for observer rows + app.addEventListener('keydown', function (e) { + var row = e.target.closest('tr[data-action="navigate"]'); + if (!row) return; + if (e.key !== 'Enter' && e.key !== ' ') return; + e.preventDefault(); + location.hash = row.dataset.value; }); // Auto-refresh every 30s refreshTimer = setInterval(loadObservers, 30000); @@ -113,13 +123,13 @@
- - + + ${filtered.map(o => { const h = healthStatus(o.last_seen); const shape = h.cls === 'health-green' ? '●' : h.cls === 'health-yellow' ? 'β–²' : 'βœ•'; - return ` + return ` diff --git a/public/packets.js b/public/packets.js index 06b7caff..80bd6440 100644 --- a/public/packets.js +++ b/public/packets.js @@ -501,6 +501,7 @@
@@ -528,7 +529,7 @@
- @@ -559,8 +560,8 @@
Observer status and statistics
StatusNameRegionLast SeenPacketsPackets/HourUptimeStatusNameRegionLast SeenPacketsPackets/HourUptime
${shape} ${h.label} ${o.name || o.id} ${o.iata ? `${o.iata}` : 'β€”'}
- - + +
RegionTimeHashSizeTypeObserverPathRptDetailsRegionTimeHashSizeTypeObserverPathRptDetails
@@ -1524,7 +1525,7 @@ } return ` - + ${rows}
OffsetFieldValueDescription
OffsetFieldValueDescription
`; }