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.
This commit is contained in:
2026-09-18 09:21:29 -07:00
parent df20b8f18d
commit 75ac930d0c
4 changed files with 943 additions and 0 deletions

193
docs/waves/wave-12.md Normal file
View File

@@ -0,0 +1,193 @@
# 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_id`s 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