feat(events): the public calendar, event pages and participation history (Phase 14a) #196
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/events-p14a-public-surface"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Events Phase 14a. One of two:
website,docs(docs#224), both ontoedge. Spec of record:docs/website/EVENTS.md§I and § API surface.Phase 14 was split into 14a and 14b (org lead), on the repo line: the app cannot be walked against a page that does not exist yet, so 14b's emulator walk is only meaningful once this has merged. The same argument 12a/12b was split on.
MODULE_API_VERSIONis untouched — nothing about the module contract changed.Four routes, none moved:
GET /public/events,/public/events/:slug,/public/events/series/:slug, andGET /player/events/history.listedis announcement, not permissionThe one decision that changed the shape of the work. Publishing is what makes a definition runnable, so with
statealone a surprise invasion would have to be advertised a fortnight in advance in order to be allowed to happen.listedis a column defaulting to 1, a switch in Phase 13's editor, and three predicates in SQL — filtering after the read works exactly as well right up until the first caller that forgets. An unlisted event still schedules, still runs and is still on the admin calendar; a draft, an archived definition and an unlisted one all answer 404, indistinguishable from a slug that never existed.It is deliberately not a second answer to what
stateanswers, which is the trapfindSchedulable's own comment warns about:statesays schedulable,listedsays announced.The public shapes are a projection, and the projection is the security boundary
Every other reader of these tables is staff. These are read by nobody at all, so nothing is spread and a field reaches a public entry because a line in
eventPublic.model.jsput it there. The day somebody adds a column toevent_runs— a claim token, an operator's note, a last error — a{ ...run }would publish it in the release after the one anybody reviewed. The test fixture carrieshealth,cleanup_status,claimed_byandlast_erroron purpose, and one test asserts the exact key set of a calendar entry.Absent by construction: the spec (a live run carries the label of its phase, resolved from the version it pinned, and nothing else), health/cleanup/claims/errors, and
member_key— module-opaque, so core cannot say what publishing one would disclose.failedandmissedare published ascancelledandpausedaslive: to a visitor the first three are one event, and the difference between them is about the deployment rather than about the event.eventUrl, which this codebase has owed since Phase 10The six public triggers gained it (version 1 → 2), and
notify.event-startedgained the button atseedVersion2. It was withheld until there was a page becausenews.posthad already paid for that mistake once — previewing a link in the template editor that was dead in every mail it sent. It carries?run=: the page lives at the definition's slug so a weekly event has one address that survives a retitle, while every one of those triggers is about one occurrence. It is optional, andemail.buttondrops itself when its url interpolates to nothing, so an unlisted event still mails correctly with the block absent rather than degraded.run.failedgains nothing — an admin reading that the machinery broke wants the console.Core also publishes an
eventscapability in theversionblock. Same word as a module's on/public/modules, separate list because core is not a module — and the value is in what is absent: an older backend omits the key, a distinction probing/public/eventscannot make.Three defects, and the live walk found all three
Both suites were green before the walk and after each fix. Two were one mistake in two files — a split reading a status where it should read a clock:
upcomingnow splits on the instant, which is what the model's own comment already claimed. A cancelled occurrence still appears under what is coming, because "next Friday is off" is exactly what somebody checking a calendar came to find out.nextstays narrower and skips it./player/events/historyis behindrequireAuthalone and self-scoped, butRequirePlayersends anyone who is not aplayerout of/account— so/account/eventsredirected the reviewing admin to the dashboard. Engagement Phase 7 hit this exact wall with the inbox;eventHistoryPathjoinsinboxPathandnotificationSettingsPathinnotificationPaths.js, and the staff path is/admin/events/mine.A fourth thing the walk exposed: the
eventAnnouncefixture had no slug, state orlisted, soeventUrlansweredundefinedin every test in that file and the new emitter code was exercised by none of them.A swagger trap worth knowing
A backtick inside a
#swagger.parametersannotation is rewritten as a quote and swagger-autogen then drops the whole annotation with a syntax error rather than failing the build.#swagger.summaryand#swagger.descriptionare plain strings and unaffected. Noted inplayer/events.router.js.Checks
Server
npm test: 2140 tests, 2051 pass, 0 fail (89 DB-skipped) — the neweventPublic.test.js(23) plus 6 ineventAnnounce. Clientnpm test: 396 pass, 0 fail, 16 new. The client builds.routes:manifest,routes.guards.json,engagement:manifestandswaggerregenerated — four routes added, none moved, and the guards manifest showssiteModeon the three public reads andnoindex, requireAuthon the player one.Walked in a browser against the local stack, end to end and with two accounts. The calendar, an event page with a published results table, an arc, and participation history as an admin (
/admin/events/mine) and as a player (/account/events) — the second showing only that account's own row, which is the self-scoping proved rather than asserted. Thelistedswitch was toggled off and on through the editor, and each time all three public surfaces agreed: gone from the calendar, 404 on its page, and dropped from its arc; then all three back.🤖 Generated with Claude Code
https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4