11 KiB
UI/UX Review: Analytics, Channels & Observers Pages
Reviewer: subagent | Date: 2026-03-19
Analytics Page
Accessibility
-
[Major] Tab buttons lack
role="tablist"/role="tab"/aria-selected— screen readers can't identify the tab pattern. (analytics.js~L60-68, the.analytics-tabsdiv and.tab-btnbuttons) -
[Major] All SVG charts (bar charts, scatter plots, histograms, sparklines) have zero text alternatives — no
role="img", noaria-label, no<title>element. Screen readers get nothing. (analytics.js—barChart()L27,sparkSvg()L14,renderScatter()L142,histogram()L42) -
[Major] Hash matrix cells use color alone (green/yellow/red) to convey collision status. Color-blind users can't distinguish them. No pattern/icon/text differentiation. (
analytics.js~L339-350) -
[Minor]
clickable-rowelements useonclickinline handlers on<tr>— not keyboard-focusable, notabindex, norole="link"orrole="button". (analytics.jsL293, L318, L328 — multiple tables) -
[Minor] Observer selector buttons in Topology tab reuse
.tab-btnclass but lack proper ARIA tab semantics. (analytics.js~L220) -
[Minor] Scatter plot quality zone labels ("Excellent", "Good", "Weak") use semi-transparent fills that may have insufficient contrast against various backgrounds. (
analytics.js~L166-170)
Mobile Responsive
-
[Major]
.analytics-rowgoesflex-direction: columnon mobile (good), but the hash matrix table (renderHashMatrix) generates a fixed-width 16×16 grid withcellSize=36px→ minimum ~600px wide. Theoverflow-x:autowrapper helps but the detail panel beside it won't fit. (analytics.js~L331,style.css— no specific mobile override for hash matrix) -
[Minor] SVG charts use fixed
max-heightvalues (e.g.,max-height:300px,max-height:160px) which may waste space or clip on very small screens. Width is100%though, which is correct. (analytics.js~L143, L189, L207) -
[Minor]
.subpath-layoutusesheight: calc(100vh - 160px)— this assumes a specific header height. If the analytics tabs wrap to 2 lines on mobile, content gets clipped. (style.css—.subpath-layout) -
[Minor] Route Patterns subpath detail panel has
min-width: 360px— won't fit on phones <375px even in column layout. (style.css—.subpath-detail)
Desktop Space Efficiency
-
[Minor]
.analytics-pagehasmax-width: 1600px— reasonable for most content but the hash matrix + detail panel side-by-side could use more width on ultrawide monitors. (style.css—.analytics-page) -
[Minor] Overview stat cards use
minmax(160px, 1fr)grid — on very wide screens you get many small cards in one row which looks sparse. Could benefit from amax-widthper card. (style.css—.stats-grid)
Bugs / Inconsistencies
-
[Critical]
svgLine()function (L7-12) is defined but never called anywhere. Dead code. (analytics.jsL7) -
[Major]
window._analyticsDatais set as a global — potential for conflicts with other scripts, and thedestroy()function only doesdelete window._analyticsDatabut doesn't clean up event listeners on#analyticsTabs. (analytics.jsL87, L460) -
[Major]
renderCollisions()andrenderHashMatrix()both independently fetch/nodes?limit=2000— duplicate API call when viewing the "Hash Collisions" tab. (analytics.js~L329, L380) -
[Minor]
renderSubpathsusesasync functionbut is called withoutawaitinrenderTab()switch — the loading state and error handling work via the function's internal try/catch, but therequestAnimationFramecolumn resize inrenderTabwill fire before the async content renders. (analytics.jsL96 calls renderSubpaths, L99-103 does column resize immediately) -
[Minor] The
renderTabfunction appliesmakeColumnsResizableto.analytics-tableelements, butmakeColumnsResizableis called without checking if it exists (it's presumably defined inapp.js). No guard. (analytics.jsL100) -
[Minor]
timeAgo()andapi()are used but not imported/defined in this file — relies on global scope fromapp.js. Not a bug per se but fragile coupling. (analytics.jsmultiple locations) -
[Minor] Hash matrix legend uses inline styles for color swatches rather than CSS classes — inconsistent with the rest of the codebase which uses
.legend-dotclass. (analytics.js~L365)
Channels Page
Accessibility
-
[Major] Channel list items are
<button>elements (good!) but message bubbles with sender links usedata-node+ base64-encoded names with click handlers via event delegation. These<span>elements withdata-nodeare not focusable via keyboard — notabindex, norole="button". (channels.js~L131highlightMentions(), ~L229 message rendering) -
[Major] The node detail panel slides in but doesn't trap focus — keyboard users can tab behind it. Close button exists but no focus management on open/close. (
channels.js~L60-80,showNodeDetail()) -
[Minor]
aria-live="polite"on scroll button is good, but the button text "↓ New messages" is static — it doesn't actually announce when new messages arrive, only when visibility toggles. (channels.js~L152) -
[Minor] Channel sidebar has
role="navigation"andaria-label="Channel list"— semantically it's more of a listbox than navigation. (channels.js~L141) -
[Minor] Node tooltip (
.ch-node-tooltip) haspointer-events: none— keyboard users can never interact with its content. (style.css—.ch-node-tooltip)
Mobile Responsive
-
[Minor] Mobile channel layout uses absolute positioning with
transform: translateX(100%)for the slide animation — this works but the sidebar getspointer-events: nonewhen main is shown, meaning you can't scroll it even if partially visible. Minor since back button exists. (style.css~L478-484) -
[Minor] Node detail panel is
max-width: 80%andwidth: 320px— on small phones this leaves only 20% visible of the messages behind it, but the panel covers the content anyway. Adequate. (style.css—.ch-node-panel) -
[Minor]
.ch-avataris 36×36px on desktop, bumped to 40×40 on mobile — meets 44px touch target when including the padding around messages, but the avatar itself is slightly under the 44px WCAG recommendation. (style.css—.ch-avatar, mobile override)
Desktop Space Efficiency
-
[Minor] Channel sidebar is fixed at 280px (
min-width: 280px) — not resizable. On wide monitors this is fine, but on 900-1024px tablets it shrinks to 220px which may truncate channel names. (style.css—.ch-sidebar, tablet media query) -
[Minor] Messages area has no
max-width— on ultrawide monitors, message bubbles stretch very wide. Chat apps typically cap message width at ~700-800px. (style.css—.ch-messageshas no max-width,.ch-msg-bubblehasmax-width: 100%)
Bugs / Inconsistencies
-
[Major]
window._chShowNode,_chCloseNode,_chHoverNode,_chUnhoverNode,_chBack,_chSelectare all set as globals and never cleaned up indestroy(). If the page is navigated away and back, these persist. Also_chSelectis defined but only used viadata-hashclick delegation, making it dead code. (channels.js~L98-103, L269) -
[Minor]
getSenderColor()checksdata-themeattribute andprefers-color-schemeat call time — this means if the user toggles dark mode without reloading, already-rendered messages keep old colors while new ones get correct colors. Not reactively updated. (channels.js~L116-120) -
[Minor]
lookupNode()caches results innodeCachebut cache is never invalidated. If node data changes (name, role), stale data persists until page reload. (channels.js~L12-21) -
[Minor]
refreshMessages()comparesmessages.lengthAND last timestamp to detect changes — but at the 200-message limit, both could be the same even if older messages rotated out. Edge case. (channels.js~L210-213)
Observers Page
Accessibility
-
[Major] Health status dots use color alone (green/yellow/red) — color-blind users can't distinguish. The text label "Online"/"Stale"/"Offline" is next to the dot in the table which helps, but the summary dots at the top have no text inside the dot itself. (
observers.js~L76-79,style.css—.health-dot) -
[Minor] Refresh button uses
onclick="window._obsRefresh()"inline handler — should be a proper event listener. Also uses emoji 🔄 as the only label with just atitleattribute — screen readers may not convey the title. (observers.js~L14) -
[Minor]
.obs-tablehas noaria-labelor<caption>element. (observers.js~L82) -
[Minor]
.spark-barprogress indicators have no ARIA — they're purely visual. Screen readers get the text "X/hr" from.spark-labelwhich is acceptable, butrole="meter"or similar would be better. (observers.js~L41-44)
Mobile Responsive
-
[Minor]
.observers-pagehasmax-width: 1200pxandpadding: 20px— on mobile this is fine. However, the table has 7 columns and no responsive override — it will require horizontal scrolling on phones. Nooverflow-x: autowrapper. (style.css—.observers-page,observers.js~L82) -
[Minor]
.spark-barhas fixedwidth: 100px— doesn't shrink on small screens, contributing to table overflow. (style.css—.spark-bar)
Desktop Space Efficiency
- [Minor]
max-width: 1200pxwithmargin: 0 autois appropriate. No issues on desktop.
Bugs / Inconsistencies
-
[Minor]
window._obsRefreshis set globally and never cleaned up indestroy(). (observers.jsL89) -
[Minor] Every WebSocket packet triggers
loadObservers()— if packets arrive rapidly (e.g., 10/sec), this fires 10 API calls per second. Should be debounced. (observers.js~L20-22) -
[Minor]
healthStatus()computes time difference usingDate.now()vs parsed date — doesn't account for timezone differences between server and client. Could show wrong status if clocks are skewed. (observers.js~L32-37)
Cross-Cutting CSS Issues
-
[Major]
@media (prefers-color-scheme: dark)only applies when nodata-themeattribute is set on:root(via:root:not([data-theme="light"])). But the dark mode toggle presumably setsdata-theme="dark". The auto-detection path (no attribute) and manual path (attribute set) duplicate all the same variables — if one is updated, the other may be forgotten. (style.cssL18-31 vs L33-47) -
[Minor]
.clickable-row:hoverusesvar(--hover-bg, rgba(0,0,0,.04))—--hover-bgis never defined in:root. It falls back correctly, but the fallbackrgba(0,0,0,.04)is nearly invisible on dark backgrounds. (style.css—.clickable-row:hover) -
[Minor]
prefers-reduced-motionmedia query correctly disables animations — good accessibility practice. (style.css~L527)