Appearance
[UX] tt-time-tracker — Jobs (superadmin)
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: user-feedback/progress-indicator
Summary
The Jobs monitor is a superadmin BullMQ dashboard: stat tiles by status, a table of recent invoice-processing jobs, and a per-job detail dialog with logs. It polls every 5s (TanStack refetchInterval) and — to its credit — wires the shared Table's error/retry surface correctly, unlike other tt list screens. The one important defect is that opening a job's details (logs, error, return value) is possible only by clicking a table row: rows are not keyboard-operable, so a keyboard or screen-reader superadmin cannot reach the diagnostic content this page exists to show. The remaining findings live in shared shell/table components and therefore recur across every admin list.
Findings
1. Job detail is unreachable without a mouse — High · A11Y-03
Where: services/client/src/components/Table.vue:92-97 (consumed by views/Admin/Jobs.vue:35-43) What: Each row is a bare <tr class="…cursor-pointer" @click="emit('select', item)"> with no tabindex, no role, and no keydown handler. The only way to fire @select — which opens the detail Dialog carrying the logs, failedReason and returnValue (Jobs.vue:292-311) — is a pointer click. (The sort headers in the same component are keyboard-enabled at :29-33, so the row omission is inconsistent even within the file.) Why it matters: The entire purpose of this superadmin screen is diagnosing failed jobs; a keyboard-only or screen-reader operator can see the row summary but can never open the error detail or logs. A11Y-03 (keyboard reachable + operable). Fix: Make the row a focusable, activatable control — e.g. render the first cell as a <button>, or give the <tr> tabindex="0" + role="button" + @keydown.enter/.space mirroring the existing header pattern. Fix in Table.vue so every list feature benefits.
2. Refresh button loses its accessible name on mobile — Medium · A11Y-05
Where: views/Admin/Jobs.vue:7-14 (button) + components/Layout/LayoutMain.vue:109-111What: The « Actualiser » button relies on its visible PrimeVue label for its accessible name (no aria-label). On mobile, LayoutMain's scoped CSS hides slotted button labels (.mobile-header-buttons :deep(.p-button-label){display:none}), leaving an icon-only pi pi-refresh button with no name. LayoutMain's own buttons (search/close) set aria-label; slotted ones like this do not. Why it matters: On mobile the refresh control announces as an unlabeled button (A11Y-05). Affects every page that passes a labelled button into LayoutMain's #buttons slot, not just Jobs. Fix: Add aria-label="Actualiser" on the button (and as a convention on any button placed in the mobile header slot).
3. Live status changes are never announced — Medium · MSG-01
Where: views/Admin/Jobs.vue:17-96 (tiles + table), poll at :248What: The tiles and table re-render every 5s as jobs move active→completed→failed, but nothing is wrapped in a live region; the shared error surface (components/ListErrorState.vue:1-22) also has no role="alert"/aria-live. A DOM swap — including a fetch failure being replaced in — is silent to assistive tech (WCAG 4.1.3). The progress-indicator pattern's "skipping announcement strategy" anti-pattern is exactly this: screen-reader users miss transient state changes when live-region behaviour is absent. Why it matters: A screen-reader superadmin gets no signal when a job fails or when the whole list fails to load. MSG-01. Fix: Give ListErrorState's root role="alert"; wrap the failed-count tile (or a summary line) in an aria-live="polite" region so status transitions are announced.
4. Stat tiles read as a healthy queue when the load fails — Low · MSG-06 (proposed, see PROJECT-LEVEL.md)
Where: views/Admin/Jobs.vue:252-260What: counts defaults to {} on error, so every tile renders ?? 0 — « 0 En cours / 0 Échoués / … » — which looks like a healthy empty queue, while the table directly below correctly shows the error+retry state. Same within-screen split flagged for tt in PROJECT-LEVEL.md (error component used on one half, error swallowed on the other). Why it matters: A superadmin glancing at the tiles to check for failures reads a reassuring "0 failed" that is actually "unknown — fetch failed". Low: it is contradicted by the visible table error immediately below, so it is misleading rather than actively wrong. Fix: When isError, render the tiles in an indeterminate state (« — ») rather than 0, or hide them behind the same error surface as the table.
5. Page has no <h1> — Medium · A11Y-04 (shell-level)
Where: components/Layout/LayoutMain.vue:6 and :74 (both render the page title as <h2>); Jobs.vue:3-5 supplies « Jobs IA » What: The LayoutMain shell renders its title slot as <h2> on both mobile and desktop; there is no <h1> on the page, so the heading outline starts at h2. Why it matters: A11Y-04 (exactly one <h1>, no level skips). This is the shared shell, so it fails identically on every admin screen — recommend promoting to a project-level shell fix rather than filing per feature. Fix: Make the LayoutMain title an <h1> (there is one per routed page).
Unverified
- A11Y-01 (contrast): many faint tokens —
text-surface-300/400on « — » placeholders andtext-[11px] text-surface-400"tentative N" (Jobs.vue:55,65,75,93), plustext-surface-400tile labels. Needs a contrast tool on rendered colours. - A11Y-06 (responsive/short viewport): the detail
Dialog(width:48rem,max-h-96log<pre>,Jobs.vue:98-199) and the 5-column tile grid on small screens need a rendered viewport to judge. - FORM-06 (loading→disabled focus drop): « Actualiser » uses PrimeVue
:loading="isFetching"(Jobs.vue:11); whether PrimeVue'sButtonsetsdisabledwhile loading (dropping focus to<body>) can't be confirmed —node_modulesnot installed (see PROJECT-LEVEL.md caveat). - Dialog focus trap / labelledby: PrimeVue
Dialoginternals unreadable; not asserted. - CONTENT-01 (i18n): not-applicable — see project-level i18n finding.
Baseline additions
A11Y-ROW-ACTIVATE(proposed, likely folds into A11Y-03): a table/list row that is the sole trigger for opening a detail view must be a focusable, keyboard-activatable control, not a click-only<tr>/<li>. Recurs across all four projects (see cross-project note) — candidate for promotion rather than a new rule.
Cross-project note
Finding 1 is the same shape as the confirmed A11Y-03 "click-only div/li rows" row in PROJECT-LEVEL.md's alignment table (fails in playout, customer-portal, members and tt-time-tracker). members' project notes already record row edit/actions as mouse-only in TableData.vue; this is tt's instance in Table.vue. Finding 3 (no live-region announcement on polled/error state) and finding 5 (shell renders title as <h2>, no <h1>) are also shared-shell issues likely mirrored in the other three stacks' list shells.