diff --git a/.github/workflows/checklist-runs.yml b/.github/workflows/checklist-runs.yml
new file mode 100644
index 00000000..5537a6a5
--- /dev/null
+++ b/.github/workflows/checklist-runs.yml
@@ -0,0 +1,31 @@
+name: Checklist runs
+on:
+ push:
+ paths: ['applications/checklist-runs/**', '.github/workflows/checklist-runs.yml']
+ pull_request:
+ paths: ['applications/checklist-runs/**', '.github/workflows/checklist-runs.yml']
+permissions:
+ contents: read
+jobs:
+ checks:
+ runs-on: ubuntu-24.04
+ defaults:
+ run:
+ working-directory: applications/checklist-runs
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '24.21.0'
+ cache: npm
+ cache-dependency-path: applications/checklist-runs/package-lock.json
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - run: npm ci
+ - run: npm run format:check
+ - run: npm run build
+ - run: python3 -m unittest discover -s tests -p 'test_*.py' -v
+ - run: python3 scripts/install_postgrest.py
+ - run: .local/bin/postgrest --version
+ - run: PGRST_JWT_SECRET=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa .local/bin/postgrest --dump-config postgrest.conf
diff --git a/applications/checklist-runs/.env.example b/applications/checklist-runs/.env.example
new file mode 100644
index 00000000..f57de9f5
--- /dev/null
+++ b/applications/checklist-runs/.env.example
@@ -0,0 +1,10 @@
+# Export these in a runtime-only terminal. Fill from your dedicated Cloud receipt.
+PGHOST=your-service-hostname
+PGPORT=5432
+PGDATABASE=postgres
+PGUSER=checklist_authenticator
+PGPASSWORD=replace-with-runtime-password
+PGSSLMODE=verify-full
+PGSSLROOTCERT=/absolute/private/path/ca.pem
+PGCONNECT_TIMEOUT=10
+PGRST_JWT_SECRET=replace-with-generated-128-character-hex-secret
diff --git a/applications/checklist-runs/.gitignore b/applications/checklist-runs/.gitignore
new file mode 100644
index 00000000..b82de939
--- /dev/null
+++ b/applications/checklist-runs/.gitignore
@@ -0,0 +1,10 @@
+.env
+*.env
+.venv/
+__pycache__/
+*.pyc
+.local/
+postgrest
+*.tar.xz
+node_modules/
+dist/
diff --git a/applications/checklist-runs/README.md b/applications/checklist-runs/README.md
new file mode 100644
index 00000000..7012291a
--- /dev/null
+++ b/applications/checklist-runs/README.md
@@ -0,0 +1,167 @@
+# Checklist runs with React, TanStack Query and PostgREST
+
+A small TypeScript interface over a SQL-first API on **ClickHouse Managed Postgres**. Choose a versioned template, start a labelled run and complete its steps with retained notes. Two synthetic operator roles see their own runs and nested records. No external task service or analytical database is required.
+
+The UI uses React 19.3.0, TanStack Query 5.104.1 and Vite 8.3.2. TypeScript 7.0.2, Node 24.21.0 LTS, PostgREST 16.4 and helper dependencies are pinned. PostgreSQL 18 is the documented Cloud major. This is a loopback demo: Vite preview is not production hosting, and the offline fixture token issuer is not a login provider.
+
+## What stays correct
+
+`start_run` locks the current operator row, checks the retained request UUID and the 100-run lifetime quota, then copies all template steps and saves its initial response in one transaction. Matching canonical payloads replay that original response even after completion. Changed payloads conflict. Labels and notes trim surrounding spaces; template UUIDs and run IDs use PostgreSQL's native canonical types. Sequence values can have gaps after rollback.
+
+`complete_step` locks the scoped parent run. It checks a retained completion before closed state, so a matching replay remains valid after the run completes. Otherwise it rejects a completed run, a missing step or a step already completed under another key. Counter update, immutable completion insertion and retained result share the native PostgREST request transaction. Both write RPCs are `VOLATILE` and use POST; GET is read-only and cannot perform their writes.
+
+The browser marks completion only after confirmation. A failed or ambiguous action keeps the exact UUID and captured payload until retry succeeds or the operator explicitly clears it. Retry is safe within that action; clearing, disconnecting or refreshing does not undo an already committed write. Keep a request UUID outside the tab if recovery must survive refresh.
+
+Run IDs and cursors are decimal **strings**, including JSONB retained results and nested records. The `list_runs` RPC compares and orders the underlying qualified bigint column, returning at most 20 rows; the UI requests 10. This is live keyset pagination in creation order, not a frozen snapshot. Templates contain at most 10 steps, and each operator retains at most 100 runs including completed ones. There is no delete/archive route.
+
+## Install on Linux
+
+Use a native Linux directory and Node 24.21.0 LTS, Python 3.12+, `psql`, and the ClickHouse CLI. All language dependencies and tests belong in that Linux environment. From this application directory:
+
+```bash
+python3 scripts/install_postgrest.py
+.local/bin/postgrest --version
+npm ci
+npm run build
+python3 -m unittest discover -s tests -p 'test_*.py' -v
+```
+
+The installer supports ARM64 and x86-64, downloads the exact [official PostgREST 16.4 release](https://github.com/PostgREST/postgrest/releases/tag/v16.4) and checks its published SHA256 before writing `.local/bin/postgrest`. The npm lockfile fixes the UI and patched Playwright 1.63.0 helper. No browser dependency is needed to serve the app.
+
+## Create a dedicated Cloud fixture
+
+Authenticate `clickhousectl` using your own Cloud API key outside source files. Choose the organization explicitly. The following no-HA AWS fixture is billable; confirm current size/region availability and pricing in the [Managed Postgres documentation](https://clickhouse.com/docs/products/managed-postgres/). Do not reuse a production service for the destructive acceptance controls.
+
+```bash
+export ORG_ID=your-organization-id
+umask 077
+mkdir -p .local/private
+clickhousectl cloud postgres create \
+ --name checklist-runs-demo --provider aws --region us-east-1 \
+ --size c6gd.large --pg-version 18 --ha-type none \
+ --org-id "$ORG_ID" --json > .local/private/create.json
+export PG_ID=$(python3 -c 'import json; print(json.load(open(".local/private/create.json"))["id"])')
+clickhousectl cloud postgres get "$PG_ID" --org-id "$ORG_ID" --json
+# Repeat get until state is running before connecting or retrieving its CA.
+clickhousectl cloud postgres certs get "$PG_ID" --org-id "$ORG_ID" \
+ --output .local/private/ca.pem
+```
+
+The create receipt supplies the administrator hostname, username and password once. Keep it private. Create four mode-600 environment files, using absolute certificate paths:
+
+| File | Contents |
+| --- | --- |
+| `admin.env` | Common connection fields plus receipt `PGUSER`/`PGPASSWORD`, generated `CHECKLIST_OWNER_PASSWORD` and `CHECKLIST_AUTH_PASSWORD` |
+| `migration.env` | Common fields plus `PGUSER=checklist_owner` and its password |
+| `runtime.env` | Common fields plus `PGUSER=checklist_authenticator`, its password and `PGRST_JWT_SECRET` |
+| `test.env` | `TEST_OWNER_USER=checklist_owner`, `TEST_OWNER_PASSWORD` only |
+
+Common fields are `PGHOST`, `PGPORT=5432`, `PGDATABASE=postgres`, `PGSSLMODE=verify-full`, `PGSSLROOTCERT=/absolute/path/ca.pem`, and `PGCONNECT_TIMEOUT=10`. Generate distinct database passwords locally with `python3 -c 'import secrets; print(secrets.token_urlsafe(30))'`, and the JWT secret with `python3 -c 'import secrets; print(secrets.token_hex(64))'`. Do not put these values in `VITE_*` variables, client code, URLs or Git.
+
+### Bootstrap and migrate
+
+Use a setup terminal. `set -a` exports the private file values to child processes:
+
+```bash
+set -a; source .local/private/admin.env; set +a
+psql -X -f sql/bootstrap.sql
+set -a; source .local/private/migration.env; set +a
+bash scripts/migrate.sh up
+psql -X -f sql/grants.sql
+psql -X -f sql/seed.sql
+```
+
+The migration takes an advisory lock and records its checksum. Repeating `up` checks the checksum and does not recreate objects; repeating the seed does not add duplicates. `down` removes this example's objects and data while preserving its administrator-created schemas; apply `up`, grants and seed afterward only on a disposable fixture. Migration/grants send `NOTIFY pgrst, 'reload schema'`; a running PostgREST reloads its schema cache. Send the same notification after later API schema changes. Do not edit an applied migration in place.
+
+### Start the two local processes
+
+Open a new terminal that has not sourced setup credentials. Export **only** `runtime.env`, then start PostgREST:
+
+```bash
+set -a; source .local/private/runtime.env; set +a
+.local/bin/postgrest postgrest.conf
+```
+
+PostgREST uses libpq environment variables, the official CA and `sslmode=verify-full` for certificate and hostname verification. It exposes only `checklist_api`. Its finite pool has four connections, a five-second acquisition timeout and 900-second maximum lifetime; role settings impose 10-second statements and five-second lock waits. Automatic database recovery is disabled so connection failure exits visibly.
+
+In a separate terminal with **no database or signing credentials**:
+
+```bash
+npm run dev
+# Or demonstrate the built assets locally:
+npm run build
+npm run preview
+```
+
+Open `http://127.0.0.1:5173`. Both Vite modes proxy same-origin `/api` requests to loopback PostgREST on port 3000. Allowed hosts and API CORS origins are restricted to the local UI; there is no wildcard workaround.
+
+In the private runtime terminal, issue a fixture token and paste it into the UI password field:
+
+```bash
+python3 scripts/tokens.py checklist_north --seconds 900
+# The second synthetic scope:
+python3 scripts/tokens.py checklist_south --seconds 900
+```
+
+The JWT remains in tab memory. Each connection owns a fresh QueryClient; disconnect cancels queries and clears its cache. A delayed old response cannot populate a new connection. Mutations already sent may still commit under the old role. Tokens expire without a revocation table: native PostgREST allows 30 seconds of clock skew. The pre-request check additionally requires an explicit audience, issued-at and expiry, and at most 900 seconds between them. It does not claim a logout/revocation provider.
+
+## Direct API examples
+
+Store a freshly issued token in the local shell variable `TOKEN`, without placing it in a URL:
+
+```bash
+export TOKEN=$(python3 scripts/tokens.py checklist_north)
+curl -s http://127.0.0.1:3000/templates -H "Authorization: Bearer $TOKEN"
+curl -s http://127.0.0.1:3000/rpc/start_run \
+ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
+ --data-binary @examples/start.json
+curl -s 'http://127.0.0.1:3000/rpc/list_runs?p_after=0&p_limit=10' \
+ -H "Authorization: Bearer $TOKEN"
+```
+
+Copy the returned string run ID into `examples/complete.json`, then POST it to `/rpc/complete_step`. Retain each request UUID when retrying. Read a detail using `/runs?id=eq.ID&select=*,run_steps(*),completions(*)`; computed relationships preserve the caller's RLS policies. Cross-scope reads return empty rows, and cross-scope completion returns 404. Payload/key conflicts and closed changes return 409; bounded scalar validation returns 422. Malformed JSON, UUIDs or bigint casts can return native 400. These are native REST statuses, not an application wrapper.
+
+## Authorization and limits
+
+The authenticator is `LOGIN NOINHERIT` with membership only in `checklist_north` and `checklist_south`, both `NOLOGIN`. It has no schema-owner membership. PostgREST validates the JWT and applies `SET LOCAL ROLE`; RLS uses that database role, never a submitted scope. `security_invoker` views and RPCs retain the caller's policies. Exposed views grant SELECT only, so HTTP clients cannot patch notes or directly alter counters/templates. Completion rows have no API-role UPDATE/DELETE grant. Template versions are inserted by the trusted migration owner; the API cannot modify them.
+
+Invoker RPCs need narrow underlying INSERT/UPDATE grants. A stolen authenticator credential can impersonate either granted scope and issue direct SQL outside RPC workflow checks. RLS still separates the chosen role, but it is not protection against this shared credential. Owner/admin credentials remain trusted and can bypass RLS. No SECURITY DEFINER helper is used.
+
+`db-max-rows=20` limits each fetched relation, not HTTP body size, embed computational cost or a JSON scalar response's internal array. RPC scalar bounds, the 100-run quota, 10-step snapshots and finite statement/lock timeouts are additional controls. This local example does not implement a general query-cost firewall or public HTTP size/rate limiter. Put production hosting, HTTPS, request limits and a real identity provider in front of it before public use.
+
+## Meaningful verification
+
+Tests require this disposable Cloud fixture with PostgREST running. In a test-only terminal, export runtime plus test credentials, never into the PostgREST child:
+
+```bash
+python3 -m venv .venv
+.venv/bin/pip install -r tests/requirements.txt
+set -a; source .local/private/runtime.env; source .local/private/test.env; set +a
+.venv/bin/python tests/acceptance.py
+POSTGREST_BIN=.local/bin/postgrest python3 tests/tls.py
+npx playwright install --with-deps chromium
+EVIDENCE_DIR=/tmp/checklist-evidence npm run test:browser
+```
+
+The HTTP suite observes independent transactions blocked on actual rows, matching concurrent starts/completions, full rollback after the counter update and child failure, retained closed-state replays, nested RLS, failed JWTs, runtime privilege boundaries, read-only GET writes, quotas and exact IDs beyond JavaScript's safe-integer range. Owner-controlled failure triggers and sequence/fixtures are test scaffolding, not application routes. Browser evidence verifies committed step states, ambiguous retry, identity switching, delayed old responses, expiry and desktop/mobile layout. Keep raw failures separately when correcting a helper. Local CI needs no Cloud credentials.
+
+To reproduce persistence, first run the browser helper above with a fixed `EVIDENCE_DIR`; it records `browser-snapshot.json`. Keep the UI process running. In the same test terminal, supply the PID of this app’s running PostgREST process and its installed binary:
+
+```bash
+POSTGREST_PID=your-postgrest-pid POSTGREST_BIN=.local/bin/postgrest \
+ EVIDENCE_DIR=/tmp/checklist-evidence .venv/bin/python tests/restart.py
+EVIDENCE_DIR=/tmp/checklist-evidence node tests/restart_browser.mjs
+```
+
+The helper verifies the PID’s executable and working directory, waits for its exit, and starts a replacement with only runtime credentials. It compares the exact saved run, all step/completion data and retained start/completion replies, then the browser uses a fresh token to read the completed run. The replacement PID is saved in `EVIDENCE_DIR/replacement.pid`; stop that process during cleanup.
+
+## Cleanup
+
+Stop the two local processes. Delete only the dedicated fixture you created and check that its exact ID is absent:
+
+```bash
+clickhousectl cloud postgres delete "$PG_ID" --org-id "$ORG_ID" --json
+clickhousectl cloud postgres list --org-id "$ORG_ID" --json
+```
+
+Deletion removes the database and its data; stop fixture billing promptly after review. If retaining the service while removing only the app, use a separate administrator terminal that explicitly restores receipt `PGUSER`/`PGPASSWORD` before dropping these schemas and roles. Never run cleanup against another application or an existing service.
diff --git a/applications/checklist-runs/examples/complete.json b/applications/checklist-runs/examples/complete.json
new file mode 100644
index 00000000..990e383d
--- /dev/null
+++ b/applications/checklist-runs/examples/complete.json
@@ -0,0 +1,6 @@
+{
+ "p_request_id": "20000000-0000-4000-8000-000000000002",
+ "p_run_id": "1",
+ "p_step_number": 1,
+ "p_note": "Lights checked"
+}
diff --git a/applications/checklist-runs/examples/start.json b/applications/checklist-runs/examples/start.json
new file mode 100644
index 00000000..39e3fb11
--- /dev/null
+++ b/applications/checklist-runs/examples/start.json
@@ -0,0 +1,5 @@
+{
+ "p_request_id": "20000000-0000-4000-8000-000000000001",
+ "p_template_id": "10000000-0000-4000-8000-000000000001",
+ "p_label": "Morning opening"
+}
diff --git a/applications/checklist-runs/index.html b/applications/checklist-runs/index.html
new file mode 100644
index 00000000..43c8dfd3
--- /dev/null
+++ b/applications/checklist-runs/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Checklist runs
+
+
+
+
+
+
diff --git a/applications/checklist-runs/package-lock.json b/applications/checklist-runs/package-lock.json
new file mode 100644
index 00000000..d99fbfb5
--- /dev/null
+++ b/applications/checklist-runs/package-lock.json
@@ -0,0 +1,1372 @@
+{
+ "name": "checklist-runs",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "checklist-runs",
+ "version": "1.0.0",
+ "dependencies": {
+ "@tanstack/react-query": "5.104.1",
+ "react": "19.3.0",
+ "react-dom": "19.3.0"
+ },
+ "devDependencies": {
+ "@types/react": "19.3.0",
+ "@types/react-dom": "19.3.0",
+ "@vitejs/plugin-react": "6.1.1",
+ "playwright": "1.63.0",
+ "prettier": "3.9.9",
+ "typescript": "7.0.2",
+ "vite": "8.3.2"
+ },
+ "engines": {
+ "node": ">=24.0.0 <25"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.152.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz",
+ "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/oxc-project"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm-eabi": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz",
+ "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz",
+ "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz",
+ "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz",
+ "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz",
+ "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz",
+ "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz",
+ "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz",
+ "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz",
+ "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz",
+ "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz",
+ "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz",
+ "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz",
+ "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz",
+ "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz",
+ "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.104.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.104.1.tgz",
+ "integrity": "sha512-TiRghcrGkUTE+M4JBxvpPSpBhxvIzQairw+cksrHYGr+2uwC77yW1SY9nYxnYRYOppvucf8rEg6b5byoz4rTpw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.104.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.104.1.tgz",
+ "integrity": "sha512-Zz70EgjahNI7aCz8BZxM3vUGU44ycxEAzMuCUjLeA7SVhhOkg3ZgPjPJPvHjs6Rmky/dtmnu1WzjGHNHmhCoZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.104.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
+ "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
+ "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.3.0"
+ }
+ },
+ "node_modules/@typescript/typescript-aix-ppc64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
+ "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-darwin-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
+ "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-darwin-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
+ "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-freebsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-freebsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
+ "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-arm": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
+ "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
+ "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-loong64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
+ "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-mips64el": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
+ "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-ppc64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
+ "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-riscv64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
+ "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-s390x": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
+ "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
+ "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-netbsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-netbsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
+ "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-openbsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-openbsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
+ "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-sunos-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
+ "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-win32-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
+ "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-win32-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
+ "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz",
+ "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxc-transform-react": "^0.145.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "oxc-transform-react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.19",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
+ "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
+ "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.63.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
+ "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.28",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.18",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.9.9",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz",
+ "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
+ "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
+ "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": "^19.3.0"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz",
+ "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.152.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm-eabi": "1.2.12",
+ "@rolldown/binding-android-arm64": "1.2.12",
+ "@rolldown/binding-darwin-arm64": "1.2.12",
+ "@rolldown/binding-darwin-x64": "1.2.12",
+ "@rolldown/binding-freebsd-x64": "1.2.12",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.12",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.12",
+ "@rolldown/binding-linux-arm64-musl": "1.2.12",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.12",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.12",
+ "@rolldown/binding-linux-x64-gnu": "1.2.12",
+ "@rolldown/binding-linux-x64-musl": "1.2.12",
+ "@rolldown/binding-openharmony-arm64": "1.2.12",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.12",
+ "@rolldown/binding-win32-x64-msvc": "1.2.12"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
+ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
+ "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc"
+ },
+ "engines": {
+ "node": ">=16.20.0"
+ },
+ "optionalDependencies": {
+ "@typescript/typescript-aix-ppc64": "7.0.2",
+ "@typescript/typescript-darwin-arm64": "7.0.2",
+ "@typescript/typescript-darwin-x64": "7.0.2",
+ "@typescript/typescript-freebsd-arm64": "7.0.2",
+ "@typescript/typescript-freebsd-x64": "7.0.2",
+ "@typescript/typescript-linux-arm": "7.0.2",
+ "@typescript/typescript-linux-arm64": "7.0.2",
+ "@typescript/typescript-linux-loong64": "7.0.2",
+ "@typescript/typescript-linux-mips64el": "7.0.2",
+ "@typescript/typescript-linux-ppc64": "7.0.2",
+ "@typescript/typescript-linux-riscv64": "7.0.2",
+ "@typescript/typescript-linux-s390x": "7.0.2",
+ "@typescript/typescript-linux-x64": "7.0.2",
+ "@typescript/typescript-netbsd-arm64": "7.0.2",
+ "@typescript/typescript-netbsd-x64": "7.0.2",
+ "@typescript/typescript-openbsd-arm64": "7.0.2",
+ "@typescript/typescript-openbsd-x64": "7.0.2",
+ "@typescript/typescript-sunos-x64": "7.0.2",
+ "@typescript/typescript-win32-arm64": "7.0.2",
+ "@typescript/typescript-win32-x64": "7.0.2"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz",
+ "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.7",
+ "postcss": "^8.5.28",
+ "rolldown": "~1.2.11",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.7.1",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/applications/checklist-runs/package.json b/applications/checklist-runs/package.json
new file mode 100644
index 00000000..0852646a
--- /dev/null
+++ b/applications/checklist-runs/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "checklist-runs",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0 <25"
+ },
+ "scripts": {
+ "dev": "vite",
+ "typecheck": "tsc --noEmit",
+ "build": "npm run typecheck && vite build",
+ "preview": "vite preview",
+ "test:browser": "node tests/browser.mjs",
+ "format:check": "prettier --check src vite.config.ts package.json tsconfig.json index.html tests/*.mjs"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "5.104.1",
+ "react": "19.3.0",
+ "react-dom": "19.3.0"
+ },
+ "devDependencies": {
+ "@types/react": "19.3.0",
+ "@types/react-dom": "19.3.0",
+ "@vitejs/plugin-react": "6.1.1",
+ "playwright": "1.63.0",
+ "typescript": "7.0.2",
+ "vite": "8.3.2",
+ "prettier": "3.9.9"
+ }
+}
diff --git a/applications/checklist-runs/postgrest.conf b/applications/checklist-runs/postgrest.conf
new file mode 100644
index 00000000..e35596a1
--- /dev/null
+++ b/applications/checklist-runs/postgrest.conf
@@ -0,0 +1,23 @@
+# libpq reads PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD,
+# PGSSLMODE=verify-full and PGSSLROOTCERT from the runtime environment.
+db-uri = "postgresql://"
+db-schemas = "checklist_api"
+db-extra-search-path = ""
+db-config = false
+db-pre-request = "checklist_storage.check_request"
+db-max-rows = 20
+db-pool = 4
+db-pool-acquisition-timeout = 5
+db-pool-max-lifetime = 900
+db-pool-max-idletime = 30
+db-pool-automatic-recovery = false
+db-tx-end = "commit"
+jwt-aud = "checklist-runs"
+jwt-role-claim-key = "$$.role"
+# PGRST_JWT_SECRET is set privately at runtime. No anonymous role.
+server-cors-allowed-origins = "http://127.0.0.1:5173,http://localhost:5173"
+server-host = "127.0.0.1"
+server-port = 3000
+server-trace-header = ""
+openapi-mode = "disabled"
+log-level = "warn"
diff --git a/applications/checklist-runs/scripts/install_postgrest.py b/applications/checklist-runs/scripts/install_postgrest.py
new file mode 100644
index 00000000..092dda9c
--- /dev/null
+++ b/applications/checklist-runs/scripts/install_postgrest.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+"""Install an exact official Linux release, verifying the published SHA256."""
+
+import hashlib
+from pathlib import Path
+import platform
+import tarfile
+import tempfile
+import urllib.request
+
+VERSION = "16.4"
+ARTIFACTS = {
+ "aarch64": (
+ "aarch64",
+ "bf544f94f305a1ff37d82f5ef1298d584f1f98b51dfd8182d1437a02073d8731",
+ ),
+ "x86_64": (
+ "x86-64",
+ "b47ecc82fce1dcebbbc4183d839e52f07f7630c9d7ad0f54db753d1939299354",
+ ),
+}
+
+
+def main() -> None:
+ if platform.system() != "Linux" or platform.machine() not in ARTIFACTS:
+ raise SystemExit("This helper supports Linux ARM64 and x86-64 only.")
+ architecture, expected = ARTIFACTS[platform.machine()]
+ artifact = f"postgrest-v{VERSION}-linux-static-{architecture}.tar.xz"
+ url = f"https://github.com/PostgREST/postgrest/releases/download/v{VERSION}/{artifact}"
+ destination = Path(__file__).resolve().parents[1] / ".local/bin"
+ destination.mkdir(parents=True, exist_ok=True)
+ with tempfile.TemporaryDirectory() as directory:
+ archive = Path(directory) / artifact
+ urllib.request.urlretrieve(url, archive)
+ digest = hashlib.sha256(archive.read_bytes()).hexdigest()
+ if digest != expected:
+ raise SystemExit(
+ "PostgREST release checksum does not match; refusing installation."
+ )
+ with tarfile.open(archive) as release:
+ entries = release.getmembers()
+ binary = next(
+ item for item in entries if Path(item.name).name == "postgrest"
+ )
+ if not binary.isfile() or binary.size > 200_000_000:
+ raise SystemExit("Unexpected release archive contents.")
+ contents = release.extractfile(binary)
+ if contents is None:
+ raise SystemExit("The release does not contain its binary.")
+ target = destination / "postgrest"
+ target.write_bytes(contents.read())
+ target.chmod(0o755)
+ print(f"Installed PostgREST {VERSION} ({architecture}); SHA256 {digest}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/applications/checklist-runs/scripts/migrate.sh b/applications/checklist-runs/scripts/migrate.sh
new file mode 100644
index 00000000..3ab35c3e
--- /dev/null
+++ b/applications/checklist-runs/scripts/migrate.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(dirname "$0")/.."
+if [[ "${PGUSER:-}" != checklist_owner ]]; then
+ printf '%s\n' 'Migrations must use checklist_owner.' >&2
+ exit 1
+fi
+if [[ "${PGSSLMODE:-}" != verify-full || -z "${PGSSLROOTCERT:-}" ]]; then
+ printf '%s\n' 'Set PGSSLMODE=verify-full and the official PGSSLROOTCERT.' >&2
+ exit 1
+fi
+direction="${1:-up}"
+case "$direction" in
+ up)
+ checksum=$(sha256sum sql/migrations/001-up.sql | cut -d ' ' -f 1)
+ psql -X -v ON_ERROR_STOP=1 -v checksum="$checksum" <<'SQL'
+BEGIN;
+SELECT pg_advisory_xact_lock(5870031002);
+CREATE TABLE IF NOT EXISTS checklist_storage.schema_migrations (
+ version integer PRIMARY KEY, checksum text NOT NULL
+);
+SELECT NOT EXISTS (SELECT FROM checklist_storage.schema_migrations WHERE version = 1) AS needed \gset
+\if :needed
+\i sql/migrations/001-up.sql
+INSERT INTO checklist_storage.schema_migrations VALUES (1, :'checksum');
+\else
+SELECT checksum = :'checksum' AS agrees FROM checklist_storage.schema_migrations WHERE version = 1 \gset
+\if :agrees
+\echo Migration 1 already applied with matching checksum.
+\else
+\echo Migration checksum changed; refusing to continue.
+\quit 1
+\endif
+\endif
+COMMIT;
+SQL
+ ;;
+ down)
+ psql -X -v ON_ERROR_STOP=1 <<'SQL'
+BEGIN;
+SELECT pg_advisory_xact_lock(5870031002);
+\i sql/migrations/001-down.sql
+COMMIT;
+SQL
+ ;;
+ *) printf '%s\n' 'Usage: scripts/migrate.sh up|down' >&2; exit 1 ;;
+esac
diff --git a/applications/checklist-runs/scripts/tokens.py b/applications/checklist-runs/scripts/tokens.py
new file mode 100644
index 00000000..af6b38f4
--- /dev/null
+++ b/applications/checklist-runs/scripts/tokens.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+"""Offline fixture issuer. There is no HTTP signing endpoint or login provider."""
+
+import argparse
+import base64
+import hashlib
+import hmac
+import json
+import os
+import time
+
+ROLES = ("checklist_north", "checklist_south")
+AUDIENCE = "checklist-runs"
+
+
+def b64url(value: bytes) -> str:
+ return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=")
+
+
+def issue(secret: str, role: str, lifetime: int = 900, now: int | None = None) -> str:
+ if len(secret.encode("utf-8")) < 64:
+ raise ValueError("Use a generated secret of at least 64 bytes.")
+ if role not in ROLES:
+ raise ValueError("Unknown fixture operator role.")
+ if type(lifetime) is not int or not 1 <= lifetime <= 900:
+ raise ValueError("Token lifetime must be between 1 and 900 seconds.")
+ issued = int(time.time()) if now is None else now
+ header = {"alg": "HS256", "typ": "JWT"}
+ payload = {"role": role, "aud": AUDIENCE, "iat": issued, "exp": issued + lifetime}
+ segments = [
+ b64url(json.dumps(v, separators=(",", ":")).encode()) for v in (header, payload)
+ ]
+ signed = ".".join(segments)
+ signature = hmac.new(
+ secret.encode(), signed.encode("ascii"), hashlib.sha256
+ ).digest()
+ return signed + "." + b64url(signature)
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("role", choices=ROLES)
+ parser.add_argument("--seconds", type=int, default=900)
+ args = parser.parse_args()
+ secret = os.environ.get("PGRST_JWT_SECRET", "")
+ try:
+ print(issue(secret, args.role, args.seconds))
+ except ValueError as exc:
+ parser.error(str(exc))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/applications/checklist-runs/sql/bootstrap.sql b/applications/checklist-runs/sql/bootstrap.sql
new file mode 100644
index 00000000..5a90a5b3
--- /dev/null
+++ b/applications/checklist-runs/sql/bootstrap.sql
@@ -0,0 +1,31 @@
+\set ON_ERROR_STOP on
+\getenv owner_password CHECKLIST_OWNER_PASSWORD
+\getenv authenticator_password CHECKLIST_AUTH_PASSWORD
+
+SELECT 'CREATE ROLE checklist_owner LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION'
+WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'checklist_owner') \gexec
+SELECT 'CREATE ROLE checklist_authenticator LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION'
+WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'checklist_authenticator') \gexec
+SELECT 'CREATE ROLE checklist_north NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION'
+WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'checklist_north') \gexec
+SELECT 'CREATE ROLE checklist_south NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION'
+WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'checklist_south') \gexec
+
+ALTER ROLE checklist_owner PASSWORD :'owner_password';
+ALTER ROLE checklist_authenticator PASSWORD :'authenticator_password';
+ALTER ROLE checklist_authenticator NOINHERIT;
+GRANT checklist_north, checklist_south TO checklist_authenticator;
+REVOKE checklist_owner FROM checklist_authenticator;
+SELECT format('GRANT CONNECT ON DATABASE %I TO checklist_owner, checklist_authenticator', current_database()) \gexec
+SELECT format('REVOKE CREATE, TEMPORARY ON DATABASE %I FROM PUBLIC', current_database()) \gexec
+REVOKE CREATE ON SCHEMA public FROM PUBLIC;
+CREATE SCHEMA IF NOT EXISTS checklist_storage AUTHORIZATION checklist_owner;
+CREATE SCHEMA IF NOT EXISTS checklist_api AUTHORIZATION checklist_owner;
+REVOKE ALL ON SCHEMA checklist_storage, checklist_api FROM PUBLIC;
+ALTER ROLE checklist_authenticator SET statement_timeout = '10s';
+ALTER ROLE checklist_authenticator SET lock_timeout = '5s';
+ALTER ROLE checklist_authenticator SET idle_in_transaction_session_timeout = '15s';
+ALTER ROLE checklist_north SET statement_timeout = '10s';
+ALTER ROLE checklist_north SET lock_timeout = '5s';
+ALTER ROLE checklist_south SET statement_timeout = '10s';
+ALTER ROLE checklist_south SET lock_timeout = '5s';
diff --git a/applications/checklist-runs/sql/grants.sql b/applications/checklist-runs/sql/grants.sql
new file mode 100644
index 00000000..1394b07b
--- /dev/null
+++ b/applications/checklist-runs/sql/grants.sql
@@ -0,0 +1,15 @@
+\set ON_ERROR_STOP on
+GRANT USAGE ON SCHEMA checklist_api, checklist_storage TO checklist_north, checklist_south;
+GRANT SELECT ON ALL TABLES IN SCHEMA checklist_storage TO checklist_north, checklist_south;
+GRANT SELECT ON ALL TABLES IN SCHEMA checklist_api TO checklist_north, checklist_south;
+GRANT UPDATE (lock_nonce) ON checklist_storage.operators TO checklist_north, checklist_south;
+GRANT INSERT ON checklist_storage.runs, checklist_storage.run_steps, checklist_storage.completions
+ TO checklist_north, checklist_south;
+GRANT UPDATE (start_result, completed_steps, completed_at) ON checklist_storage.runs
+ TO checklist_north, checklist_south;
+GRANT USAGE ON ALL SEQUENCES IN SCHEMA checklist_storage TO checklist_north, checklist_south;
+GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA checklist_api TO checklist_north, checklist_south;
+GRANT EXECUTE ON FUNCTION checklist_storage.check_request() TO checklist_north, checklist_south;
+-- Only the impersonated roles need API/storage access. The authenticator has no
+-- owner membership and does not inherit these grants without SET ROLE.
+NOTIFY pgrst, 'reload schema';
diff --git a/applications/checklist-runs/sql/migrations/001-down.sql b/applications/checklist-runs/sql/migrations/001-down.sql
new file mode 100644
index 00000000..34185080
--- /dev/null
+++ b/applications/checklist-runs/sql/migrations/001-down.sql
@@ -0,0 +1,13 @@
+-- Preserve administrator-created schemas. The migration owner deliberately
+-- lacks CREATE on the database and can still undo all application objects.
+DROP FUNCTION checklist_api.start_run(uuid, uuid, text);
+DROP FUNCTION checklist_api.complete_step(uuid, bigint, integer, text);
+DROP FUNCTION checklist_api.list_runs(bigint, integer);
+DROP FUNCTION checklist_api.run_steps(checklist_api.runs);
+DROP FUNCTION checklist_api.completions(checklist_api.runs);
+DROP VIEW checklist_api.completions, checklist_api.run_steps, checklist_api.runs, checklist_api.templates;
+DROP FUNCTION checklist_storage.check_request();
+DROP TABLE checklist_storage.completions, checklist_storage.run_steps,
+ checklist_storage.runs, checklist_storage.templates, checklist_storage.operators,
+ checklist_storage.schema_migrations;
+NOTIFY pgrst, 'reload schema';
diff --git a/applications/checklist-runs/sql/migrations/001-up.sql b/applications/checklist-runs/sql/migrations/001-up.sql
new file mode 100644
index 00000000..0bb5edc0
--- /dev/null
+++ b/applications/checklist-runs/sql/migrations/001-up.sql
@@ -0,0 +1,278 @@
+CREATE TABLE checklist_storage.operators (
+ role_name text PRIMARY KEY CHECK (role_name IN ('checklist_north', 'checklist_south')),
+ lock_nonce integer NOT NULL DEFAULT 0
+);
+
+CREATE TABLE checklist_storage.templates (
+ id uuid PRIMARY KEY,
+ version integer NOT NULL CHECK (version BETWEEN 1 AND 1000000),
+ title text NOT NULL CHECK (char_length(title) BETWEEN 1 AND 80),
+ steps jsonb NOT NULL CHECK (jsonb_typeof(steps) = 'array'
+ AND jsonb_array_length(steps) BETWEEN 1 AND 10)
+);
+
+CREATE TABLE checklist_storage.runs (
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ operator_role text NOT NULL REFERENCES checklist_storage.operators(role_name),
+ request_id uuid NOT NULL,
+ start_payload jsonb NOT NULL,
+ start_result jsonb NOT NULL,
+ template_id uuid NOT NULL REFERENCES checklist_storage.templates(id),
+ template_version integer NOT NULL,
+ template_title text NOT NULL,
+ template_snapshot jsonb NOT NULL,
+ label text NOT NULL CHECK (char_length(label) BETWEEN 1 AND 80),
+ total_steps integer NOT NULL CHECK (total_steps BETWEEN 1 AND 10),
+ completed_steps integer NOT NULL DEFAULT 0,
+ completed_at timestamptz,
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+ UNIQUE (operator_role, request_id),
+ UNIQUE (operator_role, id),
+ CHECK (completed_steps BETWEEN 0 AND total_steps),
+ CHECK ((completed_steps = total_steps) = (completed_at IS NOT NULL)),
+ CHECK (jsonb_typeof(template_snapshot) = 'array'
+ AND jsonb_array_length(template_snapshot) = total_steps)
+);
+
+CREATE TABLE checklist_storage.run_steps (
+ operator_role text NOT NULL,
+ run_id bigint NOT NULL REFERENCES checklist_storage.runs(id),
+ step_number integer NOT NULL CHECK (step_number BETWEEN 1 AND 10),
+ title text NOT NULL CHECK (char_length(title) BETWEEN 1 AND 120),
+ PRIMARY KEY (run_id, step_number),
+ FOREIGN KEY (operator_role, run_id)
+ REFERENCES checklist_storage.runs(operator_role, id)
+);
+
+CREATE TABLE checklist_storage.completions (
+ operator_role text NOT NULL,
+ request_id uuid NOT NULL,
+ run_id bigint NOT NULL REFERENCES checklist_storage.runs(id),
+ step_number integer NOT NULL,
+ payload jsonb NOT NULL,
+ result jsonb NOT NULL,
+ note text NOT NULL CHECK (char_length(note) <= 400),
+ completed_at timestamptz NOT NULL,
+ PRIMARY KEY (operator_role, request_id),
+ UNIQUE (run_id, step_number),
+ FOREIGN KEY (operator_role, run_id)
+ REFERENCES checklist_storage.runs(operator_role, id),
+ FOREIGN KEY (run_id, step_number)
+ REFERENCES checklist_storage.run_steps(run_id, step_number)
+);
+
+ALTER TABLE checklist_storage.operators ENABLE ROW LEVEL SECURITY;
+ALTER TABLE checklist_storage.runs ENABLE ROW LEVEL SECURITY;
+ALTER TABLE checklist_storage.run_steps ENABLE ROW LEVEL SECURITY;
+ALTER TABLE checklist_storage.completions ENABLE ROW LEVEL SECURITY;
+CREATE POLICY operator_scope ON checklist_storage.operators
+ USING (role_name = current_user) WITH CHECK (role_name = current_user);
+CREATE POLICY run_scope ON checklist_storage.runs
+ USING (operator_role = current_user) WITH CHECK (operator_role = current_user);
+CREATE POLICY step_scope ON checklist_storage.run_steps
+ USING (operator_role = current_user) WITH CHECK (operator_role = current_user);
+CREATE POLICY completion_scope ON checklist_storage.completions
+ USING (operator_role = current_user) WITH CHECK (operator_role = current_user);
+
+-- Views keep storage private and preserve the requesting role's RLS policies.
+CREATE VIEW checklist_api.templates WITH (security_invoker = true) AS
+ SELECT id, version, title, steps FROM checklist_storage.templates;
+CREATE VIEW checklist_api.runs WITH (security_invoker = true) AS
+ SELECT id::text AS id, template_id, template_version, template_title, label, total_steps,
+ completed_steps, completed_at, created_at
+ FROM checklist_storage.runs;
+CREATE VIEW checklist_api.run_steps WITH (security_invoker = true) AS
+ SELECT run_id::text AS run_id, step_number, title FROM checklist_storage.run_steps;
+CREATE VIEW checklist_api.completions WITH (security_invoker = true) AS
+ SELECT run_id::text AS run_id, step_number, note, completed_at FROM checklist_storage.completions;
+
+-- Cast IDs stay exact on the JSON wire. Explicit computed relationships keep
+-- embeds available without exposing a second numeric representation.
+CREATE FUNCTION checklist_api.run_steps(parent checklist_api.runs)
+RETURNS SETOF checklist_api.run_steps ROWS 10
+LANGUAGE sql STABLE SECURITY INVOKER SET search_path = pg_catalog AS $$
+ SELECT s.run_id::text, s.step_number, s.title
+ FROM checklist_storage.run_steps AS s
+ WHERE s.run_id = parent.id::bigint ORDER BY s.step_number ASC;
+$$;
+CREATE FUNCTION checklist_api.completions(parent checklist_api.runs)
+RETURNS SETOF checklist_api.completions ROWS 10
+LANGUAGE sql STABLE SECURITY INVOKER SET search_path = pg_catalog AS $$
+ SELECT c.run_id::text, c.step_number, c.note, c.completed_at
+ FROM checklist_storage.completions AS c
+ WHERE c.run_id = parent.id::bigint ORDER BY c.step_number ASC;
+$$;
+
+CREATE FUNCTION checklist_storage.check_request() RETURNS void
+LANGUAGE plpgsql STABLE SECURITY INVOKER
+SET search_path = pg_catalog AS $$
+DECLARE
+ claims jsonb := nullif(current_setting('request.jwt.claims', true), '')::jsonb;
+ issued bigint;
+ expires bigint;
+BEGIN
+ IF current_user NOT IN ('checklist_north', 'checklist_south')
+ OR claims->>'role' IS DISTINCT FROM current_user::text
+ OR claims->>'aud' IS DISTINCT FROM 'checklist-runs'
+ OR jsonb_typeof(claims->'iat') IS DISTINCT FROM 'number'
+ OR jsonb_typeof(claims->'exp') IS DISTINCT FROM 'number'
+ OR (claims->>'iat') !~ '^[0-9]{10}$'
+ OR (claims->>'exp') !~ '^[0-9]{10}$'
+ THEN
+ RAISE SQLSTATE 'PT401' USING MESSAGE = 'A scoped short-lived token is required.';
+ END IF;
+ issued := (claims->>'iat')::bigint;
+ expires := (claims->>'exp')::bigint;
+ IF expires <= issued OR expires - issued > 900 THEN
+ RAISE SQLSTATE 'PT401' USING MESSAGE = 'Token lifetime must be at most 15 minutes.';
+ END IF;
+END;
+$$;
+
+CREATE FUNCTION checklist_api.list_runs(p_after bigint DEFAULT 0, p_limit integer DEFAULT 20)
+RETURNS SETOF checklist_api.runs
+LANGUAGE plpgsql STABLE SECURITY INVOKER
+SET search_path = pg_catalog AS $$
+BEGIN
+ IF p_after IS NULL OR p_after < 0 OR p_limit IS NULL OR p_limit NOT BETWEEN 1 AND 20 THEN
+ RAISE SQLSTATE 'PT422' USING MESSAGE = 'Use a nonnegative cursor and a limit from 1 through 20.';
+ END IF;
+ RETURN QUERY SELECT r.id::text, r.template_id, r.template_version,
+ r.template_title, r.label, r.total_steps, r.completed_steps,
+ r.completed_at, r.created_at FROM checklist_storage.runs AS r
+ WHERE r.id > p_after ORDER BY r.id ASC LIMIT p_limit;
+END;
+$$;
+
+CREATE FUNCTION checklist_api.start_run(p_request_id uuid, p_template_id uuid, p_label text)
+RETURNS jsonb
+LANGUAGE plpgsql VOLATILE SECURITY INVOKER
+SET search_path = pg_catalog AS $$
+DECLARE
+ canonical_label text;
+ payload jsonb;
+ existing checklist_storage.runs%ROWTYPE;
+ template checklist_storage.templates%ROWTYPE;
+ new_id bigint;
+ result jsonb;
+BEGIN
+ IF p_request_id IS NULL OR p_template_id IS NULL OR p_label IS NULL
+ OR octet_length(p_label) > 320 OR p_label ~ '[[:cntrl:]]'
+ OR char_length(btrim(p_label)) NOT BETWEEN 1 AND 80
+ THEN
+ RAISE SQLSTATE 'PT422' USING MESSAGE = 'Provide request/template UUIDs and a label of 1 through 80 characters without controls.';
+ END IF;
+ canonical_label := btrim(p_label);
+ payload := jsonb_build_object('template_id', p_template_id, 'label', canonical_label);
+
+ -- All starts in this scope serialize the retry check and the 100-run quota.
+ PERFORM 1 FROM checklist_storage.operators AS o
+ WHERE o.role_name = current_user FOR UPDATE;
+ IF NOT FOUND THEN
+ RAISE SQLSTATE 'PT403' USING MESSAGE = 'No operator scope is available.';
+ END IF;
+ SELECT r.* INTO existing FROM checklist_storage.runs AS r
+ WHERE r.operator_role = current_user AND r.request_id = p_request_id;
+ IF FOUND THEN
+ IF existing.start_payload <> payload THEN
+ RAISE SQLSTATE 'PT409' USING MESSAGE = 'This start request key has a different payload.';
+ END IF;
+ RETURN existing.start_result;
+ END IF;
+ IF (SELECT count(*) FROM checklist_storage.runs AS r
+ WHERE r.operator_role = current_user) >= 100 THEN
+ RAISE SQLSTATE 'PT409' USING MESSAGE = 'This operator has reached the 100-run limit.';
+ END IF;
+ SELECT t.* INTO template FROM checklist_storage.templates AS t WHERE t.id = p_template_id;
+ IF NOT FOUND THEN
+ RAISE SQLSTATE 'PT404' USING MESSAGE = 'Template not found.';
+ END IF;
+
+ INSERT INTO checklist_storage.runs
+ (operator_role, request_id, start_payload, start_result, template_id,
+ template_version, template_title, template_snapshot, label, total_steps)
+ VALUES (current_user, p_request_id, payload, '{}'::jsonb, template.id,
+ template.version, template.title, template.steps, canonical_label,
+ jsonb_array_length(template.steps)) RETURNING id INTO new_id;
+
+ result := jsonb_build_object('run_id', new_id::text, 'template_id', template.id,
+ 'template_version', template.version, 'template_title', template.title,
+ 'label', canonical_label, 'total_steps', jsonb_array_length(template.steps),
+ 'completed_steps', 0, 'status', 'open');
+ UPDATE checklist_storage.runs SET start_result = result WHERE id = new_id;
+ INSERT INTO checklist_storage.run_steps (operator_role, run_id, step_number, title)
+ SELECT current_user, new_id, s.ordinality::integer, s.title
+ FROM jsonb_array_elements_text(template.steps) WITH ORDINALITY AS s(title, ordinality);
+ RETURN result;
+END;
+$$;
+
+CREATE FUNCTION checklist_api.complete_step(
+ p_request_id uuid, p_run_id bigint, p_step_number integer, p_note text DEFAULT '')
+RETURNS jsonb
+LANGUAGE plpgsql VOLATILE SECURITY INVOKER
+SET search_path = pg_catalog AS $$
+DECLARE
+ canonical_note text;
+ payload jsonb;
+ retained checklist_storage.completions%ROWTYPE;
+ locked_run checklist_storage.runs%ROWTYPE;
+ completed_time timestamptz;
+ new_count integer;
+ result jsonb;
+BEGIN
+ IF p_request_id IS NULL OR p_run_id IS NULL OR p_run_id < 1
+ OR p_step_number IS NULL OR p_step_number NOT BETWEEN 1 AND 10
+ OR p_note IS NULL OR octet_length(p_note) > 1600
+ OR char_length(p_note) > 400 OR p_note ~ '[[:cntrl:]]'
+ THEN
+ RAISE SQLSTATE 'PT422' USING MESSAGE = 'Provide a run, step from 1 through 10 and a note of at most 400 characters without controls.';
+ END IF;
+ canonical_note := btrim(p_note);
+ payload := jsonb_build_object('run_id', p_run_id::text, 'step_number', p_step_number, 'note', canonical_note);
+
+ SELECT r.* INTO locked_run FROM checklist_storage.runs AS r
+ WHERE r.operator_role = current_user AND r.id = p_run_id FOR UPDATE;
+ IF NOT FOUND THEN
+ RAISE SQLSTATE 'PT404' USING MESSAGE = 'Run not found.';
+ END IF;
+ SELECT c.* INTO retained FROM checklist_storage.completions AS c
+ WHERE c.operator_role = current_user AND c.request_id = p_request_id;
+ IF FOUND THEN
+ IF retained.payload <> payload THEN
+ RAISE SQLSTATE 'PT409' USING MESSAGE = 'This completion request key has a different payload.';
+ END IF;
+ RETURN retained.result;
+ END IF;
+ IF locked_run.completed_at IS NOT NULL THEN
+ RAISE SQLSTATE 'PT409' USING MESSAGE = 'This run is completed.';
+ END IF;
+ IF NOT EXISTS (SELECT FROM checklist_storage.run_steps AS s
+ WHERE s.run_id = p_run_id AND s.step_number = p_step_number) THEN
+ RAISE SQLSTATE 'PT404' USING MESSAGE = 'Step not found.';
+ END IF;
+ IF EXISTS (SELECT FROM checklist_storage.completions AS c
+ WHERE c.run_id = p_run_id AND c.step_number = p_step_number) THEN
+ RAISE SQLSTATE 'PT409' USING MESSAGE = 'This step already has a different completion key.';
+ END IF;
+
+ completed_time := clock_timestamp();
+ new_count := locked_run.completed_steps + 1;
+ UPDATE checklist_storage.runs SET completed_steps = new_count,
+ completed_at = CASE WHEN new_count = total_steps THEN completed_time ELSE NULL END
+ WHERE id = p_run_id;
+ result := jsonb_build_object('run_id', p_run_id::text, 'step_number', p_step_number,
+ 'note', canonical_note, 'completed_at', completed_time,
+ 'completed_steps', new_count,
+ 'status', CASE WHEN new_count = locked_run.total_steps THEN 'completed' ELSE 'open' END);
+ INSERT INTO checklist_storage.completions
+ (operator_role, request_id, run_id, step_number, payload, result, note, completed_at)
+ VALUES (current_user, p_request_id, p_run_id, p_step_number, payload, result,
+ canonical_note, completed_time);
+ RETURN result;
+END;
+$$;
+
+REVOKE ALL ON ALL FUNCTIONS IN SCHEMA checklist_api FROM PUBLIC;
+REVOKE ALL ON ALL FUNCTIONS IN SCHEMA checklist_storage FROM PUBLIC;
+NOTIFY pgrst, 'reload schema';
diff --git a/applications/checklist-runs/sql/seed.sql b/applications/checklist-runs/sql/seed.sql
new file mode 100644
index 00000000..305d4188
--- /dev/null
+++ b/applications/checklist-runs/sql/seed.sql
@@ -0,0 +1,11 @@
+\set ON_ERROR_STOP on
+INSERT INTO checklist_storage.operators (role_name)
+VALUES ('checklist_north'), ('checklist_south') ON CONFLICT DO NOTHING;
+INSERT INTO checklist_storage.templates (id, version, title, steps) VALUES
+('10000000-0000-4000-8000-000000000001', 1, 'Meeting room opening',
+ '["Check lights", "Arrange chairs", "Test display"]'),
+('10000000-0000-4000-8000-000000000002', 1, 'Demo station reset',
+ '["Clear sample data", "Run smoke check"]'),
+('10000000-0000-4000-8000-000000000003', 2, 'Loan desk closing',
+ '["Count returned items", "Lock storage", "Leave handover note"]')
+ON CONFLICT DO NOTHING;
diff --git a/applications/checklist-runs/src/api.ts b/applications/checklist-runs/src/api.ts
new file mode 100644
index 00000000..f2c20242
--- /dev/null
+++ b/applications/checklist-runs/src/api.ts
@@ -0,0 +1,131 @@
+export interface Template {
+ id: string;
+ version: number;
+ title: string;
+ steps: string[];
+}
+
+export interface Run {
+ id: string;
+ template_id: string;
+ template_version: number;
+ template_title: string;
+ label: string;
+ total_steps: number;
+ completed_steps: number;
+ completed_at: string | null;
+ created_at: string;
+}
+
+export interface Step {
+ run_id: string;
+ step_number: number;
+ title: string;
+}
+
+export interface Completion {
+ run_id: string;
+ step_number: number;
+ note: string;
+ completed_at: string;
+}
+
+export interface RunDetail extends Run {
+ run_steps: Step[];
+ completions: Completion[];
+}
+
+export type Action =
+ | {
+ kind: "start";
+ payload: { p_request_id: string; p_template_id: string; p_label: string };
+ }
+ | {
+ kind: "complete";
+ payload: {
+ p_request_id: string;
+ p_run_id: string;
+ p_step_number: number;
+ p_note: string;
+ };
+ };
+
+export interface MutationResult {
+ run_id: string;
+ completed_steps: number;
+ status: "open" | "completed";
+}
+
+export class ApiError extends Error {
+ constructor(
+ message: string,
+ readonly status: number,
+ ) {
+ super(message);
+ }
+}
+
+export async function request(
+ token: string,
+ path: string,
+ options: { signal?: AbortSignal; payload?: unknown } = {},
+): Promise {
+ let response: Response;
+ try {
+ response = await fetch(`/api/${path}`, {
+ method: options.payload === undefined ? "GET" : "POST",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ ...(options.payload === undefined
+ ? {}
+ : { "Content-Type": "application/json" }),
+ },
+ credentials: "omit",
+ signal: options.signal,
+ body:
+ options.payload === undefined
+ ? undefined
+ : JSON.stringify(options.payload),
+ });
+ } catch (error) {
+ if (error instanceof DOMException && error.name === "AbortError")
+ throw error;
+ throw new ApiError(
+ "The response did not arrive. Retry the same action to check its result.",
+ 0,
+ );
+ }
+ const data: unknown = await response.json().catch(() => null);
+ if (!response.ok) {
+ if (response.status === 401) {
+ throw new ApiError(
+ "This token is missing, invalid or expired. Disconnect and connect with a fresh token.",
+ 401,
+ );
+ }
+ const safeMessage =
+ data &&
+ typeof data === "object" &&
+ "message" in data &&
+ typeof data.message === "string"
+ ? data.message
+ : "The action could not be confirmed. Retry it or refresh the run.";
+ throw new ApiError(safeMessage, response.status);
+ }
+ if (data === null)
+ throw new ApiError(
+ "The response could not be read. Retry the same action.",
+ 0,
+ );
+ return data as T;
+}
+
+export function submit(token: string, action: Action): Promise {
+ return request(
+ token,
+ `rpc/${action.kind === "start" ? "start_run" : "complete_step"}`,
+ {
+ payload: action.payload,
+ },
+ );
+}
diff --git a/applications/checklist-runs/src/main.tsx b/applications/checklist-runs/src/main.tsx
new file mode 100644
index 00000000..413b65ca
--- /dev/null
+++ b/applications/checklist-runs/src/main.tsx
@@ -0,0 +1,506 @@
+import { StrictMode, useState, type FormEvent } from "react";
+import { createRoot } from "react-dom/client";
+import {
+ QueryClient,
+ QueryClientProvider,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from "@tanstack/react-query";
+import {
+ request,
+ submit,
+ type Action,
+ type Run,
+ type RunDetail,
+ type Template,
+} from "./api";
+import "./style.css";
+
+interface Connection {
+ token: string;
+ generation: number;
+ client: QueryClient;
+}
+
+function App() {
+ const [connection, setConnection] = useState(null);
+ const [entry, setEntry] = useState("");
+ const [generation, setGeneration] = useState(0);
+ const [entryError, setEntryError] = useState("");
+
+ function connect(event: FormEvent) {
+ event.preventDefault();
+ const token = entry.trim();
+ if (
+ !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token) ||
+ token.length > 4096
+ ) {
+ setEntryError("Paste the complete short-lived operator token.");
+ return;
+ }
+ const next = generation + 1;
+ setGeneration(next);
+ setConnection({
+ token,
+ generation: next,
+ client: new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, refetchOnWindowFocus: false },
+ mutations: { retry: false },
+ },
+ }),
+ });
+ setEntry("");
+ setEntryError("");
+ }
+
+ function disconnect() {
+ if (connection) {
+ void connection.client.cancelQueries();
+ connection.client.clear();
+ }
+ setConnection(null);
+ }
+
+ return (
+
+
+ {connection ? (
+
+
+
+ ) : (
+
+ Operator connection
+ Connect to your work
+
+ Paste a short-lived token issued locally for your operator. It stays
+ in this tab’s memory; refreshing clears it.
+
+
+
+ This local demo has no login provider. Disconnecting clears the
+ view; an already-sent action can still finish.
+
+
+ )}
+ A shared routine, a durable record.
+
+ );
+}
+
+function Dashboard({ token }: { token: string }) {
+ const client = useQueryClient();
+ const [cursor, setCursor] = useState("0");
+ const [selected, setSelected] = useState(null);
+ const [templateId, setTemplateId] = useState("");
+ const [label, setLabel] = useState("");
+ const [action, setAction] = useState(null);
+ const [success, setSuccess] = useState("");
+ const templates = useQuery({
+ queryKey: ["templates"],
+ queryFn: ({ signal }) =>
+ request(token, "templates?order=title.asc", { signal }),
+ });
+ const runs = useQuery({
+ queryKey: ["runs", cursor],
+ queryFn: ({ signal }) =>
+ request(token, `rpc/list_runs?p_after=${cursor}&p_limit=10`, {
+ signal,
+ }),
+ });
+ const detail = useQuery({
+ queryKey: ["run", selected],
+ enabled: selected !== null,
+ queryFn: ({ signal }) =>
+ request(
+ token,
+ `runs?id=eq.${selected}&select=*,run_steps(*),completions(*)&run_steps.order=step_number.asc`,
+ { signal },
+ ),
+ });
+ const mutation = useMutation({
+ mutationFn: (next: Action) => submit(token, next),
+ onSuccess: async (result, submitted) => {
+ setSuccess(
+ submitted.kind === "start"
+ ? "Run started. Your checklist is ready."
+ : "Step completion confirmed.",
+ );
+ if (submitted.kind === "start") {
+ setSelected(result.run_id);
+ setLabel("");
+ }
+ await Promise.all([
+ client.invalidateQueries({ queryKey: ["runs"] }),
+ client.invalidateQueries({ queryKey: ["run", result.run_id] }),
+ ]);
+ setAction(null);
+ },
+ });
+
+ function send(next: Action) {
+ setSuccess("");
+ setAction(next);
+ mutation.mutate(next);
+ }
+
+ function start(event: FormEvent) {
+ event.preventDefault();
+ if (action) return;
+ send({
+ kind: "start",
+ payload: {
+ p_request_id: crypto.randomUUID(),
+ p_template_id: templateId || templates.data?.[0]?.id || "",
+ p_label: label,
+ },
+ });
+ }
+
+ const readError = templates.error || runs.error || detail.error;
+ const current = detail.data?.[0];
+ return (
+ <>
+ {readError && (
+
+ {readError.message}{" "}
+ void client.invalidateQueries()}
+ >
+ Retry reads
+
+
+ )}
+ {success && (
+
+ {success}
+
+ )}
+ {mutation.isPending && (
+
+ Confirming your action…
+
+ )}
+ {mutation.error && action && (
+
+ {mutation.error.message}
+
+ Retry sends the same request key and captured details. A missing
+ response does not prove the action failed.
+
+ mutation.mutate(action)}
+ disabled={mutation.isPending}
+ >
+ Retry same action
+
+ {
+ setAction(null);
+ mutation.reset();
+ setSuccess(
+ "Failed action cleared. Refresh the run before starting another action; clearing does not undo a committed write.",
+ );
+ }}
+ >
+ Clear failed action
+
+ Request key: {action.payload.p_request_id}
+
+ )}
+
+
+
+ New routine
+ Start a run
+ {templates.isPending ? (
+ Loading templates…
+ ) : (
+
+ )}
+
+
+
+
Your runs
+ void runs.refetch()}
+ >
+ Refresh
+
+
+ {runs.isPending ? (
+ Loading runs…
+ ) : !runs.data?.length ? (
+
+ No runs on this page. Start a checklist to begin.
+
+ ) : (
+
+ {runs.data.map((run) => (
+
+ {
+ setSelected(run.id);
+ setSuccess("");
+ }}
+ >
+ {run.label}
+ {run.template_title}
+
+ {run.completed_steps}/{run.total_steps} steps
+ {run.completed_at ? " · Completed" : " · Open"}
+
+
+
+ ))}
+
+ )}
+
+ setCursor("0")}
+ >
+ First page
+
+ setCursor(runs.data![runs.data!.length - 1].id)}
+ >
+ Next page
+
+
+
+ Up to 10 runs per page, in creation order.
+
+
+
+
+ {selected === null ? (
+
+
Ready when you are
+
Choose a run
+
Pick a run to see its saved checklist and completion notes.
+
+ ) : detail.isPending ? (
+ Loading checklist…
+ ) : !current ? (
+
+ No accessible run was found. Refresh the list or choose another
+ run.
+
+ ) : (
+
+ send({
+ kind: "complete",
+ payload: {
+ p_request_id: crypto.randomUUID(),
+ p_run_id: current.id,
+ p_step_number: step,
+ p_note: note,
+ },
+ })
+ }
+ />
+ )}
+
+
+
+ Token and cached results exist only for this connection. Disconnecting
+ does not revoke the token or undo an action already sent.
+
+ >
+ );
+}
+
+function RunView({
+ run,
+ disabled,
+ complete,
+}: {
+ run: RunDetail;
+ disabled: boolean;
+ complete: (step: number, note: string) => void;
+}) {
+ return (
+ <>
+
+ Saved checklist · version {run.template_version}
+
+
+
+
{run.label}
+
{run.template_title}
+
+
+ {run.completed_at ? "Completed" : "Open"}
+
+
+
+
+ {run.completed_steps} of {run.total_steps} steps complete
+
+
+
+ {run.completed_at && (
+ This run is complete. Its record is retained.
+ )}
+
+ {run.run_steps.map((step) => {
+ const completion = run.completions.find(
+ (c) => c.step_number === step.step_number,
+ );
+ return (
+ complete(step.step_number, note)}
+ />
+ );
+ })}
+
+ >
+ );
+}
+
+function StepCard({
+ title,
+ number,
+ note,
+ done,
+ disabled,
+ complete,
+}: {
+ title: string;
+ number: number;
+ note?: string;
+ done: boolean;
+ disabled: boolean;
+ complete: (note: string) => void;
+}) {
+ const [draft, setDraft] = useState("");
+ return (
+
+
+ {done ? "✓" : number}
+
{title}
+
+ {done ? (
+ <>
+ Completion confirmed
+ {note || "No note added."}
+ >
+ ) : (
+
+ )}
+
+ );
+}
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/applications/checklist-runs/src/style.css b/applications/checklist-runs/src/style.css
new file mode 100644
index 00000000..2dd4c1c8
--- /dev/null
+++ b/applications/checklist-runs/src/style.css
@@ -0,0 +1,330 @@
+:root {
+ font-family: ui-sans-serif, system-ui, sans-serif;
+ color: #193b3a;
+ background: #f1f4ef;
+ font-synthesis: none;
+ line-height: 1.5;
+}
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+}
+main {
+ max-width: 1180px;
+ margin: auto;
+ padding: 42px 30px 20px;
+}
+h1,
+h2,
+h3,
+p {
+ margin: 0 0 12px;
+}
+h1 {
+ font-size: clamp(2rem, 5vw, 3rem);
+ line-height: 1.1;
+ letter-spacing: -0.04em;
+}
+h2 {
+ font-size: 1.45rem;
+ letter-spacing: -0.02em;
+}
+h3 {
+ font-size: 1rem;
+ margin: 0;
+}
+.masthead {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 22px;
+ margin-bottom: 30px;
+}
+.masthead p {
+ max-width: 560px;
+ color: #526664;
+ margin-top: 16px;
+}
+.eyebrow {
+ display: block;
+ text-transform: uppercase;
+ letter-spacing: 0.13em;
+ font-size: 0.7rem;
+ font-weight: 750;
+ color: #567e70;
+ margin-bottom: 10px;
+}
+.card {
+ background: #fff;
+ border: 1px solid #dae3da;
+ border-radius: 18px;
+ padding: 26px;
+ box-shadow: 0 5px 22px #23352c05;
+}
+.workspace {
+ display: grid;
+ grid-template-columns: 340px 1fr;
+ gap: 24px;
+ align-items: start;
+}
+aside {
+ display: grid;
+ gap: 22px;
+}
+.connect {
+ max-width: 580px;
+ margin: 48px auto 100px;
+}
+.connect p {
+ margin-bottom: 24px;
+}
+form {
+ display: grid;
+ gap: 10px;
+}
+label {
+ font-size: 0.85rem;
+ font-weight: 650;
+ margin-top: 5px;
+}
+input,
+select {
+ width: 100%;
+ min-height: 44px;
+ font: inherit;
+ background: #fff;
+ border: 1px solid #b7c9c0;
+ border-radius: 8px;
+ padding: 9px 12px;
+ color: #193b3a;
+}
+input:focus,
+select:focus {
+ outline: 3px solid #c6ead9;
+ outline-offset: 1px;
+}
+button {
+ font: inherit;
+ font-size: 0.85rem;
+ font-weight: 650;
+ min-height: 42px;
+ cursor: pointer;
+ color: #fff;
+ border: 1px solid #215650;
+ background: #215650;
+ border-radius: 8px;
+ padding: 10px 16px;
+}
+form > button {
+ margin-top: 8px;
+}
+button:hover:not(:disabled) {
+ filter: brightness(0.94);
+}
+button:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+button.secondary {
+ color: #215650;
+ background: #fff;
+ border-color: #bfd0c7;
+}
+button.inline {
+ background: none;
+ color: #215650;
+ padding: 4px;
+ min-height: 34px;
+ border: 0;
+ text-decoration: underline;
+}
+.muted {
+ color: #61736d;
+ font-weight: 400;
+}
+.tiny {
+ font-size: 0.75rem;
+ margin: 14px 0 0;
+}
+.notice {
+ padding: 16px 20px;
+ border-radius: 10px;
+ background: #e3ede6;
+ margin-bottom: 20px;
+ overflow-wrap: anywhere;
+}
+.error {
+ color: #7e3632;
+ background: #fff0ec;
+}
+.error button {
+ margin-right: 8px;
+ margin-bottom: 10px;
+}
+.error small {
+ display: block;
+}
+.success {
+ color: #246b49;
+}
+.section-head {
+ display: flex;
+ align-items: start;
+ justify-content: space-between;
+ gap: 15px;
+}
+.run-list ul,
+.steps {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+.run-list li {
+ margin-bottom: 9px;
+}
+button.run {
+ width: 100%;
+ text-align: left;
+ display: grid;
+ gap: 3px;
+ color: #284842;
+ background: #f6f8f4;
+ border-color: #e3e9e1;
+}
+button.run strong {
+ overflow-wrap: anywhere;
+}
+button.run span,
+button.run small {
+ font-weight: 400;
+ font-size: 0.78rem;
+}
+button.run.selected {
+ background: #e2f0e7;
+ border-color: #90b9a4;
+}
+.pagination {
+ display: flex;
+ justify-content: space-between;
+ gap: 10px;
+ margin-top: 18px;
+}
+.pagination button {
+ font-size: 0.75rem;
+ padding: 8px 10px;
+}
+.detail {
+ min-height: 440px;
+ padding: 30px;
+}
+.empty {
+ text-align: center;
+ margin: 95px auto;
+ max-width: 290px;
+ color: #56706a;
+}
+.badge {
+ font-size: 0.7rem;
+ font-weight: 700;
+ border-radius: 20px;
+ padding: 6px 12px;
+ background: #eef0e7;
+ color: #687047;
+ white-space: nowrap;
+}
+.badge.done {
+ background: #dff1e6;
+ color: #2b6e4b;
+}
+.progress-label {
+ font-size: 0.85rem;
+ margin: 18px 0 8px;
+}
+progress {
+ width: 100%;
+ height: 9px;
+ accent-color: #327e61;
+ margin-bottom: 24px;
+}
+.steps {
+ display: grid;
+ gap: 16px;
+}
+.step {
+ border: 1px solid #dde6dd;
+ border-radius: 12px;
+ padding: 19px;
+}
+.step.complete {
+ background: #f4faf5;
+}
+.step-heading {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 15px;
+}
+.step-number {
+ display: grid;
+ place-items: center;
+ background: #e9f0e8;
+ border-radius: 50%;
+ width: 28px;
+ height: 28px;
+ flex-shrink: 0;
+ font-size: 0.8rem;
+ font-weight: 700;
+}
+.completion-label {
+ color: #327052;
+ font-size: 0.73rem;
+ font-weight: 750;
+ display: block;
+ margin-bottom: 5px;
+}
+.step p {
+ margin: 0;
+ overflow-wrap: anywhere;
+}
+.connection-note {
+ margin: 23px 0;
+ font-size: 0.78rem;
+}
+footer {
+ text-align: center;
+ color: #7d8e82;
+ font-size: 0.75rem;
+ margin-top: 38px;
+ padding: 20px 0;
+}
+@media (max-width: 740px) {
+ main {
+ padding: 25px 16px 12px;
+ }
+ .workspace {
+ grid-template-columns: 1fr;
+ gap: 20px;
+ }
+ .masthead {
+ gap: 12px;
+ }
+ .masthead button {
+ padding: 8px 10px;
+ font-size: 0.75rem;
+ }
+ .card,
+ .detail {
+ padding: 22px;
+ }
+ .connect {
+ margin: 24px auto 50px;
+ }
+ .empty {
+ margin: 45px auto;
+ }
+ .notice {
+ padding: 15px;
+ }
+}
diff --git a/applications/checklist-runs/src/vite-env.d.ts b/applications/checklist-runs/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/applications/checklist-runs/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/applications/checklist-runs/tests/acceptance.py b/applications/checklist-runs/tests/acceptance.py
new file mode 100644
index 00000000..d980abc8
--- /dev/null
+++ b/applications/checklist-runs/tests/acceptance.py
@@ -0,0 +1,419 @@
+"""Real HTTP/RLS/transaction controls against an explicitly disposable Cloud fixture."""
+
+from concurrent.futures import ThreadPoolExecutor
+import hashlib
+import hmac
+import importlib.util
+import json
+import os
+from pathlib import Path
+import time
+import unittest
+import urllib.error
+import urllib.parse
+import urllib.request
+import uuid
+
+import psycopg
+
+spec = importlib.util.spec_from_file_location(
+ "tokens", Path(__file__).parents[1] / "scripts/tokens.py"
+)
+tokens = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(tokens)
+BASE = os.environ.get("API_BASE_URL", "http://127.0.0.1:3000")
+TEMPLATE = "10000000-0000-4000-8000-000000000001"
+SECRET = os.environ["PGRST_JWT_SECRET"]
+
+
+def signed(claims, secret=SECRET):
+ values = [{"alg": "HS256", "typ": "JWT"}, claims]
+ body = ".".join(
+ tokens.b64url(json.dumps(v, separators=(",", ":")).encode()) for v in values
+ )
+ return (
+ body
+ + "."
+ + tokens.b64url(
+ hmac.new(secret.encode(), body.encode(), hashlib.sha256).digest()
+ )
+ )
+
+
+def call(path, token=None, payload=None, method=None, headers=None):
+ request_headers = {"Authorization": "Bearer " + token} if token else {}
+ request_headers.update(headers or {})
+ data = None if payload is None else json.dumps(payload).encode()
+ if data is not None:
+ request_headers["Content-Type"] = "application/json"
+ request = urllib.request.Request(
+ BASE + "/" + path, data=data, headers=request_headers, method=method
+ )
+ try:
+ response = urllib.request.urlopen(request, timeout=30)
+ except urllib.error.HTTPError as error:
+ response = error
+ raw = response.read()
+ return response.status, json.loads(raw) if raw else None, dict(response.headers)
+
+
+def owner():
+ return psycopg.connect(
+ user=os.environ["TEST_OWNER_USER"], password=os.environ["TEST_OWNER_PASSWORD"]
+ )
+
+
+def wait_for_blockers(connection, count=2):
+ blocker = connection.info.backend_pid
+ deadline = time.monotonic() + 4
+ while time.monotonic() < deadline:
+ observed = connection.execute(
+ """WITH RECURSIVE blockers(request_pid, blocked_by) AS (
+ SELECT DISTINCT pid, unnest(pg_blocking_pids(pid)) FROM pg_locks WHERE NOT granted
+ UNION
+ SELECT b.request_pid, unnest(pg_blocking_pids(b.blocked_by)) FROM blockers b
+ ) SELECT count(DISTINCT request_pid) FROM blockers WHERE blocked_by = %s""",
+ (blocker,),
+ ).fetchone()[0]
+ if observed >= count:
+ print(
+ f"Observed {observed} independent HTTP transactions blocked by owner PID {blocker}.",
+ flush=True,
+ )
+ return
+ time.sleep(0.05)
+ raise AssertionError("Competing HTTP work did not reach the held database lock.")
+
+
+class CloudAcceptance(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.north = tokens.issue(SECRET, "checklist_north")
+ cls.south = tokens.issue(SECRET, "checklist_south")
+
+ def start(self, label, key=None, token=None):
+ payload = {
+ "p_request_id": key or str(uuid.uuid4()),
+ "p_template_id": TEMPLATE,
+ "p_label": label,
+ }
+ status, value, _ = call("rpc/start_run", token or self.north, payload)
+ self.assertEqual(status, 200, value)
+ self.assertIsInstance(value["run_id"], str)
+ return value, payload
+
+ def complete(self, run_id, step, key=None, note="", token=None):
+ payload = {
+ "p_request_id": key or str(uuid.uuid4()),
+ "p_run_id": run_id,
+ "p_step_number": step,
+ "p_note": note,
+ }
+ return call("rpc/complete_step", token or self.north, payload), payload
+
+ def test_01_lifecycle_snapshot_and_retained_results(self):
+ initial, payload = self.start(" Acceptance opening ")
+ status, repeated, _ = call(
+ "rpc/start_run", self.north, {**payload, "p_label": "Acceptance opening"}
+ )
+ self.assertEqual((status, repeated), (200, initial))
+ self.assertEqual(
+ call("rpc/start_run", self.north, {**payload, "p_label": "Changed"})[0], 409
+ )
+ path = (
+ "runs?id=eq."
+ + initial["run_id"]
+ + "&select=*,run_steps(*),completions(*)&run_steps.order=step_number.asc"
+ )
+ status, rows, _ = call(path, self.north)
+ self.assertEqual(status, 200, rows)
+ self.assertEqual(
+ [s["title"] for s in rows[0]["run_steps"]],
+ ["Check lights", "Arrange chairs", "Test display"],
+ )
+ first = None
+ for step in range(1, 4):
+ (status, result, _), completion_payload = self.complete(
+ initial["run_id"], step, note=" Checked "
+ )
+ self.assertEqual(status, 200, result)
+ self.assertEqual(result["completed_steps"], step)
+ if step == 1:
+ first = result, completion_payload
+ self.assertEqual(result["status"], "completed")
+ self.assertEqual(
+ call("rpc/complete_step", self.north, first[1])[:2], (200, first[0])
+ )
+ self.assertEqual(
+ call("rpc/complete_step", self.north, {**first[1], "p_note": "Changed"})[0],
+ 409,
+ )
+ self.assertEqual(self.complete(initial["run_id"], 1)[0][0], 409)
+ self.assertEqual(call("rpc/start_run", self.north, payload)[:2], (200, initial))
+ final = call(path, self.north)[1][0]
+ self.assertEqual((final["completed_steps"], len(final["completions"])), (3, 3))
+
+ def test_02_competing_matching_starts(self):
+ payload = {
+ "p_request_id": str(uuid.uuid4()),
+ "p_template_id": TEMPLATE,
+ "p_label": "Concurrent start",
+ }
+ with owner() as connection, ThreadPoolExecutor(max_workers=2) as workers:
+ connection.execute(
+ "SELECT 1 FROM checklist_storage.operators WHERE role_name = 'checklist_north' FOR UPDATE"
+ )
+ requests = [
+ workers.submit(call, "rpc/start_run", self.north, payload)
+ for _ in range(2)
+ ]
+ wait_for_blockers(connection)
+ connection.commit()
+ responses = [request.result() for request in requests]
+ self.assertEqual([r[0] for r in responses], [200, 200], responses)
+ self.assertEqual(responses[0][1], responses[1][1])
+ with owner() as connection:
+ count = connection.execute(
+ "SELECT count(*) FROM checklist_storage.runs WHERE operator_role = 'checklist_north' AND request_id = %s",
+ (payload["p_request_id"],),
+ ).fetchone()[0]
+ self.assertEqual(count, 1)
+
+ def test_03_competing_completions_increment_once(self):
+ run, _ = self.start("Concurrent completion")
+ payload = {
+ "p_request_id": str(uuid.uuid4()),
+ "p_run_id": run["run_id"],
+ "p_step_number": 1,
+ "p_note": "One retained note",
+ }
+ with owner() as connection, ThreadPoolExecutor(max_workers=2) as workers:
+ connection.execute(
+ "SELECT 1 FROM checklist_storage.runs WHERE id = %s FOR UPDATE",
+ (run["run_id"],),
+ )
+ requests = [
+ workers.submit(call, "rpc/complete_step", self.north, payload)
+ for _ in range(2)
+ ]
+ wait_for_blockers(connection)
+ connection.commit()
+ responses = [request.result() for request in requests]
+ self.assertEqual([r[0] for r in responses], [200, 200], responses)
+ self.assertEqual(responses[0][1], responses[1][1])
+ with owner() as connection:
+ row = connection.execute(
+ "SELECT completed_steps, (SELECT count(*) FROM checklist_storage.completions WHERE run_id = r.id) FROM checklist_storage.runs r WHERE r.id = %s",
+ (run["run_id"],),
+ ).fetchone()
+ self.assertEqual(row, (1, 1))
+ self.assertEqual(self.complete(run["run_id"], 1)[0][0], 409)
+
+ def test_04_after_update_child_failure_rolls_back(self):
+ run, _ = self.start("Rollback control")
+ with owner() as connection:
+ connection.execute(
+ """CREATE FUNCTION checklist_storage.fail_completion() RETURNS trigger
+ LANGUAGE plpgsql SET search_path = pg_catalog AS $$ BEGIN
+ IF NEW.note = 'after-update-failure' THEN RAISE check_violation USING MESSAGE = 'Synthetic child failure'; END IF;
+ RETURN NEW; END; $$"""
+ )
+ connection.execute(
+ "CREATE TRIGGER fail_completion BEFORE INSERT ON checklist_storage.completions FOR EACH ROW EXECUTE FUNCTION checklist_storage.fail_completion()"
+ )
+ try:
+ (status, value, _), payload = self.complete(
+ run["run_id"], 1, note="after-update-failure"
+ )
+ self.assertEqual(status, 400, value)
+ self.assertEqual(value["code"], "23514")
+ with owner() as connection:
+ row = connection.execute(
+ "SELECT completed_steps, (SELECT count(*) FROM checklist_storage.completions WHERE run_id = r.id) FROM checklist_storage.runs r WHERE r.id = %s",
+ (run["run_id"],),
+ ).fetchone()
+ self.assertEqual(row, (0, 0))
+ finally:
+ with owner() as connection:
+ connection.execute(
+ "DROP TRIGGER fail_completion ON checklist_storage.completions"
+ )
+ connection.execute("DROP FUNCTION checklist_storage.fail_completion()")
+ self.assertEqual(call("rpc/complete_step", self.north, payload)[0], 200)
+
+ def test_05_scope_and_exposed_write_denial(self):
+ run, _ = self.start("North private")
+ self.assertEqual(
+ call(
+ "runs?id=eq." + run["run_id"] + "&select=*,run_steps(*),completions(*)",
+ self.south,
+ )[:2],
+ (200, []),
+ )
+ self.assertEqual(
+ call("run_steps?run_id=eq." + run["run_id"], self.south)[:2], (200, [])
+ )
+ self.assertEqual(self.complete(run["run_id"], 1, token=self.south)[0][0], 404)
+ self.assertEqual(call("runs", self.north, {"label": "Direct insert"})[0], 403)
+ self.assertEqual(
+ call("templates", self.north, {"title": "Edited"}, method="PATCH")[0], 403
+ )
+ self.assertEqual(
+ call("runs", self.north, headers={"Accept-Profile": "checklist_storage"})[
+ 0
+ ],
+ 406,
+ )
+ southern, _ = self.start("South private", token=self.south)
+ self.assertNotIn(
+ southern["run_id"], [r["id"] for r in call("rpc/list_runs", self.north)[1]]
+ )
+
+ def test_06_native_jwt_rejections(self):
+ now = int(time.time())
+ good = {
+ "role": "checklist_north",
+ "aud": "checklist-runs",
+ "iat": now,
+ "exp": now + 60,
+ }
+ self.assertEqual(call("templates")[0], 401)
+ cases = [
+ signed(good, "b" * 128),
+ signed({**good, "iat": now - 180, "exp": now - 120}),
+ signed({**good, "aud": "wrong-audience"}),
+ signed({k: v for k, v in good.items() if k != "aud"}),
+ signed({**good, "exp": now + 901}),
+ ]
+ for value in cases:
+ self.assertEqual(call("templates", value)[0], 401)
+ self.assertEqual(
+ call("templates", signed({**good, "role": "checklist_owner"}))[0], 403
+ )
+
+ def test_07_numeric_cursor_exact_bigint_and_get_read_only(self):
+ self.assertEqual(call("rpc/list_runs?p_limit=21", self.north)[0], 422)
+ self.assertEqual(call("rpc/list_runs?p_after=-1", self.north)[0], 422)
+ self.assertLessEqual(len(call("runs?limit=1000", self.north)[1]), 20)
+ with owner() as connection:
+ connection.execute(
+ "ALTER SEQUENCE checklist_storage.runs_id_seq RESTART WITH 9007199254740993"
+ )
+ run, payload = self.start("Exact large ID")
+ self.assertEqual(run["run_id"], "9007199254740993")
+ self.assertEqual(
+ call("rpc/list_runs?p_after=9007199254740992", self.north)[1][0]["id"],
+ run["run_id"],
+ )
+ self.assertEqual(self.complete(run["run_id"], 1)[0][0], 200)
+ params = urllib.parse.urlencode({**payload, "p_request_id": str(uuid.uuid4())})
+ status, value, _ = call("rpc/start_run?" + params, self.north)
+ self.assertEqual(status, 405, value)
+ self.assertEqual(value["code"], "25006")
+ self.assertEqual(
+ call("rpc/start_run", self.north, {**payload, "p_label": "x" * 81})[0], 422
+ )
+ self.assertEqual(
+ call("rpc/start_run", self.north, {**payload, "p_label": "bad\u0001label"})[
+ 0
+ ],
+ 422,
+ )
+
+ def test_09_native_json_and_scalar_boundaries(self):
+ payload = {
+ "p_request_id": str(uuid.uuid4()),
+ "p_template_id": TEMPLATE,
+ "p_label": "Native input boundary",
+ }
+ self.assertEqual(
+ call("rpc/start_run", self.north, {**payload, "p_label": "\ud800"})[0], 400
+ )
+ self.assertEqual(
+ call(
+ "rpc/start_run", self.north, {**payload, "p_template_id": "not-a-uuid"}
+ )[0],
+ 400,
+ )
+ self.assertEqual(
+ call("rpc/start_run", self.north, {**payload, "p_label": None})[0], 422
+ )
+ completion = {
+ "p_request_id": str(uuid.uuid4()),
+ "p_run_id": "1" * 5000,
+ "p_step_number": 1,
+ "p_note": "",
+ }
+ self.assertEqual(call("rpc/complete_step", self.north, completion)[0], 400)
+ with owner() as connection:
+ self.assertEqual(
+ connection.execute(
+ "SELECT count(*) FROM checklist_storage.runs WHERE request_id = %s",
+ (payload["p_request_id"],),
+ ).fetchone()[0],
+ 0,
+ )
+
+ def test_08_authenticator_memberships_roles_and_quota(self):
+ with psycopg.connect() as connection:
+ self.assertEqual(
+ connection.execute(
+ "SELECT rolinherit FROM pg_roles WHERE rolname = current_user"
+ ).fetchone()[0],
+ False,
+ )
+ roles = connection.execute(
+ "SELECT parent.rolname FROM pg_auth_members m JOIN pg_roles parent ON parent.oid = m.roleid JOIN pg_roles child ON child.oid = m.member WHERE child.rolname = current_user ORDER BY parent.rolname"
+ ).fetchall()
+ self.assertEqual(roles, [("checklist_north",), ("checklist_south",)])
+ for sql in (
+ "SET ROLE checklist_owner",
+ "CREATE TABLE public.denied(id int)",
+ "CREATE TEMP TABLE denied(id int)",
+ "SELECT * FROM checklist_storage.runs",
+ ):
+ with self.assertRaises(psycopg.errors.InsufficientPrivilege):
+ connection.execute(sql)
+ connection.rollback()
+ connection.execute("SET ROLE checklist_north")
+ self.assertEqual(
+ connection.execute(
+ "SELECT count(*) FROM checklist_storage.runs WHERE operator_role = 'checklist_south'"
+ ).fetchone()[0],
+ 0,
+ )
+ with self.assertRaises(psycopg.errors.InsufficientPrivilege):
+ connection.execute(
+ "UPDATE checklist_storage.completions SET note = 'edited'"
+ )
+ connection.rollback()
+ with owner() as connection:
+ count = connection.execute(
+ "SELECT count(*) FROM checklist_storage.runs WHERE operator_role = 'checklist_south'"
+ ).fetchone()[0]
+ connection.execute(
+ """INSERT INTO checklist_storage.runs
+ (operator_role, request_id, start_payload, start_result, template_id, template_version,
+ template_title, template_snapshot, label, total_steps)
+ SELECT 'checklist_south', gen_random_uuid(), '{}', '{}', t.id, t.version,
+ t.title, t.steps, 'quota-fixture', jsonb_array_length(t.steps)
+ FROM checklist_storage.templates t CROSS JOIN generate_series(1, %s)
+ WHERE t.id = %s""",
+ (100 - count, TEMPLATE),
+ )
+ try:
+ payload = {
+ "p_request_id": str(uuid.uuid4()),
+ "p_template_id": TEMPLATE,
+ "p_label": "Above quota",
+ }
+ self.assertEqual(call("rpc/start_run", self.south, payload)[0], 409)
+ finally:
+ with owner() as connection:
+ connection.execute(
+ "DELETE FROM checklist_storage.runs WHERE label = 'quota-fixture'"
+ )
+
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/applications/checklist-runs/tests/browser.mjs b/applications/checklist-runs/tests/browser.mjs
new file mode 100644
index 00000000..9e3d2ca6
--- /dev/null
+++ b/applications/checklist-runs/tests/browser.mjs
@@ -0,0 +1,298 @@
+import assert from "node:assert/strict";
+import { createHmac } from "node:crypto";
+import { mkdir } from "node:fs/promises";
+import path from "node:path";
+import { chromium } from "playwright";
+
+const evidence = process.env.EVIDENCE_DIR || "/tmp/checklist-evidence";
+const base = process.env.UI_BASE_URL || "http://127.0.0.1:5173";
+const secret = process.env.PGRST_JWT_SECRET;
+assert.ok(
+ secret && secret.length >= 64,
+ "Export the private fixture signing secret for this helper only.",
+);
+await mkdir(evidence, { recursive: true });
+
+function token(role, issued = Math.floor(Date.now() / 1000), seconds = 900) {
+ const header = Buffer.from(
+ JSON.stringify({ alg: "HS256", typ: "JWT" }),
+ ).toString("base64url");
+ const payload = Buffer.from(
+ JSON.stringify({
+ role,
+ aud: "checklist-runs",
+ iat: issued,
+ exp: issued + seconds,
+ }),
+ ).toString("base64url");
+ const body = `${header}.${payload}`;
+ return `${body}.${createHmac("sha256", secret).update(body).digest("base64url")}`;
+}
+
+function deferred() {
+ let resolve;
+ const promise = new Promise((done) => {
+ resolve = done;
+ });
+ return { promise, resolve };
+}
+
+async function connect(page, value) {
+ await page.goto(base);
+ await page.getByLabel("Operator token", { exact: true }).fill(value);
+ await page.getByRole("button", { name: "Connect", exact: true }).click();
+ await page
+ .getByRole("heading", { name: "Start a run", exact: true })
+ .waitFor({ timeout: 30000 });
+}
+
+async function ready(page) {
+ await page
+ .getByLabel("Checklist", { exact: true })
+ .locator("option")
+ .first()
+ .waitFor({ state: "attached", timeout: 30000 });
+ await page
+ .getByRole("button", { name: "Start run", exact: true })
+ .waitFor({ timeout: 30000 });
+}
+
+const browser = await chromium.launch({
+ env: { HOME: process.env.HOME, PATH: process.env.PATH, LANG: "C.UTF-8" },
+});
+const northToken = token("checklist_north");
+const southToken = token("checklist_south");
+const desktop = await browser.newContext({
+ viewport: { width: 1280, height: 900 },
+});
+const mobile = await browser.newContext({
+ viewport: { width: 390, height: 844 },
+ isMobile: true,
+ deviceScaleFactor: 1,
+});
+const a = await desktop.newPage();
+const b = await mobile.newPage();
+const unique = Date.now().toString();
+const northLabel = `Browser opening ${unique}`;
+const southLabel = `Browser closing ${unique}`;
+await desktop.tracing.start({
+ screenshots: true,
+ snapshots: true,
+ sources: true,
+});
+
+try {
+ await connect(a, northToken);
+ await ready(a);
+ await a
+ .getByLabel("Checklist", { exact: true })
+ .selectOption("10000000-0000-4000-8000-000000000001");
+ await a.getByLabel("Run label", { exact: true }).fill(northLabel);
+ const requests = [];
+ const committedStart = deferred();
+ let firstStart = true;
+ let savedRunId;
+ await a.route("**/api/rpc/start_run", async (route) => {
+ requests.push(route.request().postDataJSON());
+ if (firstStart) {
+ firstStart = false;
+ const response = await route.fetch();
+ assert.equal(response.status(), 200);
+ savedRunId = (await response.json()).run_id;
+ assert.equal(typeof savedRunId, "string");
+ await route.abort("failed");
+ committedStart.resolve();
+ } else {
+ await route.continue();
+ }
+ });
+ await a.getByRole("button", { name: "Start run", exact: true }).click();
+ await committedStart.promise;
+ await a
+ .getByRole("button", { name: "Retry same action", exact: true })
+ .waitFor({ timeout: 30000 });
+ assert.ok(await a.getByLabel("Run label", { exact: true }).isDisabled());
+ await a
+ .getByRole("button", { name: "Retry same action", exact: true })
+ .click();
+ await a
+ .getByRole("heading", { name: northLabel, exact: true })
+ .waitFor({ timeout: 30000 });
+ assert.equal(requests.length, 2);
+ assert.deepEqual(requests[0], requests[1]);
+ console.log(
+ "Ambiguous start committed, then same captured UUID/payload replayed; no automatic new key.",
+ );
+
+ const completionCommitted = deferred();
+ const releaseCompletion = deferred();
+ const refreshHeld = deferred();
+ const releaseRefresh = deferred();
+ let holdRefresh = false;
+ let firstCompletion = true;
+ await a.route("**/api/rpc/complete_step", async (route) => {
+ if (!firstCompletion) return route.continue();
+ firstCompletion = false;
+ const response = await route.fetch();
+ assert.equal(response.status(), 200);
+ completionCommitted.resolve();
+ await releaseCompletion.promise;
+ holdRefresh = true;
+ await route.fulfill({ response });
+ });
+ await a.route("**/api/runs?**", async (route) => {
+ if (!holdRefresh) return route.continue();
+ holdRefresh = false;
+ const response = await route.fetch();
+ refreshHeld.resolve();
+ await releaseRefresh.promise;
+ await route.fulfill({ response });
+ });
+ await a
+ .getByLabel("Completion note (optional)")
+ .first()
+ .fill("Lights and display checked");
+ await a.getByRole("button", { name: "Complete step 1", exact: true }).click();
+ await completionCommitted.promise;
+ assert.equal(
+ await a.getByText("Completion confirmed", { exact: true }).count(),
+ 0,
+ );
+ assert.ok(
+ await a
+ .getByRole("button", { name: "Complete step 2", exact: true })
+ .isDisabled(),
+ );
+ releaseCompletion.resolve();
+ await refreshHeld.promise;
+ assert.ok(
+ await a
+ .getByRole("button", { name: "Complete step 2", exact: true })
+ .isDisabled(),
+ );
+ releaseRefresh.resolve();
+ await a
+ .getByText("Completion confirmed", { exact: true })
+ .first()
+ .waitFor({ timeout: 30000 });
+ for (const number of [2, 3]) {
+ await a
+ .getByRole("button", { name: `Complete step ${number}`, exact: true })
+ .click();
+ await a
+ .getByText("Completion confirmed", { exact: true })
+ .nth(number - 1)
+ .waitFor({ timeout: 30000 });
+ }
+ await a
+ .getByText("This run is complete. Its record is retained.", { exact: true })
+ .waitFor({ timeout: 30000 });
+ assert.equal(await a.getByRole("progressbar").getAttribute("value"), "3");
+ assert.equal(
+ await a.evaluate(() => localStorage.length + sessionStorage.length),
+ 0,
+ );
+ assert.ok(!a.url().includes(northToken));
+ console.log(
+ "No optimistic completion: controls stay pending through invalidation; all three confirmed steps are durable.",
+ );
+ await a.screenshot({
+ path: path.join(evidence, "desktop.png"),
+ fullPage: true,
+ });
+
+ await connect(b, southToken);
+ await ready(b);
+ await b.getByLabel("Run label", { exact: true }).fill(southLabel);
+ await b.getByRole("button", { name: "Start run", exact: true }).click();
+ await b
+ .getByRole("heading", { name: southLabel, exact: true })
+ .waitFor({ timeout: 30000 });
+ assert.equal(await b.getByText(northLabel, { exact: true }).count(), 0);
+ await b.screenshot({
+ path: path.join(evidence, "mobile.png"),
+ fullPage: true,
+ });
+ assert.ok(
+ await b.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
+ );
+ console.log(
+ "Second browser context sees its own scope; mobile layout fits the viewport.",
+ );
+
+ const oldReadCaptured = deferred();
+ const releaseOldRead = deferred();
+ await a.route("**/api/rpc/list_runs?**", async (route) => {
+ if (route.request().headers().authorization !== `Bearer ${northToken}`)
+ return route.continue();
+ const response = await route.fetch();
+ const rows = await response.json();
+ assert.ok(rows.some((row) => row.label === northLabel));
+ oldReadCaptured.resolve();
+ await releaseOldRead.promise;
+ try {
+ await route.fulfill({ response });
+ } catch {
+ /* The consumed AbortSignal can cancel the old route. */
+ }
+ });
+ await a.getByRole("button", { name: "Refresh", exact: true }).click();
+ await oldReadCaptured.promise;
+ await a.getByRole("button", { name: "Disconnect", exact: true }).click();
+ await a.getByLabel("Operator token", { exact: true }).fill(southToken);
+ await a.getByRole("button", { name: "Connect", exact: true }).click();
+ await a
+ .getByRole("button", { name: new RegExp(southLabel) })
+ .waitFor({ timeout: 30000 });
+ releaseOldRead.resolve();
+ await a.waitForTimeout(400);
+ assert.equal(await a.getByText(northLabel, { exact: true }).count(), 0);
+ assert.equal(
+ await a.getByText("Lights and display checked", { exact: true }).count(),
+ 0,
+ );
+ console.log(
+ "Actual old-scope HTTP read captured before switch and released afterward; fresh cache never displays old run/notes.",
+ );
+
+ const expired = await browser.newContext({
+ viewport: { width: 1100, height: 800 },
+ });
+ const expiredPage = await expired.newPage();
+ await connect(
+ expiredPage,
+ token("checklist_north", Math.floor(Date.now() / 1000) - 180, 60),
+ );
+ await expiredPage.getByRole("alert").waitFor({ timeout: 30000 });
+ assert.match(
+ await expiredPage.getByRole("alert").innerText(),
+ /invalid or expired/,
+ );
+ await expired.close();
+ console.log(
+ "Expired token produces an actionable connection error; tokens are never stored in browser persistence.",
+ );
+
+ // Preserve only nonsecret identifiers for the separate real process-restart check.
+ const { writeFile } = await import("node:fs/promises");
+ await writeFile(
+ path.join(evidence, "browser-snapshot.json"),
+ JSON.stringify(
+ { run_id: savedRunId, label: northLabel, request: requests[0] },
+ null,
+ 2,
+ ),
+ );
+ await desktop.tracing.stop();
+ console.log("Browser workflow passed.");
+} catch (error) {
+ await a
+ .screenshot({ path: path.join(evidence, "failed.png"), fullPage: true })
+ .catch(() => {});
+ await desktop.tracing
+ .stop({ path: path.join(evidence, "failed-trace.zip") })
+ .catch(() => {});
+ throw error;
+} finally {
+ await browser.close();
+}
diff --git a/applications/checklist-runs/tests/requirements.in b/applications/checklist-runs/tests/requirements.in
new file mode 100644
index 00000000..aa0ab28b
--- /dev/null
+++ b/applications/checklist-runs/tests/requirements.in
@@ -0,0 +1 @@
+psycopg[binary]==3.3.6
diff --git a/applications/checklist-runs/tests/requirements.txt b/applications/checklist-runs/tests/requirements.txt
new file mode 100644
index 00000000..6c6cc80f
--- /dev/null
+++ b/applications/checklist-runs/tests/requirements.txt
@@ -0,0 +1,3 @@
+psycopg==3.3.6
+psycopg-binary==3.3.6
+typing_extensions==4.16.0
diff --git a/applications/checklist-runs/tests/restart.py b/applications/checklist-runs/tests/restart.py
new file mode 100644
index 00000000..485aecad
--- /dev/null
+++ b/applications/checklist-runs/tests/restart.py
@@ -0,0 +1,111 @@
+"""Replace this fixture's exact PostgREST process and compare durable HTTP state.
+
+Requires POSTGREST_PID, runtime/test env and the browser-snapshot.json recorded
+by the browser helper in EVIDENCE_DIR. Never targets an arbitrary process.
+"""
+
+import importlib.util
+import json
+import os
+from pathlib import Path
+import signal
+import subprocess
+import time
+
+import psycopg
+
+from acceptance import call, owner
+from tls import APP, BINARY, environment
+
+spec = importlib.util.spec_from_file_location(
+ "fixture_tokens", APP / "scripts/tokens.py"
+)
+tokens = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(tokens)
+evidence = Path(os.environ.get("EVIDENCE_DIR", "/tmp/checklist-evidence"))
+evidence.mkdir(parents=True, exist_ok=True)
+fixture = json.loads((evidence / "browser-snapshot.json").read_text())
+old = int(os.environ["POSTGREST_PID"])
+process = Path(f"/proc/{old}")
+assert process.exists(), "The original PostgREST process is not live."
+assert (process / "cwd").resolve() == APP
+arguments = (process / "cmdline").read_bytes().split(b"\0")
+assert Path(arguments[0].decode()).name == "postgrest"
+assert b"postgrest.conf" in arguments
+token = tokens.issue(os.environ["PGRST_JWT_SECRET"], "checklist_north")
+detail_path = (
+ "runs?id=eq."
+ + fixture["run_id"]
+ + "&select=*,run_steps(*),completions(*)&run_steps.order=step_number.asc&completions.order=step_number.asc"
+)
+status, before, _ = call(detail_path, token)
+assert status == 200 and len(before) == 1 and before[0]["completed_steps"] == 3
+with owner() as connection:
+ completion = connection.execute(
+ "SELECT request_id::text, step_number, note FROM checklist_storage.completions WHERE run_id = %s ORDER BY step_number LIMIT 1",
+ (fixture["run_id"],),
+ ).fetchone()
+completion_request = {
+ "p_request_id": completion[0],
+ "p_run_id": fixture["run_id"],
+ "p_step_number": completion[1],
+ "p_note": completion[2],
+}
+start_before = call("rpc/start_run", token, fixture["request"])
+completion_before = call("rpc/complete_step", token, completion_request)
+assert start_before[0] == 200 and completion_before[0] == 200
+
+os.kill(old, signal.SIGTERM)
+deadline = time.monotonic() + 20
+while process.exists() and time.monotonic() < deadline:
+ time.sleep(0.1)
+assert not process.exists(), "The original process did not exit; refusing replacement."
+print(f"Original PostgREST PID {old} exited before replacement.", flush=True)
+output = (evidence / "replacement-private.log").open("ab")
+child = subprocess.Popen(
+ [BINARY, "postgrest.conf"],
+ cwd=APP,
+ env=environment(),
+ stdout=output,
+ stderr=output,
+ start_new_session=True,
+)
+(evidence / "replacement.pid").write_text(str(child.pid))
+deadline = time.monotonic() + 30
+while time.monotonic() < deadline:
+ assert child.poll() is None, "The replacement process exited before becoming ready."
+ try:
+ status, after, _ = call(detail_path, token)
+ if status == 200:
+ break
+ except OSError:
+ pass
+ time.sleep(0.1)
+else:
+ raise AssertionError("Replacement did not become ready.")
+assert before == after
+assert call("rpc/start_run", token, fixture["request"])[:2] == start_before[:2]
+assert call("rpc/complete_step", token, completion_request)[:2] == completion_before[:2]
+child_keys = {
+ item.split(b"=", 1)[0].decode()
+ for item in Path(f"/proc/{child.pid}/environ").read_bytes().split(b"\0")
+ if item
+}
+assert not any("OWNER" in key or "TEST_" in key or "ADMIN" in key for key in child_keys)
+assert os.environ["PGUSER"] == "checklist_authenticator"
+(evidence / "persistence.json").write_text(
+ json.dumps(
+ {
+ "old_pid": old,
+ "new_pid": child.pid,
+ "before": before,
+ "after": after,
+ "start_result": start_before[1],
+ "completion_result": completion_before[1],
+ },
+ indent=2,
+ )
+)
+print(
+ f"Replacement PID {child.pid} uses runtime-only env; exact run, three completions and both retained results agree."
+)
diff --git a/applications/checklist-runs/tests/restart_browser.mjs b/applications/checklist-runs/tests/restart_browser.mjs
new file mode 100644
index 00000000..18028209
--- /dev/null
+++ b/applications/checklist-runs/tests/restart_browser.mjs
@@ -0,0 +1,58 @@
+import assert from "node:assert/strict";
+import { createHmac } from "node:crypto";
+import { readFile } from "node:fs/promises";
+import path from "node:path";
+import { chromium } from "playwright";
+
+const evidence = process.env.EVIDENCE_DIR || "/tmp/checklist-evidence";
+const fixture = JSON.parse(
+ await readFile(path.join(evidence, "browser-snapshot.json"), "utf8"),
+);
+const issued = Math.floor(Date.now() / 1000);
+const header = Buffer.from(
+ JSON.stringify({ alg: "HS256", typ: "JWT" }),
+).toString("base64url");
+const claims = Buffer.from(
+ JSON.stringify({
+ role: "checklist_north",
+ aud: "checklist-runs",
+ iat: issued,
+ exp: issued + 900,
+ }),
+).toString("base64url");
+const body = `${header}.${claims}`;
+const token = `${body}.${createHmac("sha256", process.env.PGRST_JWT_SECRET).update(body).digest("base64url")}`;
+const browser = await chromium.launch({
+ env: { HOME: process.env.HOME, PATH: process.env.PATH, LANG: "C.UTF-8" },
+});
+try {
+ const page = await browser.newPage({
+ viewport: { width: 1280, height: 900 },
+ });
+ await page.goto(process.env.UI_BASE_URL || "http://127.0.0.1:5173");
+ await page.getByLabel("Operator token", { exact: true }).fill(token);
+ await page.getByRole("button", { name: "Connect", exact: true }).click();
+ await page
+ .getByRole("button", { name: new RegExp(fixture.label) })
+ .waitFor({ timeout: 30000 });
+ await page.getByRole("button", { name: new RegExp(fixture.label) }).click();
+ await page
+ .getByText("This run is complete. Its record is retained.", { exact: true })
+ .waitFor({ timeout: 30000 });
+ assert.equal(
+ await page.getByText("Completion confirmed", { exact: true }).count(),
+ 3,
+ );
+ await page
+ .getByText("Lights and display checked", { exact: true })
+ .waitFor({ timeout: 30000 });
+ await page.screenshot({
+ path: path.join(evidence, "restarted.png"),
+ fullPage: true,
+ });
+ console.log(
+ "Fresh browser token after actual PostgREST replacement displays the saved completed run and three immutable notes.",
+ );
+} finally {
+ await browser.close();
+}
diff --git a/applications/checklist-runs/tests/test_tokens.py b/applications/checklist-runs/tests/test_tokens.py
new file mode 100644
index 00000000..d5ad27e2
--- /dev/null
+++ b/applications/checklist-runs/tests/test_tokens.py
@@ -0,0 +1,51 @@
+import base64
+import hashlib
+import hmac
+import importlib.util
+import json
+from pathlib import Path
+import unittest
+
+spec = importlib.util.spec_from_file_location(
+ "tokens", Path(__file__).parents[1] / "scripts/tokens.py"
+)
+tokens = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(tokens)
+
+
+class FixtureIssuerTests(unittest.TestCase):
+ def test_signed_scoped_short_lived_token(self):
+ secret = "a" * 128
+ value = tokens.issue(secret, "checklist_north", 60, now=1790960000)
+ head, body, signature = value.split(".")
+ decoded = json.loads(base64.urlsafe_b64decode(body + "=" * (-len(body) % 4)))
+ self.assertEqual(
+ decoded,
+ {
+ "role": "checklist_north",
+ "aud": "checklist-runs",
+ "iat": 1790960000,
+ "exp": 1790960060,
+ },
+ )
+ expected = hmac.new(
+ secret.encode(), (head + "." + body).encode(), hashlib.sha256
+ ).digest()
+ self.assertEqual(tokens.b64url(expected), signature)
+
+ def test_unknown_role_cannot_be_issued(self):
+ with self.assertRaises(ValueError):
+ tokens.issue("a" * 128, "checklist_owner")
+
+ def test_short_secret_rejected(self):
+ with self.assertRaises(ValueError):
+ tokens.issue("too-short", "checklist_south")
+
+ def test_lifetime_bounds(self):
+ for seconds in (0, 901, True):
+ with self.assertRaises(ValueError):
+ tokens.issue("a" * 128, "checklist_south", seconds)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/applications/checklist-runs/tests/tls.py b/applications/checklist-runs/tests/tls.py
new file mode 100644
index 00000000..5cb7c7ce
--- /dev/null
+++ b/applications/checklist-runs/tests/tls.py
@@ -0,0 +1,79 @@
+"""Exercise libpq verification in the actual PostgREST application process."""
+
+import os
+from pathlib import Path
+import socket
+import subprocess
+import tempfile
+import unittest
+
+APP = Path(__file__).resolve().parents[1]
+BINARY = os.environ.get("POSTGREST_BIN", str(APP / ".local/bin/postgrest"))
+RUNTIME_KEYS = (
+ "PGHOST",
+ "PGPORT",
+ "PGDATABASE",
+ "PGUSER",
+ "PGPASSWORD",
+ "PGSSLMODE",
+ "PGSSLROOTCERT",
+ "PGCONNECT_TIMEOUT",
+ "PGRST_JWT_SECRET",
+)
+
+
+def environment():
+ return {
+ "HOME": os.environ["HOME"],
+ "PATH": "/usr/bin:/bin",
+ "LANG": "C.UTF-8",
+ **{key: os.environ[key] for key in RUNTIME_KEYS},
+ }
+
+
+class NativeTLS(unittest.TestCase):
+ def assert_connection_fails(self, changes, expected_fragments):
+ with tempfile.TemporaryFile() as output:
+ child = subprocess.Popen(
+ [BINARY, "postgrest.conf"],
+ cwd=APP,
+ env={**environment(), "PGRST_SERVER_PORT": "3001", **changes},
+ stdout=output,
+ stderr=output,
+ )
+ try:
+ code = child.wait(timeout=25)
+ finally:
+ if child.poll() is None:
+ child.terminate()
+ child.wait(timeout=5)
+ output.seek(0)
+ message = output.read().decode(errors="replace").lower()
+ self.assertNotEqual(code, 0)
+ self.assertTrue(
+ any(fragment in message for fragment in expected_fragments),
+ "The native failure did not identify the intended TLS verification control.",
+ )
+ print(
+ "Actual PostgREST process failed for the expected certificate/name reason; exit",
+ code,
+ )
+
+ def test_untrusted_ca(self):
+ self.assert_connection_fails(
+ {"PGSSLROOTCERT": "/etc/ssl/certs/ca-certificates.crt"},
+ ("certificate verify failed", "unable to get local issuer certificate"),
+ )
+
+ def test_wrong_hostname_same_actual_server(self):
+ actual_ip = socket.getaddrinfo(
+ os.environ["PGHOST"], 5432, type=socket.SOCK_STREAM
+ )[0][4][0]
+ self.assert_connection_fails(
+ {"PGHOST": "wrong-hostname.invalid", "PGHOSTADDR": actual_ip},
+ ("does not match host name", "does not match hostname"),
+ )
+
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/applications/checklist-runs/tsconfig.json b/applications/checklist-runs/tsconfig.json
new file mode 100644
index 00000000..76ff729a
--- /dev/null
+++ b/applications/checklist-runs/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "noEmit": true,
+ "allowImportingTsExtensions": true,
+ "skipLibCheck": true
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/applications/checklist-runs/vite.config.ts b/applications/checklist-runs/vite.config.ts
new file mode 100644
index 00000000..691fb911
--- /dev/null
+++ b/applications/checklist-runs/vite.config.ts
@@ -0,0 +1,30 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+const proxy = {
+ "/api": {
+ target: "http://127.0.0.1:3000",
+ changeOrigin: false,
+ rewrite: (path: string) => path.replace(/^\/api(?=\/|$)/, ""),
+ },
+};
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ host: "127.0.0.1",
+ port: 5173,
+ strictPort: true,
+ allowedHosts: ["localhost", "127.0.0.1"],
+ cors: false,
+ proxy,
+ },
+ preview: {
+ host: "127.0.0.1",
+ port: 5173,
+ strictPort: true,
+ allowedHosts: ["localhost", "127.0.0.1"],
+ cors: false,
+ proxy,
+ },
+});