feat(events): the minimal admin surface (Phase 3) #185
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/events-phase-3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Event System Phase 3 (
EVENTS_PLAN.md). Docs half: RunicGateway/docs#211.Three screens, an Events nav group, and the six live run controls Phase 1 left absent on purpose because nothing was in flight and Phase 2 gave something to act on.
Four decisions, settled 2026-09-02, all as recommended
pause,resume,cancelon a run;confirm,skip,retryon a step — alladmin+moderator.advanceis 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. Cancel takes{ reason }, not{ cleanup, reason }— the flag arrives with Phase 8's ledger, and acleanup: falsethat changes nothing is the "control that answers 200 and does nothing" Phases 1 and 2 both refused.admin,editor,moderator) rather than admin-only like Engagement. §K makes every read herestaff, and the moderator's entire power over this feature is the run console — hiding it 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.confirmtakes an optional note saying what was actually done in-client.Three things the build settled — the first is a defect in shipped code
advanceRundrains up toEVENT_STEPS_PER_TICKsteps from one run inside a single tick and only checked the run's status at the top of it — so the entire value of a pause, that it takes effect now, was absent. The loop now re-reads the status between steps (runsDb.statusOf, one indexed column by primary key). Found by writing the test, and the test was re-run against the unfixed code to confirm it fails — it does.seqthat is not settled, which looks equivalent to "the step the run is stopped at" and is not:nextOpenStepselectspendingandrunningonly, so the runner steps over afailedstep. 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 sitspendingfor ever. The rule is nowMAX(seq) WHERE status <> 'pending'— the furthest the phase has reached. The test that found it names the case, and the live walk proves it (below).paused, and a paused run is paused at that step. Splitting them would read as honesty and behave as a trap.attemptsreturns 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.The controls, and why they are compare-and-set
The runner ticks every fifteen seconds, so a console rendered thirty seconds ago describes a run that has since moved. Every control is guarded in its
WHEREclause rather than by a read-then-write, and a refusal is a409naming the status the run is actually in.client/src/lib/eventAuthoring.jsmodels the same guards so a button the server will refuse is not offered in the first place — the copy is deliberate, andeventAuthoring.test.jsis where it is checked against the original.Two derived response fields arrived with the console, both derived rather than columns: a run's
waitingSteps(parked on a human) so the run list can say so, and a step'sparkedboolean so the console can tell a cue waiting on a person from a step some process is mid-dispatch on — without being shownclaimed_by/claim_expires_at, which are the runner's business.A cue nobody notices is a run that never advances, and it looks perfectly healthy from outside:
running, nothing failed. So the list leads with a banner naming every run waiting on a person.Verify
npm test— 1714 tests, 1662 pass, 51 skipped, 1 fail. The one failure isengagementManifest.test.js, pre-existing and environmental (CRLF undercore.autocrlf=true); confirmed by stashing this branch and re-running:edgeis 1682/1638/43/1, the same single failure.eventRunControls.test.js(22, almost all refusals — a control that works from a status it should not is a staff member changing a live world from a stale screen); 8 more ineventRunnerSql.test.jsproving the four new statements against a real MariaDB (27/27 with a database, skipped without); 2 ineventRunner.test.js;eventAuthoring.test.js(20) on the client.npm run routes:manifestandnpm run swagger— six routes added, none moved.check:modulesandcheck:hostsclean. The client builds.The live walk, on the local review stack
Server + Vite against the
uomm-dbcontainer, five runs, asnavadminandnavmod:navmodgot 403 on publish and 403 on start, and 200 on confirm.scheduledrun; pause on a running one; skip on a step behind a ten-minute wait let the run finish at once; resume clearedlast_errorand lefthealth: degraded.seq 0was 409 ("the run is not stopped at this step") and retry onseq 2was 200. The run console offers the button on seq 2 only.cancelledandwaitingStepsdropped to 0 — a cancelled run stops claiming to wait on somebody.Two defects the walk found in this branch, both fixed here: the degraded banner said "It is still running" on a paused run, and the editor offered Save to a moderator, which the server 403s.
Three screens, an Events nav group and the six live run controls Phase 1 left absent on purpose because nothing was in flight. An admin can now author, publish, start and watch an event that announces things and cues a human; a moderator can stop one that is going wrong. Six controls, not eight. `advance` is absent because a phase today advances when its steps go terminal — the per-step skip already does that — and Phase 5 is what gives a phase an advance condition. Cancel takes `{ reason }`, not `{ cleanup }`, until Phase 8's ledger exists. Every control is a compare-and-set on the status it may act from, so a console rendered thirty seconds ago cannot act on a run that has moved. Fixes a defect in the Phase 2 runner: `advanceRun` drained up to EVENT_STEPS_PER_TICK steps while only checking the run's status at the top of the tick, so a pause pressed mid-batch did nothing for up to 24 more steps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL