diff --git a/CHANGELOG.md b/CHANGELOG.md index 62de66f..9247077 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [1.41.1] - 2026-10-27 + +### Changed + +- **No licence now means read-only**, the same as a suspended licence: a site that isn't connected + to the Hub can't create events, registrations or payments until a licence key is connected + (admins see a red banner). `LICENCE_CHECK_DISABLED=true` switches this off outside production, + for the end-to-end tests. +- **Read-only mode also blocks editing events**, their options and their variants (suspended, + revoked and unlicensed sites). +- The site **checks in with the Hub 30 seconds after starting**, and 30 seconds after a licence + key is connected, instead of waiting up to 15 minutes. + +### Fixed + +- A site the Hub no longer knows (`INSTALL_NOT_FOUND`, e.g. the licence was released) kept showing + "Active" on its old licence. It now drops the connection and licence, and shows "No licence". + ## [1.41.0] - 2026-10-26 ### Added diff --git a/FEATURES.md b/FEATURES.md index fa98549..3d99c0b 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -657,8 +657,9 @@ can't start, the same jobs run on in-process timers. Licence) and checks in every 15 minutes with its version, health and totals (no personal information; income as a range). Licences are signed tokens checked on the site, so a Hub outage never switches it off: past the end date there's a grace period (banner for site admins only), - then — or when suspended or revoked — the site is read-only (no new events, registrations or - payments; tickets still scan, data can be exported). Admins see when an update is available. + then — or when suspended, revoked or not licensed — the site is read-only (events can't be + created or edited; no new registrations or payments; tickets still scan, data can be exported). + The site checks in 30 seconds after starting and after a licence key is connected. Admins see when an update is available. See `docs/hub-protocol.md`. - **`/health`** — status, version, database, background jobs and last backup. - **PM2** (`ecosystem.config.js`) runs backend and frontend, restarting either diff --git a/SETTINGS.md b/SETTINGS.md index 319752c..a93a6a7 100644 --- a/SETTINGS.md +++ b/SETTINGS.md @@ -90,8 +90,9 @@ Managed in the Payments tab (through `/api/payments/providers/settings` and `/ap |---|---|---| | `HUB_URL` (env) | `https://events-hub.crosscode.co.za` | Where the Hub runs. | | `HUB_PUBLIC_KEY` (env) | The production key built into `backend/src/hub/publicKey.js` | Only for a local or test Hub. Licence tokens are checked with this key. | -| `HUB_HEARTBEAT_ENABLED` (env) | `true` | The 15-minute `hub-heartbeat` job. Off means the licence isn't refreshed — after 30 days the site goes into grace, then read-only. | -| `hub_install_id`, `hub_install_secret` (encrypted), `hub_licence`, `hub_last_heartbeat_at`, `hub_last_error`, `hub_latest_version` | — | Written only by `src/hub/client.js` when connecting and on each heartbeat. Never readable or writable through the generic settings endpoints. Deleting them disconnects the site (it becomes "not connected"). | +| `HUB_HEARTBEAT_ENABLED` (env) | `true` | The 15-minute `hub-heartbeat` job (plus one check-in 30 s after starting and after connecting). Off means the licence isn't refreshed — after 30 days the site goes into grace, then read-only. | +| `LICENCE_CHECK_DISABLED` (env) | `false` | `true` turns read-only mode off — **ignored when `NODE_ENV=production`**. For the end-to-end tests and local development without a Hub. | +| `hub_install_id`, `hub_install_secret` (encrypted), `hub_licence`, `hub_last_heartbeat_at`, `hub_last_error`, `hub_latest_version` | — | Written only by `src/hub/client.js` when connecting and on each heartbeat. Never readable or writable through the generic settings endpoints. Deleting them disconnects the site (it becomes "no licence", which is read-only). The heartbeat deletes the install and licence keys itself when the Hub answers `INSTALL_NOT_FOUND`. | ## WhatsApp diff --git a/backend/package-lock.json b/backend/package-lock.json index 7576ab0..234ae36 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "event-management-backend", - "version": "1.41.0", + "version": "1.41.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "event-management-backend", - "version": "1.41.0", + "version": "1.41.1", "hasInstallScript": true, "dependencies": { "@prisma/adapter-pg": "^7.10.1", diff --git a/backend/package.json b/backend/package.json index 2e8f00f..4dc9857 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "event-management-backend", - "version": "1.41.0", + "version": "1.41.1", "description": "Event Management System Backend", "main": "src/index.js", "scripts": { diff --git a/backend/src/controllers/hubController.js b/backend/src/controllers/hubController.js index 4eebd18..110282d 100644 --- a/backend/src/controllers/hubController.js +++ b/backend/src/controllers/hubController.js @@ -63,6 +63,7 @@ const connect = async (req, res) => { if (!licenceKey) { res.status(400); throw new Error('Enter your licence key.'); } const me = await prisma.user.findUnique({ where: { id: req.user.id }, select: { name: true, email: true } }); const claims = await hub.connect(licenceKey, me); + hub.heartbeatSoon(); logAdminAction({ actorId: req.user.id, actorRole: req.user.role, action: 'hub_connected', targetType: 'hub', metadata: { customer: claims.customer, hubUrl: hub.hubUrl() }, ip: getClientIp(req) }); res.json(await statusPayload({ fresh: true })); } catch (e) { diff --git a/backend/src/hub/client.js b/backend/src/hub/client.js index 9a3aaec..e7d4ba9 100644 --- a/backend/src/hub/client.js +++ b/backend/src/hub/client.js @@ -114,10 +114,29 @@ async function heartbeat() { return { ok: true, status: claims?.status || null }; } catch (e) { await save({ hub_last_error: `${new Date().toISOString()} ${e.code || ''} ${e.message}`.slice(0, 500) }); - // The Hub doesn't know this install any more (licence released): show "Reconnect". - if (e.code === 'INSTALL_NOT_FOUND') return { ok: false, reconnect: true }; + // The Hub doesn't know this install any more (licence released): drop the connection and the + // licence, so the site is unlicensed (read-only) and shows "Connect" until a key is entered. + if (e.code === 'INSTALL_NOT_FOUND') { + await save({ hub_install_id: null, hub_install_secret: null, hub_licence: null }); + return { ok: false, reconnect: true }; + } throw e; } } -module.exports = { PROTOCOL_VERSION, HUB_KEYS, hubUrl, HubError, connect, heartbeat }; +/** + * A heartbeat soon (on startup, and after connecting), so a licence change on the Hub shows + * without waiting for the next 15-minute run. Never throws; skipped when the job is switched off. + */ +let soonTimer = null; +function heartbeatSoon(delayMs = 30 * 1000) { + if (String(process.env.JOBS_ENABLED || 'true').toLowerCase() === 'false') return; + if (String(process.env.HUB_HEARTBEAT_ENABLED || 'true').toLowerCase() === 'false') return; + clearTimeout(soonTimer); + soonTimer = setTimeout(() => { + heartbeat().catch(e => console.warn('[hub] Heartbeat failed:', e?.message || e)); + }, delayMs); + soonTimer.unref?.(); +} + +module.exports = { PROTOCOL_VERSION, HUB_KEYS, hubUrl, HubError, connect, heartbeat, heartbeatSoon }; diff --git a/backend/src/hub/licence.js b/backend/src/hub/licence.js index 770687e..8f79d45 100644 --- a/backend/src/hub/licence.js +++ b/backend/src/hub/licence.js @@ -7,7 +7,10 @@ * suspended — suspended by the Hub, or grace ran out: read-only (no new events, * registrations or payments); tickets still scan, data can be exported * revoked — like suspended, with a permanent banner - * unlicensed — never connected to the Hub: works, admins see "Connect to Hub" + * unlicensed — not connected to the Hub (never, or the Hub released this install): read-only, + * like suspended, until a licence key is connected + * + * LICENCE_CHECK_DISABLED=true turns read-only off outside production (the end-to-end tests). */ const crypto = require('crypto'); @@ -15,7 +18,7 @@ const prisma = require('../config/db'); const { hubPublicKey } = require('./publicKey'); const DAY = 24 * 60 * 60 * 1000; -const READ_ONLY_STATES = new Set(['suspended', 'revoked']); +const READ_ONLY_STATES = new Set(['suspended', 'revoked', 'unlicensed']); const CACHE_MS = 30 * 1000; let publicKeyObject = null; @@ -53,7 +56,7 @@ function verifyToken(token, key = publicKey()) { */ function stateFor(claims, installId, now = new Date()) { if (!claims || (installId && claims.installId !== installId)) { - return { state: 'unlicensed', readOnly: false, endsAt: null, graceEndsAt: null, daysLeft: null }; + return { state: 'unlicensed', readOnly: true, endsAt: null, graceEndsAt: null, daysLeft: null }; } const ends = [claims.licenceExpiresAt, claims.validUntil].filter(Boolean).map(d => new Date(d)); const endsAt = ends.length ? new Date(Math.min(...ends)) : null; @@ -68,6 +71,10 @@ function stateFor(claims, installId, now = new Date()) { return { state, readOnly: READ_ONLY_STATES.has(state), endsAt, graceEndsAt, daysLeft, expired: state === 'suspended' && claims.status === 'active' }; } +function checkDisabled() { + return process.env.NODE_ENV !== 'production' && String(process.env.LICENCE_CHECK_DISABLED || '').toLowerCase() === 'true'; +} + let cache = null; /** This install's licence: claims, state and Hub connection details. Cached for 30 s. */ @@ -82,6 +89,7 @@ async function licenceStatus({ fresh = false } = {}) { installId: s.hub_install_id || null, claims: claims && claims.installId === s.hub_install_id ? claims : null, ...stateFor(claims, s.hub_install_id || null), + ...(checkDisabled() ? { readOnly: false } : {}), lastHeartbeatAt: s.hub_last_heartbeat_at || null, lastError: s.hub_last_error || null, latestVersion: s.hub_latest_version || null, diff --git a/backend/src/hub/readOnly.js b/backend/src/hub/readOnly.js index c229f1c..c2e4cf6 100644 --- a/backend/src/hub/readOnly.js +++ b/backend/src/hub/readOnly.js @@ -1,6 +1,7 @@ /** - * Read-only mode while the licence is suspended or revoked: no *new* activity — new events, - * registrations, tickets added, payments started or recorded, waitlist places, coupons or bundles. + * Read-only mode while the licence is suspended, revoked or missing (not connected to the Hub): + * no new or edited events, and no *new* activity — registrations, tickets added, payments started + * or recorded, waitlist places, coupons or bundles. * Everything else keeps working on purpose: scanning tickets for events people already paid for, * viewing and exporting data, refunds and cancellations, privacy requests, logging in, and the * settings needed to renew the licence. Payment webhooks for payments already started also still @@ -11,8 +12,11 @@ const { licenceStatus } = require('./licence'); const BLOCKED = [ ['POST', /^\/api\/events\/?$/], + ['PUT', /^\/api\/events\/[^/]+\/?$/], ['POST', /^\/api\/events\/[^/]+\/options\/?$/], + ['PUT', /^\/api\/events\/options\/[^/]+\/?$/], ['POST', /^\/api\/events\/options\/[^/]+\/variants\/?$/], + ['PUT', /^\/api\/events\/variants\/[^/]+\/?$/], ['POST', /^\/api\/registrations\/?$/], ['POST', /^\/api\/registrations\/manual\/?$/], ['PUT', /^\/api\/registrations\/[^/]+\/options\/?$/], @@ -36,8 +40,10 @@ async function licenceReadOnly(req, res, next) { return res.status(403).json({ code: 'LICENCE_READ_ONLY', message: status.state === 'revoked' - ? "This site's licence has been revoked, so nothing new can be added. Tickets still scan and all data can still be viewed and exported." - : "This site's licence is suspended, so nothing new can be added for now. Tickets still scan and all data can still be viewed and exported. Please contact the site's administrator.", + ? "This site's licence has been revoked, so nothing new can be added and events can't be changed. Tickets still scan and all data can still be viewed and exported." + : status.state === 'unlicensed' + ? "This site has no licence, so nothing new can be added and events can't be changed. A site admin can connect a licence key in Site settings → Licence." + : "This site's licence is suspended, so nothing new can be added and events can't be changed for now. Tickets still scan and all data can still be viewed and exported. Please contact the site's administrator.", }); } catch { // Never let a licence check failure take the site down. diff --git a/backend/src/index.js b/backend/src/index.js index 66bbbce..54ec990 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -162,7 +162,7 @@ app.use((req, res, next) => { next(); }); -// Licence suspended or revoked: no new events, registrations or payments (src/hub/readOnly.js). +// Licence suspended, revoked or missing: no new or edited events, registrations or payments (src/hub/readOnly.js). app.use(require('./hub/readOnly').licenceReadOnly); // Health for monitoring (and the Hub): no secrets, no personal information. @@ -1570,6 +1570,9 @@ app.listen(PORT, () => { // backup, temp cleanup) run on the pg-boss job queue — see src/jobs/index.js. require('./jobs').startJobs().catch(e => console.warn('[jobs] Not started:', e?.message || e)); + // Check in with the Hub 30 s after starting, so the licence is fresh without waiting 15 minutes. + require('./hub/client').heartbeatSoon(); + // Scheduled emails worker (polling) try { const enabled = String(process.env.SCHEDULED_EMAILS_ENABLED || 'true').toLowerCase() !== 'false'; diff --git a/backend/tests/hub.test.js b/backend/tests/hub.test.js index fdb57ee..9ae74ed 100644 --- a/backend/tests/hub.test.js +++ b/backend/tests/hub.test.js @@ -33,8 +33,8 @@ describe('licence tokens', () => { }); describe('licence states', () => { - test('never connected → unlicensed, not read-only', () => { - expect(stateFor(null, null, NOW)).toMatchObject({ state: 'unlicensed', readOnly: false }); + test('never connected → unlicensed, read-only like suspended', () => { + expect(stateFor(null, null, NOW)).toMatchObject({ state: 'unlicensed', readOnly: true }); }); test('a token for another install counts as none', () => { @@ -80,6 +80,9 @@ describe('heartbeat report', () => { describe('read-only mode', () => { test.each([ ['POST', '/api/events'], + ['PUT', '/api/events/ev_1'], + ['PUT', '/api/events/options/opt_1'], + ['PUT', '/api/events/variants/var_1'], ['POST', '/api/registrations'], ['POST', '/api/registrations/manual'], ['PUT', '/api/registrations/abc/options'], @@ -92,6 +95,7 @@ describe('read-only mode', () => { test.each([ ['POST', '/api/tickets/scan/QR123'], ['GET', '/api/events'], + ['PUT', '/api/events/ev_1/notify-recipients'], ['PUT', '/api/registrations/abc'], ['POST', '/api/payments/refund'], ['POST', '/api/privacy/me/requests'], diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index 30f8baf..6a44653 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -1,6 +1,6 @@ # Handover — where things stand and how to carry on -Last updated: 2026-10-26, after **1.41.0** (and Hub 1.0.0). The plan itself is in +Last updated: 2026-10-27, after **1.41.1** (and Hub 1.0.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). @@ -8,6 +8,7 @@ features is in [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md). | Version | What | |---------|------| +| 1.41.1 | Licence fixes — no licence (never connected, or the Hub answers `INSTALL_NOT_FOUND`, which now drops the old licence) is read-only like suspended; read-only also blocks editing events/options/variants; heartbeat 30 s after start and after connecting; `LICENCE_CHECK_DISABLED` for e2e | | 1.41.0 | Hope Events Hub connection — licence step in `/setup` and Site settings → Licence, 15-minute heartbeat (health, totals, income bands), licence states checked offline (grace banner for site admins only, read-only when suspended/revoked), `/health`, "update available", `scripts/install.sh`. The Hub itself is `joshua/events-hub` 1.0.0 | | 1.40.0 | POPIA toolkit part 3 — readiness checklist with score, retention policies + nightly `retention` job (inactive accounts anonymised after 5 years with a 30-day warning, on by default), breach register with notification drafts, children's/medical flag in the access log, `{retention}` in the notice | | 1.39.0 | POPIA toolkit part 2 — Admin → Privacy: privacy notice editor (built-in text as the start, placeholders, drafts, numbered versions, re-acceptance after login), operator register, consent log, access log; Information Officer on the contact page | @@ -165,7 +166,7 @@ features is in [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md). active / grace / suspended / revoked / unlicensed, cached 30 s), `client.js` (connect, heartbeat; AppSetting `hub_*` keys, which the generic settings endpoints never touch), `report.js` (health, totals, income bands), `readOnly.js` (the routes blocked in read-only mode — add any new - "create" route there). + "create" or event-edit route there). Read-only = suspended, revoked or unlicensed. - Job `hub-heartbeat` (every 15 min), routes `/api/hub/*`, `/health`. - Frontend: `components/admin/LicenceTab.tsx` (Site settings → Licence; its `ConnectForm` is also on the last `/setup` screen), `components/shared/LicenceBanner.tsx` (site admins only). diff --git a/docs/hub-protocol.md b/docs/hub-protocol.md index 65779b6..1dab96a 100644 --- a/docs/hub-protocol.md +++ b/docs/hub-protocol.md @@ -140,9 +140,9 @@ out its state: |-------|------|--------------| | **active** | `status` is active and now is before `min(licenceExpiresAt, validUntil)` | Everything works | | **grace** | after that date, for up to `graceDays` | Everything works; **only site admins** see a banner "Licence needs renewing — X days left" | -| **suspended** | `status` is suspended, or grace has run out | Read-only: no new events, registrations or payments. Tickets still scan; admins can view and export all data; privacy requests still work | +| **suspended** | `status` is suspended, or grace has run out | Read-only: events can't be created or edited; no new registrations or payments. Tickets still scan; admins can view and export all data; privacy requests still work | | **revoked** | `status` is revoked | Same as suspended, with a permanent banner | -| **unlicensed** | never connected | Works; admins see "Connect to Hub" | +| **unlicensed** | never connected, or the Hub answered `INSTALL_NOT_FOUND` (the install drops its token) | Same as suspended (events can't be created or edited either); admins see "Connect to Hub" | `validUntil` is refreshed by every heartbeat, so a Hub outage only matters after 30 days (then grace starts). Blocking the Hub can't keep a suspended licence active for more than 30 days. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ae53e03..0741d62 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "hope-events-frontend", - "version": "1.41.0", + "version": "1.41.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hope-events-frontend", - "version": "1.41.0", + "version": "1.41.1", "dependencies": { "@dnd-kit/core": "^6.3.1", "@hookform/resolvers": "^5.2.1", diff --git a/frontend/package.json b/frontend/package.json index d3fdfc9..283c237 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "hope-events-frontend", - "version": "1.41.0", + "version": "1.41.1", "private": true, "scripts": { "dev": "next dev --turbopack", diff --git a/frontend/src/app/setup/page.tsx b/frontend/src/app/setup/page.tsx index 91cd194..d0413db 100644 --- a/frontend/src/app/setup/page.tsx +++ b/frontend/src/app/setup/page.tsx @@ -601,7 +601,7 @@ export default function SetupPage() {

✓ Connected — licensed to {licenceConnected}.

) : ( <> -

Enter the licence key you received to connect this site to the Hope Events Hub. You can also do this later under Admin → Site Settings → Licence.

+

Enter the licence key you received to connect this site to the Hope Events Hub. Until a licence is connected, events can't be created or edited. You can also do this later under Admin → Site Settings → Licence.

setLicenceConnected(s.licence?.customer || "your organisation")} /> )} diff --git a/frontend/src/components/admin/LicenceTab.tsx b/frontend/src/components/admin/LicenceTab.tsx index cad3e6f..d72132b 100644 --- a/frontend/src/components/admin/LicenceTab.tsx +++ b/frontend/src/components/admin/LicenceTab.tsx @@ -26,9 +26,9 @@ export type HubStatus = { const STATE: Record = { active: { label: "Active", tone: "bg-green-50 text-green-800 border-green-200", text: "Everything works." }, grace: { label: "Needs renewing", tone: "bg-amber-50 text-amber-900 border-amber-200", text: "Everything still works for now — renew before the grace period ends." }, - suspended: { label: "Suspended", tone: "bg-red-50 text-red-800 border-red-200", text: "Read-only: no new events, registrations or payments. Tickets still scan and all data can be exported." }, - revoked: { label: "Revoked", tone: "bg-red-50 text-red-800 border-red-200", text: "Read-only: no new events, registrations or payments. Tickets still scan and all data can be exported." }, - unlicensed: { label: "Not connected", tone: "bg-gray-50 text-gray-700 border-gray-200", text: "Connect this site to the Hope Events Hub with your licence key." }, + suspended: { label: "Suspended", tone: "bg-red-50 text-red-800 border-red-200", text: "Read-only: events can't be created or edited, and no new registrations or payments. Tickets still scan and all data can be exported." }, + revoked: { label: "Revoked", tone: "bg-red-50 text-red-800 border-red-200", text: "Read-only: events can't be created or edited, and no new registrations or payments. Tickets still scan and all data can be exported." }, + unlicensed: { label: "No licence", tone: "bg-red-50 text-red-800 border-red-200", text: "Read-only until you connect a licence key: events can't be created or edited, and no new registrations or payments." }, }; const fmt = (d: string | null) => (d ? new Date(d).toLocaleString(undefined, { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) : "—"); @@ -100,7 +100,7 @@ export function LicenceTab({ active }: { active: boolean }) {

Licence

-

This site checks in with the Hope Events Hub every 15 minutes: version, health and totals only — never anyone's personal information.

+

This site checks in with the Hope Events Hub 30 seconds after starting and every 15 minutes after that: version, health and totals only — never anyone's personal information.

diff --git a/frontend/src/components/shared/LicenceBanner.tsx b/frontend/src/components/shared/LicenceBanner.tsx index 3c532f0..08d0877 100644 --- a/frontend/src/components/shared/LicenceBanner.tsx +++ b/frontend/src/components/shared/LicenceBanner.tsx @@ -37,16 +37,16 @@ export function LicenceBanner() {
- The licence is {status.state === "revoked" ? "revoked" : "suspended"}: the site is read-only — no new events, registrations or payments. Tickets still scan and all data can be exported. {link} + The licence is {status.state === "revoked" ? "revoked" : "suspended"}: the site is read-only — events can't be created or edited, and no new registrations or payments. Tickets still scan and all data can be exported. {link}
); } - if (status.state === "unlicensed" && status.keyConfigured) { + if (status.state === "unlicensed" && status.readOnly) { return ( -
+
- This site isn't connected to the Hope Events Hub yet. {link} + This site has no licence, so it's read-only — events can't be created or edited. Connect your licence key in {link}
); } diff --git a/package-lock.json b/package-lock.json index 70eabdb..8666325 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hope-events", - "version": "1.41.0", + "version": "1.41.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hope-events", - "version": "1.41.0", + "version": "1.41.1", "license": "ISC", "devDependencies": { "@playwright/test": "^1.64.0", diff --git a/package.json b/package.json index 44555cd..d8fde2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hope-events", - "version": "1.41.0", + "version": "1.41.1", "main": "index.js", "scripts": { "dev:backend": "cd backend && npm run dev", diff --git a/playwright.config.ts b/playwright.config.ts index 68c0581..c78b7e8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -46,6 +46,8 @@ export default defineConfig({ FRONTEND_URL: `http://localhost:${WEB_PORT}`, APP_BASE_URL: `http://localhost:${WEB_PORT}`, JOBS_ENABLED: "false", + // The test site has no licence; without this it would be read-only. + LICENCE_CHECK_DISABLED: "true", SCHEDULED_EMAILS_ENABLED: "false", // The kiosk test signs a supervisor in with a password (no 2FA code by email). TWO_FACTOR_REQUIRED_ROLES: "false",