- Campus model, Event.campusId (null = all campuses), User.homeCampusId,
per-campus summary recipients; campuses_enabled setting (off by default)
- /api/campuses: public list and landing page, admin CRUD/order/archive,
"everyone at campus X" audience (home campus + optional 12-month registrants)
- Events: campus on create/update (audited when moved), ?campus= on the
public list (with church-wide events) and staff list; campus included
- Home campus on the profile; cleared when anonymising
- Overview stats ?campus= filter; campus summary recipients; campus on
ticket PDFs; heartbeat reports the real campus setting
- Migration also adds hub_update_requested, missing since 1.42.0
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>
DEPLOYMENT.md: install with --no-ssl, switch URLs to https, TRUST_PROXY,
nginx and /etc/frp/frpc.ini for TLS on the VPS or in frpc (https2http),
checking visitor IPs, updates and troubleshooting. Linked from the README.
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
- scripts/deploy.sh with no tag deploys the latest main (detached
origin/main); with a tag, that release.
- ecosystem.config.js is no longer in git (now ecosystem.config.example.js;
the deploy creates the install's copy from it once). The deploy keeps the
install's copy safe across checkouts and rollbacks, including releases
that still tracked it.
- The script runs from a temp copy, so checking out a new deploy.sh can't
change it mid-run; set -E so failures in functions still roll back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>