feat(events): schedule, recurrence and the calendar (Phase 4)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 37s
PR Checks / client-build (pull_request) Successful in 43s
PR Checks / server-tests (pull_request) Successful in 13m26s

The four closed recurrence shapes computed in the definition's own IANA zone,
a fourteen-day materialisation horizon with projections beyond it, series as a
managed thing, and the admin calendar that replaces the plugin this feature
exists to replace. An event now happens on its own.

No schema change: Phase 1 built every column this needed.

- events/recurrence.js is the ONE place an occurrence is computed, so the
  runner's expansion and the calendar's forecast cannot disagree. No date
  library added — Node ships the tzdata one would vendor, behind Intl.
- The runner's materialise leg is now two halves: expand, then sweep. The
  window starts at `now - grace`, so an occurrence nobody could have seen is
  never invented retroactively; the horizon is what makes the missed sweep
  mean anything for a recurrence.
- Publishing is the schedule switch and archiving turns it off, and publishing
  re-pins every occurrence that has not started.
- A projection is never drawn over an instant a run occupies, so a cancelled
  occurrence does not reappear as a forecast.

54 new tests, incl. the DST fixture set the plan asked for and three new
statements proved against a real MariaDB. Suite 1768/1711/56 skipped/1 fail
(pre-existing CRLF). Walked end to end on the local review stack.

Docs: RunicGateway/docs#PENDING

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-02 16:10:16 -05:00
parent a481248bc0
commit 6e73660b52
30 changed files with 3722 additions and 77 deletions

View File

@@ -491,6 +491,15 @@
"requireAuth"
]
},
{
"method": "GET",
"path": "/api/v1/admin/events/calendar",
"handlers": 1,
"gates": [
"noindex",
"requireAuth"
]
},
{
"method": "GET",
"path": "/api/v1/admin/events/catalog",
@@ -590,6 +599,33 @@
"requireAuth"
]
},
{
"method": "POST",
"path": "/api/v1/admin/events/series",
"handlers": 2,
"gates": [
"noindex",
"requireAuth"
]
},
{
"method": "DELETE",
"path": "/api/v1/admin/events/series/:seriesId",
"handlers": 2,
"gates": [
"noindex",
"requireAuth"
]
},
{
"method": "PUT",
"path": "/api/v1/admin/events/series/:seriesId",
"handlers": 2,
"gates": [
"noindex",
"requireAuth"
]
},
{
"method": "GET",
"path": "/api/v1/admin/invites",