Files
website/server/src/router/v1/player/events.controller.js
wtclaude 1667e636bd 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
2026-09-08 06:18:38 -05:00

30 lines
1.2 KiB
JavaScript

// Player · Events — the one handler behind /player/events/history (Phase 14a).
//
// Self-scoped on `req.user.id` and on nothing the caller sent. The model does
// the same joins the public surface does — rehearsals and unlisted events are
// absent — so a participant cannot learn from their own history that an
// unannounced event exists.
const events = require('../../../model/events/eventPublic.model')
const log = require('../../../utils/logger')('player:events')
async function getHistory(req, res) {
try {
// A non-integer cursor is dropped rather than bound. `Number('abc')` is NaN,
// and NaN reaching a placeholder is a driver-level failure — a 500 for what
// is a malformed query string, and the honest answer to one is the first
// page.
const cursor = Number(req.query.before)
const result = await events.history(req.user.id, {
limit: req.query.limit ? Number(req.query.limit) : undefined,
before: Number.isInteger(cursor) && cursor > 0 ? cursor : null,
})
return res.json(result)
} catch (err) {
log.error('participation history failed', { message: err.message })
return res.status(500).json({ message: 'Internal Server Error' })
}
}
module.exports = { getHistory }