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:
@@ -252,6 +252,29 @@ test('the form round-trips a definition without losing a step', () => {
|
||||
assert.equal(built.payload.concurrencyKey, 'invasion:{region}')
|
||||
})
|
||||
|
||||
test('`listed` round-trips, and an unlisted event is not quietly re-listed', () => {
|
||||
// The trap this guards is `||` where `??` is meant. A definition an operator
|
||||
// deliberately unlisted sends `listed: false`, and `event?.listed || true`
|
||||
// would put it back on the public calendar on the author's next save — a
|
||||
// surprise event announced by a typo fix.
|
||||
const unlisted = payloadFromForm(
|
||||
formFromDefinition({ title: 'Invasion', listed: false, spec: { schedule: { kind: 'manual' }, phases: [] } }),
|
||||
)
|
||||
assert.equal(unlisted.payload.listed, false)
|
||||
|
||||
const listed = payloadFromForm(
|
||||
formFromDefinition({ title: 'Invasion', listed: true, spec: { schedule: { kind: 'manual' }, phases: [] } }),
|
||||
)
|
||||
assert.equal(listed.payload.listed, true)
|
||||
})
|
||||
|
||||
test('a new definition defaults to listed', () => {
|
||||
// The column's own default, and the ordinary case: unlisting is the
|
||||
// deliberate act, not listing.
|
||||
const fresh = payloadFromForm(formFromDefinition({ spec: { schedule: { kind: 'manual' }, phases: [] } }))
|
||||
assert.equal(fresh.payload.listed, true)
|
||||
})
|
||||
|
||||
test('an unchosen onFailure is omitted rather than invented', () => {
|
||||
// The server defaults it from the action's risk class, which is the whole
|
||||
// reason `risk` is required at registration. A form that posted a value would
|
||||
|
||||
Reference in New Issue
Block a user