Appearance
[UX] tt-time-tracker — Overview
Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: Dr-Wade/tt-time-tracker · Branch:
develop@bb3238c· Files reviewed: 10 Patterns: none consulted (baseline sufficient; budget conserved)
Summary
The Overview (/admin/overview) is the admin-wide hours table: a filter bar (period + user/project/status), server-computed totals, an infinite-scroll table with batch approve/reject, and per-entry detail modal. It is well built in several respects the baseline cares about — totals come from a real aggregate endpoint rather than the loaded page, reject is confirmed and approve is one-click-with-undo, sort headers are keyboard-operable with aria-sort, and toasts carry role/aria-live. The material gaps are all accessibility: table rows can only be opened with a mouse, and on mobile the header action buttons lose their accessible names.
Findings
1. Time-entry rows can only be opened with a mouse — High · A11Y-03
Where: services/client/src/components/InfiniteTable.vue:98-102 (consumed at services/client/src/views/Admin/Overview.vue:240-242) What: Each data row is a <tr class="cursor-pointer" @click="emit('select', item)"> with no tabindex, role, or key handler. Opening an entry's detail modal (ModalEntryDetails, the only way to view/edit remark, note, times) is a pure click. The sort headers on the same table are keyboard-operable (role="button", tabindex="0", Enter/Space) — so the omission is on the rows specifically, and the sub-row (:130-135) shares it. Why it matters: A keyboard or screen-reader user can filter and read the table but cannot open, inspect, or edit any time entry — a hard barrier on the admin's core review surface. Fix: Make the row a focusable control: tabindex="0", role="button", an accessible name, and @keydown.enter/.space.prevent="emit('select', item)" — mirroring what the header cells already do. (Cross-project theme: click-only rows, see PROJECT-LEVEL.md A11Y-03.)
2. Mobile header action buttons have no accessible name — Medium · A11Y-05
Where: services/client/src/components/Layout/LayoutMain.vue:109-111 (affects Overview's #buttons: Export, Ajouter, "Approuver jusqu'au…", Overview.vue:9-32) What: On mobile the shell hides PrimeVue's .p-button-label with display:none, collapsing each labelled button to its icon. The icon is a decorative <i class="pi pi-download"> with no text. display:none also removes the label from the accessible-name computation, so the button is announced only as "button". The two shell-owned icon buttons beside them (search, close) correctly set aria-label; the slotted action buttons do not. Why it matters: A screen-reader user on a phone hears three unlabelled "button"s for Export / Add / Approve. Fix: Add aria-label to each action button (they already pass label, so mirror it), or hide the label with a visually-hidden utility instead of display:none. Caveat: whether PrimeVue's Button auto-mirrors label→aria-label could not be confirmed — node_modules is absent (PROJECT-LEVEL.md caveat); by default it does not. If a follow-up pnpm install shows it does, this downgrades. Shell-wide, not Overview-only.
3. Filter controls rely on placeholders instead of labels — Medium · FORM-01
Where: services/client/src/views/Admin/Overview.vue:88-162What: The period SelectButton, the year/month/range DatePickers, the user select, the project select and the status Select have no programmatically associated <label>. Identity is conveyed by placeholder text where present (FieldSelectUser.vue:8 "Utilisateur"; status has none — it shows the selected value) and by nothing at all for the SelectButton/DatePickers. Why it matters: A screen-reader user tabbing the filter bar hears "combobox" with no indication of which axis it filters; once a value is chosen even the placeholder is gone. Fix: Give each control a <label> (visually-hidden is fine for a compact filter bar) or an aria-label naming the filter axis.
4. Admin pages have no <h1> — Medium · A11Y-04
Where: services/client/src/components/Layout/LayoutMain.vue:6,74What: The page title slot ("Heures") is rendered as <h2> in both the mobile and desktop headers; no <h1> exists anywhere on the Overview page (grep of src/ finds <h1> in other views but not in Overview or the admin shell). The document heading hierarchy therefore starts at h2. Why it matters: Screen-reader users navigating by heading find no top-level heading, and the level skip misrepresents structure. Affects the whole /admin section, not just Overview. Fix: Render the page title as <h1> in LayoutMain.
5. Whole-list load error is not announced — Low · MSG-01
Where: services/client/src/components/ListErrorState.vue:1-22 (rendered by InfiniteTable.vue:62-77) What: When the entries query fails, InfiniteTable swaps in ListErrorState, which has a retry button but no role="alert" and sits in no live region. A filter/period change that errors updates the DOM silently for assistive tech. (The toast path — layout.showError, toast.success — is fine: PebbleToast.vue:5 sets role and PebbleToastHost.vue is an aria-live region.) Why it matters: A screen-reader user who re-filters into an error sees no rows and hears nothing explaining why. Fix: Give ListErrorState's container role="alert".
6. Small inline text-button targets — Low · A11Y-02
Where: services/client/src/views/Admin/Overview.vue:200-204 ("approuver toute la période"); services/client/src/components/MobileFilterSheet.vue:33-41 ("Réinitialiser les filtres") What: Both are bare text <button>s well under the 24×24 CSS-px minimum (11-13px inline text, no padding to speak of). Why it matters: Hard to hit accurately, especially on touch. Fix: Pad to a ≥24px target or promote to a small Button.
7. Period and filter state is not in the URL — Low · NAV-06-proposed (view/filter state reflected in URL)
Where: services/client/src/stores/filters.store.ts:5-58What: All filter state (year, month, range, user, project, status) lives in plain Pinia refs — no URL sync and no persistence. A browser refresh resets to the current month; a specific filtered/period view cannot be deep-linked, shared, or restored via Back. Why it matters: An admin who filters to e.g. one user's rejected hours in March cannot bookmark or share that view, and loses it on reload. Fix: Reflect the period + filters in query params (the same theme flagged for customer-portal list features in PROJECT-LEVEL.md).
Unverified
- A11Y-01 (contrast): the many
text-surface-400secondary strings (empty-state descriptionOverview.vue:311,ListErrorState/ListEmptyStatedetail text) and the 10px uppercasetext-surface-700totals labels (Overview.vue:167,175) need a contrast tool. - A11Y-06 (responsive): the inline filter bar wraps to a mobile "Filtres" sheet; behaviour at ~700px height / with a keyboard open needs a rendered viewport.
- FORM-06: the batch approve/reject buttons use PrimeVue
:loading; whether that disables the just-activated button and drops focus is a PrimeVue internal —node_modulesabsent. - Finding 2 outcome: depends on whether PrimeVue
Buttonmirrorslabel→aria-label(see caveat there).
Baseline additions
- NAV-06-proposed (view/filter state reflected in URL): list/period/filter selection that changes what data is shown should be reflected in the URL so it survives refresh and can be deep-linked. (One of several colliding NAV-06 proposals — orchestrator to reconcile per PROJECT-LEVEL.md.)
- CONTENT-01: not-applicable — see project-level i18n finding.
- NAV-03: fails project-wide (
index.html:17is "Tim" everywhere) — see PROJECT-LEVEL.md; not re-filed here. - Positive confirmations worth recording: totals use the server
entryControllerAggregateendpoint (allEntries.store.ts:88-90), not a page sum — the "aggregate from one page" theme in PROJECT-LEVEL.md does not apply to this screen. Reject is confirmed (MSG-05,Overview.vue:588); approve is one-click with an 8s undo toast (justified deviation).
Cross-project note
- A11Y-03 (click-only rows): already confirmed in all four projects (see PROJECT-LEVEL.md cross-project table); this is another instance.
- FORM-01 (placeholder-only filter labels): likely present in customer-portal / members filter bars — worth checking their list filters.
- Finding 2 (mobile label hidden via
display:none): tt-time-tracker–specific — it comes from this repo'sLayoutMain, not a shared library.