feat(events): the public calendar, event pages and participation history (Phase 14a)
The anonymous surface an event was always for: GET /public/events, /public/events/:slug and /public/events/series/:slug, plus GET /player/events/history, and the four screens over them. Four org-lead decisions taken up front: split Phase 14 into 14a (website) and 14b (the app); add a `listed` flag rather than letting `state` mean both schedulable and announced; put the `events` capability string in the version block rather than publishing core as a pseudo-module; and drop "venue" from the spec rather than adding a field nothing had ever built. `listed` is announcement, not permission. Publishing is what makes a definition runnable, so without a separate flag a surprise event would have to be advertised in order to be allowed to happen. It is a column, a switch in Phase 13's editor, and three SQL predicates -- never a filter applied after a read, which works exactly as well until the first caller that forgets. The public shapes are a projection, and the projection is the security boundary: nothing is spread, so a column added to event_runs next year does not ride out through it. The spec, health, cleanup, claims, errors and member_key are all absent by construction. The six public event triggers gained `eventUrl` (version 1 -> 2), carrying ?run= because the page lives at the definition's slug while every trigger is about one occurrence. notify.event-started gained the button, at seedVersion 2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -92,10 +92,32 @@ async function baseFor(run) {
|
||||
summary: definition.summary || undefined,
|
||||
seriesName: definition.series_name || undefined,
|
||||
timezone: run.timezone || definition.timezone || undefined,
|
||||
eventUrl: eventUrl(definition, run),
|
||||
definition,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The public page for one occurrence (Phase 14a).
|
||||
*
|
||||
* **Site-relative, and it carries the run.** The page lives at the definition's
|
||||
* slug — one stable address for a weekly event, which is what makes a link in
|
||||
* Discord survive a retitle — so the occurrence has to be in the query string or
|
||||
* a mail about last Friday's invasion would open next Friday's.
|
||||
*
|
||||
* **`undefined` when the event is not public**, rather than a path that answers
|
||||
* 404. An unlisted or not-yet-`ready` definition has no page, and `eventUrl` is
|
||||
* declared optional precisely so its block can disappear from a template instead
|
||||
* of rendering a dead button. That is `news.post`'s lesson applied before it
|
||||
* costs anything: a link nobody can follow is worse than no link, because it
|
||||
* advertises one.
|
||||
*/
|
||||
function eventUrl(definition, run) {
|
||||
if (!definition.slug) return undefined
|
||||
if (definition.state !== 'ready' || !definition.listed) return undefined
|
||||
return `/site/events/${encodeURIComponent(definition.slug)}?run=${run.id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire one lifecycle trigger.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user