Files
parking_solution/wiki/decisions/open-questions.md
T
julian d5e41500a8
Build desktop / desktop (push) Successful in 4m42s
Build & push images / images (push) Successful in 2m53s
CI / check (push) Successful in 39s
feat(backup): admin UI with admin-chosen target directory
The backup destination is now chosen by the on-site admin in the UI (Setup ->
Backup), not a server env var. An env-pinned target defeats the purpose: the admin
can't point backups at a freshly-plugged USB or a NAS mount without editing .env
and restarting. The encryption key stays a server secret.

Target storage:
- New site_config.backup_target_dir (migration 0016, nullable; null = not
  configured). BackupService reads it fresh each run, so a UI change takes effect
  with no restart. Only BACKUP_KEY stays env -- a key must never live in the DB it
  backs up.

Routes:
- PUT /api/backup/config  -- set/clear the target (backup:update; upserts id=1).
- POST /api/backup/test   -- probe a candidate path server-side (exists / is a
  directory / writable) so the admin gets feedback before relying on it.
- status() now exposes targetDir + keyPresent, so the UI distinguishes
  'no target set' from 'BACKUP_KEY missing'.

UI (apps/web/src/BackupSettings.tsx):
- A Setup -> Backup tab (gated backup:read): an editable target-path field with a
  Test-target probe (localized ok/missing/not-a-dir/not-writable), Save, the status
  panel (config state, last-run size/pruned/error, a distinct amber missing-key
  warning), a Back up now button, and the restore-is-out-of-band note. Full i18n
  (sq + en); nav.backup.
- API client: fetchBackupStatus / setBackupTarget / testBackupTarget / runBackup.

Also includes a small in-progress copy trim to the setup-intro i18n strings.

Verified live with Playwright: typed a path -> Test reported writable -> Save
persisted it -> status reflected it and showed the key-missing warning. Whole
monorepo build/lint/test green. Wiki backup-recovery + open-question #5 updated.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-29 12:21:26 +02:00

122 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: decision
tags: [parking, decisions, open]
sources: [parking-system-architecture]
updated: 2026-06-29
status: open
---
# Open Questions / Next Steps
**Not yet decided** (or decided-but-not-yet-built), and they drive everything else — settle before
procurement. (See [[parking-system-architecture]] §10.)
1. **Lane topology.** One host per lane, or one central host driving networked devices in each
lane? Decides how many controllers, printers, UPSs, and [[sqlite]] instances exist, and the
failure blast radius. (A single central host is a single point of failure for *all* lanes.)
2. **Failure modes.** Define per direction what happens to barriers on host/power/network loss —
particularly **fail-open on exit** for egress safety. Currently unaddressed. See
[[fail-state-safety]].
3. **Payment subsystem.** Manned booth (P2PE terminal + cash drawer) vs unmanned pay station;
confirm **PCI scope is kept out of the application** via a standalone certified terminal
(see [[bom]]).
4. **Reconciliation channel.** Even if "offline," establish *some* periodic path (USB, hotspot,
manager visit) to reconcile the signed log against an external authority — the real anti-fraud
control. See [[reconciliation]].
5. **Durability / backup.** _(Design SETTLED + engine/target/UI BUILT 2026-06-29 — see
[[backup-recovery]]; engine + admin-configured target (site_config, migration 0016) + daily timer +
manual route + admin UI (target field, Test-target probe, status, Back-up-now) done; SFTP +
restore-tooling pending. The target dir is admin-chosen in the UI, not env; only BACKUP_KEY is a
server secret.)_ A disk failure / stolen-or-destroyed PC currently leaves **total revenue-history
loss**.
Settled design: an **admin-driven encrypted full-DB backup** (online-backup/`VACUUM INTO`, snapshots
included) to a **local/USB · SMB/NFS · SFTP** target, **manual button + in-process daily timer**,
**keep-last-N + dailies** retention, encrypted with a **dedicated `park_buzi_backup_key`** (separate
Komodo secret, *not* the signing key). Recovery = backup file **+** the two escrowed keys held
out-of-band; a restored copy must still `verifyChain`. **Key-custody stance:** `EVENT_SIGNING_KEY`
stays **decoupled from the TPM** (an extractable, escrowed software key) precisely so it survives
total hardware loss — the conscious trade against #6 (a TPM-sealed signing key would be unforgeable
but **unverifiable after the machine dies**). **Restore is admin-only/out-of-band** (operator-adversary
surface — [[threat-model]]). See [[backup-recovery]], [[fleet-deployment-komodo]], [[disk-os-hardening]],
[[reconciliation]] (#4).
6. **Secure-element integration.** Confirm [[atecc608]] wiring/usage on the host (event
signing). The [[esp32-custom-controller]] command-authentication use is **deferred — not
being implemented for now** (access control is the [[dingtian-relay]] behind
[[network-isolation]]); revisit only if prevention-grade device auth becomes a requirement.
7. **JWT signing: symmetric vs. asymmetric key.** _(Raised by the commit security review, not the
source doc.)_ Auth currently uses a symmetric HMAC secret (`@fastify/jwt`, see
[[local-jwt-auth]]) — the same secret signs *and* verifies, so it must live on every host that
validates tokens. Consider rotating to an **asymmetric key (RS256 / EdDSA)** so the server
holds only the **public key** to verify; the private signing key can then live in the
[[atecc608]] or a key-management step. This mirrors the "store only the public key" property
already used for [[atecc608]] event signing and the [[challenge-response-auth]] scheme —
compromising a verifying host yields nothing that can forge a token. Decide before
multi-host / multi-lane deployment (see #1 lane topology), since that's when shared-secret
distribution becomes the liability.
8. **Exchange-rate (FX) system.** _(Raised by the [[tariff]] design, 2026-06-15.)_ Currency is
selectable per tariff version and the money model is FX-ready (`payment` stores currency + a
reserved `fxRate`), but **no conversion is built**. If multi-currency pricing/charging is ever
needed, it requires an **offline** rate source (rates can't depend on the network —
[[offline-first]]), a base currency, and a rounding policy. Deferred; nothing blocks adding it
later without migrating stored amounts.
9. **Pay-station money corners — receipts & refunds/change.** _(Raised by the scope sweep,
2026-06-15; deferred until pay-station hardware is chosen.)_ Not yet designed: **receipts / VAT
invoices** (fiscal receipt with tax number + sequential numbering may be legally required — could
change what the `payment` event must store) and **refunds / overpayment / change** (cash change,
"exact change only", a refund as a signed reversal event). Both depend on the unmanned-vs-manned
payment subsystem (#3) and the note/coin/card acceptor hardware. Revisit at procurement.
10. **Snapshot retention.** _(Raised by the [[entry-exit-points]] camera-snapshot build, 2026-06-16.)_
Entry/exit snapshots are stored as BLOBs in the [[sqlite]] `snapshots` table. This grows the
single DB file fast (~100–300 KB per image × every entry **and** exit), and SQLite doesn't
reclaim deleted-blob pages without `VACUUM`. **Undecided:** pruning policy (age-based vs.
total-size cap), VACUUM cadence, and how this interacts with the #5 backup strategy (blobs
bloat every backup). Until decided, snapshots accumulate unbounded. See [[entry-exit-points]].
_(Update 2026-06-29: pruning is now disk-pressure based — see [[entry-exit-points]]; and the
settled #5 backup includes snapshot BLOBs by default, with a noted future "exclude snapshots"
toggle since they dominate backup size — see [[backup-recovery]].)_
11. **Appliance OS image → WebKitGTK version (Tauri dependency).** _(Raised by
[[desktop-shell-tauri]], 2026-06-21; narrowed same day.)_ The chosen
[[desktop-shell-tauri|Tauri v2 desktop shell]] renders through the **host's WebKitGTK**, not a
bundled browser. The risk reduces to which OS the appliance runs:
- **Best case — Ubuntu 26.04 LTS desktop (intended):** ships a current, distro-maintained
WebKitGTK → this question is **effectively resolved**; just confirm the built SPA renders on
the actual image and pin it.
- **Worst case — Windows 11 + WSL + Docker:** this **conflicts with the standing platform
decision** (Linux appliance, *not* Windows/WSL — see [[standing-decisions]],
[[disk-os-hardening]]) and a GUI shell doesn't live inside headless WSL/Docker. Fallback is
**no native shell — a kiosk browser** at the local [[fastify]]-served SPA (Electron only if a
standalone Windows installer is mandated). See [[desktop-shell-tauri]] for the decision table.
Close this once the appliance OS image is fixed and the SPA is verified against its WebView.
(Ties to #1 lane topology / image standardization.)
12. **TPM 2.0 hardening — implementation (to build).** _(Recorded 2026-06-21; analysis in [[tpm]].)_
On the Ubuntu 26.04 LTS appliance, harden using the host **TPM**: (a) **sealed-LUKS auto-unlock**
(`systemd-cryptenroll --tpm2-device`) so the encrypted disk auto-unlocks only on an untampered
boot → unattended reboot after power loss; (b) optionally hold the **non-extractable host
event-signing key** in the TPM (a new `Signer` impl — no `EventLog` change; mirrors the
[[atecc608]] swap), defeating the offline pull-the-disk-and-re-sign attack. **Must include:**
require **TPM 2.0** (reject 1.2), **prefer fTPM** + a per-op **PIN/auth policy** (not PCR-only —
bus-sniff), a **LUKS recovery passphrase**, and a **re-seal-on-update runbook** (kernel/GRUB/BIOS
updates change the PCRs and lock the disk). TPM **complements, never replaces**, [[reconciliation]];
it does nothing against a rooted live host or the operator. Moot in the Windows + WSL fallback. See
[[tpm]], [[disk-os-hardening]]; relates to #6 (host secure-element by platform) and #13.
13. **Startup chain-integrity self-check (to build).** _(Raised by the pull-the-disk trace,
2026-06-21.)_ `verifyChain()` exists and pinpoints any tamper, but **nothing invokes it on
boot** — a tampered DB loads and serves normally (detectable but undetected). Wire a **startup
self-check** that runs `verifyChain()` and, on a break, **flags degraded state / writes a signed
`anomaly` + alarms** (surfaced to the booth footer / next reconciliation). Open: refuse-to-serve
vs. serve-degraded — lean **serve-degraded + loud alarm** (fail-open on exit still governs;
refusing to boot could strand a lane). Software-only, independent of the TPM/[[atecc608]] hardware.
See [[append-only-event-chain]].
14. **Printer USB transport — confirm the on-site printer + bake the provisioning.** _(Recorded
2026-06-24; the transport code is built — see [[printer-usb-transport]].)_ The ESC/POS drivers
now drive **TCP (port 9100) OR local USB (`/dev/usb/lp0`)** behind one render layer, selectable
per device. **Open:** is the actual booth printer USB or network? (The site's verified units are
*networked* — Cashino `10.0.10.9`, Rongta `10.0.10.10` — so USB may be unused here; the original
BOM listed "Epson TM / Citizen (USB **or** network)", so a future site may need it.) If USB is
used, the **appliance image** must (a) load/keep the **`usblp`** kernel module bound to the
printer (CUPS can claim the interface first), and (b) ship a **udev rule** giving the non-root
server process write access to `/dev/usb/lp*`. Both are [[appliance-provisioning]] steps, **not
app code**, and are **unverified on hardware**. Close this once the printer transport per site is
fixed and (if USB) the udev/usblp rule is in the image and a real USB print is verified. Relates
to #1 (lane topology / image standardization). See [[printer-usb-transport]], [[rongta-printer]].