An event action may declare an optional `progress(envelope)`: how its
step stands while the run is live. Core accepts two shapes and
interprets neither:
- a count: { label, left, of }, rendered "Bandits: 3 of 8 left";
- a percentage: { label, percent }, rendered "The Juggernaut: 62%".
How core asks:
- `events/progress.js` asks each step of a live run whose action has
one. It passes the step's own held ledger rows and the reader's
account id, so the module applies its own visibility.
- Each call has a 2 s deadline. A throw, a late answer or any other
shape is a line not shown, never an error on the page.
- One answer per run and reader is cached for 5 s.
- The public event's live occurrence carries the lines as `progress`.
The route takes an optional session for the reader.
- The page renders them under the phase label and re-reads them every
15 s while the event is live.
Core stays game-agnostic: no game's words are in it. Additive, so a
minor bump. Module-uo declares ^1.10.0, registers no action with
progress(), and its actions and pages are unchanged.
From RunicNPC stage 8 (D304, D305).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
74 lines
2.8 KiB
JavaScript
74 lines
2.8 KiB
JavaScript
// Public · Events — the anonymous event surface (EVENTS.md § API surface).
|
|
//
|
|
// Phase 14a. Three reads and no writes: the calendar, one event, one arc.
|
|
//
|
|
// **Every one of them is a thin pass-through to `eventPublic.model`, and that is
|
|
// deliberate.** The projection — which fields exist at all on a public entry — is
|
|
// the security boundary, and it belongs in one file rather than in three
|
|
// controllers that would each have to remember it. What is left here is the
|
|
// HTTP: parse the query, map the model's `status` onto a response code, and turn
|
|
// a thrown read into a 500 rather than a stack trace.
|
|
//
|
|
// **A 404 here means "no such public event"** and cannot be told from "no such
|
|
// slug at all". A draft, an archived definition and an unlisted one answer
|
|
// identically, which is the whole point: an operator who has not announced
|
|
// something has not announced its existence either.
|
|
|
|
const events = require('../../../model/events/eventPublic.model')
|
|
const log = require('../../../utils/logger')('public:events')
|
|
|
|
const fail = (res, err, what) => {
|
|
log.error(`${what} failed`, { message: err.message })
|
|
return res.status(500).json({ message: 'Internal Server Error' })
|
|
}
|
|
|
|
const answer = (res, result) =>
|
|
result.ok
|
|
? res.json(result)
|
|
: res.status(result.status || 400).json({ message: result.errors?.[0] || 'Bad Request', errors: result.errors })
|
|
|
|
async function getCalendar(req, res) {
|
|
try {
|
|
const seriesId = req.query.seriesId ? Number(req.query.seriesId) : null
|
|
if (req.query.seriesId && !Number.isInteger(seriesId)) {
|
|
return res.status(400).json({ message: 'seriesId must be an integer' })
|
|
}
|
|
const result = await events.calendar({
|
|
from: req.query.from || null,
|
|
to: req.query.to || null,
|
|
seriesId,
|
|
})
|
|
return answer(res, result)
|
|
} catch (err) {
|
|
return fail(res, err, 'public calendar')
|
|
}
|
|
}
|
|
|
|
async function getEvent(req, res) {
|
|
try {
|
|
// `run` is optional and un-validated beyond being carried through as a
|
|
// string: the model matches it against this definition's own runs and
|
|
// ignores anything else, so a garbage value renders the page rather than an
|
|
// error. See the model's note on why it is not refused.
|
|
// `userId` is the reader's, when they sent a session (`optionalAuth`): a
|
|
// module's progress line may show a signed-in reader more than a stranger.
|
|
const result = await events.event(req.params.slug, {
|
|
runId: req.query.run || null,
|
|
userId: req.user && req.user.id != null ? req.user.id : null,
|
|
})
|
|
return answer(res, result)
|
|
} catch (err) {
|
|
return fail(res, err, 'public event')
|
|
}
|
|
}
|
|
|
|
async function getSeries(req, res) {
|
|
try {
|
|
return answer(res, await events.series(req.params.slug))
|
|
} catch (err) {
|
|
return fail(res, err, 'public series')
|
|
}
|
|
}
|
|
|
|
module.exports = { getCalendar, getEvent, getSeries }
|