- entry-exit-points.md: the snapshot content-type bug + serve-side cleanType fix (Hikvision image/jpeg; charset="UTF-8" broke every legacy render). - booth-exit-flow.md: the Active-Sessions/modal rework — inline barrier button removed -> modal; closed-within-grace view; live grace countdown; actual paid amount; read-only snapshot review in the closed-session view. - local-dev-workflow.md: the gated `pnpm db:reset` training tool + flag table + the booth (docker exec, no pnpm) note. - appliance-provisioning.md: new §7d — reset on the booth via docker exec into the server container (script ships in the deploy bundle; DATABASE_URL= /data/parking.sqlite), ledger-truncation warning + the two safety gates. - index.md catalog line; log.md entries. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
5.2 KiB
type, tags, sources, updated
| type | tags | sources | updated | |||
|---|---|---|---|---|---|---|
| reference |
|
2026-06-30 |
Local Dev Workflow
Dev-environment reference, not product architecture. How to run the stack locally and the gotchas that have bitten us. For device testing under WSL also read wsl-dev-networking.
First-time setup
pnpm install
cp apps/server/.env.example apps/server/.env # then fill in JWT_SECRET
# JWT_SECRET=$(openssl rand -hex 32) # server refuses to start without a strong one
pnpm --filter @parking/db exec drizzle-kit migrate # create the SQLite schema
pnpm seed:admin # create the first admin (see [[local-jwt-auth]])
apps/server/.env and the *.sqlite files are gitignored (local-only). Leave NODE_ENV
unset in dev so the auth cookies aren't Secure-only (Vite dev is plain http).
Running
pnpm dev # turbo runs both: Vite (web, :5173) + Fastify (server, :3000)
Open http://localhost:5173. The Vite dev proxy forwards /api + /health to the backend, so
the SPA and API are same-origin and the local-jwt-auth works without CORS.
Production uses an nginx reverse proxy (deploy/nginx.conf) for the same same-origin setup.
Gotchas (all fixed, recorded so they don't recur)
- Server dev must not be
node --experimental-strip-types src/index.ts. Type-stripping does not rewrite.jsimport specifiers to.ts, so it crashed withERR_MODULE_NOT_FOUNDand silently never started — the symptom was the SPA hanging for minutes (the Vite proxy waiting on a dead backend), then finally erroring. Thedevscript usestsx watchinstead. - Vite proxy →
127.0.0.1, notlocalhost.localhostresolves to IPv6::1first while the backend binds IPv4; Node's proxy can stall on the v6 attempt. Same class of "slow then works" hang, worse under WSL2 mirrored mode (wsl-dev-networking). .envmust actually be loaded. The server readsprocess.envonly; the dev/start scripts load the file via Node's--env-file-if-exists=.env. An emptyJWT_SECRET=makes the server fail-fast at boot.- Seed into the DB the server reads.
seed:adminand the server must use the sameDATABASE_URL; running viapnpm seed:admin(which loadsapps/server/.env) keeps them aligned.
Useful one-offs
- First admin:
pnpm seed:admin(prompts; blank username →admin). Non-interactive:ADMIN_USER=.. ADMIN_PASS=.. pnpm seed:admin. Reset a password: addFORCE=1. - Hardware test scripts (UHPPOTE):
apps/server/scripts/uhppote-listen.mjs(live events),uhppote-relay.mjs(guarded door-open). See uhppote-controller.
Database reset — training / demo only (2026-06-30)
A site is sometimes run live to train operators/admins on the real app; afterwards the demo data
must go without leaving an obvious self-serve button (an operator must not be able to wipe history).
So the reset is a CLI script, not UI: packages/db/scripts/reset-db.mjs, run via pnpm db:reset.
RESET_ALLOWED=1 pnpm db:reset --financial # default DB = apps/server/parking.sqlite
RESET_ALLOWED=1 DATABASE_URL=/path node packages/db/scripts/reset-db.mjs --all
Category flags (combinable; ≥1 required) — grounded in which tables hold what:
| Flag | Wipes | Keeps |
|---|---|---|
--financial |
ledger_events (entry/exit/payment/void/shift/cash/anomaly), device_events, snapshots, subscription instances + credentials/plates, blocklist |
users, devices, config, tariffs, subscription plans |
--config |
site_config, devices, setup_state (→ re-runs first-run setup), tariffs + versions, subscription plans |
everything else |
--users |
users, roles, role_permissions, auth sessions |
everything else |
--all |
every table (blank slate) | — |
⚠
--financial/--allTRUNCATE the append-only, signed append-only-event-chain. That is the anti-fraud record; a partial delete would break the hash chain, so a financial reset wipes the whole ledger back to empty (re-seeding starts a NEW chain under the sameEVENT_SIGNING_KEY— the key is not touched). This is the opposite of how the ledger is meant to behave, hence the gates below. It is a training/demo tool; never point it at a live booth.
Two safety gates (threat-model):
RESET_ALLOWED=1env must be set — a real booth never sets it, so the command is inert in production even if typed.- Typed confirmation of the DB filename (interactive).
--yesskips it for CI/scripted training setup only.
Runs as a single transaction (all-or-nothing) + VACUUM to shrink the re-used demo DB. After
--users/--all (users cleared), re-seed an admin: pnpm seed:admin. The EVENT_SIGNING_KEY and
BACKUP_KEY are intentionally left alone (see backup-recovery on key custody).
On the BOOTH there is no
pnpm— only Docker containers.pnpm db:resetis the dev form; on an appliance, run the same script viadocker execinto theservercontainer (node node_modules/@parking/db/scripts/reset-db.mjs …,DATABASE_URL=/data/parking.sqlite). Full booth procedure: appliance-provisioning §7d.