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
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
type: concept
|
||||
tags: [parking, domain, booth, exit, payment, threat-model]
|
||||
sources: []
|
||||
updated: 2026-06-18
|
||||
updated: 2026-06-30
|
||||
status: open
|
||||
---
|
||||
|
||||
@@ -162,6 +162,22 @@ server-side in `reopenBarrier`: allow only when `subscription` OR (`paidAt != nu
|
||||
paidAt + graceExitMin`). A future reason-required *force exit* for genuine disputes (car already gone)
|
||||
would be a separately-audited path — see Open.
|
||||
|
||||
> **Open-barrier moved INTO the modal — the inline row button is gone (2026-06-30).** The audited
|
||||
> re-pulse was previously an inline button on the paid-in-grace Active Sessions *row*. It was removed:
|
||||
> clicking any row now opens the modal, which carries the Open-barrier action. Why: a paid-and-exited
|
||||
> session is `open=false`, so clicking its row used to dead-end on *"This session is already closed"* —
|
||||
> useless for the very case (paid, barrier didn't confirm) where the operator needs to re-pulse. The
|
||||
> modal now recognizes a **closed-within-grace** transient (`found && !open && withinGrace`) and renders
|
||||
> the session view + **Open barrier** instead of the dead-end notice. The server guard is unchanged
|
||||
> (`reopenBarrier` already handled the closed-but-in-grace case — the T-397815c0 fix above). The
|
||||
> Active Sessions list distinguishes these rows with a **live grace-remaining countdown** badge
|
||||
> (`exited · M:SS`, ticking each second off `graceExpiresAt`) instead of a static "exited" label.
|
||||
> Settled amounts now show the **actual sum paid** (new `SessionLookup.paidMinor`, summed across
|
||||
> payments) rather than a flat "PAID" badge. And a **fully-closed (grace-expired) session** is no longer
|
||||
> a pure dead-end: its modal shows a read-only **review view** — figures + paid amount + the entry/exit
|
||||
> [[entry-exit-points#camera-snapshots-evidence-not-a-gate|snapshot strip]] — so an operator can review
|
||||
> evidence for a car that just left (disputes/audits), with no pay/exit/open controls.
|
||||
|
||||
### Subscription occurrences in the booth (built 2026-06-18)
|
||||
|
||||
A subscriber's car shows in Active Sessions as a **subscription** session (badge "abonim"; labelled by
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: concept
|
||||
tags: [parking, architecture, devices, setup]
|
||||
sources: []
|
||||
updated: 2026-06-16
|
||||
updated: 2026-06-30
|
||||
---
|
||||
|
||||
# Entry / Exit Points (pool-of-spaces model)
|
||||
@@ -114,6 +114,21 @@ re-encode is **storage-only** — ANPR recognition runs on the **original full-r
|
||||
(downscaling hurts OCR). Fail-soft: a re-encode error stores the original, never drops the snapshot
|
||||
(`snapshot.ts` `encodeForStorage`).
|
||||
|
||||
> **Content-type bug — every legacy snapshot rendered blank (fixed 2026-06-30).** Symptom: *no*
|
||||
> snapshot showed in the booth modal. Root cause: some cameras (Hikvision) return
|
||||
> `Content-Type: image/jpeg; charset="UTF-8"` — a charset param on a binary body is **malformed**, and
|
||||
> browsers refuse to decode an `<img>` declared that way. Old capture code persisted that raw header
|
||||
> into `snapshots.content_type` (100 of 101 rows in the dev DB), and the serve route
|
||||
> (`GET /api/snapshots/:id`) re-emitted it **verbatim** → broken render for every legacy row. The
|
||||
> capture path was *already* hardened (`encodeForStorage` re-encodes to a clean `image/jpeg`; its
|
||||
> fail-soft branch calls `cleanType` to strip `; charset=…`), so NEW rows were fine — but the serve
|
||||
> route trusted the stored value. Fix: the route now also runs `cleanType(row.contentType)` on the way
|
||||
> out (a bare `image/jpeg`), which un-breaks all legacy rows with **no data migration**. Verified: a
|
||||
> previously-unrenderable 2560×1440 row now decodes in-browser. Lesson: **normalize a camera-supplied
|
||||
> content-type both on capture AND on serve** — a stored value from an untrusted device is itself input.
|
||||
> The stored `content_type` column could be backfilled to `image/jpeg` for cleanliness, but serving
|
||||
> normalizes so it isn't required.
|
||||
|
||||
**Retention (2026-06-28, resolves the old open question) — DISK-PRESSURE safety valve.** Snapshots
|
||||
are unsigned/advisory, so they prune freely. The day-to-day shrink is the re-encode above; pruning is
|
||||
a backstop that only fires under real disk pressure. A **daily** check (`snapshot-retention.ts`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: reference
|
||||
tags: [parking, dev-environment, workflow]
|
||||
sources: []
|
||||
updated: 2026-06-15
|
||||
updated: 2026-06-30
|
||||
---
|
||||
|
||||
# Local Dev Workflow
|
||||
@@ -54,3 +54,44 @@ Production uses an **nginx** reverse proxy (`deploy/nginx.conf`) for the same sa
|
||||
`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.
|
||||
|
||||
Reference in New Issue
Block a user