Appearance
[UX] tt-time-tracker — Profile
Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: Dr-Wade/tt-time-tracker · Branch:
develop@bb3238c· Files reviewed: 6 Patterns: authentication/account-settings
Summary
profile is a thin, read-only account card: identity, two hours-stats tiles, an org switcher, an admin/user-space toggle, and sign-out. There are no editable fields, so most FORM rules are not-applicable. The two real issues are that a password-based user is sent to their administrator to change a password the app can actually self-service, and that a failed stats fetch renders as a plausible-looking "0" with no error surface. Nothing here is a blocker.
Findings
1. Password change is a manual admin request, though self-service exists — Medium · MSG-04
Where: services/client/src/views/Profile.vue:89-94What: For a local (username/password) user the only password affordance is a static warning: « Pour changer votre mot de passe, contactez votre administrateur. » It is plain text — no link, no button. Yet the app ships a working self-service path: forgot-password / reset-password routes and a forgetPassword() client call (services/client/src/lib/auth.ts:18). The profile neither offers a "change password" flow nor links the reset flow it already has. Why it matters: A user who wants to rotate their password is told to open a support channel and wait, when the product could let them do it in seconds. It is a dead end that points at a slower path than the one that exists. Fix: Surface a "Changer le mot de passe" action that either opens a change-password form (current + new, re-auth) or, at minimum, links the existing reset flow instead of instructing the user to email an admin.
2. Failed hours-stats fetch renders as "0", indistinguishable from a real zero — Medium · MSG-06 (project-level)
Where: services/client/src/views/Profile.vue:156-181What: The « Ce mois » / « Cette année » tiles read monthData.value?.total ?? 0 and yearData.value?.total ?? 0. The two useQuery calls expose no error or loading branch, so an errored aggregate request paints "0 h" with the same styling as a genuine zero, and the value also flashes "0" during load before the real total arrives. Why it matters: A user glancing at their own recorded hours can be shown "0" for the month when the request actually failed — a silently wrong number about their own time, with no retry and no indication anything went wrong. Fix: Gate the tiles on isError / isPending (skeleton while loading, an error/retry affordance on failure) rather than coalescing every non-value to 0. This is the project-wide "error components exist and are not used" theme — see PROJECT-LEVEL.md (tt-time-tracker, MSG-06).
3. Routed page content sits outside any <main> landmark — Low · A11Y-04
Where: services/client/src/components/Layout/LayoutApp.vue:10-13What: The shell renders <RouterView> (which mounts Profile and every other app view) inside plain <div>s; the only landmark in the shell is the bottom <nav>. Profile itself has exactly one <h1> (« Mon compte »), so the heading rule passes, but there is no <main> for a screen-reader user to jump to. Why it matters: Assistive-tech users cannot skip the repeated nav chrome to reach page content — it fails the "content sits in landmarks" half of A11Y-04. Fix: Wrap the <RouterView> in <main> (layout-level fix, benefits every route, not just Profile).
Unverified
- A11Y-01 (contrast) — the danger sign-out row (
text-(--danger)onhover:bg-danger-bg/40,Profile.vue:74-80), the--text-secondaryeyebrow and stat captions, and the accent avatar chip all need a contrast tool against computed CSS-variable values; not settleable from source. - A11Y-06 (responsive / short viewport) — Profile is a fixed
max-w-lgcolumn withoverflow-y-autocontent and a floating bottom nav; behaviour under a ~700px height / open mobile keyboard needs a rendered viewport. - PrimeVue
Menuinternals — the org-switch popup (Profile.vue:83-87) and its keyboard/focus/ariabehaviour live in PrimeVue, whosenode_modulesis not installed in this checkout; cannot assert A11Y-03/A11Y-05 on it.
Not-applicable / not re-filed
- FORM-01…11 — no editable inputs on this view; the profile is read-only.
- SEC-03 / SEC-05 — no password change or password field occurs on
profileitself (it defers to admin, see finding 1), so neither rule is exercised here. Note for the alignment pass: this view also exposes no session management (active-sessions list / "sign out other devices"), which the authentication/account-settings pattern lists as a core security control — a gap worth checking against #17 Settings where credential controls may live. - CONTENT-01 — not-applicable — see project-level i18n finding.
- NAV-03 — fails project-wide (
index.htmltitle « Tim » everywhere); see PROJECT-LEVEL.md, do not re-file.
Baseline additions
none. (Finding 2 is the already-proposed MSG-06 family; no new rule needed.)
Cross-project note
- Finding 1 mirrors the playout Profile gap recorded in PROJECT-LEVEL.md (a password set via Profile that cannot be self-changed). tt-time-tracker is the milder case — a reset flow exists but is not surfaced. Worth a single cross-project line: "every credential-holding Profile should link the app's own password-change/reset path rather than routing users off-product." Likely also relevant to customer-portal and members profile/settings surfaces.
- Finding 2 is the cross-project MSG-06 shape (failed load renders as a benign empty/zero state) already confirmed in customer-portal, members and elsewhere in tt-time-tracker.