61dc692088f75ffb61634167c031ac28636883b7
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 7d7840eb6b |
fix(events): midnight in an announcement is 12:00 am on the Node we ship
`startsAtLabel` asked for `hour12: true` on `en-GB`. That is not the same request as a 12-hour clock, and it does not survive a Node upgrade: for a locale whose default cycle is h23, Node 20 resolves `hour12: true` to h11, whose hours run 0-11, so midnight renders "0:00 am". Node 22 and later resolve it to h12 and it renders "12:00 am". Same ICU on both sides, so it is V8's ECMA-402 behaviour rather than locale data. The image ships node:20-alpine and CI runs Node 20, while a dev machine is newer -- which is how this rendered correctly in front of everyone who wrote it and wrongly for every real recipient. An event mail announcing a midnight start said "0:00 am" while the schedule editor beside it said "12:00 AM": one instant, two spellings, which is the exact contradiction the option was added to prevent. `hourCycle: 'h12'` is the request that means what was meant. `recurrence.js` already states the mirror-image rule for `h23`, and every other formatter in this repo and in module-uo uses `hourCycle`; there is no `hour12` left here. This is the one test that has been red on every events PR since #192, and the only one -- each of those runs reported `# fail 1`. Verified by running the suite under node:20-alpine, where the test fails without this change and 2152 tests pass with it; on Node 22+ it passes either way, so the test's comment now says that a green run on a dev machine is not evidence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 |
|||
| eb167558e3 |
fix(events): staff could not reach their own participation history
Found by the live walk, signed in as an admin: /account/events redirected to the dashboard. `GET /player/events/history` is behind requireAuth alone and self-scoped on req.user.id -- staff are a superset of players -- but the WEB has two logged-in shells, and RequirePlayer sends anyone who is not a `player` out of /account. A single mount there is a screen the reviewing admin can never open. Engagement Phase 7 hit this exact wall with the inbox and answered it with two routes, one pair of components and one mapping. `eventHistoryPath` joins `inboxPath` and `notificationSettingsPath` in notificationPaths.js rather than starting a second file with the same comment at the top of it. The staff path is /admin/events/mine, in the Events section of the sidebar, and it is the one row in that group with no `roles`. Also: the eventAnnounce fixture carried no slug, state or `listed`, so `eventUrl` answered undefined in every test in that file and the new code was exercised by none of them. The fixture now looks like a definition row, and three tests cover the link, the unlisted case and the draft case. The run.failed assertion that came with them was reading the wrong layer: `baseFor` assembles eventUrl for every trigger and the SEAM drops the keys a trigger does not declare, so the declaration test is what proves it. Removed, with a note saying where the rule actually lives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 |
|||
| 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 |
|||
| 7d3d6d5abd |
feat(events): the integrations — lifecycle triggers, participants, results (Phase 10)
`EVENTS_PLAN.md` Phase 10. Core registers its own `event.` triggers, records who took part, publishes a results table, and announces a post through the legs the news pipeline already uses. Events owns none of the delivery: a run says what happened and an operator's rule decides who is told, so email, the in-app inbox, push tickles, Discord and the town crier all arrive without anything in `events/` growing a second delivery path. **No route was added and nothing moved.** The whole surface is two more derived fields on a run — `participants` and `resultsPublishedAt` — and a zero-line `routes.manifest.json` diff proves it. Seven triggers: six at ceiling `authenticated` / audience `subscribers`, exactly where `news.post` sits, and `run.failed` at `admin` on both halves. Every one keys its cooldown on the RUN. Two rules seeded, both off, under a third one-shot key so a deployment that has already stamped the Team and news keys still gets them. **The phase's own defect was a promise nothing kept.** `EVENTS.md` §I says a rehearsal runs for real "with announcements ceilinged to `staff`" — but a ceiling is declared on the TRIGGER, and a rehearsal fires the same trigger as the real thing, so the moment this phase gave a run something to announce, rehearsing a published event would have mailed every subscriber. The emit envelope now takes an optional `ceiling` and the send-time G24 gate applies `meet(declared, emitted)`. It only narrows; two incomparable ceilings refuse every rule rather than resolving to either. `MODULE_API_VERSION` stays 1.10.0, amended in place — `main` declares 1.9.0, so 1.10.0 has not shipped and the org lead's 2026-09-03 rule applies for the third time. Three defects the live walk found, none visible to a unit test: 1. **A channel that reported success while reaching nobody.** The seeded `run.started` rule named `push`, because §8.5 and the plan both do. Push delivery joins `notification_subscriptions`, only ever written for an id the preferences screen offered push for — and it offers push only for a registered STREAM. So the tickle went nowhere every time while `pushChannel.deliver` answered "tickle published". `event.run.started` is now a stream as well as a trigger; the other six are not. 2. **A trigger's `description` reaches a recipient.** It is the structural projection's `intro` fallback, so `run.failed`'s line ending "Staff-facing." put those words in an administrator's own inbox item. 3. **`affectedRows` cannot tell an insert from an unchanged upsert.** The connector sends `CLIENT_FOUND_ROWS`, so a "was this new" flag would have counted every idempotent retried collect as a fresh participant. And one caught before it shipped: ranking with a session variable is wrong here, because `query()` takes a pool connection per call — the variable would be set on one connection and read on another. A window function needs no session state. ## Verification - `npm test --prefix server` — **1981 pass, 1 fail**, and that one (`botScore.test.js`) passes standalone at 18/18: a file-level flake under parallel load. Run with an empty `MODULES_DIR`, as CI does. - `npm test --prefix client` — 362 pass, 0 fail. `npm run build` green. - Zero-line `routes.manifest.json` / `routes.guards.json` diff. - A live walk on a real rig: MariaDB, the site with no module, mailpit. The mail arrived, headed with the event's title and its start time in the shard's own zone; the rehearsal fired the same trigger and produced zero outbox rows where the real run produced three; `run.failed` reached the administrator's inbox and no player's; `core.announce.post` queued a second job without touching the news pipeline's back-pointer or `announced_at`; and `rankRun` and the upsert were run against real MariaDB 11. ## One thing for a reviewer, out of scope and not fixed **Every `#swagger.description` in this repo is truncated in the generated spec.** swagger-autogen does not honour a backslash-escaped apostrophe, so a description is cut at the first `\'` — 175 of the 177 in `server/src/router/**`. It is pre-existing and repo-wide. Only the one annotation this phase edits is fixed here (a typographic apostrophe), because otherwise this phase's own addition to it would be dead text. The rest wants its own change. - [x] AI-assisted: Claude Code (Opus 5). Docs: RunicGateway/docs#TBD. Co-Authored-By: Claude <noreply@anthropic.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) |