22544ecf63
button-light-indicator: failure backoff + rate-limited logging rationale; app-logs: storm coalescing invariant + --diagnostics wipe; local-dev-workflow and appliance-provisioning §7d: new reset flag table + drift guard; log entry tying all three layers to the ENETUNREACH incident. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
104 lines
5.7 KiB
Markdown
104 lines
5.7 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 + **drafts**, subscription plans | everything else |
|
|
| `--users` | `users`, `roles`, `role_permissions`, auth `sessions` | everything else |
|
|
| `--diagnostics` | `app_logs` (the unsigned [[app-logs]] store behind `/setup/logs`) | everything else |
|
|
| `--all` | every table (blank slate) | — |
|
|
|
|
**Drift guard** (2026-07-08): before doing anything, the script compares the category union against
|
|
`sqlite_master` and **refuses if any table is uncategorized** — `app_logs` and `tariff_drafts` had
|
|
silently survived every reset (including `--all`) because the hand-maintained table list lagged the
|
|
schema. A new table now forces a deliberate one-line categorization decision.
|
|
|
|
> **⚠ `--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.
|