Files
Project-SDE-WP-Suite/docs/waves/wave-12.md
Matt Mabrey 75ac930d0c waves 11-13: planning docs for CR-019, CR-020, D18
CR-019 - usage/activity metrics (admin console), wave 11
CR-020 - bulk editing of users, wave 12
D18    - Okta/AD deprovisioning detection and auto-disable, wave 13

Raised by Matt Mabrey 2026-09-17. Decision record and task breakdowns
only in this commit - no feature code yet.
2026-09-18 09:21:29 -07:00

7.3 KiB

Wave 12 — Bulk editing of users

Items: CR-020 Depends on: wave 10 merged (it is). Independent of wave 11 (CR-019) — no shared files, may build in either order or in parallel. Decision record: docs/waves/decisions-2026-09-17.md

Six tasks, in build order.


T12.1 — CR-020: bulk endpoints

  • Items: CR-020
  • Depends on: nothing (first task)
  • Blocks: T12.2, T12.4
  • Surface: server/
  • Files: server/app.py

Do: Add bulk variants of the four existing single-user actions — role change, active/disabled, project assignment (add/remove + project-role), and delete. Each takes a list of user_ids plus the action's parameters and applies require_see_user/require_manage_user/grantable_roles per row, exactly as the single-user endpoint does today — a super user's bulk request cannot reach further than their existing single-user requests can. Do not skip the self-action guard: an actor's own account is rejected out of any batch that would disable, demote, or delete it, same as today.

Each successful row writes its own AuditLog entry via log_event, same action names the single endpoints already use. A row that fails is reported in the response (user id, reason) and does not stop the rest of the batch from being attempted.

Do not: invent a single opaque "bulk_action" audit entry in place of the per-row entries. Do not build a soft-delete path for bulk delete that doesn't exist for the single case — bulk delete stays a hard delete, matching delete_user today.

Done when:

  • each of the four bulk actions is callable with a list of user ids and a single set of parameters
  • a batch containing the actor's own account rejects only that row, not the whole batch — verified for disable, demote, and delete
  • a super user's batch that includes a user/project outside what they manage rejects only that row, with a stated reason
  • every successful row produces its own AuditLog entry, identical in shape to what the single-user endpoint would have written
  • a batch with some failing rows still applies to the rows that succeed, and the response lists exactly which rows failed and why

T12.2 — CR-020: row selection in the User Directory table

  • Items: CR-020
  • Depends on: T12.1
  • Blocks: T12.3
  • Surface: html/
  • Files: html/users.js, html/users.html

Do: Add a checkbox per row and a select-all control, respecting whatever filter (role, active/disabled, project) is currently applied — select-all selects the filtered set, not every user in the system regardless of what's shown. A bulk-action toolbar appears once at least one row is checked and disappears at zero.

Done when:

  • select-all selects exactly the rows currently visible under the active filter, not the full unfiltered table
  • changing the filter while rows are selected does something sane and visible (either clears the selection or keeps it explicit which rows are still selected) — pick one and state it, don't leave it undefined
  • the toolbar is keyboard-reachable and only present when >=1 row is selected

T12.3 — CR-020: bulk-action toolbar

  • Items: CR-020
  • Depends on: T12.2
  • Blocks: T12.5
  • Surface: html/
  • Files: html/users.js

Do: Wire the toolbar to T12.1's endpoints for role change, activate/ disable, and project assignment (add to / remove from a project + project- role). Confirmation before applying uses the wp-dialog kit (T7.9) — wpConfirmDialog, not a native confirm() — naming exactly how many users are affected. On completion, report per-row results if anything failed (T12.1 already returns this) rather than a single success/failure toast that hides a partial failure.

Delete is built separately, in T12.5 — do not wire delete here.

Done when:

  • role change, activate/disable, and project assignment each work end to end against a multi-row selection
  • the confirmation dialog names the exact affected count before anything is sent
  • a batch with a partial failure shows which rows failed, not just an undifferentiated error
  • aria-live announces the outcome

T12.4 — CR-020: CSV upload path

  • Items: CR-020
  • Depends on: T12.1
  • Blocks: T12.6
  • Surface: html/ + server/
  • Files: html/users.js, server/app.py

Do: An upload accepting a list of usernames plus the action to apply, following the validate-and-report pattern CR-005 established: reject and report bad rows (username not found, actor lacks permission over that user) rather than silently dropping them. This is a second entry point onto the same T12.1 endpoints, not a third implementation of the bulk logic.

Done when:

  • a CSV with a mix of valid and invalid usernames applies to the valid rows and reports the invalid ones by row, with a reason
  • the same permission/self-action guards from T12.1 apply here — a CSV cannot reach a user a checkbox-driven batch couldn't
  • duplicate usernames in one CSV are handled without double-applying or erroring confusingly

T12.5 — CR-020: bulk delete confirmation

  • Items: CR-020
  • Depends on: T12.3
  • Blocks: T12.6
  • Surface: html/
  • Files: html/users.js

Do: Wire delete into the toolbar with a heavier confirmation than the other three actions, per the decision record's recommendation: list the affected usernames and require typing a confirmation phrase (e.g. DELETE) before the request is sent, regardless of how many rows are selected. This is flagged in the decision record as a recommendation Matt has not explicitly signed off on — if the PR reviewer wants a lighter or heavier mechanism, that's the moment to change it, not a reason to skip building a real confirmation now.

Done when:

  • the affected usernames are listed in the confirmation dialog before delete is sent
  • the request is not sent until the confirmation phrase is typed correctly
  • the actor's own account, if somehow selected, is rejected with a clear reason rather than silently included or silently dropped

T12.6 — CR-020: verification

  • Items: CR-020
  • Depends on: T12.4, T12.5
  • Blocks: nothing
  • Surface: html/ + server/
  • Files: as touched above

Do: Full verification per CLAUDE.md: run locally, exercise bulk role change, activate/disable, project assignment, CSV upload, and bulk delete at 390px and 1440px, before/after screenshots, smoke test, seed_demo.py, full suite.

Done when:

  • all CR-020 acceptance criteria in decisions-2026-09-17.md are met or a failure is stated with a reason
  • screenshots committed
  • smoke test and seed_demo.py both still pass
  • full test suite passes

Wave 12 exit criteria

  • all four bulk actions work against both a checkbox selection and a CSV upload
  • every existing single-user guardrail (self-action, scope, grantable roles) holds under bulk use
  • bulk delete requires typed confirmation and lists affected usernames
  • partial failures are always reported, never hidden behind a blanket success
  • CR-020 fully accounted for, no open acceptance criteria