docs(events): Phase 3 as built — the minimal admin surface

EVENTS.md: the six controls that exist and the two that do not, and why;
retry as one control with its two guards; what a cancel does to a parked cue;
the run status re-read between steps; the two derived response fields.

EVENTS_PLAN.md: Phase 3 complete, the four org-lead decisions, and the three
things the build settled — including the mid-batch pause defect and the retry
guard that was reading the wrong end of the phase.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL
This commit is contained in:
2026-09-02 08:39:52 -05:00
parent 1feca4e70d
commit 261cce6423
2 changed files with 134 additions and 4 deletions

View File

@@ -289,6 +289,71 @@ mid-dispatch. Test both in-process. The decision is not licence to drop a CAS.
### Phase 3 — The minimal admin surface (`website`)
> **Complete.** `edge` in `website`. Three screens, a nav group and **six live run controls** — the
> routes Phase 1 left absent on purpose because nothing was in flight, and Phase 2 gave something to
> act on. **An admin can now author, publish, schedule, start and watch an event that announces
> things and cues a human, and a moderator can stop one that is going wrong.** This is the first
> phase with a demo.
>
> **Four decisions the org lead settled (2026-09-02), all as recommended:**
>
> - **Six controls, not four and not eight.** `pause`, `resume`, `cancel` on a run; `confirm`,
> `skip`, `retry` on a step. **`advance` is not built** — a phase today advances when its steps go
> terminal, and the per-step skip already does that one step at a time, so a force-advance now would
> silently change meaning under the operator when Phase 5 gives a phase an advance *condition*.
> `cleanup` needs Phase 8's ledger.
> - **Cancel takes `{ reason }`, not `{ cleanup, reason }`.** The flag arrives with the thing it would
> act on. A `cleanup: false` that changes nothing is the "control that answers 200 and does nothing"
> Phases 1 and 2 both refused.
> - **Its own top-level nav group, staff-wide** (`admin`, `editor`, `moderator`) — not admin-only like
> Engagement's. §K makes every read here `staff`, and the moderator's entire power over this feature
> is the run console; hiding it from them would leave the one role that exists for incident response
> unable to see the incident. The narrow gates are on the actions instead, and each button follows
> the route it calls.
> - **The params box is a raw JSON field with the action's declaration rendered beside it.** Both are
> already in the catalog — name, type, required, description, example — so the placeholder is usable
> without reading source, and it is captioned as a placeholder so it does not read as Phase 13's
> schema-driven form.
>
> **Three things the build settled, and the first is a defect in shipped code:**
>
> - **A pause pressed mid-tick did nothing for up to 24 more steps.** `advanceRun` drains up to
> `EVENT_STEPS_PER_TICK` steps from one run inside a single tick and only checked the run's status at
> the top of it — so the whole value of a pause, that it takes effect *now*, was absent. The loop
> re-reads the status between steps (`runsDb.statusOf`, one indexed column by primary key). Found by
> writing the test; the test was re-run against the unfixed code to confirm it fails, and it does.
> - **The retry guard was reading the wrong end of the phase.** The first draft asked for the lowest
> `seq` that is not *settled*, which looks equivalent to "the step the run is stopped at" and is not:
> `nextOpenStep` selects `pending` and `running` only, so the runner steps *over* a `failed` step.
> A phase whose second step failed-and-skipped and whose fifth then failed-and-paused would have
> offered retry on the second, re-queueing a row behind the runner's own cursor where it sits
> `pending` for ever. The rule is now `MAX(seq) WHERE status <> 'pending'` — the furthest the phase
> has reached — and the test that found it is the one that names the case.
> - **Retry and resume are one control, because there is no state in which you would want half of
> it.** Retry is legal only from `paused`, and a paused run is paused *at* that step; re-queueing
> without resuming leaves the run exactly where it was with a second button to find. `attempts`
> returns to zero: the ceiling bounds what the runner does **unattended**, and a named person
> deciding once is the thing it is unattended from. That is not Engagement Phase 14's rule being
> broken — that rule is about automatic *sweeps*.
>
> **Two smaller ones, taken as assumptions rather than asked:** the console polls every 5s while the
> run is non-terminal and stops the moment it is not (§N5, poll not SSE — a run changes on a
> fifteen-second tick and a console is a tab left open for two hours); and `confirm` takes an optional
> note saying what was actually done in-client, which is kept on the step and in the log.
>
> **A cue nobody notices is a run that never advances**, and it looks perfectly healthy from the
> outside — `running`, nothing failed. So `waitingSteps` is on the run LIST as well as the console,
> as a derived count rather than a column, and the list leads with a banner naming every run that is
> waiting on a person.
>
> **Verify, as run.** `npm test` — the pre-existing `engagementManifest.test.js` CRLF failure is the
> only red, exactly as in Phase 2. **42 new tests**: `eventRunControls.test.js` (22, almost all of them
> *refusals* — a control that works from a status it should not have is a staff member changing a live
> world from a stale screen), 8 more in `eventRunnerSql.test.js` proving the four new statements
> against a real MariaDB, 2 more in `eventRunner.test.js`, and `eventAuthoring.test.js` (20) on the
> client. `routes:manifest` and `swagger` regenerated — **six routes added, none moved**. The client
> builds.
`client/src/routes/admin/views/EventsAdmin.jsx`, `EventEditor.jsx`, `EventRun.jsx`, plus the nav rows.
A list with state and next occurrence; a create/edit form; publish; start now; cancel; and a run
console showing the phase, the step list with status and attempts, and the log.
@@ -298,6 +363,14 @@ console showing the phase, the step list with status and attempts, and the log.
consult does not exist yet — a button that is admin-only later and open now is a gate nobody
notices was missing.
**As built, the control set is six**: `pause`, `resume`, `cancel` on a run and `confirm`, `skip`,
`retry` on a step, all `admin` + `moderator`. Every one of them is a compare-and-set on the status it
may act from, never a read-then-write — the runner ticks every fifteen seconds, so a console rendered
thirty seconds ago describes a run that has moved, and a control that checked in JavaScript and then
wrote would race the tick it exists to interrupt. A refusal is a `409` naming the status the run is
actually in, and the client models the same guards so a button the server will refuse is not offered
in the first place.
The spec is edited as **structured fields for the parts that exist** (name, description, schedule,
phases with their steps) and the step's params as a raw JSON field — a deliberate placeholder that P13
replaces with the schema-driven editor. Say so in the UI, so it does not read as the finished thing.