Skip to content

UX audit — cross-project alignment & rule reconciliation ​

Capstone of the 2026-07-30/31 overnight batch. 114 features audited across four Vue apps (playout 21, customer-portal 37, members 31, tt-time-tracker 25), each against BASELINE.md and the local UX Patterns MCP. Every audit was read-only and nothing was filed as a GitHub issue — this is documentation to review and act on.

This file is the reviewer's entry point. It does three things:

  1. Reconciles the proposed new rule IDs (agents ran in parallel and collided on IDs).
  2. Completes the cross-project alignment table now that all four apps are fully audited.
  3. Ranks the cross-cutting issues, each with the reference implementation to copy.

Per-feature detail lives in drafts/<project>--<feature>.md (114 files). Architectural root-cause notes live in PROJECT-LEVEL.md. This file does not repeat them; it organises and prioritises.


1. Read these first — the seven headline findings ​

Ranked by severity, not by project. Each links to the draft with the evidence.

  1. Fabricated customer-facing telemetry — customer-portal LiveTracking.LiveTrackingPage.vue presents synthetic operating hours, battery, fuel and GPS pins (hashed from the asset id / string-matched to ~20 hardcoded cities) as "real time". Route resolves and is gated only on read CustomerAsset, so it is reachable by deep link despite the hidden nav entry. The one place in the programme a user is shown invented facts about their own equipment with no tell. Proposed DATA-SYNTHETIC. → customer-portal--customer-assets.md. Review first; decide whether the route should resolve at all before release.

  2. Offboarding does not revoke access — tt-time-tracker. archived is a display flag; no auth path reads it, no sessions are deleted, API DELETE is also just archive. A removed employee keeps signing in and logging hours while invisible to every admin screen. Blocker. Fix = adopt customer-portal's per-request permission rebuild (see §4). → tt-time-tracker--users.md (pre-batch) and the offboarding thread in PROJECT-LEVEL.md.

  3. Admin-role grant via window.prompt, no confirm/validation/OOB notice — members Superadmin. Plus org/sub-org deactivation and module removal persist on one unconfirmed click, and a failed orgs load renders as "no search match". Three High findings on one screen. → members--superadmin.md.

  4. Password set in playout Profile can never be changed or recovered on develop. Profile lets a user attach a password; there is no updatePassword/reset on the audited branch (those live only on unmerged PR #437). Reframes #437 as the fix for an active lockout path, not a nicety. → playout--profile.md, PROJECT-LEVEL.md §"unmerged password-reset".

  5. Cross-sub-org data exposure — members widgets (DATA-01).DonsProgress.vue reads an undeclared suborg prop, so the host attribute is dropped and the widget always authenticates as b-active, able to show another sub-org's donation totals. Same undeclared-prop shape in four sibling widgets. → members--donation-progress.md (pre-batch) + PROJECT-LEVEL.md.

  6. Evening dates save one day early — members Events (FORM-13).SelectDateTime concatenates a UTC date with a local time; all four Events date fields serialise the wrong wall-clock day, and downstream year-derivation compounds it. Silent data corruption. → members--events.md.

  7. /dev component gallery ships to production, publicly reachable — tt-time-tracker (SEC-DEV-SURFACE). No requiresAuth, no build-time strip in vite.config.ts. Leaks no sensitive data (static demo content), so Medium — but it should be build-stripped, not merely unauthenticated. → tt-time-tracker--component-gallery.md.


2. Reconciled rule IDs — proposed additions to BASELINE.md ​

Agents proposed new rules independently and reused the next free ID, so IDs collided. Below is the canonical set: one meaning per ID, with the drafts to rewrite once these land. Nothing here is in BASELINE.md yet — treat as the review queue for extending the rubric. Grouped by family.

MSG (feedback) ​

  • MSG-06 — failed load must be a distinct, recoverable state. A fetch/SDK failure must never render as an empty state, a permanent skeleton, a first-run "get started" screen, or a success. Must show an error and a retry. This absorbs the separately-proposed (a)/(b)/(d) MSG-06 variants and the playout "success fires before the write resolves" shape. Most-confirmed rule of the batch — all four projects, ~20 drafts.
  • MSG-07 — action feedback must reflect the real outcome. (was MSG-STATUS-STYLE, DATA-STATUS-FIDELITY.) A result banner's severity/colour must map to what actually happened, and a status indicator must map every backend enum value to a distinct treatment. Sightings: pmo sync log always styled error; commands severity.ts maps a non-existent PROCESSING and collapses QUEUED/RUNNING/RETRYING/CANCELED to one amber.
  • NAV-06 — consumed token stripped from the URL after use (reset/invite/verify).
  • NAV-07 — list/detail view state (tab, filters, period, page) reflected in the URL and restored on Back. (was NAV-URL-STATE; customer-portal has no URL sync or KeepAlive anywhere — Back resets every list.)
  • NAV-08 — every user-relevant routable surface has a discoverable in-app path. (was NAV-ENTRYPOINT; tt Organizations' organization-details has zero in-app links — URL-only.)
  • (async-guard-timeout and await-auth-before-branching are SEC/robustness, see below.)

FORM ​

  • FORM-12 — warn before discarding entered/edited form data. (Members Events detail, members Settings, superadmin inline edits all lose work silently on nav/reload.) This is the canonical FORM-12; the other "FORM-12" proposals are re-homed: capture=/gallery → FORM-15, inert filters → FORM-16.
  • FORM-13 — date/time serialisation preserves the intended wall-clock day. (members Events timezone bug.)
  • FORM-14 — native input validity only fires inside a <form>. A field saved via a click handler validates nothing; wire real validation. (was FORM-VALIDATE-CONTEXT; customer-portal admin-configuration.)
  • FORM-15 — file capture= must not suppress the gallery/file picker on desktop. (from earlier batch.)
  • FORM-16 — controls must not be inert (filters that never reach the API).
  • FORM-17 — a resource with enabled/disabled state offers a non-destructive toggle; delete/disconnect must not be the only "stop". (was FORM-TOGGLE-DISABLE; tt Integrations.)

A11Y — the old "A11Y-07" split; click-only rows unified ​

  • A11Y-03 (existing, broadened) — anything that acts as a control is a real control. Rows/cards/chips/<li>/<div @click> that open, select or activate must be focusable and key-operable (Enter/Space) with a role. This absorbs the proposed A11Y-ROW-SEMANTICS, A11Y-ROW-ACTIVATE, A11Y-08-clickable and AFFORD-01 — do not mint those. Confirmed all four projects.
  • A11Y-07 — state must not be conveyed by colour alone, and selection/toggle/ current state must be exposed programmatically (aria-pressed, aria-current, aria-selected, text). (roles-permissions matrix is colour-only; release-notes active version colour-only; objectives score colour-only.)
  • A11Y-08 — non-text content has a text alternative. Charts need role="img"+label or a data-table equivalent; meaningful images need alt, decorative ones alt="". (b-active-dashboard rings; tt error illustration.)
  • A11Y-09 — SPA route changes are announced and move focus to the new view's heading. (was A11Y-ROUTE-ANNOUNCE; likely all four SPAs.)
  • A11Y-10 — a drawn/required input (signature) has a non-pointer path. (was A11Y-SIGNATURE; customer-portal orders-logistics signature canvas.)
  • A11Y-11 — every page exposes one <h1> inside a <main> landmark. Shell- level: tt LayoutMain renders the title as <h2> with no <h1>/<main>; chrome-less error/auth routes across projects sit in no landmark. (This is the positive statement of the recurring A11Y-04 shell findings.)

SEC / robustness ​

  • SEC-03 (existing, tightened) — a password change revokes other sessions by default, not as an opt-in the user must discover and tick. (customer-portal & tt profiles both default-off.)
  • SEC-07 — stored scope id (tenant/account/org) is a hint to validate against current membership, never authoritative. Revalidate on boot and clear on sign-out. Single rule for the three-project "persisted scope" defect (§4).
  • SEC-08 — internal/dev surfaces are build-stripped, not merely unauthenticated. (was SEC-DEV-SURFACE; tt /dev.)
  • SEC-09 — async route guards are bounded (timeout + retry + error surface). members while(!session.initialized) and tt's isPending/role waits hang forever on a failed dependency. (Robustness sibling of the NAV split.)

DATA (data integrity) — new family, high-value ​

  • DATA-01 — a component declares every attribute the host can set and never reads an undeclared prop. (members widgets cross-sub-org exposure.)
  • DATA-02 DATA-TRUTHFUL-AGGREGATE — a displayed total/count/aggregate covers the whole set or states its coverage. No page-scoped sums under a whole-set label. Absorbs DATA-TRUTHFUL-COUNT, FILTER-SCOPE, DATA-PAGE-AGGREGATE, DATA-PAGE-SCOPE. (customer-portal dashboard/trade-in/logistics/sync; tt projects/overview/admin-dashboard.)
  • DATA-03 DATA-SYNTHETIC — placeholder/sampled/derived data is never presented as measured. If a surface ships before its source exists, it says so on the surface. (customer-portal LiveTracking — headline #1.)
  • DATA-04 DATA-SCOPE-01 — the active scope is read reactively at every use site and re-read immediately before any write. Conditionally a defect: a remount saves you on read, nothing saves you on write (customer-portal service- plans Blocker vs dashboard non-defect — keep both, do not collapse).
  • DATA-05 DATA-ID-BOUNDARY — send the id the target layer expects. (customer-portal Contacts/Locations send Dataverse externalId where the repo looks up the local uuid → every edit 404s.)
  • DATA-06 — a figure's scope label matches and reacts to the data shown. (was DATA-LABEL-SCOPE/DATA-TEMPORAL-DEFAULT; b-active-dashboard year label vs pinned getFullYear(); b-active-memberships hard-coded Annee:2026.)

CONTENT ​

  • CONTENT-05 — currency precision: money is formatted at its stored precision (2 fraction digits), and a correct shared formatter is not a pass — grep toFixed(, Math.round(, parseInt( against money too. (§4 currency.)
  • CONTENT-06 — render enum/status via its display label, never the raw stored key, consistently across views. (was CONTENT-DISPLAY-LABEL; members Sponsors detail shows raw prospect/formule-1.)
  • CONTENT-07 — a view named for an action exposes that action in-app. (was ACTION-SURFACE-01; members relances list candidates but has no send/select.)

Responsive / feedback polish ​

  • RESP-01 RESPONSIVE-CONTROL-PARITY — desktop list/search/filter controls stay reachable on mobile. Asserted from code (desktop-only #filters slot), so distinct from the render-only A11Y-06. (customer-portal Emails.)
  • FEEDBACK-COPY-01 — a copy-to-clipboard control confirms success and handles clipboard failure with manual-copy guidance. (tt API keys.)

LIVE — broadcast/on-air (playout only, no baseline equivalent) ​

Keep as its own section; MSG/A11Y don't cover an on-air state. LIVE-01 on-air output always visible + clear-vs-navigate distinguishable; LIVE-02 live state never colour-alone, transitions announced; LIVE-03 an inverse control doesn't replace its counterpart in place. Plus: global keyboard shortcuts unbound on unmount (playout Mousetrap ctrl+f bound in five views, never unbound).


3. Completed cross-project alignment table ​

All four apps now fully audited, so the earlier blanks are filled. ✓ = conforms, ✗ = fails, — = not applicable.

Theme (canonical rule)playoutcustomer-portalmemberstt-time-tracker
NAV-03 per-route document titles✗ none✗ one static✗ one static✗ one static
A11Y-03 click-only rows/cards/chips✗✗✗✗
MSG-06 failed load rendered as empty/success✗✗✗✗
A11Y-11 <h1>/<main> on every page✗ (VTitle dbl-h1)✗ auth/onboard shells✗ some✗ LayoutMain h2
SEC-01 account enumeration✗✗—✗
SEC-07 persisted scope revalidated✗✗—✗
DATA-02 page-scoped aggregate shown as whole—✗~✗
CONTENT-05 currency precision✓ no money path✗ 9 sites + bid øre✗ widgets + .toFixed(0) admin bypass~ invoices (see draft)
Offboarding actually revokes access✗ no last-admin guard✓ reference~ no client guard, server Unverified✗ Blocker
404 catch-all that explains + links out✗ has route, chrome-less dead end✗ missing✗ missing✓ reference (NotFound.vue)
Forbidden/unauthorized surface~ dead-code route, inline 403✓✓✗ silent redirect
Zero-scope dead end (no tenants/orgs)✗——✗

Two reference implementations emerged from the batch — copy these:

  • Offboarding/revocation: customer-portal PermissionsService.buildContext re-reads user + links + roles from Postgres on every ability-gated request and filters status === "linked"; no session snapshot, so unlinking revokes on the next request. Recommend this shape for tt's OrganizationGuard.
  • 404: tt-time-tracker NotFound.vue — one <h1>, human French copy, a context-aware primary CTA plus a back link. The others should match it (not copy playout, whose error routes are chrome-less dead ends). Residual: it still sits outside LayoutApp, so no <main> landmark — fix under A11Y-11.

4. The four cross-cutting issues worth one ticket each ​

These are single root causes behind many per-feature findings. Fix once.

  1. MSG-06 — "the error components exist and aren't used." Every app has error/ retry primitives (ListErrorState, Table error props, DataTable, TableData) that callers skip, so failures render as empty/success. tt is the sharpest example: ProjectList wires it, ProjectDetails doesn't; Dashboard summary has retry, its entry list swallows errors. One convention + a lint that flags a query used without its error branch.

  2. A11Y-03 — click-only rows, one shared table each. Root cause is the shared list component per app (playout @playout/ui MenuDropdown, customer-portal packages/ui DataTable, members TableData.vue, tt Table.vue). Fixing the four shared components clears the bulk of the a11y findings across ~60 features.

  3. SEC-07 — persisted scope treated as authoritative. playout localStorage.tenant (a public screen URL can reassign your default), customer-portal admin-selected-account (never cleared on sign-out, next user inherits it), tt org id (revalidation branch unreachable behind an early return). One rule, same fix shape three times.

  4. CONTENT-05 — currency precision, and a correct formatter is not a pass. members admin useCurrency.ts is correct yet Cotisations.vue:136 and SponsorApp.vue:421 bypass it with .toFixed(0); customer-portal has nine hand-rolled formatters and the bid input accepts øre so re-saving rewrites the amount. Audit money by grepping toFixed/Math.round/parseInt, not by reading the shared formatter.


5. Whole-class defects (one app, but not one feature) ​

  • customer-portal CASL subject-name mismatch. router/index.ts and AppLayout.vue gate on bare-string subjects ("Contact") that don't match what the API grants ("CustomerContact"), so every non-admin is redirected to /forbidden and the nav entry never appears — invisible to admins because of manage all. ~30 abilityCheck subjects have no type relationship to the permission catalogue. Fix: a shared typed subject union so the compiler catches the next rename. → customer-portal--contacts.md.
  • playout @playout/ui. VDialog opacity-hide (closed dialogs stay tab- reachable), global unguarded Escape (fires the wrong handler on Bible overlay — writes stale reference to the live document, rated Blocker), broken aria-labelledby, VTitle double-<h1>, VButton disabled focus-loss. One issue each against the library, cross-referenced from feature drafts.

6. Verification debt (settle with one command) ​

node_modules is not installed in any checkout, so PrimeVue / @bcc-code / external internals could not be read; agents correctly filed those under Unverified rather than asserting. Recurring: PrimeVue Button :loading→disabled focus drop (FORM-06), Message role="alert" (MSG-01), Card/Panel title element (A11Y-11). pnpm install in each repo lets a follow-up pass settle these. A11Y-01 (contrast) and A11Y-06 (responsive/short-viewport) need a rendered page and are Unverified by design in every draft.

In-repo exceptions (asserted from source, not Unverified): playout packages/ui & packages/schemas; customer-portal packages/ui (PageLayout, MobileListHeader, DataTable).


7. Suggested review order ​

  1. §1 headline #1 (LiveTracking DATA-SYNTHETIC) — a release-gate decision.
  2. §1 #2–#4 (offboarding, superadmin window.prompt, playout password lockout) — the security/lockout cluster.
  3. §4 the four one-ticket root causes — biggest coverage per fix.
  4. §2 rule reconciliation — approve the canonical IDs, then the drafts can be rewritten to cite them and (optionally) filed.
  5. Everything else per-project via the drafts.

No issues were filed. When you're ready to file, the rule IDs in §2 should be settled first so drafts cite a stable rubric.