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

@@ -472,6 +472,45 @@ export const api = {
setEngagementRetention: (body) =>
req('/admin/engagement/retention', { method: 'PUT', body }),
// Events (docs/website/EVENTS.md, Phase 3). Reads are staff-wide; authoring
// is admin+editor, publish and start are admin ONLY, and the six live
// controls are admin+moderator — the one gate in this feature wider than
// admin, because stopping a run at 2am is incident response and starting
// one is not (§N2). The buttons follow the same split, and the server
// re-checks every one of them.
listEvents: (state) => req(`/admin/events${state ? `?state=${encodeURIComponent(state)}` : ''}`),
getEvent: (id) => req(`/admin/events/${id}`),
createEvent: (body) => req('/admin/events', { method: 'POST', body }),
updateEvent: (id, body) => req(`/admin/events/${id}`, { method: 'PUT', body }),
publishEvent: (id) => req(`/admin/events/${id}/publish`, { method: 'POST' }),
archiveEvent: (id) => req(`/admin/events/${id}`, { method: 'DELETE' }),
listEventVersions: (id) => req(`/admin/events/${id}/versions`),
eventCatalog: () => req('/admin/events/catalog'),
eventSeries: () => req('/admin/events/series'),
startEventRun: (id, body) => req(`/admin/events/${id}/runs`, { method: 'POST', body }),
listEventRuns: ({ definitionId, status, limit } = {}) => {
const qs = new URLSearchParams()
if (definitionId) qs.set('definitionId', String(definitionId))
if (status) qs.set('status', status)
if (limit) qs.set('limit', String(limit))
const suffix = qs.toString()
return req(`/admin/events/runs${suffix ? `?${suffix}` : ''}`)
},
getEventRun: (runId) => req(`/admin/events/runs/${runId}`),
getEventRunLog: (runId, limit) =>
req(`/admin/events/runs/${runId}/log${limit ? `?limit=${Number(limit)}` : ''}`),
pauseEventRun: (runId, reason) =>
req(`/admin/events/runs/${runId}/pause`, { method: 'POST', body: { reason } }),
resumeEventRun: (runId) => req(`/admin/events/runs/${runId}/resume`, { method: 'POST' }),
cancelEventRun: (runId, reason) =>
req(`/admin/events/runs/${runId}/cancel`, { method: 'POST', body: { reason } }),
confirmEventStep: (runId, stepId, note) =>
req(`/admin/events/runs/${runId}/steps/${stepId}/confirm`, { method: 'POST', body: { note } }),
skipEventStep: (runId, stepId, reason) =>
req(`/admin/events/runs/${runId}/steps/${stepId}/skip`, { method: 'POST', body: { reason } }),
retryEventStep: (runId, stepId) =>
req(`/admin/events/runs/${runId}/steps/${stepId}/retry`, { method: 'POST' }),
// Teams (docs/website/TEAMS.md §2.11). Three of these mean something
// different depending on who calls them: for a moderator, unhide and
// setTeamDisplayName file a request and the response says `pending: true`.