Files
member-console/docs/operator-a11y-baseline.md
T
cgalo5758 8608c871de Apply the acceptance fixes and archive the change (10k.4)
Six review rounds on the September walk's 40 findings, executed as the
acceptance-fixes change (design D1 to D31) and archived as
openspec/changes/archive/2026-09-03-acceptance-fixes/ with its 20
deltas synced into openspec/specs.

Shell and conventions: the location trail on every page rooted at the
surface; click-opened help popovers; buttons by role with the pressed
rule retired and tertiary openers with a rotating plus; the code chip
and white card headers; Remove versus Delete on row actions.

Operator surface: three overview tiles, one Integrations card, the
getting-started banner as the first instance setting
(core.instance_settings, migration 00015); record creation on its own
page for products, entitlement sets, and plan ladders; Visibility as
one Public checkbox; the Stripe provider page; the plan ladder
validation page retired and ranks ascending; org-types default change
with dispositions, Discard, and the settle-cloning fix; the rebuilt
composite billing card; hints instead of placeholders; the person page's
identity-provider glyph on its field labels; non-UUID ids answer 404.

Found by review and fixed: a session now ends when its person no longer
exists (the foreign-key failure on a ladder reorder after a snapshot
rebuild); the CSRF cookie lives as long as the session and its toast
says the page is out of date; htmx's settle delay is 0 app-wide.

Verification: unit suites, browser walkthroughs, the screens baseline
(31 screens, accepted), and Lighthouse at 100 on both widths, recorded
in docs/operator-a11y-baseline.md. Three stale issue entries archived.
2026-09-03 17:00:14 -05:00

22 KiB

title, audience, summary
title audience summary
UI Accessibility Baseline
developer
The accessibility baseline every UI change is diffed against: Lighthouse scores and failed audits for every captured screen on both surfaces at both widths, what must not regress, and how to re-run the capture.

UI accessibility baseline

First captured 2026-09-01 as the 10k.4 deliverable (change ui-acceptance), re-captured in place on 2026-09-02 after change acceptance-fixes, and re-captured again on 2026-09-03 after round 2 of change acceptance-fixes. Supersedes the 2026-09-02 capture (dropped, since the "What changed" sections below record its deltas) and the 2026-09-01 and 2026-05-18 captures, kept below as appendices. Subsequent changes touching templates or app.css MUST diff against this file: a lower score on any screen at either width is a blocker until it is restored or the reason is recorded here (spec ui-quality-gate, "The accessibility baseline covers every captured screen at both widths").

Tool. Lighthouse via the chrome-devtools MCP lighthouse_audit tool, snapshot mode (the loaded, authenticated page as it stands), once per device emulation (desktop, mobile). Audited against the running test stack (http://member-console.localhost:9431) authenticated as alice (operator-member) on the demo state (test/reset-app-db.sh) — the same rows the contact sheets show, so scores are reproducible.

Scope. Every screen in the capture manifest (test/e2e/screens/manifest.go): 25 operator screens and the 3 member pages, 28 in all, 56 score cells. Accessibility category only; performance, SEO, best-practices, and "agentic browsing" are out of scope as in May. Dialogs and post-submit states are covered by their parent screen's snapshot only.

Reading the table. Snapshot mode audits the static DOM and CSS, so desktop and mobile scores are identical on every screen; the mobile column is kept because a future layout-dependent audit (or a stacked phone layout) would diverge them. The failed-audit counts are Lighthouse's element counts.

Summary by screen

Re-captured 2026-09-03 after round 2 of change acceptance-fixes; the rounds after it are recorded in the "What changed" sections below.

Screen Path Desktop Mobile Failed audits
operator-overview /operator 100 100 none
operator-setup /operator/setup 100 100 none
operator-people /operator/persons 100 100 none
operator-person-detail /operator/persons/{personID} 100 100 none
operator-organizations /operator/organizations 100 100 none
operator-organization-detail /operator/organizations/{orgID} 100 100 none
operator-grants /operator/grants 100 100 none
operator-billing-accounts /operator/billing/accounts 100 100 none
operator-billing-subscriptions /operator/billing/subscriptions 100 100 none
operator-billing-invoices /operator/billing/invoices 100 100 none
operator-billing-invoice-detail /operator/billing/invoices/{invoiceID} 100 100 none
operator-billing-payments /operator/billing/payments 100 100 none
operator-products /operator/products 100 100 none
operator-product-detail /operator/products/{productID} 100 100 none
operator-entitlement-sets /operator/entitlement-sets 100 100 none
operator-entitlement-set-detail /operator/entitlement-sets/{setID} 100 100 none
operator-plan-ladders /operator/plan-ladders 100 100 none
operator-plan-ladder-detail /operator/plan-ladders/{ladderID} 100 100 none
operator-org-types /operator/org-types 100 100 none
operator-domains /operator/domains 100 100 none
operator-integrations /operator/integrations 100 100 none
operator-integration-stripe /operator/integrations/stripe 100 100 none
operator-integration-settings-stripe /operator/integrations/stripe/settings 100 100 none
operator-integration-fedwiki /operator/integrations/fedwiki 100 100 none
operator-integration-discourse /operator/integrations/discourse 100 100 none
member-dashboard / 100 100 none
member-products /products 100 100 none
member-billing /billing 100 100 none

Range: 100 across all 28 screens. Median 100; every score cell passes. The plan ladder validation screen is retired (round 2); the Stripe provider page (operator-integration-stripe) replaces it in the manifest and audits clean.

Failed audits, by frequency

Audit Cells (of 56) Meaning Where
none 0 n/a all 56 cells pass; the banner-link fix under "What changed on 2026-09-03" cleared the last ten

What changed since May

  • color-contrast persists on every route, at a constant per-surface count. May recorded it as "the global nav (/logout styled text-danger, active nav link) repeat across every route; fix is single-location". The shell was rebuilt since (unify-shell, 2026-08-30); the failure survived the rebuild at a higher count (10 per operator page vs May's smaller set). The element lists (read from the MCP's own reports on / and /operator) name exactly two shell colours: every rail entry a.nav-link — Bootstrap's --bs-primary #0d6efd on the rail's bg-light — measures 4.26:1 (AA needs 4.5:1 for this size), the active entry 3.84:1, and the top bar's surface label (span.navbar-text.text-secondary.app-surface-label, "Operator") 3.28:1. Nine rail links plus the label make the operator's ten; the member rail's four links make its four. Two declarations in app.css (a darker rail link colour, a darker surface label) lift all 56 cells.
  • aria-required-children on the four billing views is gone — May's 90s are 96s now; the billing fragment was rebuilt on the list scaffold in the anatomy sweep.
  • heading-order on person detail is gone — the sweep's heading-skip lint rule holds it.
  • select-name on Organization types persists (May: 89; now: 89): the default-plan <select> still has no associated label.
  • New: landmark-one-main on the member surface. The member pages render content outside a <main> element; the operator layout wraps it. Both surfaces share one shell now, so the fix is in the member root pages (index.html, products.html, billing.html).
  • New: td-has-header on Integrations. One <td> in the seven-column registry table has no header association.
  • Coverage grew from 14 operator routes to 28 screens on both surfaces, and both widths are recorded.

What changed on 2026-09-02 (change acceptance-fixes)

  • color-contrast is gone from the shell. The rail links moved from #0d6efd on bg-light (4.26:1, active 3.84:1) to Bootstrap's primary text-emphasis colour (12.8:1, active 11.5:1), and the top bar's surface label from the muted grey (3.28:1 on the dark bar) to the dark-navbar muted white (5.66:1). The May note called for a "darker" label; the label sits on a dark bar, so the honest fix was lighter. Spot-checked in snapshot mode after the change: /operator 96 → 100, / 94 → 100, with zero failed audits on either.
  • landmark-one-main is gone from the member surface: the three member root pages now wrap their content in one <main>.
  • select-name on Organization types: the default-plan select gained a label (the score above will move at the next full re-run).
  • td-has-header on Integrations: the provider name is now the row header cell.
  • The full re-run (the table above) confirms it: 25 screens at 100 on both widths; landmark-one-main, select-name, and td-has-header are gone. Six color-contrast cells remain, all page content the shell's noise had hidden:
    • Organization detail (2 elements): the composite's Active/History tab links (.card-header .nav-link) and the "Issue grant" button (.card-header .btn-outline-primary), Bootstrap's primary #0d6efd on the card header's grey tint #f8f8f9, 4.24:1. On white the same blue is 4.53:1, so the header tint is the cause. Two honest fixes, both visible: a white card header (Bootstrap's --bs-card-cap-bg) or a darker link colour inside card headers. Left for the maintainer's decision; recorded here as the reason the score stays 97.
    • FedWiki and Discourse operator pages (1 element each): the not-configured alert's Settings link, a secondary outline button (#6c757d) on the warning alert's #fff3cd, 4.23:1. Fixed the same day: a control inside an alert takes the alert's text colour (app.css), which clears AA on every alert tone; both screens re-audited at 100 with zero failed audits the same day.

What changed on 2026-09-03 (change acceptance-fixes, round 2)

  • The organization composite's card-header contrast is gone. The Active/History tab links and the "Issue grant" button that measured 4.24:1 on the card header's grey tint no longer appear in any failed audit; organization detail is 100 on both widths with zero failed audits, confirming one of the 2026-09-02 note's "two honest fixes" landed.
  • The top bar's surface label element no longer exists. May and the 2026-09-01 capture tracked its contrast (span.navbar-text.text-secondary.app-surface-label, "Operator"); the string is now a banned literal in operator_sidebar_render_test.go, and no audit in this run reports it on any screen.
  • color-contrast on the test-mode banner's "Stripe settings" link, found and fixed same day. Round 2 replaced the round-1 header badge with a banner above each billing page's header (design D14, review items 22-23, operator_pages.go's TestModeBanner): div.alert.alert-warning holding the sentence and a plain <a href="/operator/integrations/stripe/settings">Stripe settings</a>. The link carried no class, so it fell outside the existing .alert .btn-outline-secondary rule in app.css that gives an alert-scoped control the alert's own text colour; it rendered in Bootstrap's default link blue #0d6efd on the warning alert's #fff3cd, 4.06:1 (AA needs 4.5:1), on all five billing pages, both widths (operator-billing-accounts, operator-billing-subscriptions, operator-billing-invoices, operator-billing-invoice-detail, operator-billing-payments; each had been 100 with zero failed audits on 2026-09-02, before the banner existed). Fixed the same day: the anchor now carries Bootstrap's alert-link class, which takes the alert's own (darker) text colour, the same shape as the FedWiki/Discourse fix. The app was rebuilt and restarted; re-audited, all five billing pages read 100 on both widths with zero failed audits.
  • Manifest churn, both sides confirmed clean. The plan ladder validation screen was retired (round 2 drops the page, D4); the Stripe provider page (operator-integration-stripe, /operator/integrations/stripe) was added and audits at 100 on both widths with zero failed audits. Coverage holds at 28 screens, 56 cells.
  • The full re-run: all 28 screens at 100 on both widths, zero failed audits anywhere. No screen timed out or needed a retry.

What changed on 2026-09-03 (round 4)

A spot check, not a full re-run: 13 of the manifest's screens, chosen to cover round 4's new markup (the catalog create/detail pages, the organization composite's "Issue grant" panel, the person page's grouped identity-provider fields, and the org-types default-plan preview). Accessibility category only, snapshot mode, both widths, authenticated as alice on the demo state.

Screen Path Desktop Mobile Failed audits
operator-overview /operator 100 100 none
operator-products /operator/products 100 100 none
operator-product-new /operator/products/new 100 100 none
operator-product-detail /operator/products/{productID} 100 100 none
operator-entitlement-sets /operator/entitlement-sets 100 100 none
operator-entitlement-set-new /operator/entitlement-sets/new 100 100 none
operator-entitlement-set-detail /operator/entitlement-sets/{setID} 100 100 none
operator-plan-ladders /operator/plan-ladders 100 100 none
operator-plan-ladder-new /operator/plan-ladders/new 100 100 none
operator-plan-ladder-detail /operator/plan-ladders/{ladderID} 100 100 none
operator-organization-detail /operator/organizations/{orgID} 100 100 none
operator-person-detail /operator/persons/{personID} 100 (98 before the same-day fix) 100 (98 before the fix) none after the fix; heading-order (1) before it
operator-org-types /operator/org-types 100 100 none

Range: 100 on all 13 screens after the person-detail fix was rebuilt and re-audited (98 before it). Median 100.

  • Manifest coverage grew by three screens: operator-product-new, operator-entitlement-set-new, and operator-plan-ladder-new (the record-creation pages for products, entitlement sets, and plan ladders) are new entries in test/e2e/screens/manifest.go since the round-2 capture; none were in that capture's table. All three audit clean.
  • The opener buttons with the plus glyph (entitlement set "Add rule", organization composite "Issue grant", pool "Extend tier", org-types panels) and the hint texts (the button "Help: ..." controls next to Active, Pools, Issuance, Delivery, Quantity, Valid until, Change default to, and similar fields) audit clean everywhere they appear in this set of 13 screens, closed and open.
  • The organization composite's "Issue grant" panel, re-audited open at desktop (the form with Product, Quantity, Valid until, Reason, and Description fields visible), stays at 100 with zero failed audits; the tertiary opener's expanded state introduces nothing new.
  • The product create/edit form's "Public" checkbox and its help icon audit clean on operator-product-new.
  • The light "Getting started" card on the overview (the dismissible status banner with "Continue setup" and the dismiss button) audits clean.
  • Org types' default-plan preview, re-audited at desktop after selecting "None (no default plan)" in "Change default to" (the disposition radios, the disabled "Apply change" button, and "Discard" all rendered), stays at 100 with zero failed audits.
  • New: heading-order on person detail. The Details section's "Managed by the identity provider" group (added round 4, grouping the identity-provider-owned name and email fields under their own heading: internal/embeds/templates/partials/operator_person_detail.html) renders as <h3 class="h6 mb-1 mt-3">, directly after the page's <h1> and before the "Org memberships" <h2>; the h1-to-h3 jump skips a level. One element, both widths, both 98. Fixed the same day: the heading changed from <h3> to <h2> (same h6 sizing class, so the visual size is unchanged); the two render tests that asserted the old <h3> markup (internal/server/operator_person_detail_render_test.go, internal/server/operator_anatomy_test.go) were updated to expect <h2>. The app was then rebuilt and the person page re-audited at desktop: 100, zero failed audits, so the table's 98 is the score with the bug live and 100 the score that stands.
  • Round 5, same day. The person page changed again on the maintainer's review (the group heading and its sentence removed; the person glyph moved onto the Name and Email labels with "Managed by the identity provider" as its accessible name): re-audited at desktop, 100, zero failed audits. The organization composite's rebuilt billing card (balance, subscriptions, latest invoice on two lines, scoped links): audited at desktop, 100, zero failed audits.
  • Round 6, same day. The setup banner returned to the blue info alert ("I back the blue"), its link in the alert's own link colour: the overview re-audited at desktop, 100, zero failed audits (the plain link on that tint was the round-2 failure; alert-link keeps it clear). The billing card's Latest invoice cell became one line (badge, number, date; no amount): the composite re-audited at desktop, 100, zero failed audits. Note for the record: the round-4 table's product, set, and ladder record pages were audited by database id, so they were the record pages; the screens sheets of rounds 4 and 5 had captured the create pages under those names (a resolver bug, fixed in round 6).

How to re-run

  1. cd test && ./reset-app-db.sh (demo state), the stack up, the app running on the port in test/.env.
  2. Chrome on :9222 (chrome-debug &), signed in as alice.
  3. For each screen in the manifest, navigate and call lighthouse_audit twice (mode: snapshot, device: desktop then mobile); record the accessibility score and failed audits. The 10k.4 run used a Sonnet agent with the brief kept at the run's job directory; the procedure is the same by hand. Note: the MCP server writes its own report files under /tmp/chrome-devtools-mcp-*/ and rejects outputDirPath outside its workspace roots — read the report there if element lists are needed.
  4. Update the table in place; move the previous table to the appendix; list every audit that appeared or disappeared under "What changed".

Notes & known issues

  • No open residuals as of 2026-09-03: the test-mode banner's "Stripe settings" link (cause and fix under "What changed on 2026-09-03"), the shell's contrast (rail link, surface label), and landmark-one-main on the member surface are all resolved; the full re-run comes back clean on all 28 screens. Consider a contrast assertion in the anatomy tests for the shell's nav and label tones, and a lint or test asserting that .alert controls keep the alert's text colour, so a future rebuild or a future banner cannot reintroduce either.
  • Tooltip-only reasons on disabled controls (a title on a wrapper <span> around a disabled button) are not caught by any Lighthouse audit but have no keyboard path; recorded in the 10k.4 findings (ACC-8), not here.

Appendix: the 2026-09-01 capture (superseded)

The 10k.4 capture, before acceptance-fixes lifted the shell's contrast, added the member <main> landmark, labelled the org-types select, and made the Integrations provider name the row header.

Screen Desktop Mobile Failed audits
operator-overview 96 96 color-contrast (10)
operator-setup 95 95 color-contrast (10)
operator-people 96 96 color-contrast (10)
operator-person-detail 96 96 color-contrast (10)
operator-organizations 96 96 color-contrast (10)
operator-organization-detail 97 97 color-contrast (12)
operator-grants 96 96 color-contrast (10)
operator-billing-accounts 96 96 color-contrast (10)
operator-billing-subscriptions 96 96 color-contrast (10)
operator-billing-invoices 96 96 color-contrast (10)
operator-billing-invoice-detail 96 96 color-contrast (10)
operator-billing-payments 96 96 color-contrast (10)
operator-products 96 96 color-contrast (10)
operator-product-detail 96 96 color-contrast (10)
operator-entitlement-sets 96 96 color-contrast (10)
operator-entitlement-set-detail 96 96 color-contrast (10)
operator-plan-ladders 96 96 color-contrast (10)
operator-plan-ladder-detail 97 97 color-contrast (10)
operator-plan-ladder-validation 96 96 color-contrast (10)
operator-org-types 89 89 color-contrast (10), select-name (1)
operator-domains 96 96 color-contrast (10)
operator-integrations 96 96 color-contrast (10), td-has-header (1)
operator-integration-settings-stripe 96 96 color-contrast (10)
operator-integration-fedwiki 96 96 color-contrast (10)
operator-integration-discourse 96 96 color-contrast (11)
member-dashboard 94 94 color-contrast (4), landmark-one-main (1)
member-products 93 93 color-contrast (4), landmark-one-main (1)
member-billing 93 93 color-contrast (4), landmark-one-main (1)

Appendix: the 2026-05-18 operator-only baseline (superseded)

Captured as the M7f.3 deliverable, Lighthouse 12 snapshot mode, authenticated as alice against the test stack of the time (:9481), seeded test data. Routes that no longer exist (/operator/fedwiki-sites, now /operator/integrations/fedwiki) are shown as they were.

Route Score Contrast Heading order Failed audits
/operator 96 FAIL PASS color-contrast
/operator/organizations 95 FAIL N/A color-contrast
/operator/organizations/{orgID} 96 FAIL PASS color-contrast
/operator/grants 95 FAIL N/A color-contrast
/operator/billing/accounts 90 FAIL N/A aria-required-children, color-contrast
/operator/billing/subscriptions 90 FAIL N/A aria-required-children, color-contrast
/operator/billing/invoices 90 FAIL N/A aria-required-children, color-contrast
/operator/billing/payments 90 FAIL N/A aria-required-children, color-contrast
/operator/org-types 89 FAIL PASS color-contrast, select-name
/operator/products 96 FAIL PASS color-contrast
/operator/entitlement-sets 96 FAIL PASS color-contrast
/operator/plan-ladders 96 FAIL PASS color-contrast
/operator/fedwiki-sites 95 FAIL N/A color-contrast
/operator/persons/{personID} 94 FAIL FAIL color-contrast, heading-order

May's notes, for the record: the nav contrast failures were single-location (the shared nav partial); the four billing routes shared one fragment and one failure profile; the heading-order jump was tracked in status/issues.md at the time. All three are resolved or carried forward above.