Skip to content

[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) on hover:bg-danger-bg/40, Profile.vue:74-80), the --text-secondary eyebrow 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-lg column with overflow-y-auto content and a floating bottom nav; behaviour under a ~700px height / open mobile keyboard needs a rendered viewport.
  • PrimeVue Menu internals — the org-switch popup (Profile.vue:83-87) and its keyboard/focus/aria behaviour live in PrimeVue, whose node_modules is 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 profile itself (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.html title « 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.