Files
parking_solution/wiki/concepts/local-dev-workflow.md
T
julian 266e9b0027
Build desktop / desktop (push) Successful in 4m34s
Build & push images / images (push) Successful in 3m1s
CI / check (push) Successful in 40s
docs(wiki): record session findings — snapshot fix, booth rework, db reset
- 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
2026-06-30 17:58:43 +02:00

98 lines
5.2 KiB
Markdown

---
type: reference
tags: [parking, dev-environment, workflow]
sources: []
updated: 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
```bash
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
```bash
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|cookie 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 `.js` import specifiers to `.ts`, so it crashed with `ERR_MODULE_NOT_FOUND` and
silently never started — the symptom was the SPA hanging for *minutes* (the Vite proxy waiting
on a dead backend), then finally erroring. The `dev` script uses **`tsx watch`** instead.
- **Vite proxy → `127.0.0.1`, not `localhost`.** `localhost` resolves to IPv6 `::1` first 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]]).
- **`.env` must actually be loaded.** The server reads `process.env` only; the dev/start scripts
load the file via Node's `--env-file-if-exists=.env`. An empty `JWT_SECRET=` makes the server
fail-fast at boot.
- **Seed into the DB the server reads.** `seed:admin` and the server must use the same
`DATABASE_URL`; running via `pnpm seed:admin` (which loads `apps/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: add `FORCE=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`.
```bash
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`/`--all` TRUNCATE the append-only, signed [[append-only-event-chain|ledger]].**
> 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 **same**
> `EVENT_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|operator-as-adversary]]):**
1. **`RESET_ALLOWED=1`** env must be set — a real booth never sets it, so the command is inert in
production even if typed.
2. **Typed confirmation** of the DB filename (interactive). `--yes` skips 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:reset` is the *dev* form;
> on an appliance, run the same script via `docker exec` into the `server` container
> (`node node_modules/@parking/db/scripts/reset-db.mjs …`, `DATABASE_URL=/data/parking.sqlite`).
> Full booth procedure: [[appliance-provisioning]] §7d.