Appearance
[UX] tt-time-tracker — API keys
Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: Dr-Wade/tt-time-tracker · Branch:
develop@bb3238c· Files reviewed: 7 Patterns: content-management/modal (consulted); data-display/table, user-feedback/notification (baseline-covered)
Summary
The API-keys surface is well-built on the security essentials: the created key is shown once behind a :closable="false" dialog with an explicit "copy now" warning, the table shows only keyPrefix... (never the full secret), and both create and revoke are correctly gated — revoke goes through a PrimeVue confirm that names the key and says the action is irreversible (MSG-05 pass). The defects are accessibility and form-affordance polish, not secret exposure: the create-dialog labels are not programmatically associated with their inputs, the page has no <h1>, and the "Créer" submit is disabled instead of explaining the block. No blocker or high-severity issue found.
Findings
1. Create-dialog labels are not associated with their inputs — Medium · FORM-01
Where: services/client/src/views/Admin/ApiKeys.vue:93-99, :101-110What: Both fields use a bare <label class="text-sm font-medium">Nom</label> (and Expiration) with no for, and the InputText / DatePicker have no id. The visible text is present but there is no programmatic label→control association. Why it matters: A screen-reader user tabbing into the name field hears an unlabelled edit box; clicking the visible label text does not focus the field. Fix: Add id/for pairs (e.g. <label for="apiKeyName"> + <InputText id="apiKeyName">), or wrap each field so the label is programmatically tied to its control.
2. Page has no <h1> — heading starts at <h2> — Medium · A11Y-04
Where: services/client/src/views/Admin/ApiKeys.vue:2-5 via components/Layout/LayoutMain.vue:6 and :74What: The page title "Clés API" is passed to LayoutMain's #title slot, which renders it inside an <h2> (both the mobile :6 and desktop :74 headings are <h2>). Nothing in the shell renders an <h1>, so the document's top heading level is missing and the hierarchy starts at 2. Why it matters: Screen-reader heading navigation and document outline break when there is no <h1>; the primary landmark heading is absent (WCAG 1.3.1 / 2.4.10). Note this stems from LayoutMain, so it is shared by every admin view using that shell (Hours/Profile/Dashboard render their own <h1> in-template and are unaffected) — worth fixing once in LayoutMain. Fix: Render the #title slot as <h1> in LayoutMain (it is the page's main heading), or have views that use LayoutMain supply an <h1>.
3. "Créer" submit is disabled while the form is invalid — Medium · FORM-05
Where: services/client/src/views/Admin/ApiKeys.vue:119-125What: The primary action is :disabled="!form.name.trim()". An empty name gives the user a greyed-out button and no message explaining what is required — there is no field-level "Nom requis" hint (FORM-04/FORM-05). Why it matters: A disabled submit offers nothing to act on; the user cannot tell whether the button is broken or what is missing, and disabled controls are skipped by some AT. Fix: Keep the button enabled and show a validation message on attempted submit (or a persistent "Le nom est requis" hint under the field).
4. Copy button's name comes only from a tooltip — Medium · A11Y-05
Where: services/client/src/views/Admin/ApiKeys.vue:143-149What: The icon-only copy Button (icon="pi pi-copy", no label) has no aria-label; its only accessible-name source is v-tooltip.top. PrimeVue's tooltip typically wires aria-describedby, not an accessible name, so this control may expose no name at all to AT (whether the tooltip suffices is Unverified — PrimeVue internals not installed). Why it matters: On the one screen where copying the secret is the whole point, a screen-reader user may reach an unnamed button. Fix: Add an explicit aria-label="Copier la clé" (keep the tooltip for sighted hover). Same applies to LayoutMain's search trigger only if reused.
5. Clipboard-copy failure is silent — Low · MSG-03 (see also proposed FEEDBACK-COPY-01)
Where: services/client/src/views/Admin/ApiKeys.vue:211-215What: copyKey awaits navigator.clipboard.writeText with no try/catch. In an insecure context or when clipboard permission is denied, the write rejects, copied never flips, the tooltip stays "Copier", and the user gets no "copy failed — select and copy manually" guidance. Why it matters: This is the show-once secret; a user who assumes the copy worked closes the dialog and loses the key permanently (only recourse: create a new one). The select-all fallback exists but is not surfaced as guidance. Fix: Wrap in try/catch; on failure show an error toast telling the user to select the highlighted key and copy manually.
6. Table rows show a clickable affordance but do nothing — Low · A11Y-03-adjacent (proposed AFFORD-01)
Where: services/client/src/components/Table.vue:95 (used by ApiKeys) What: Every <tr> renders cursor-pointer ... hover:bg-surface-25 and emits select on click unconditionally. ApiKeys wires no @select handler (row actions live in the Révoquer button), so rows present a hover+pointer "I'm clickable" affordance that leads nowhere. Why it matters: Mouse users get a false affordance; the row looks interactive but only the button is. Minor here, but it is shared Table.vue behaviour across the app. Fix: Gate the cursor-pointer/hover/@click on whether a select listener is attached.
Unverified
- A11Y-01 (contrast) — not assessable from code (
text-surface-400onbg-surface-100/800in the key box and empty-state description look low-contrast, but needs a contrast tool). - A11Y-06 (responsive / short viewport) — the 420/480px dialogs and the key box need a rendered viewport to judge.
- FORM-06 — PrimeVue
Button :loading("Créer"/"Révoquer") may set the nativedisabledand drop focus to<body>on activation; PrimeVue internals not installed (node_modulesabsent), so this cannot be asserted. - Modal a11y (aria-modal / aria-labelledby / focus trap / focus-return / Esc) — PrimeVue
Dialogwith aheaderprop is expected to supply these, but the library source is not present to confirm. The show-once dialog's:closable="false"is a deliberate acknowledge-before-exit pattern for a one-time secret, not a "trapped modal" defect.
Baseline additions
- FEEDBACK-COPY-01 (proposed) — a copy-to-clipboard control must confirm success and handle failure, telling the user how to copy manually when the Clipboard API is unavailable. (Finding 5.)
- AFFORD-01 (proposed) — a row/element must not present an interactive affordance (pointer cursor, hover state, click handler) unless it is actually actionable. (Finding 6; shared
Table.vue.) - CONTENT-01 — not-applicable — see project-level i18n finding (no i18n layer by design).
- NAV-03 — fails project-wide (no per-route title;
index.html:17is « Tim » everywhere); see PROJECT-LEVEL.md. Not re-filed here.
Cross-project note
- FORM-01 unassociated dialog labels and A11Y-05 icon-only copy button are the kind of hand-rolled dialog markup likely repeated in tt-time-tracker's other create/edit modals (Vehicles, Projects, Task lists) — worth a sweep.
- A11Y-04 no
<h1>is aLayoutMain-shell issue: any tt-time-tracker admin view using the#titleslot inherits it. Mirrors the shell-level<h1>gaps noted for customer-portal (auth/onboarding shells) and playout (VTitle); the root cause differs per project but the class is shared across all four. - AFFORD-01 false-clickable rows lives in shared
Table.vue, so it affects every tt-time-tracker list; the click-only-row A11Y-03 theme is already flagged cross-project in PROJECT-LEVEL.md.