feat(events): the minimal admin surface (Phase 3)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 30s
PR Checks / server-tests (pull_request) Successful in 5m26s
PR Checks / client-build (pull_request) Successful in 8m30s

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
This commit is contained in:
2026-09-02 08:39:35 -05:00
parent 2ba397eff7
commit 7b570c8ea1
20 changed files with 3775 additions and 8 deletions

View File

@@ -292,6 +292,14 @@ async function advanceRun(run, now) {
const carry = {}
for (let n = 0; n < STEPS_PER_TICK; n++) {
// Re-read the run's status between steps, not just at the top of the tick.
// This loop drains up to STEPS_PER_TICK steps from one run, and Phase 3 put
// a pause and a cancel in a human's hand: without this, pausing a run in the
// middle of a batch would answer by dispatching another two dozen steps,
// which is not a pause. One indexed column read per step, against a control
// whose entire value is that it takes effect at once.
if (n > 0 && (await runsDb.statusOf(run.id)) !== 'running') return 'stopped'
const phaseIndex = phases.findIndex((p) => p.key === phaseKey)
if (phaseIndex < 0) {
await runsDb.transition(run.id, ['running'], 'failed', { error: `phase "${phaseKey}" is not in the pinned version` })