Skip to content

fix(a11y): unique analytics table ids and readable VCR mode badge text - #60

Merged
dborup merged 4 commits into
masterfrom
codex/fix-axe-scopes-dup-id-live-badge-contrast
Sep 17, 2026
Merged

dborup merged 4 commits into
masterfrom
codex/fix-axe-scopes-dup-id-live-badge-contrast

Conversation

@dborup

@dborup dborup commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Problem

With the Kpa-clawbot#1706 route coverage in place (#59), the full axe-core gate (test-a11y-axe-1668.js) reports two pre-existing UI problems on master e17377d8 (reproduced locally, identical with and without #59):

  1. Scopes: duplicate-id analytics-tbl-scopes-0 (4 cells: desktop/mobile × light/dark).
    renderTab() in public/analytics.js numbers .analytics-table elements by position inside a requestAnimationFrame. The pass runs again after the first theme-refresh (~300 ms after load, via _themeRefreshHandler). By then the Scopes tab's async sections have inserted tables ahead of the static overview table, which already holds -0, so the first inserted table was also given -0. Traced with a MutationObserver in the browser: second pass produced scopes-0, scopes-0, scopes-2, scopes-3.
  2. Live: color-contrast on the VCR mode badge #vcrMode (desktop light).
    .vcr-mode-live used the swatch hue --status-green as text: 2.27:1 on the light VCR bar. The PAUSED (--status-yellow, 1.81:1) and REPLAY (--accent, 2.52:1) states have the same problem; the axe gate does not see them only because the page starts in LIVE.

Changes

public/analytics.js — the id loop moves into assignAnalyticsTableIds(el, tab), which skips ids that are already taken. A single pass yields exactly the same ids and meshcore-analytics-<tab>-<i>-col-widths keys as before; nothing else references these ids. Exposed as window._analyticsAssignTableIds for tests, next to the existing _analytics* hooks.

public/live.css — badge text now uses the existing semantic text tokens; tinted backgrounds, the pulsing LIVE dot and all behaviour are unchanged:

Mode Before After (light / dark)
LIVE --status-green --status-green-text (#15803d / #22c55e)
PAUSED --status-yellow --warning (#b45309 / #fcd34d)
REPLAY --accent --link-color (#2563eb / #60a5fa)

Dark theme was already AA; it stays AA (LIVE unchanged, PAUSED 7.3→9.7:1, REPLAY 5.3→5.7:1).

New regression tests

  • test-analytics-table-ids-unique.js — drives the real assignAnalyticsTableIds against a fake DOM with the observed Scopes insertion order; asserts uniqueness, that an assigned id never changes, the exact post-fix sequence (scopes-1, -0, -2, -3), positional numbering for a single pass, and that column resizing is still wired to every table.
  • test-live-vcr-mode-contrast.js — static WCAG check for all three modes in light theme and both dark token blocks ([data-theme="dark"] and prefers-color-scheme: dark), modelling .vcr-bar's color-mix(… 95%, transparent) background over --surface-0/--surface-1 plus each mode's tint. The modelled backgrounds match what Chrome/axe measured (#feffff, #fff8e9, #eef6fe).

Test registration — both tests added once to test-all.sh and once to the existing "Run JS unit tests (packet-filter)" step in .github/workflows/deploy.yml. No other workflow change: triggers, permissions, concurrency, env, jobs, needs, if/fork-guards and all other steps are semantically identical to master (YAML compared job by job); cmd/server fork-guard workflow tests pass.

Evidence (local, macOS arm64 — not GitHub CI)

  • Red → green:
    • Contrast test on master CSS: 6 failing combinations (LIVE/PAUSED/REPLAY, light). On this branch's first commit (LIVE only fixed): 4 failing (PAUSED/REPLAY). On HEAD: 18/18 pass.
    • Id test with the old positional algorithm restored in a scratch copy of this branch (test hook kept): fails on the duplicate-id assertion with scopes-0, scopes-0, scopes-2, scopes-3. On HEAD: 4/4 pass. The mutation is not in this PR.
  • Integration (master e17377d8 + fix(map): drop the M4-banned caret from a map.js comment #56 52ae9d1c + test(a11y): gate the foreign-traffic, wardriving and areas analytics tabs #59 e3f57426 + this branch):
    • Workflow JS step (set -e, 69 commands from the integrated deploy.yml): all pass.
    • Full axe gate against an isolated local Go server + copy of test-fixtures/e2e-fixture.db (prepared like CI): 116/116 cells clean (master + fix(map): drop the M4-banned caret from a map.js comment #56 + test(a11y): gate the foreign-traffic, wardriving and areas analytics tabs #59 without this branch: 111/116).
    • Browser check of #vcrMode with the app's own mode classes, desktop light/dark: LIVE 5.0 / 7.56, PAUSED 4.74 / 9.74, REPLAY 4.73 / 5.72 — all AA. The badge is hidden on mobile (display: none ≤640px).
    • Run on the earlier integration with this branch's first commit (id fix + LIVE; the later commits only change PAUSED/REPLAY text color and tests):
      • All 20 analytics tabs: table count, ids and column-resize wiring identical to baseline except the removed Scopes duplicate.
      • 16 related Playwright E2E tests (tables, charts, live/VCR layout): identical results on baseline and integration (6 pass on both; 10 fail on both for local-environment reasons, see below).
    • node --check, sh -n test-all.sh, YAML parse, git diff --check, XSS --diff gate: clean.

Dependencies and limitations

🤖 Generated with Claude Code

Dennis Jakobsen and others added 4 commits September 17, 2026 07:23
Scopes: renderTab numbers .analytics-table elements by position, and the
pass runs again after the tab's async sections are inserted ahead of the
static overview table (and on every theme refresh). The first inserted
table then reused the static table's positional id, giving axe
duplicate-id analytics-tbl-scopes-0. Skip ids that are already taken; a
single pass still yields the same numbering and column-width keys.

Live: .vcr-mode-live rendered its text in --status-green, the swatch hue,
at 2.27:1 on the light-theme VCR bar. Use the existing text token
--status-green-text (#15803d light, unchanged #22c55e dark); the pulsing
dot keeps the swatch hue.

Add focused regression tests for both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PAUSED and REPLAY badges had the same problem as LIVE: text in the
swatch hues --status-yellow and --accent, 1.81:1 and 2.52:1 on the
light-theme VCR bar. Use the semantic text tokens --warning and
--link-color (light #b45309 / #2563eb, dark #fcd34d / #60a5fa); the
badges' tinted backgrounds and the LIVE dot keep their indicator colors.

Extend the contrast test to all three modes, the light theme and both
dark token blocks, modelling the bar's color-mix background and each
mode's tint (renamed to test-live-vcr-mode-contrast.js).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assert the ids the browser shows on the Scopes tab after the fix
(scopes-1, -0, -2, -3), in addition to uniqueness, so the test fails on
the duplicate-id assertion with the old positional algorithm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run test-analytics-table-ids-unique.js and test-live-vcr-mode-contrast.js
in the JS unit-test step and in test-all.sh, once each. No other workflow
change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dborup
dborup merged commit 6c5b40a into master Sep 17, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant