Compare commits

...
Author SHA1 Message Date
joshua b2906011fd Merge branch 'feat/palette-excelts' 2026-10-29 11:16:38 +02:00
joshuaandClaude Opus 5.5 70850df4f6 Complete command palette, ExcelTS 10, roadmap: v2 for webhooks and sent links (1.51.6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls1vMdg2GYqbnKZmPaX1ot
2026-10-29 11:14:19 +02:00
joshua cbebcdbd48 Merge branch 'chore/next16' 2026-10-29 11:00:31 +02:00
joshuaandClaude Opus 5.5 849998acb3 Next.js 16, React 19.3, ESLint 10 (1.51.5)
- Next 16 (Turbopack dev and build), React 19.3; tsconfig jsx react-jsx; images remotePatterns.
- ESLint 10 with eslint-config-next 16 flat configs; npm run lint = eslint src. React
  Compiler-era react-hooks rules set to warn (181 existing places) until screens move to v2.
- Removed the empty pass-through middleware.ts.
- e2e: wait for the palette to be ready before Ctrl+K on the staff dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls1vMdg2GYqbnKZmPaX1ot
2026-10-29 11:00:30 +02:00
joshua c5fe17a1ef Merge branch 'chore/dependencies' 2026-10-29 10:19:43 +02:00
joshuaandClaude Opus 5.5 6164fdcb3b Security and dependency updates, Node 24 for new installs (1.51.4)
- npm audit fixes: next 15.5.28 and @sentry/nextjs 10.78 (critical), zod 4.6.7, multer 2.5.0
  (high), nodemailer 10.3.0, fast-uri, ip-address (moderate). Minimum versions raised.
- cron-parser 5 (v4 unmaintained).
- install.sh installs Node 24 LTS; deploy.sh warns on Node < 22; DEPLOYMENT.md "Upgrading Node".
- Roadmap: every endpoint moves to v2 before 2.0 (1.51.5 - 1.51.11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls1vMdg2GYqbnKZmPaX1ot
2026-10-29 10:19:43 +02:00
joshua 1510c2a057 Merge branch 'chore/roadmap-v1-decision' 2026-10-29 09:49:54 +02:00
joshuaandClaude Opus 5.5 fa4191d18c Roadmap: 2.0 retires only the v1 endpoints that have a v2 replacement
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls1vMdg2GYqbnKZmPaX1ot
2026-10-29 09:49:54 +02:00
joshua e0dae5647f Merge branch 'feat/api-v2-express5' 2026-10-29 09:46:24 +02:00
28 changed files with 1371 additions and 1875 deletions
+63
View File
@@ -7,6 +7,69 @@ and this project follows [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [1.51.6] - 2026-10-29
### Fixed
- **Command palette (Ctrl+K) was missing things.** It listed 9 of the 19 reports — now every
report (built from the report catalogue, so new ones appear by themselves), plus Dashboard,
Reports, Ticket options and My groups, and your own pages (My events & tickets, My payments,
My privacy, My kids, My serving, My attendee forms, Make a donation). Attendee forms now opens
for staff and supervisors too (the page allows them; the palette had it as admin-only). An e2e
test fails when a dashboard page is neither in the palette nor deliberately left out.
### Changed
- **Excel exports use ExcelTS** (`@cj-tech-master/excelts` 10) instead of exceljs, which is
unmaintained: same API, no dependencies — the backend has 87 fewer packages and none of
exceljs's deprecated ones (fstream, glob 7, inflight, lodash.isequal, rimraf 2). New
`e2e/excel.spec.ts` opens a real report and attendance export. ExcelTS itself now says it's no
longer developed (its successor, documonster, is pre-1.0); existing versions keep working.
- The backend needs **Node 22 or newer** (`engines`); the servers run Node 24 LTS.
- Roadmap: incoming webhooks and links in sent messages get v2 URLs too (1.51.13), with the v1 paths
still answering; the area releases are now 1.51.7 – 1.51.13.
## [1.51.5] - 2026-10-29
### Changed
- **Next.js 16** (from 15.5) and **React 19.3**. Builds and dev use Turbopack (Next 16's default);
`tsconfig.json` `jsx` is `react-jsx` (required by Next 16); `images.domains` → `remotePatterns`.
- **ESLint 10** (ESLint 9 is out of support) with Next 16's own flat configs
(`eslint-config-next/core-web-vitals` and `/typescript`, no more FlatCompat). `npm run lint` is
`eslint src` (`next lint` was removed). Next 16's React Compiler-era rules
(`react-hooks/set-state-in-effect`, `purity`, `immutability`, `static-components`, `refs`) are
warnings for now — 181 places, mostly loading data in `useEffect`.
- Removed the empty `middleware.ts` (a pass-through that did nothing; Next 16 renames middleware to
"proxy"), which also drops its 117 kB bundle.
## [1.51.4] - 2026-10-29
### Security
- **Next.js 15.5.28** (critical: remote code execution through the image optimiser with SVGs, and a
Server Actions denial of service), **@sentry/nextjs 10.78** (critical: middleware bypass),
**zod 4.6.7** (high: memory exhaustion on oversized collections; backend and frontend),
**multer 2.5.0** (high: denial of service from a pending file filter), **nodemailer 10.3.0**
(moderate: three address-parsing flaws that could add recipients), fast-uri 3.1.9 and
ip-address 10.7.5 (moderate). Minimum versions raised in package.json so a fresh install can't
resolve an older one. `npm audit`: frontend and root clean; the backend's 6 remaining reports are
low and dev-only (jest → deepmerge), with no real fix (npm suggests downgrading Jest to 25).
### Changed
- **cron-parser 5** (v4 is unmaintained): `CronExpressionParser.parse` in `src/jobs/index.js`.
- **Node 24 LTS** for new installs (`scripts/install.sh`); Node 20 reached end of life in April 2026.
`deploy.sh` warns on servers older than Node 22; DEPLOYMENT.md has "Upgrading Node".
- Roadmap: every endpoint moves to v2 before 2.0 (1.51.5 – 1.51.11).
### Known
- Still deprecated, with reasons: exceljs (unmaintained; brings fstream, glob 7, inflight,
lodash.isequal, rimraf 2) — replacing it with ExcelTS waits for approval; ESLint 9 (out of
support) — ESLint 10 needs Next.js 16 (1.51.5); @opentelemetry/sdk-trace-base — pulled in by the
latest @sentry/nextjs, for Sentry to update; glob 10 — inside Jest (dev only).
## [1.51.3] - 2026-10-29
### Changed
+19 -1
View File
@@ -56,7 +56,7 @@ curl -fsSLO https://c-git.crosscode.co.za/joshua/hope-events/raw/branch/main/scr
sudo bash install.sh --domain events.yourchurch.org --no-ssl --version v1.41.2
```
It installs Node 20, PostgreSQL, PM2 and nginx, creates the database, writes the config, builds
It installs Node 24 LTS, PostgreSQL, PM2 and nginx, creates the database, writes the config, builds
and starts the app (in `/opt/hope-events`, run by the `hopeevents` user). Use `--name`, `--dir`,
`--web-port` and `--api-port` if other apps already use those names or ports; adjust the ports
below to match.
@@ -311,6 +311,24 @@ and goes back to the previous version if anything fails first. `backend/.env`,
`frontend/.env.production`, `ecosystem.config.js` and the nginx/frpc configs are not in git, so
updates keep your changes from this guide.
### Upgrading Node
Node 20 stopped getting security fixes in April 2026; Cross Events runs on Node 24 LTS (supported
until April 2028). `deploy.sh` warns when a server is still on an older Node. To upgrade:
```bash
node -v # what you have now
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo bash -
sudo apt-get install -y nodejs # replaces the old Node
sudo npm install -g pm2 # PM2 for the new Node
cd /opt/hope-events
sudo -u hopeevents -H pm2 update # restart PM2's daemon on the new Node
sudo -u hopeevents -H bash scripts/deploy.sh v1.51.4 # reinstall packages and rebuild for Node 24
```
Use the version you're on (or a newer one) in the last line — the deploy reinstalls every package
and rebuilds, which a Node upgrade needs.
## Troubleshooting
| Symptom | Likely cause |
+1
View File
@@ -898,6 +898,7 @@ can't start, the same jobs run on in-process timers.
link (1.51.3).
- **TypeScript backend** (1.51.0): `.ts` files are checked strictly, `.js` compiles as it is;
deploys build `dist/`, which `src/index.js` runs.
- **Command palette** (Ctrl+K): every page, settings tab, report and quick action for your role.
- **Tests** — Jest unit tests in `backend/tests` (`npm test`), and Playwright
end-to-end tests in `e2e/` (`npm run test:e2e`) that start their own backend and
frontend against a separate, freshly seeded database: public pages, free and paid
+1 -1
View File
@@ -107,7 +107,7 @@ curl -fsSLO https://c-git.crosscode.co.za/joshua/hope-events/raw/branch/main/scr
sudo bash install.sh --domain events.yourchurch.org --email you@yourchurch.org [--version v1.41.0]
```
It installs Node 20, PostgreSQL (with its own database and a random password), PM2, nginx (the
It installs Node 24 LTS, PostgreSQL (with its own database and a random password), PM2, nginx (the
API, uploads, `/health` and `/docs` to the backend, everything else to the site) and an HTTPS
certificate, writes `backend/.env` and `frontend/.env.production`, migrates, builds and starts the
app. Then open `https://<domain>/setup`: create the admin account and enter the **licence key**
+17
View File
@@ -12,6 +12,23 @@ Write for church admins, not developers: no environment variables, endpoints or
developer log is CHANGELOG.md. A tagged version without a section here shows "No details given".
-->
## 1.51.6
- **Find anything with Ctrl+K.** The search box (Ctrl+K) now lists every report, and pages that
were missing: the dashboard, all reports, ticket options, your groups, and your own events,
payments, privacy, kids, serving, forms and donations.
- Excel downloads use a newer, maintained library. They look the same.
## 1.51.5
- **Updated framework.** The site now runs on the latest version of its web framework. Nothing
changes in how it works.
## 1.51.4 — security
- **Security updates** to the parts the site is built on, including a critical fix in the web
framework. Recommended for everyone.
## 1.51.3
- **More groundwork for 2.0.** The ticket scanner and check-in at the door now run on the new version
+50 -913
View File
File diff suppressed because it is too large Load Diff
+9 -6
View File
@@ -1,6 +1,6 @@
{
"name": "event-management-backend",
"version": "1.51.3",
"version": "1.51.6",
"description": "Event Management System Backend",
"main": "src/index.js",
"scripts": {
@@ -20,22 +20,22 @@
"openapi": "tsx scripts/openapi.ts"
},
"dependencies": {
"@cj-tech-master/excelts": "^10.2.0",
"@prisma/adapter-pg": "^7.10.1",
"@prisma/client": "^7.10.1",
"@sentry/node": "^10.71.0",
"axios": "^1.11.0",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"cron-parser": "^4.9.0",
"cron-parser": "^5.10.2",
"dotenv": "^16.3.1",
"exceljs": "^4.4.0",
"express": "^5.3.0",
"express-rate-limit": "^8.6.2",
"ics": "^3.12.0",
"jsonwebtoken": "^9.0.3",
"multer": "^2.2.0",
"multer": "^2.5.0",
"node-fetch": "^2.7.0",
"nodemailer": "^10.0.16",
"nodemailer": "^10.3.0",
"pdfkit": "^0.17.1",
"pg-boss": "^10.4.2",
"qrcode": "^1.5.4",
@@ -44,7 +44,7 @@
"tsx": "^4.23.15",
"typescript": "^5.9.3",
"uuid": "^11.1.1",
"zod": "^4.4.3"
"zod": "^4.6.7"
},
"devDependencies": {
"@types/express": "^5.0.6",
@@ -72,5 +72,8 @@
"@parcel/watcher": false,
"@scarf/scarf": false,
"esbuild": false
},
"engines": {
"node": ">=22"
}
}
@@ -1,4 +1,5 @@
const ExcelJS = require('exceljs');
// ExcelTS: a maintained TypeScript port of exceljs (same API, no dependencies).
const ExcelJS = require('@cj-tech-master/excelts');
const PDFDocument = require('pdfkit');
const prisma = require('../config/db');
const { safeErrorMessage } = require('../utils/errorUtils');
+2 -1
View File
@@ -1,5 +1,6 @@
const PDFDocument = require('pdfkit');
const ExcelJS = require('exceljs');
// ExcelTS: a maintained TypeScript port of exceljs (same API, no dependencies).
const ExcelJS = require('@cj-tech-master/excelts');
const fs = require('fs');
const path = require('path');
const { sendMail, emailWrapper } = require('../utils/email');
+3 -3
View File
@@ -15,7 +15,7 @@
* same jobs run on in-process timers instead, as before.
*/
const cronParser = require('cron-parser');
const { CronExpressionParser } = require('cron-parser');
const TIMEZONE = process.env.APP_TIMEZONE || 'Africa/Johannesburg';
const SCHEMA = 'pgboss';
@@ -105,7 +105,7 @@ function cronFor(job) {
const override = process.env[`JOB_SCHEDULE_${job.name.toUpperCase().replace(/-/g, '_')}`];
if (override) {
try {
cronParser.parseExpression(override, { tz: TIMEZONE });
CronExpressionParser.parse(override, { tz: TIMEZONE });
return override;
} catch {
console.warn(`[jobs] Ignoring invalid schedule for ${job.name}: "${override}"`);
@@ -116,7 +116,7 @@ function cronFor(job) {
/** When a cron schedule next fires after `from`. */
function nextRun(cron, from = new Date()) {
return cronParser.parseExpression(cron, { tz: TIMEZONE, currentDate: from }).next().toDate();
return CronExpressionParser.parse(cron, { tz: TIMEZONE, currentDate: from }).next().toDate();
}
function summarize(result) {
+30 -7
View File
@@ -1,6 +1,6 @@
# Handover — where things stand and how to carry on
Last updated: 2026-10-29, after **1.51.3** (and Hub 1.5.0). The plan itself is in
Last updated: 2026-10-29, after **1.51.6** (and Hub 1.5.0). The plan itself is in
[ROADMAP-v2.md](./ROADMAP-v2.md) (decisions table at the end); the previous round of
features is in [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md).
@@ -17,6 +17,7 @@ encrypted settings unreadable or break existing installs.
| 1.51.3 | **Express 5** (qs query parser and `req.body = {}` kept); scanner and at-the-door on v2 check-in; shared middleware answers v2-shaped on `/api/v2` (`legacyErrorsToV2`, `reason`); v1 `Deprecation` + `successor-version` headers (`src/v2/deprecation.ts`) |
| 1.51.2 | **v2 accounts and check-in**: signup, login (+ code step), `PATCH /me`; check-in look-up and scan with `outcome`s. `src/services/auth.ts` (from userController) and `src/services/checkin.ts` (from ticketController); login/signup/profile pages on v2 |
| 1.51.1 | **v2 registrations and payments**: register, get, cancel, checkout; create/cancel moved to `src/services/registrations.ts`, online checkout to `src/services/payments.ts` (v1 uses them too); typed frontend calls (`apiV2`, `npm run api:types`); register page, Pay now and My Events cancel on v2 |
| 1.51.6 | Command palette lists every report (from `ReportCatalog`) and every dashboard page (e2e drift check); exceljs → ExcelTS 10 (`@cj-tech-master/excelts`, no dependencies; e2e `e2e/excel.spec.ts`); backend `engines.node` >= 22 |
| 1.51.0 | **TypeScript backend + `/api/v2` foundations** (§14): tsconfig (`allowJs`), build to `dist/`, launcher `src/index.js` → `src/app.js`; v2 route declarations with zod → validation, response stripping and OpenAPI 3.1 at `/docs/v2`; first endpoints events, event, me, my registrations. Tests `tests/v2.test.ts`, e2e `e2e/api-v2.spec.ts` |
| 1.50.2 | Searchable **event picker** (`components/shared/EventPicker.tsx`) on staff/supervisor pages and the kiosk; searchable reports events list; **How webhooks work** guide on Site settings → Webhooks (examples from `EVENT_TYPES[].example`). e2e `e2e/event-picker.spec.ts` |
| 1.50.1 | Long lists: audit-log search/role filter/all actions and inclusive To date, data-request search and paging, webhook-delivery status filter and Load older, supervisor Payments loads only what it needs (`unpaid`, `userId`, `from`, `donations` filters). `components/shared/Pager.tsx`; e2e `e2e/long-lists.spec.ts` |
@@ -65,9 +66,13 @@ encrypted settings unreadable or break existing installs.
Hub 1.1.0 is deployed with update requests rolled out, and `scripts/install.sh` has been tried on
a test server (both done 2026-10-28).
1. **Decide how far v1 goes before 2.0** (roadmap "Still open"): v2 covers what members, guests and
the scanner use; the staff/supervisor/admin screens still call ~350 v1 routes. Then **2.0.0 —
AI helpers** (§15), AI endpoints on v2 only.
1. **1.51.7 – 1.51.13: every endpoint onto v2**, one area per release (roadmap build order, row
11b): services in TS, screens on `apiV2`, v1 routes into `SUCCESSORS`. 1.51.13 gives incoming
webhooks and links in sent messages v2 URLs, with the v1 paths still answering (links redirect,
webhooks handled by the same code). Then **2.0.0** — AI helpers and v1 removed.
2. **Servers run Node 24** (Joshua, 2026-10-29), so the backend may need Node 22+. Still to do:
pg-boss 11 (it would drop pg-boss 10's cron-parser 4), and move from ExcelTS — deprecated in
favour of `documonster` by the same author (pre-1.0 in Oct 2026) — once documonster is stable.
Languages (Afrikaans, §10) are removed from scope (2026-10-28).
2. AI rules decided 2026-10-29 (see §15 in the roadmap): the AI buttons show only when the
licence's monthly cap is above R0, and are disabled (not hidden) once it's reached. The Hub
@@ -83,7 +88,7 @@ a test server (both done 2026-10-28).
or serious fix) → `git merge --no-ff`
into `main` → `git tag vX.Y.Z` → `git push origin main <branch> vX.Y.Z`. One minor
version per feature.
- **Before tagging:** run `npx eslint src` in `frontend` over the **whole** app (the deploy runs `next build`, which fails on any lint error, e.g. an unescaped ' or " in JSX text). If a dev server is running, a full check build needs its own folder: `NEXT_DIST_DIR=.next-buildcheck npx next build` (then delete it and `git checkout frontend/tsconfig.json`).
- **Before tagging:** run `npx eslint src` in `frontend` over the **whole** app (since Next 16 `next build` no longer lints — it still fails on TypeScript errors — so lint errors only show up if you run it; keep it at 0 errors). If a dev server is running, a full check build needs its own folder: `NEXT_DIST_DIR=.next-buildcheck npx next build` (then delete it and `git checkout frontend/tsconfig.json`).
- **Changelog:** add entries to `[Unreleased]` as you go, in plain language. CHANGELOG.md is the
full log; WHATS-NEW.md is what churches see before updating (the Hub reads it from main every
15 minutes, for tagged versions only and sets the latest version from the tags — nothing to do in the Hub).
@@ -95,8 +100,8 @@ a test server (both done 2026-10-28).
change — delete those two blocks from the generated SQL. It also rewrites the comment
in `migration_lock.toml`; `git checkout` that file back. New `AdminAuditAction` values
need a migration.
- **Server Node version:** the deployment notes install Node 20, so dependencies must
support Node 20 (that's why pg-boss is pinned to 10.x).
- **Server Node version:** Node 24 LTS on every server (2026-10-29); new installs get it too.
The backend declares `engines.node >= 22`. pg-boss is still on 10.x (11 is the next step).
- **Deploying:** `bash scripts/deploy.sh [tag]` on the server (no tag = latest main). Each install
keeps its own `ecosystem.config.js` (PM2 app names, port), which is not in git. The script
runs from a temp copy of itself and rolls back on failure before the PM2 reload. It was
@@ -456,6 +461,24 @@ a test server (both done 2026-10-28).
`fetchAllUsers` (Payments, Email/WhatsApp attendees, Forms, Manual) — a server-side search
picker would replace them; the coupons list; `GET /api/registrations` without `page` (other callers still use the plain array).
## Command palette and Excel (1.51.6)
- **Palette:** reports come from `components/reports/ReportCatalog.ts` — add a report there and it
appears. A new dashboard page needs a line in `lib/commandPages.ts`, or an entry in
`NOT_IN_PALETTE` in `e2e/dashboard.spec.ts` (which fails on any page that's neither).
- **Excel:** `require('@cj-tech-master/excelts')` — the exceljs API (`new ExcelJS.Workbook()`,
`xlsx.write(res)`). Reports need `kind: 'table'` (or `'layered'`) for the main table.
## Next.js 16 (1.51.5)
- Turbopack for dev and build (the default). Lint with `npm run lint` / `npx eslint src` (`next lint`
is gone). There's no middleware/proxy file — add `src/proxy.ts` (Next 16's name) only if needed.
- **React Compiler lint rules are warnings, not errors** (`eslint.config.mjs`): 181 existing places,
mostly `useEffect(() => { load(); }, [...])`. Fix them in a screen when it's moved onto v2
(1.51.6 – 1.51.11), then turn the rules back to errors. Don't add new ones.
- `eslint-config-next` 16's bundled plugins (react, import, jsx-a11y) don't list ESLint 10 as a peer
yet; they work, and npm notes the override. Revisit when they publish ESLint 10 support.
## TypeScript and API v2 (1.51.0)
- **Starting the backend:** PM2 runs `src/index.js`, a launcher. With plain Node it runs
+6 -10
View File
@@ -40,7 +40,8 @@ shipped; 1.49.1 **bulk actions** and 1.49.2 **saved filters** shipped — §9 is
(Afrikaans) are removed from scope (2026-10-28), so everything after moves up by one. 1.50.0 **accessibility
pass** shipped. 1.50.1 **pagination and search on long lists** and 1.50.2 **event picker and webhook guide** shipped. 1.51.0 **TypeScript backend and `/api/v2`
foundations**, 1.51.1 **v2 registrations and payments**, 1.51.2 **v2 accounts and check-in** and 1.51.3 **Express 5,
scanner on v2, v1 deprecation headers** shipped. **Next: 2.0.0 (AI helpers) — see "Still open" on v1.**
scanner on v2, v1 deprecation headers** shipped. 1.51.4 **security and dependency updates**, 1.51.5 **Next.js 16** and 1.51.6 **the complete command palette and
ExcelTS** shipped. **Next: 1.51.7 – 1.51.13 (every endpoint onto v2), then 2.0.0.**
**Repositories:**
@@ -1009,7 +1010,8 @@ income justify the hardware:
| 9 | 1.47.0 ✅ – 1.47.3 ✅, 1.48.0 ✅ | Event series & cloning (1.47.0 clone, 1.47.1 series, 1.47.2 year-on-year reports, 1.47.3 season pass), Outbound webhooks (Wallet passes removed from scope) | Smaller, independent wins |
| 10 | 1.49.0 ✅ – 1.49.2 ✅, 1.50.0 ✅, 1.50.1 ✅, 1.50.2 ✅ | Admin dashboard/Ctrl+K/bulk actions/saved filters, Accessibility pass, pagination and search on long lists, event picker and webhook guide (Afrikaans removed from scope) | Polish before the big release |
| 11 | 1.51.0 ✅, 1.51.1 ✅, 1.51.2 ✅, 1.51.3 ✅ | TypeScript backend + OpenAPI + `/api/v2` alongside v1: 1.51.0 foundations (build, launcher, route declarations, errors, paging, OpenAPI at `/docs/v2`, first read endpoints); 1.51.1 registrations and payments as TS services, typed frontend calls; 1.51.2 check-in and accounts; 1.51.3 Express 5, the scanner on v2, v1 deprecation headers, v2-shaped shared middleware | The breaking-change groundwork |
| 12 | **2.0.0** | **AI helpers** (§15 — headline feature: draft an event, summarise reports and feedback, draft messages, improve text; through the Hub with a rand cap per licence), v1 API removed, migration guide, marketing site/docs for selling | The release. AI needs the Hub (phase 6) for the key, metering and caps |
| 11b | 1.51.4 ✅, 1.51.5 ✅, 1.51.6 ✅, 1.51.7 – 1.51.13 | **Every endpoint on v2 before 2.0** (decided 2026-10-29): 1.51.4 dependency audit and Node 24 LTS; 1.51.5 Next.js 16 and ESLint 10 (ESLint 9 is out of support); 1.51.6 the complete command palette and ExcelTS instead of exceljs; 1.51.7 events and event set-up (events, sections, costs, uploads, series, season passes, bundles, coupons, campuses); 1.51.8 registrations and money, staff side (manage registrations, payments, cashup, provider transactions, waitlist, sponsorship, consent, forms, feedback); 1.51.9 tickets and attendance, staff side (tickets, scan locks, attendance, allocations); 1.51.10 kids check-in and volunteer rosters; 1.51.11 people and messaging (users, WhatsApp, broadcasts, scheduled emails, automations, banner, saved filters, dashboard, stats, analytics, reports); 1.51.12 privacy and admin (privacy, admin, settings, setup, Hub, outbound webhooks, backups); 1.51.13 **incoming webhooks and links in sent messages**: Yoco, PayFast and WAWP webhooks and the links in emails and WhatsApp (unsubscribe, consent, ticket transfer, calendar feeds, waitlist offers) get v2 URLs — new messages and provider registrations use them, and the v1 paths keep answering (links by a 308 redirect to v2, provider webhooks handled by the same code, since providers may not follow a redirect on a POST). Each moves its area's logic into TS services, its screens onto `apiV2`, and its v1 routes into `SUCCESSORS` | 2.0 can then remove v1 |
| 12 | **2.0.0** | **AI helpers** (§15 — headline feature: draft an event, summarise reports and feedback, draft messages, improve text; through the Hub with a rand cap per licence), v1 removed (all of it is on v2 by then; the old webhook URLs and links already sent in messages keep forwarding to v2), migration guide, marketing site/docs for selling | The release. AI needs the Hub (phase 6) for the key, metering and caps |
Each phase should add matching help content (`frontend/src/content/help/`),
audit log entries, Jest tests, a CHANGELOG entry, and updates to FEATURES.md
@@ -1046,6 +1048,7 @@ feature.
| — | AI in 2.0 | The **headline feature of 2.0**: one-button helpers only (draft an event, summarise reports and feedback, draft messages, improve text) — no chat yet (2026-10-24) |
| — | AI provider and key | One provider, **Claude** (Haiku 5.5 to start, for cost). Joshua provides the key; installs call AI **through the Hub**, which holds the key and meters usage (2026-10-24) |
| — | AI cost limits | A **monthly rand cap per install**, set in the Hub per licence to match what the church pays; admins see their usage in Site settings → AI (2026-10-24) |
| — | v1 in 2.0 | **Every endpoint moves to v2 before 2.0** (1.51.6 – 1.51.11, by area), then 2.0 removes v1. Changed the same day from "retire only what's replaced". Incoming webhooks (Yoco, PayFast, WAWP) and links in emails and WhatsApp (unsubscribe, consent, ticket transfer, calendar feeds) also get v2 URLs (1.51.13); their v1 paths keep working — links redirect (308) to v2, provider webhooks are handled by the same code — because old messages and provider registrations still use them (2026-10-29) |
| — | AI buttons and markup | AI buttons show only when the licence's monthly cap is **above R0**. When the cap is reached they stay visible but **disabled** until next month or until the cap is raised. Usage is charged at the real cost **plus a markup %** set in the Hub (a default, plus an override per licence). The markup covers small overruns and is the profit. The cap is counted in charged rand, and each call is checked against its worst-case cost before it's sent (2026-10-29) |
| — | AI later | Ask AI (read-only, tools only), help/navigation assistant, actions with confirmation, more helpers, and long-term **self-hosted** AI behind the same Hub endpoint — not tied to versions (2026-10-24) |
| — | POPIA: message kinds | Bulk messages are either **Event updates** (to registrants — still sent after unsubscribing) or **News & announcements** (marketing — opt-in, one-click unsubscribe). Every bulk email links to the preferences (2026-10-23) |
@@ -1068,11 +1071,4 @@ feature.
## Still open
- **How much of v1 goes before 2.0** (raised 2026-10-29, after 1.51.3). v2 now covers what members,
guests and the scanner use (events, accounts, registering, paying, cancelling, check-in). The
staff, supervisor and admin screens still call about 350 v1 routes (reports, cashup, messaging,
settings, kids, volunteers…). Porting them all is several releases of work with little gain for
churches. Options: (a) port everything, then remove v1 in 2.0 as planned; (b) **recommended** —
2.0 removes only the v1 endpoints that have a v2 replacement, and the rest stays as an internal API
(no deprecation header, not in the public docs) that moves to v2 when each screen is next
worked on; (c) keep all of v1 through 2.0 and decide later.
Nothing for now.
+41
View File
@@ -1,5 +1,18 @@
import fs from "fs";
import path from "path";
import { test, expect, loginAs } from "./fixtures";
// Dashboard pages the palette leaves out on purpose: role home pages it reaches through
// "Dashboard", pages that only redirect or reuse another, and an older form nothing links to.
const NOT_IN_PALETTE = new Set([
"/dashboard/admin", "/dashboard/staff", "/dashboard/supervisor", // role homes ("Dashboard")
"/dashboard/admin/settings", // its tabs are listed one by one
"/dashboard/admin/whatsapp", "/dashboard/supervisor/sections", "/dashboard/admin/forms", // redirects / the same page
"/dashboard/admin/cashup/[id]", // one event's cashup, from Cashup
"/dashboard/supervisor/manual-registration", // the older manual form
"/dashboard/user/reset-password", // from My profile & security
]);
// Dashboard "Today" and "Waiting for you", and the Ctrl+K command palette (1.49.0).
test.describe("dashboard and command palette", () => {
test("the admin dashboard shows what's on today and what's waiting", async ({ page }) => {
@@ -39,6 +52,8 @@ test.describe("dashboard and command palette", () => {
// Staff don't get admin pages.
await loginAs(page, "staff");
await page.goto("/dashboard/staff");
// The shortcut listens once the signed-in user has loaded (the search button shows then).
await expect(page.getByRole("button", { name: /Search or jump to/ })).toBeVisible();
await page.keyboard.press("Control+k");
const staffDialog = page.getByRole("dialog", { name: "Go to or search" });
await staffDialog.getByRole("combobox", { name: "Search" }).fill("users");
@@ -46,4 +61,30 @@ test.describe("dashboard and command palette", () => {
await staffDialog.getByRole("combobox", { name: "Search" }).fill("scan");
await expect(staffDialog.getByRole("option", { name: "Scan tickets" })).toBeVisible();
});
test("the palette lists every report and every dashboard page", async ({ page }) => {
const root = path.join(__dirname, "..", "frontend", "src");
const palette = fs.readFileSync(path.join(root, "lib", "commandPages.ts"), "utf8");
const pages: string[] = [];
const walk = (dir: string) => {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
if (e.isDirectory()) walk(path.join(dir, e.name));
else if (e.name === "page.tsx") pages.push("/" + path.relative(path.join(root, "app"), dir).split(path.sep).join("/"));
}
};
walk(path.join(root, "app", "dashboard"));
const missing = pages.filter(p => !NOT_IN_PALETTE.has(p) && !palette.includes(`href: "${p}"`) && !palette.includes(`href: "${p}?`));
expect(missing).toEqual([]);
// Reports come from the catalogue: one the old hand-made list didn't have.
await loginAs(page, "supervisor");
await page.goto("/dashboard/supervisor");
await expect(page.getByRole("button", { name: /Search or jump to/ })).toBeVisible();
await page.keyboard.press("Control+k");
const dialog = page.getByRole("dialog", { name: "Go to or search" });
await dialog.getByRole("combobox", { name: "Search" }).fill("medical");
await expect(dialog.getByRole("option", { name: /Report: Medical & emergency list/ })).toBeVisible();
await dialog.getByRole("combobox", { name: "Search" }).fill("profit");
await expect(dialog.getByRole("option", { name: /Report: Profit report/ })).toBeVisible();
});
});
+48
View File
@@ -0,0 +1,48 @@
import path from "path";
import { test, expect, seed, API } from "./fixtures";
// Excel exports (ExcelTS since 1.51.6): a styled report and the attendance register come back as
// real workbooks that open and hold what was asked for.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { Workbook } = require(path.join(__dirname, "..", "backend", "node_modules", "@cj-tech-master", "excelts"));
const XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
async function open(body: Buffer) {
const wb = new Workbook();
await wb.xlsx.load(body);
return wb;
}
test.describe("Excel exports", () => {
test("a report with stats, a chart and a table", async ({ request }) => {
const res = await request.post(`${API}/api/reports/excel`, {
headers: { Authorization: `Bearer ${seed().users.supervisor.token}` },
data: {
title: "E2E Payments",
kind: "table",
subtitle: "1–31 October",
stats: [{ label: "Total", value: "R 1 200.00" }],
chart: { title: "By method", data: [{ label: "Card", value: 900 }, { label: "Cash", value: 300 }] },
table: { columns: ["Name", "Amount"], rows: [["Thandi", "R 600.00"], ["Sipho", "R 600.00"]] },
},
});
expect(res.status()).toBe(200);
expect(res.headers()["content-type"]).toContain(XLSX);
const ws = (await open(await res.body())).worksheets[0];
const values: string[] = [];
ws.eachRow((row: { values: unknown[] }) => values.push(...row.values.filter(Boolean).map(String)));
expect(values).toEqual(expect.arrayContaining(["E2E Payments", "Total", "Card", "Thandi", "Sipho"]));
});
test("the attendance register", async ({ request }) => {
const res = await request.get(`${API}/api/attendance/${seed().events.course}/export?format=xlsx`, {
headers: { Authorization: `Bearer ${seed().users.supervisor.token}` },
});
expect(res.status()).toBe(200);
expect(res.headers()["content-type"]).toContain(XLSX);
const ws = (await open(await res.body())).getWorksheet("Attendance");
expect(ws.getRow(1).getCell(1).value).toBe("Name");
expect(ws.rowCount).toBeGreaterThan(1);
});
});
+9
View File
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->
## This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
+23 -13
View File
@@ -1,16 +1,26 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
// Next.js 16's own flat configs (no more FlatCompat / eslintrc). Run with `npm run lint`
// (`next lint` was removed in Next 16).
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
{
// React Compiler-era rules that eslint-plugin-react-hooks 7 (with Next 16) turns on as
// errors. They flag patterns the app uses on purpose and that work (mostly loading data in
// useEffect), so they're warnings while the code is moved off them screen by screen —
// see HANDOVER, "Next.js 16". New code should follow them.
rules: {
"react-hooks/set-state-in-effect": "warn",
"react-hooks/purity": "warn",
"react-hooks/immutability": "warn",
"react-hooks/static-components": "warn",
"react-hooks/refs": "warn",
},
},
globalIgnores([".next/**", ".next-*/**", "out/**", "build/**", "next-env.d.ts", "src/lib/api-v2/schema.d.ts"]),
]);
export default eslintConfig;
+2 -1
View File
@@ -6,7 +6,8 @@ const nextConfig: NextConfig = {
// from clashing with one already running.
distDir: process.env.NEXT_DIST_DIR || ".next",
images: {
domains: ['images.unsplash.com'],
// (images.domains is deprecated in Next 16.)
remotePatterns: [{ protocol: 'https', hostname: 'images.unsplash.com' }],
},
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
+886 -670
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -1,12 +1,12 @@
{
"name": "hope-events-frontend",
"version": "1.51.3",
"version": "1.51.6",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint src",
"api:types": "openapi-typescript src/lib/api-v2/openapi.json -o src/lib/api-v2/schema.d.ts"
},
"dependencies": {
@@ -29,33 +29,33 @@
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.14",
"@radix-ui/react-tooltip": "^1.2.7",
"@sentry/nextjs": "^10.71.0",
"@sentry/nextjs": "^10.78.0",
"@zxing/browser": "^0.1.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"idb": "^8.0.4",
"lucide-react": "^0.536.0",
"next": "^15.5.24",
"next": "^16.4.3",
"qrcode": "^1.5.4",
"react": "19.1.0",
"react": "^19.3.1",
"react-day-picker": "^9.8.1",
"react-dom": "19.1.0",
"react-dom": "^19.3.1",
"react-hook-form": "^7.62.0",
"react-webcam": "^7.2.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"zod": "^4.0.15"
"zod": "^4.6.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/qrcode": "^1.5.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"autoprefixer": "^10.4.21",
"eslint": "^9",
"eslint-config-next": "^15.5.24",
"eslint": "^10.13.0",
"eslint-config-next": "^16.4.3",
"openapi-typescript": "^7.13.0",
"postcss": "^8.5.6",
"tailwindcss": "3.4",
+1 -3
View File
@@ -1,9 +1,7 @@
// Next.js server instrumentation entry point. A no-op when NEXT_PUBLIC_SENTRY_DSN isn't
// set — see instrumentation-client.ts for the browser-side counterpart.
//
// Node runtime only, deliberately — this app's middleware.ts (which runs in Next's edge
// runtime) is a trivial pass-through with no real error surface, so an edge-runtime branch
// here would only inflate that shared middleware bundle for no actual coverage benefit.
// Node runtime only — the app has no middleware/proxy running in the edge runtime.
export async function register() {
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return;
if (process.env.NEXT_RUNTIME !== "nodejs") return;
+111 -211
View File
@@ -116,21 +116,17 @@
"startDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"endDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"location": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"price": {
@@ -138,13 +134,9 @@
"description": "The headline price in rand (0 = free)"
},
"pictureUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"requiresRegistration": {
@@ -333,21 +325,17 @@
"startDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"endDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"location": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"price": {
@@ -355,13 +343,9 @@
"description": "The headline price in rand (0 = free)"
},
"pictureUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"requiresRegistration": {
@@ -434,13 +418,9 @@
]
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"registrationDeadline": {
@@ -448,7 +428,7 @@
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
{
"type": "null"
@@ -510,33 +490,25 @@
"maxLength": 64
},
"label": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"startsAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"endsAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"location": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"cancelled": {
@@ -692,7 +664,7 @@
"type": "string",
"maxLength": 200,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
"phone": {
"type": "string",
@@ -876,7 +848,7 @@
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"event": {
"type": "object",
@@ -892,12 +864,12 @@
"startDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"endDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
}
},
"required": [
@@ -917,13 +889,9 @@
"type": "string"
},
"variant": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"quantity": {
@@ -961,13 +929,9 @@
"description": "false = added to an existing registration"
},
"couponError": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
}
},
@@ -1062,7 +1026,7 @@
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"event": {
"type": "object",
@@ -1078,12 +1042,12 @@
"startDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"endDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
}
},
"required": [
@@ -1103,13 +1067,9 @@
"type": "string"
},
"variant": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"quantity": {
@@ -1252,7 +1212,7 @@
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"event": {
"type": "object",
@@ -1268,12 +1228,12 @@
"startDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"endDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
}
},
"required": [
@@ -1293,13 +1253,9 @@
"type": "string"
},
"variant": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"quantity": {
@@ -1593,13 +1549,9 @@
"type": "string"
},
"phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"role": {
@@ -1637,7 +1589,7 @@
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
}
},
"required": [
@@ -1703,7 +1655,7 @@
"type": "string",
"maxLength": 200,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
"phone": {
"description": "null or empty removes it",
@@ -1775,13 +1727,9 @@
"type": "string"
},
"phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"role": {
@@ -1819,7 +1767,7 @@
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
}
},
"required": [
@@ -1955,7 +1903,7 @@
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"event": {
"type": "object",
@@ -1971,12 +1919,12 @@
"startDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"endDate": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
}
},
"required": [
@@ -1996,13 +1944,9 @@
"type": "string"
},
"variant": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"quantity": {
@@ -2172,15 +2116,11 @@
]
},
"message": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "For a transferred ticket: who has it now"
"description": "For a transferred ticket: who has it now",
"type": [
"string",
"null"
]
},
"ticket": {
"anyOf": [
@@ -2198,13 +2138,9 @@
"maxLength": 64
},
"eventTitle": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"ticketOptionId": {
@@ -2221,46 +2157,30 @@
"description": "For section checks"
},
"ticketName": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"variant": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"attendee": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The attendee named on it, if any"
"description": "The attendee named on it, if any",
"type": [
"string",
"null"
]
},
"bookedBy": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Who registered"
"description": "Who registered",
"type": [
"string",
"null"
]
},
"quantity": {
"type": "integer",
@@ -2299,15 +2219,11 @@
]
},
"sessionError": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Why no session is open for check-in now"
"description": "Why no session is open for check-in now",
"type": [
"string",
"null"
]
},
"consentStatus": {
"type": "string",
@@ -2508,25 +2424,17 @@
"maxLength": 64
},
"eventTitle": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"ticketName": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The ticket option, e.g. \"Adult\""
"description": "The ticket option, e.g. \"Adult\"",
"type": [
"string",
"null"
]
},
"quantity": {
"type": "integer",
@@ -2594,13 +2502,9 @@
]
},
"consentStatus": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
"type": [
"string",
"null"
]
},
"canOverride": {
@@ -2697,7 +2601,7 @@
"type": "string",
"maxLength": 200,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
"password": {
"type": "string",
@@ -2988,15 +2892,11 @@
]
},
"destination": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Where the code went, partly hidden"
"description": "Where the code went, partly hidden",
"type": [
"string",
"null"
]
},
"enrolling": {
"type": "boolean",
+18 -7
View File
@@ -1,5 +1,8 @@
// What the Ctrl+K command palette can jump to without searching (1.49.0): pages, settings tabs,
// reports and quick actions, each for the roles that can open it. `keywords` widen matching.
// Reports come from the report catalogue, so a new report shows up here by itself; a new page
// needs a line below (e2e/dashboard.spec.ts checks every dashboard page is listed or excluded).
import { REPORTS } from "@/components/reports/ReportCatalog";
export type Role = "staff" | "supervisor" | "admin";
export type CommandPage = { label: string; href: string; group: "Actions" | "Pages" | "Settings" | "Reports"; roles: Role[]; keywords?: string };
@@ -18,6 +21,10 @@ export const COMMAND_PAGES: CommandPage[] = [
{ group: "Actions", label: "Email attendees", href: "/dashboard/supervisor/email-attendees", roles: SUP, keywords: "message send mail" },
{ group: "Actions", label: "WhatsApp attendees", href: "/dashboard/supervisor/whatsapp-attendees", roles: SUP, keywords: "message send" },
// Pages
{ group: "Pages", label: "Dashboard", href: "/dashboard", roles: S, keywords: "home today waiting overview" },
{ group: "Pages", label: "Reports", href: "/dashboard/supervisor/reports", roles: SUP, keywords: "all reports export pdf excel" },
{ group: "Pages", label: "Ticket options", href: "/dashboard/supervisor/event-options", roles: SUP, keywords: "event options prices variants early bird" },
{ group: "Pages", label: "My groups", href: "/dashboard/staff/my-groups", roles: S, keywords: "rooms groups leader allocations" },
{ group: "Pages", label: "Events", href: "/dashboard/supervisor/events", roles: SUP, keywords: "manage edit copy" },
{ group: "Pages", label: "Event series", href: "/dashboard/supervisor/series", roles: SUP, keywords: "season pass follow" },
{ group: "Pages", label: "Registrations", href: "/dashboard/admin/registrations", roles: ADM, keywords: "orders cancel status" },
@@ -37,23 +44,27 @@ export const COMMAND_PAGES: CommandPage[] = [
{ group: "Pages", label: "Bundle deals", href: "/dashboard/supervisor/bundles", roles: SUP, keywords: "family deal" },
{ group: "Pages", label: "Event tickets & printing", href: "/dashboard/staff/event-tickets", roles: S, keywords: "print list" },
{ group: "Pages", label: "Offline scanning", href: "/dashboard/supervisor/offline-scans", roles: SUP, keywords: "devices sync" },
{ group: "Pages", label: "Attendee forms", href: "/dashboard/admin/forms", roles: ADM, keywords: "answers" },
{ group: "Pages", label: "Attendee forms", href: "/dashboard/supervisor/forms", roles: S, keywords: "answers questions responses" },
{ group: "Pages", label: "Audit log", href: "/dashboard/admin/audit-log", roles: ADM, keywords: "history who changed" },
{ group: "Pages", label: "Privacy (POPIA)", href: "/dashboard/admin/privacy", roles: ADM, keywords: "notice operators retention breaches readiness" },
{ group: "Pages", label: "Privacy requests", href: "/dashboard/admin/data-requests", roles: ADM, keywords: "delete download correction popia" },
{ group: "Pages", label: "My profile & security", href: "/dashboard/user/profile", roles: S, keywords: "password two-factor" },
// Their own side, as members (staff register for things too)
{ group: "Pages", label: "My events & tickets", href: "/dashboard/user", roles: S, keywords: "my registrations tickets" },
{ group: "Pages", label: "My payments", href: "/dashboard/user/payments", roles: S, keywords: "my receipts invoices" },
{ group: "Pages", label: "My privacy", href: "/dashboard/user/privacy", roles: S, keywords: "my data download delete news popia" },
{ group: "Pages", label: "My kids", href: "/dashboard/user/kids", roles: S, keywords: "children family pickup" },
{ group: "Pages", label: "My serving", href: "/dashboard/user/serving", roles: S, keywords: "volunteer roster cover" },
{ group: "Pages", label: "My attendee forms", href: "/dashboard/user/forms", roles: S, keywords: "fill in answers" },
{ group: "Pages", label: "Make a donation", href: "/dashboard/user/donate", roles: S, keywords: "give donate" },
// Settings
...([
["organisation", "Organisation"], ["campuses", "Campuses"], ["branding", "Branding"], ["notifications", "Notifications"],
["email", "Email"], ["legal", "Legal"], ["whatsapp", "WhatsApp"], ["payments", "Payments"], ["webhooks", "Webhooks"],
["backups", "Backups"], ["jobs", "Background jobs"], ["licence", "Licence & updates"],
] as const).map(([tab, label]): CommandPage => ({ group: "Settings", label: `Settings: ${label}`, href: `/dashboard/admin/settings?tab=${tab}`, roles: ADM, keywords: "site settings" })),
// Reports
...([
["payments", "Payments between dates"], ["revenue", "Revenue summary"], ["regStatus", "Registration status"],
["funnel", "Registration funnel"], ["feedbackSummary", "Feedback summary"], ["usage", "Ticket usage"],
["financeReport", "Finance report"], ["seriesComparison", "Series: year on year"], ["discounts", "Discounts"],
] as const).map(([key, label]): CommandPage => ({ group: "Reports", label: `Report: ${label}`, href: `/dashboard/supervisor/reports?report=${key}`, roles: SUP, keywords: "report" })),
// Reports — every report in the catalogue
...REPORTS.map((r): CommandPage => ({ group: "Reports", label: `Report: ${r.label}`, href: `/dashboard/supervisor/reports?report=${r.key}`, roles: SUP, keywords: `report ${r.category} ${r.description}` })),
];
/** Pages for a role, matching every word of the query (in the label or keywords). */
-6
View File
@@ -1,6 +0,0 @@
import { NextResponse } from "next/server";
// Minimal pass-through middleware to satisfy Next.js requirements
export function middleware() {
return NextResponse.next();
}
+2 -2
View File
@@ -11,7 +11,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -22,6 +22,6 @@
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next-e2e/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", ".next-e2e/types/**/*.ts", ".next-e2e/dev/types/**/*.ts"],
"exclude": ["node_modules"]
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "hope-events",
"version": "1.51.3",
"version": "1.51.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hope-events",
"version": "1.51.3",
"version": "1.51.6",
"license": "ISC",
"devDependencies": {
"@axe-core/playwright": "^4.14.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hope-events",
"version": "1.51.3",
"version": "1.51.6",
"main": "index.js",
"scripts": {
"dev:backend": "cd backend && npm run dev",
+7
View File
@@ -53,6 +53,13 @@ checkout() {
echo "==> Deploying $TARGET (currently $PREVIOUS)"
# Node 20 reached end of life in April 2026 (no more security fixes). Releases still run on it,
# but the server should move to Node 24 LTS — see DEPLOYMENT.md, "Upgrading Node".
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
if [ "$NODE_MAJOR" -lt 22 ]; then
echo "!!> This server runs Node $(node -v), which no longer gets security fixes. Upgrade to Node 24 LTS (DEPLOYMENT.md, \"Upgrading Node\")."
fi
echo "==> Backing up the database"
(cd backend && node -e "require('dotenv').config(); require('./src/utils/backupUtils').runBackup().then(r => console.log(' ', r.filename)).catch(e => { console.error(' Backup failed:', e.message); process.exit(1); })")
+5 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Installs Cross Events on a fresh Ubuntu server (22.04 or 24.04): Node 20, PostgreSQL, PM2,
# Installs Cross Events on a fresh Ubuntu server (22.04 or 24.04): Node 24 LTS, PostgreSQL, PM2,
# nginx and an HTTPS certificate, then the app itself. Finish in the browser at /setup.
#
# Usage (as root, or with sudo):
@@ -58,9 +58,10 @@ apt-get update -q
apt-get install -y -q curl git nginx postgresql postgresql-client openssl build-essential ca-certificates gnupg
[ "$SSL" = 0 ] || apt-get install -y -q certbot python3-certbot-nginx
step "Node.js 20"
if ! command -v node >/dev/null || [ "$(node -p 'process.versions.node.split(".")[0]')" -lt 20 ]; then
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
# Node 24 is the LTS line supported until April 2028 (Node 20 reached end of life in April 2026).
step "Node.js 24"
if ! command -v node >/dev/null || [ "$(node -p 'process.versions.node.split(".")[0]')" -lt 24 ]; then
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y -q nodejs
fi
node -v