Appearance
[UX] tt-time-tracker — Error states
Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: Dr-Wade/tt-time-tracker · Branch:
develop@bb3238c· Files reviewed: 4 Patterns: none (feature plainly covered by baseline; MCP budget preserved)
Summary
tt-time-tracker's 404 (NotFound.vue) is the best error surface seen in this batch and the reference-quality 404 the alignment pass was looking for: a clear illustration, an <h1>, human French copy, and — crucially — two real routes out (context-aware "Retour à l'accueil"/"Se connecter" plus "Page précédente"). It cleanly passes MSG-04, where playout's equivalent fails. Remaining issues are minor accessibility/polish: the page renders in no landmark, the logo alt is redundant, and (project-wide) the route sets no document title and SPA navigation to it is silent to assistive tech.
Note: this feature is the catch-all 404 (routes.ts:71 → NotFound.vue) noted in PROJECT-LEVEL.md; the silent no-forbidden-surface redirect is a separate project-level finding, referenced below, not re-derived here.
Findings
1. 404 content sits in no landmark — Medium · A11Y-04
Where: services/client/src/views/NotFound.vue:2; services/client/src/App.vue:2-6What: not-found is a top-level route rendered outside LayoutApp (routes.ts:71), so it does not inherit any page shell. App.vue's root is a plain <div id="app"> with no <main>, and NotFound.vue's outer element is a bare <section> with no accessible name. A <section> is only exposed as a landmark when it has an accessible name, so the entire 404 content — heading and both recovery buttons — lives outside every landmark. The single-<h1> half of A11Y-04 passes. Why it matters: A screen-reader user navigating by landmarks (a primary strategy) finds nothing on this page and cannot jump to its main content or its "way out" buttons; they must read linearly from the top. Fix: Make the wrapper a landmark — change the <section> to <main>, or add role="main"/an aria-label. Cheapest at the App.vue level: wrap the top-level <RouterView> in a <main> so every chrome-less route (login, 404, etc.) is covered at once.
2. Logo image carries a redundant, non-descriptive alt — Low · A11Y-05
Where: services/client/src/views/NotFound.vue:4-8What: The masthead logo uses alt="logo". It is decorative here (the page's meaning is fully carried by the <h1> and copy), so it should be alt="" to be skipped, or carry the product/organisation name if it is meant to be informative. "logo" is neither — a screen reader announces the literal word "logo, image". Why it matters: Small, but it adds a meaningless announcement on a page whose whole job is to orient a lost user quickly. Fix: alt="" (decorative) or the org name (informative). Prefer alt="" here.
3. "Page précédente" can bounce to another dead end — Low · MSG-04
Where: services/client/src/views/NotFound.vue:61-65What: goBack calls router.back() whenever window.history.length > 1. History length only proves some prior entry exists, not that it is a valid in-app page — if the user reached the bad URL by editing another bad URL, or the prior entry was itself a 404, "Page précédente" returns them to a broken/again-404 state (or, if the prior entry is external, leaves the SPA entirely). Why it matters: The secondary escape hatch can loop the user instead of recovering them. The primary "Retour à l'accueil" button is unaffected and always works, which keeps this Low rather than higher. Fix: Acceptable as-is given the reliable primary action; if hardened, gate router.back() on a same-origin/known-route check and fall through to goHome otherwise (the else branch already does the right thing).
Unverified
- A11Y-01 (contrast): Not settled from code. Watch the muted description (
text-surface-500onbg-surface-50,NotFound.vue:26) and the "Page précédente" text button — low-emphasis greys on a near-white surface are the usual sub-4.5:1 offenders. Verify with a contrast tool against the active theme. - A11Y-06 (short viewport / mobile keyboard): The centred
justify-centerlayout with atext-[8rem]numeral (:17) is tall; needs a rendered ~700px viewport to confirm nothing clips. No keyboard is involved on this page. - PrimeVue
Buttoninternals (target size A11Y-02):node_modulesis not installed, so the rendered hit area of thetextsecondary button cannot be read from source. Both buttons carry visible labels, so no A11Y-05 icon-name gap.
Baseline additions
A11Y-ROUTE-ANNOUNCE(proposed): On client-side navigation the new route must be announced to assistive tech — move focus to the new<h1>/main, or update anaria-liveregion. Reaching this 404 via an in-app broken link swaps the DOM underApp.vue's<Transition>(App.vue:6-21) with no focus move and no live-region update, so a screen-reader user is silently left on "Page introuvable" with no signal the page changed. This is the SPA-navigation analogue of NAV-03 and recurs project-wide; flagging once here.A11Y-ALT-TEXT(proposed): Meaningful images carry descriptivealt; decorative images usealt="". Covers finding 2; overlaps the separately proposed A11Y-07(c) in PROJECT-LEVEL.md — orchestrator to reconcile.
Project-level references (not re-filed)
- NAV-03 — no route sets a document title;
index.html:17is « Tim » everywhere, including this 404. See PROJECT-LEVEL.md. - Silent permission redirect / no forbidden surface — non-admins are bounced with no explanation; distinct from this 404 and already in PROJECT-LEVEL.md.
- CONTENT-01 — not-applicable; no i18n layer by design, see project-level i18n finding.
Cross-project note
The A11Y-04 "chrome-less error/auth route sits in no <main>" shape is likely shared: playout (not-found/unauthorized sit outside Layout per PROJECT-LEVEL.md) and customer-portal/members auth+onboarding shells are the exact class already flagged for the <h1> variant of A11Y-04 — check whether those shells provide a <main>. The A11Y-ROUTE-ANNOUNCE gap (SPA route change unannounced) almost certainly affects all four Vue SPAs. Conversely, this 404's MSG-04 pass is worth holding up as the target the other three should copy — none of them currently has a 404 that both explains and links out.