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:
@@ -332,23 +332,29 @@ const SEEDS = [
|
||||
// single absent variable renders nothing, in both parts). A standalone event
|
||||
// has no `seriesName` and its line disappears rather than reading "Part of .".
|
||||
//
|
||||
// **No `{{actionUrl}}` and no button, deliberately.** There is no public event
|
||||
// page until Phase 14, so the six public triggers declare no `url` variable at
|
||||
// all (see `coreTriggers.js`), and a button here would render as an inert grey
|
||||
// label in every mail — worse than none, because it advertises a link the
|
||||
// reader cannot follow. Phase 14 adds the variable and the block together.
|
||||
// **The button arrived with the page it points at** (Phase 14a). Until then
|
||||
// there was no public event page, the six public triggers declared no url
|
||||
// variable, and a button here would have rendered as an inert grey label in
|
||||
// every mail — worse than no button, because it advertises a link the reader
|
||||
// cannot follow. `eventUrl` is optional and `email.button` drops itself when
|
||||
// its url interpolates to nothing, so an event that is not public still mails
|
||||
// correctly: the block disappears rather than degrading.
|
||||
{
|
||||
key: 'notify.event-started',
|
||||
name: 'Event starting',
|
||||
channel: 'email',
|
||||
protected: false,
|
||||
seedVersion: 1,
|
||||
// Bumped with the button. A deployment whose operator has not customized
|
||||
// this template gets the new one; one that has is left alone and reported as
|
||||
// stale, which is the whole mechanism.
|
||||
seedVersion: 2,
|
||||
subject: '{{title}} is starting',
|
||||
variables: [
|
||||
{ name: 'title', type: 'string', required: true, example: 'The Yew Invasion' },
|
||||
{ name: 'summary', type: 'string', required: false, example: 'Orcish warbands are massing north of Yew.' },
|
||||
{ name: 'seriesName', type: 'string', required: false, example: 'The Yew Campaign' },
|
||||
{ name: 'startsAtLabel', type: 'string', required: false, example: 'Saturday 12 September at 8:00 pm (America/New_York)' },
|
||||
{ name: 'eventUrl', type: 'string', required: false, example: '/site/events/the-yew-invasion?run=3692' },
|
||||
{ name: 'unsubscribeUrl', type: 'string', required: false, example: 'https://example.com/unsubscribe/abc123' },
|
||||
],
|
||||
blocks: [
|
||||
@@ -356,6 +362,7 @@ const SEEDS = [
|
||||
text('summary', '{{summary}}'),
|
||||
text('when', '{{startsAtLabel}}', { muted: true }),
|
||||
text('series', '{{seriesName}}', { muted: true }),
|
||||
button('open', 'Read more', '{{eventUrl}}', 'Read more about it here:'),
|
||||
divider('rule'),
|
||||
button('unsub', 'Unsubscribe', '{{unsubscribeUrl}}', 'To stop these emails, use this link:'),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user