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.
22 KiB
title, audience, summary
| title | audience | summary | |
|---|---|---|---|
| UI Accessibility Baseline |
|
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-contrastpersists on every route, at a constant per-surface count. May recorded it as "the global nav (/logoutstyledtext-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 entrya.nav-link— Bootstrap's--bs-primary#0d6efdon the rail'sbg-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 inapp.css(a darker rail link colour, a darker surface label) lift all 56 cells.aria-required-childrenon 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-orderon person detail is gone — the sweep's heading-skip lint rule holds it.select-nameon Organization types persists (May: 89; now: 89): the default-plan<select>still has no associated label.- New:
landmark-one-mainon 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-headeron 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-contrastis gone from the shell. The rail links moved from#0d6efdonbg-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:/operator96 → 100,/94 → 100, with zero failed audits on either.landmark-one-mainis gone from the member surface: the three member root pages now wrap their content in one<main>.select-nameon Organization types: the default-plan select gained a label (the score above will move at the next full re-run).td-has-headeron 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, andtd-has-headerare gone. Sixcolor-contrastcells 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#0d6efdon 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.
- Organization detail (2 elements): the composite's Active/History tab
links (
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 inoperator_sidebar_render_test.go, and no audit in this run reports it on any screen. color-contraston 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'sTestModeBanner):div.alert.alert-warningholding 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-secondaryrule inapp.cssthat gives an alert-scoped control the alert's own text colour; it rendered in Bootstrap's default link blue#0d6efdon 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'salert-linkclass, 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, andoperator-plan-ladder-new(the record-creation pages for products, entitlement sets, and plan ladders) are new entries intest/e2e/screens/manifest.gosince 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-orderon 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>(sameh6sizing 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-linkkeeps 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
cd test && ./reset-app-db.sh(demo state), the stack up, the app running on the port intest/.env.- Chrome on
:9222(chrome-debug &), signed in asalice. - For each screen in the manifest, navigate and call
lighthouse_audittwice (mode: snapshot,device: desktopthenmobile); 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 rejectsoutputDirPathoutside its workspace roots — read the report there if element lists are needed. - 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-mainon 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.alertcontrols keep the alert's text colour, so a future rebuild or a future banner cannot reintroduce either. - Tooltip-only reasons on disabled controls (a
titleon a wrapper<span>around adisabledbutton) 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.