Skip to content

[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> are cursor-pointer divs with a bare @click and 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-140 has no isError branch, so a failed /api/membres load 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-79 assigns one of 34 *-subtle/-subtler/-bolder BCC contexts by a hash of the tag string; several -subtler tokens 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 PMOSync flex-wrap header and the fixed-layout table need a rendered narrow/short viewport to judge.
  • FORM-06 focus-drop DOM consequence — depends on BccButton internals (node_modules absent); see finding 1.
  • MSG-01 on BccMessage — whether BccMessage severity="error" carries role="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. The TableData cluster itself is members-only (customer-portal's equivalent is its packages/ui DataTable).