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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user