Appearance
[UX] members — PMO
Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: bcc-nancy/members · Branch:
develop@2c22f5a· Files reviewed: 6 Patterns: none consulted (baseline + PROJECT-LEVEL TableData cluster fully cover table + filter-panel)
Summary
PMO is a thin wrapper over the shared TableData (a member worklist with search, column filters via FilterDrawer, and a row → ModalMember), plus an admin-only PMOSync panel that triggers a server sync. It therefore inherits the whole already-confirmed TableData defect cluster (see PROJECT-LEVEL.md — do not re-file). The PMO-specific issues all live in the sync panel and one emoji cell: the sync button disables itself on click (FORM-06), the raw error string is shown verbatim (MSG-02), and a completed sync is never announced (MSG-01). No blocker.
Findings
1. Sync button disables itself the instant it is clicked — Medium · FORM-06
Where: admin/src/client/components/app/PMOSync.vue:31-37What: <BccButton :disabled="pmo.syncRunning" @click="pmo.sync" />. syncRunning is api.loading, which flips true synchronously inside the click handler, so the just-activated control becomes disabled in the same tick. Why it matters: FORM-06 — disabling a control the user just activated drops keyboard focus to <body>, so a keyboard/AT user loses their place and (combined with finding 3) is given no spoken confirmation the action started. The label does change to « Synchronisation… », which is good waiting-state copy (CONTENT-04), but that does not restore focus. Fix: Keep the button enabled and guard re-entry in the handler; convey the in-flight state with aria-busy="true" + the label swap rather than disabled. Note: whether the native disabled focus-drop actually fires depends on how @bcc-code/component-library-vue's BccButton renders — its internals are not readable (node_modules absent), so the exact DOM consequence is Unverified; the disable-on-click wiring itself is asserted from the template.
2. Raw sync error string shown verbatim to the admin — Medium · MSG-02
Where: admin/src/client/stores/pmo.store.ts:41-48; surfaced at PMOSync.vue:40-44What: On sync failure the store stores err instanceof Error ? err.message : "Failed to sync" into syncLogs.log, which PMOSync renders directly in a BccMessage. The err.message is an unmapped provider/network string (e.g. a fetch or HTTP error), and the fallback is the English literal « Failed to sync » in an otherwise-French UI. Why it matters: MSG-02 — raw SDK/transport prose reaches the user with no mapping to a human sentence and no next step (MSG-03). This is a fresh, admin-side instance, distinct from the widgets-level widgets/src/api.ts raw-HTTP <status> finding already in PROJECT-LEVEL.md. Fix: Map known failure codes to French sentences with a recovery hint ("Réessayez dans quelques minutes / contactez un administrateur"); never render err.message and never fall back to English copy.
3. A completed sync is never announced — Medium · MSG-01
Where: admin/src/client/components/app/PMOSync.vue:6-38, stores/pmo.store.ts:41-48What: On success sync() → fetchLogs() silently swaps the « Dernière synchronisation » date and the Créées/Mises à jour/Total stat tiles. None of these sit in an aria-live/role="status" region, and no toast fires. Why it matters: MSG-01 (and design principle #4, "feedback that reassures") — a screen-reader user gets nothing back after pressing Synchroniser; the only signal is a visual DOM swap. Compounded by finding 1, which drops focus so even the label revert isn't spoken. Fix: Wrap the last-sync line / stats (or a dedicated confirmation) in role="status" aria-live="polite", or emit a BccToast on completion.
4. Sync log is always styled as an error — Low · MSG-01 (see Baseline additions)
Where: admin/src/client/components/app/PMOSync.vue:40-44What: <BccMessage v-if="pmo.syncLogs.log" severity="error"> renders whenever a log value exists, hardcoded to error. The syncLogs.status field ("failed" vs otherwise) is never consulted, so any informational/success log the /api/sync/status endpoint returns would show in red error styling. Why it matters: Miscoloured feedback tells the admin a successful sync failed. Severity is Low because it is unconfirmed whether the API populates log on success. Fix: Derive severity from syncLogs.status (status === "failed" ? "error" : "info").
5. « API » column meaning carried by a bare ⚠️ emoji — Low · A11Y-05
Where: admin/src/client/views/Admin/PMO.vue:95-99What: The Not_in_core_api column header is « API » and each flagged cell renders the string "⚠️" with no text, title, aria-label or legend. Why it matters: The meaning ("this member is absent from the core API") is conveyed by an icon alone — a screen reader announces "warning" with no context, and no sighted user can decode « API » + ⚠️ without documentation. A11Y-05 in spirit (icon-only, no accessible name), though the cell is non-interactive. Fix: Pair the emoji with visually-hidden text (« Absent de l'API principale ») or a tooltip, and give the column a clearer header.
Inherited from the shared TableData cluster — already filed, DO NOT re-file
PMO renders through TableData.vue, so per PROJECT-LEVEL.md (members §"TableData.vue"):
- A11Y-03 / mouse-only — sortable
<th>arecursor-pointerdivs with a bare@clickand no keydown/role (TableData.vue:91-97); row-open is a<tr>@click(:151); the « Désactiver » row action is right-click-only (:142-152). All of PMO's sortable columns (Age, Date, Genre, Formations, Tags, API) and its row → member modal are therefore keyboard-unreachable. - MSG-06 / failed load renders as empty —
TableData:129-140has noisErrorbranch, so a failed/api/membresload shows « Aucun élement », not an error. - FilterDrawer focus handling is broken (same source). These are the members-wide shared-component findings; recorded here only as a cross-reference.
Unverified
- A11Y-01 (contrast) — the tag palette in
TagMember.vue:45-79assigns one of 34*-subtle/-subtler/-bolderBCC contexts by a hash of the tag string; several-subtlertokens on light text may fail 4.5:1. Needs a contrast tool on a rendered page. (Tag text is present, so this is not a colour-only-meaning defect.) - A11Y-06 (responsive / short viewport) — the
PMOSyncflex-wrap header and the fixed-layout table need a rendered narrow/short viewport to judge. - FORM-06 focus-drop DOM consequence — depends on
BccButtoninternals (node_modulesabsent); see finding 1. - MSG-01 on BccMessage — whether
BccMessage severity="error"carriesrole="alert"is a library-internal detail that cannot be read from source.
Baseline additions
- CONTENT-01 — not-applicable (members has no i18n layer; see project-level i18n finding).
- DATA-01 (undeclared host-mount prop) — not-applicable: PMO is a routed admin view, not an embedded widget, so there are no host mount attributes to drop.
- Proposed
MSG-STATUS-STYLE(orchestrator to renumber): feedback styling (severity/colour, icon) must reflect the operation's actual outcome, never be hardcoded to one severity. Covers finding 4; adjacent to the MSG-06 family.
Cross-project note
The sync-panel patterns are not members-specific:
- FORM-06 (disable-on-click) and MSG-01 (unannounced async completion) are very likely present on any admin action button in customer-portal and tt-time-tracker — check their bulk/sync/refresh controls.
- MSG-02 (raw error string surfaced) already fails project-wide in customer-portal (
getErrorMessage) and tt-time-tracker per PROJECT-LEVEL.md; this is the members admin-store instance. TheTableDatacluster itself is members-only (customer-portal's equivalent is itspackages/uiDataTable).