Site settings -> Webhooks: admins add https endpoints and choose event
types (registration created/paid/cancelled, payment received/refunded,
ticket checked in, account created, event published). Events are
noticed by a Prisma client extension so every write path is covered,
re-checked before sending, and delivered as signed JSON
(X-CrossEvents-Signature, HMAC-SHA256) with retries, a 30-day delivery
log, Send test and Resend. Public https addresses only; each endpoint
is added to the operator register; kids and volunteer tickets are never
sent. New `webhooks` job. Migration 20261028160000_webhooks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After asking for an update, a newer version coming out made the Licence
page claim the newer version was requested, with no way to ask for it.
Now it shows the version asked for, that a newer one is out, and "Ask
for X instead"; the banner shows again. Hub 1.5.0 moves the open request
on (`changed`); with an older Hub the site withdraws and asks again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pass sells a ticket at each of several events in a series for one
price. Supervisors set passes up on the Event series page (events and
ticket per event; fixed once sold). Buying on the series page registers
the buyer at every event at once with one checkout (free passes give
tickets straight away; Pay now if online payment isn't possible). The
price is split over the events in proportion to their normal prices:
each registration line is priced at its share and the webhook records
one Payment per event, so cashups stay per event. Pass registrations
aren't repriced, take no coupons and can't be edited. Migration
20261028150000_season_pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New report (Reports -> Other, or "Compare" on the Event series page):
each event in a series side by side, oldest first - registrations,
tickets, income (dashboard definition), people checked in (average per
session on multi-session events) and attendance rate, with the change
from the event before. Exports like the other reports; campus-scoped
staff see only their campuses' events. GET /api/series/:id/compare.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Group separate events (Camp 2025, Camp 2026) on a series page at
/series/<slug> with upcoming and past events. Events join a series from
the event editor (copies keep it); a series can span campuses. Event
pages show "Part of ..." and "Next in this series". Signed-in people can
follow a series; the new `series` job emails followers once when a new
event in it goes live, as News & announcements (opted-in only, with
unsubscribe). Follows are in the data export and removed on
anonymising. Migration 20261028140000_event_series.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Copy" on each event card opens the event editor filled in from that
event (campus, sessions, options, variants, early birds, sections, form,
recipients, feedback questions, certificates, reminders). "New start"
moves every date with it. Allocation groups and, for admins, planned
costs are copied by POST /api/events/:id/copy-from. Registrations,
coupons, attachments, kids check-in and volunteer roles are not copied.
Roadmap: event series and cloning ships as 1.47.0-1.47.3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Hub reads this file from main (Hub 1.4.1), so these show on sites' Licence
page without a new release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client-facing release notes the Hub reads from git with the version tags, so
the latest version and its notes reach installs without touching the Hub.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Site settings -> Licence shows the release notes for every newer version from
the Hub; urgent (security/serious) updates are marked and can't be put off.
- Withdraw an update request (the provider is told through the Hub).
- "Not now" on the banner or Licence page hides update reminders until the
next version.
- Notices from the provider show as a dashboard banner for site admins and
supervisors, in their time window, and on the Licence page.
- New audit actions (migration), API docs, help, protocol doc, tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There's no Actions runner (no machine that can run Docker), so every push
left a run stuck on "Waiting". Tests and lint are run by hand before a
release, and releases are deployed with scripts/deploy.sh on the server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Site settings -> Campuses tab; campus picker in the event editor; campus
filter and badges on the events list, public events page (remembered,
starts on home campus) and reports/dashboards; /campus/<slug> pages and
sitemap; home campus on the profile; "Everyone at a campus" in email and
WhatsApp broadcasts. Help, FEATURES, SETTINGS, API docs, protocol doc
(campusCount), handover and roadmap updated; campuses seeded for e2e.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records the decisions: scoped staff view and scan "All campuses" events but
can't edit them; "everyone at campus X" is home campus plus optional recent
registrants; campus landing pages in 1.43.0. Later phases move up by one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PRODUCTION_PUBLIC_KEY is the key the production Hub serves at
/api/public-key, normalised to PEM, so installs no longer need
HUB_PUBLIC_KEY. Test covers the built-in key with the env var unset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.41.3 and 1.42.0 / Hub 1.1.0 recorded; Multi-campus is now 1.43.0 and
later phases move up by one. Next steps: roll out update requests, build
the Hub public key in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Site settings -> Licence sends a signed update request to the Hub
(docs/hub-protocol.md 3a) instead of showing bash scripts/deploy.sh. Shows
"Update requested" until the site runs that version and hides the update
banner meanwhile. Audit-logged as hub_update_requested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Display text only: storage keys, slugs, the encryption salt, the repo URL
and applied migrations keep their old names so existing installs keep working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pg-boss timestamps are timestamptz, and Prisma's pg adapter drops the offset in raw queries (10:55+02 came back as 10:55Z, shown as 12:55). Read them AT TIME ZONE 'UTC'.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A site the Hub answers INSTALL_NOT_FOUND for drops its install and licence, so it
no longer shows "Active" on the old token.
- Unlicensed is read-only like suspended; read-only also blocks editing events,
options and variants. LICENCE_CHECK_DISABLED (ignored in production) for e2e.
- Heartbeat 30 s after the server starts and after a licence key is connected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Connect with a licence key (/setup last screen, Site settings -> Licence)
- hub-heartbeat job every 15 min: version, health, totals (income as a band)
- Licence tokens verified with the Hub's public key; states active / grace
(banner for site admins only) / suspended / revoked (read-only) /
unlicensed; Hub keys kept out of the generic settings endpoints
- /health; "update available" for admins; scripts/install.sh
- docs/hub-protocol.md shared with the events-hub repo
- Unit tests (hub.test.js); docs
The production Hub public key (backend/src/hub/publicKey.js) must be set
before this is released.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
- §15 AI helpers (2.0): one-button helpers (draft an event, summarise
reports and feedback, draft messages, improve text) on Claude Haiku 5.5,
called through the Hub, which holds the key, meters usage in rand and
enforces a monthly cap per licence; admin usage view; POPIA notes
- §16 AI later stages (unversioned): Ask AI, help/navigation assistant,
actions with confirmation, more helpers, long-term self-hosted AI
- Hub: AI cap as the first per-licence limit, AiUsage model; decisions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
- Retention policies (Admin -> Privacy -> Retention) applied by a nightly
`retention` job: inactive accounts anonymised after 5 years with a
30-day warning (on by default; never staff roles or people with an
upcoming registration), form answers, login history, payer details in
payment notifications, audit/access logs
- User.lastLoginAt (set on login, backfilled) and inactivityWarnedAt
- Breach register (SecurityBreach) with drafts for the people affected
and the Information Regulator
- Readiness checklist with a score
- Children's/medical flag in the access log; {retention} placeholder in
the privacy notice
- Unit tests (retention.test.js), e2e (privacy-retention.spec.ts); docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
- Admin -> Privacy (POPIA): edit the privacy notice (built-in text as the
start, placeholders for org/IO/operators), drafts, preview, numbered
versions; earlier versions readable on /legal/privacy?version=N
- "Significant change" versions are accepted once after login
(NoticePrompt); sign-up records the version accepted
- Operator register (seeded with the services the app uses), listed in
the notice via {operators}
- Consent log (news opt-ins, notice acceptances, parental consent) and
access log (report exports, registers, certificates, group lists, form
answers, consent/medical lists, backups, impersonation)
- Information Officer on the contact page
- Unit tests (privacyNotice.test.js), e2e (privacy-notice.spec.ts); docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
- Privacy page: news opt-in per channel with history, download my data
(JSON + PDF), correction and deletion requests
- Bulk messages are an Event update or News & announcements; news only
reaches people who opted in, with unsubscribe links, one-click
List-Unsubscribe headers and "Reply STOP" on WhatsApp
- WhatsApp STOP via WAWP's message webhook, checked against a secret in
the webhook URL
- Sign-up opt-in box; existing accounts kept opted in and asked once
- Admin -> Data requests with 30-day due dates and a daily reminder job
- Deleting an account is a request; anonymising now covers attendees,
form answers, consent details, payer details in provider payloads, etc.
- Unit tests (privacy.test.js) and e2e (privacy.spec.ts); docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
The deploy's next build fails on react/no-unescaped-entities. Bump to
1.37.3; the handover now says to lint the whole frontend before tagging.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- src/payments/status.js: open checkouts, checkCheckouts and the
payment-status job (every 15 min, checkouts 10 min - 3 days old). Found
payments are settled exactly as their webhook would be (settlePayment,
extracted from the webhook handler: amount check, record, emails).
- PayFast: signed API client (alphabetical signature, testing=true in
sandbox), findPayments from the transaction history CSV matched on
M Payment ID, and testConnection (signed /ping). Verified against a
real sandbox account.
- Yoco documents no checkout lookup, so its checkouts are "can't check".
- Payments -> Reconcile: "Waiting for payment" with Check status /
Check all; PayFast card: Test connection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shared useOnlinePayments hook (GET /api/payments/providers) gates the
registration success page, My Events (Make payment, Make donation), the
donate page, Sponsor a spot, the register form's donation box and the
supervisor Payment Link tab; they explain why where it helps.
Roadmap: SnapScan and Zapper dropped; later versions move up by two.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PayFast (custom integration): signed form (PHP-style encoding, checked
against a real sandbox), ITN checks (signature over all posted fields,
PayFast host, VALID from /eng/query/validate, amount vs checkout).
- ProviderCheckout records every checkout (amount, metadata, request);
webhooks match payments to it and hold a different amount for review
(ProviderTransaction.note).
- /pay/<id>: payers choose a provider when several are on, and the
browser posts PayFast's form from there, so payment links work for
every provider.
- Site settings → Payments: PayFast card (merchant ID/key, encrypted
passphrase, sandbox/live) while PayFast is enabled.
- Payer-facing wording no longer names Yoco; legal pages name both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- src/payments: provider registry and PaymentProvider interface
(createCheckout, verifyWebhook/parseWebhook, getTransaction, refund,
listTransactions as capabilities), Yoco as the first provider. All four
direct Yoco checkout calls go through payments.createCheckout.
- Webhook split into the provider part (signature, parsing) and shared
processing; same URL (/api/webhooks/yoco), same behaviour.
- YocoTransaction renamed in place to ProviderTransaction (+ provider,
unique per provider + externalId); Payment.provider added and
backfilled to "yoco". Payment.method stays the wallet type so cashups
are unchanged.
- Site settings → Payments: Enabled toggle and order per provider; Yoco's
settings shown while it's enabled. R15 minimum for all online payments.
- New endpoint names (/api/payments/checkout, /api/provider-transactions);
the old /yoco-checkout and /yoco-transactions paths still work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Waitlist per session: entries for pick-sessions tickets name a session;
offers use that session's free places, never run past its start, and
claim links tick it. Held offers count towards session capacity.
- At the door: the Select items pop-up has the session picker, with
today's session preselected for new attendees.
- On pick-sessions events every ticket shows the session list straight
away. "All sessions" tickets record the ticked sessions
(RegistrationOption.sessionIds) at one price; scanning, attendance,
certificates, reminders and capacity follow them.
- Fix: per-session stock limits weren't enforced (takenCount filtered
WaitlistEntry by a sessionId column that didn't exist, and the error
was swallowed), and the public event page lost its stock info.
- Fix: editing a door registration with picked sessions couldn't be saved.
- Fix: the offline-scanning service worker served /_next/static cache-first
forever, so dev browsers that had opened the scanner ran stale code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Event wizard: how people book (whole course / pick sessions), capacity
per session and per-option session mode. Session picker on the public
registration page, Manual registration and the kiosk; at the door points
to Manual registration. Register, tickets and My Events handle picked
sessions. Adds docs/ROADMAP-v2.md and docs/HANDOVER.md so work can carry
on from another machine, and a pick-sessions e2e test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>