feat(events): the minimal admin surface (Phase 3)
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:
@@ -53,6 +53,7 @@ const IconList = () => <Icon><path d="M8 6h13M8 12h13M8 18h13" /><circle cx="4"
|
||||
const IconTemplate = () => <Icon><rect x="4" y="3" width="16" height="18" rx="2" /><path d="M8 8h8M8 12h8M8 16h4" /></Icon>
|
||||
const IconSpark = () => <Icon><path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z" /><path d="M18 16l.9 2.1L21 19l-2.1.9L18 22l-.9-2.1L15 19l2.1-.9z" /></Icon>
|
||||
const IconLog = () => <Icon><path d="M4 5h16v14H4z" /><path d="M8 9h8M8 12h8M8 15h5" /></Icon>
|
||||
const IconCalendar = () => <Icon><rect x="3" y="5" width="18" height="16" rx="2" /><path d="M3 10h18M8 3v4M16 3v4" /><circle cx="12" cy="15" r="1.4" /></Icon>
|
||||
|
||||
// Nav is grouped into collapsible categories. A group with no `title` renders
|
||||
// its items ungrouped (Dashboard at top, Account at bottom). Each item's `roles`
|
||||
@@ -120,6 +121,20 @@ export const NAV = [
|
||||
{ to: '/admin/engagement/retention', label: 'Retention', icon: IconGear, roles: ['admin'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Its own top-level group rather than a row under Content, and staff-wide
|
||||
// rather than admin-only. Both follow EVENTS.md §K: every read here is
|
||||
// `staff`, and the moderator's entire power over this feature is the run
|
||||
// console — the thing they open when an event is doing something wrong at
|
||||
// 2am. Hiding it from them would leave the one role that exists for incident
|
||||
// response unable to see the incident. The narrower gates live on the
|
||||
// actions: authoring is admin+editor and publish/start are admin only, both
|
||||
// enforced server-side and mirrored on the buttons.
|
||||
title: 'Events',
|
||||
items: [
|
||||
{ to: '/admin/events', label: 'Events', icon: IconCalendar, roles: ['admin', 'editor', 'moderator'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'System',
|
||||
items: [
|
||||
@@ -202,6 +217,8 @@ const TITLES = {
|
||||
'/admin/engagement/suppressions': 'Suppressions',
|
||||
'/admin/engagement/sends': 'Send Log',
|
||||
'/admin/engagement/retention': 'Retention',
|
||||
'/admin/events': 'Events',
|
||||
'/admin/events/new': 'New event',
|
||||
}
|
||||
|
||||
// An installed module's admin pages are not in TITLES and cannot be — core does
|
||||
@@ -222,6 +239,10 @@ function sectionTitle(pathname) {
|
||||
if (pathname.startsWith('/admin/moderation')) return 'Moderation'
|
||||
if (pathname.startsWith('/admin/users/')) return 'User'
|
||||
if (pathname.startsWith('/admin/engagement')) return 'Engagement'
|
||||
// /admin/events/:id and /admin/events/runs/:runId are both dynamic, and both
|
||||
// belong to the same section as far as the page title is concerned.
|
||||
if (pathname.startsWith('/admin/events/runs/')) return 'Event run'
|
||||
if (pathname.startsWith('/admin/events/')) return 'Event'
|
||||
return 'Admin'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user