The v0.1.4 Origin fix cleared only the first of two gates in /api/ws's preHandler. The second, req.jwtVerify(), reads the HttpOnly cookie — which tauri-plugin-websocket (a bare tungstenite client, no cookie jar) can never send. Every desktop handshake 401'd and use-live-feed reconnected every 10s (confirmed in the park-2 server log). - routes/ws.ts: POST /api/ws/ticket (cookie + CSRF auth) mints a 30s, single-use, in-memory ticket; the WS preHandler accepts it via an x-ws-ticket header after the Origin check, then the same report:read role check. Browser cookie path unchanged; JWT stays out of JS. - platform-ws.ts: fetch a ticket before connect, send it with the Origin header; connect failures now go through logClient (rate-limited). - logger.ts: flush read the CSRF token from document.cookie, null on desktop, so every desktop POST /api/logs 403'd and was dropped silently — no desktop client log had ever reached app_logs. Stash moved to a dependency-free lib/desktop-csrf.ts shared by api.ts and logger.ts. - backend-config.ts: ConnectScreen probe uses the unauthenticated /health (now also returns app: "parking-system") instead of accepting any 401. - README: local-AppImage release gate — tauri dev runs at http://localhost:5173, not tauri://localhost, so none of these origin-dependent bugs reproduce there. - wiki: new section + log entry; four citation corrections. Requires the server image with this commit deployed before the new desktop build connects (the ticket endpoint must exist). Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
4.0 KiB
@parking/desktop — Tauri v2 kiosk shell
A thin native desktop window over the @parking/web SPA. It contains no UI and no business
logic of its own: the window renders the same web app the browser does, so the desktop and the
browser stay identical and never drift. Device/auth/ledger logic stays in @parking/server. See
wiki/decisions/desktop-shell-tauri.md.
How the "same look & functionality" guarantee works
| Source of the UI | |
|---|---|
Dev (tauri dev) |
the window loads http://localhost:5173 — the @parking/web Vite dev server. Edit a component in apps/web → HMR updates the desktop window live. |
Prod (tauri build) |
the window bundles apps/web's built dist/. beforeBuildCommand rebuilds the SPA first. |
There is only one UI codebase (apps/web); this package just wraps it.
Backend connection
The SPA talks to Fastify over HTTP/WS. In a browser that's same-origin (relative /api). In the
desktop build the bundled assets load from tauri://localhost, so set VITE_API_BASE (read at
web build time — see .env.example) to the appliance's Fastify origin, e.g.
http://127.0.0.1:3000. The CSP connect-src in tauri.conf.json is already allowed for that
origin, and the backend must include the Tauri origin in WS_ALLOWED_ORIGINS for the live feed.
Commands
pnpm --filter @parking/desktop dev # native window over the web dev server (HMR)
pnpm --filter @parking/desktop bundle # build the SPA + bundle the desktop app (.deb/.rpm/.AppImage)
buildis a no-op in this package soturbo run buildstays fast — the real desktop bundle (compiles Rust, minutes long) is the explicitbundlescript above.
Requires the Rust toolchain and (on Linux) WebKitGTK 4.1 + libsoup-3 dev libraries. Under WSL2 the window needs a display (WSLg or an X server).
Auto-update
Signed updates are built and published by .gitea/workflows/release.yml on a vX.Y.Z tag, mirrored
to the public mca/public_releases repo (this repo is private; the updater runs on offline-first
field appliances with no Gitea credentials, so its endpoint must be reachable unauthenticated —
see that workflow's header and wiki/decisions/desktop-shell-tauri.md). The updater config and
signing pubkey live in tauri.conf.json; the private signing key is held outside the repo, never
committed.
Release gate — run the REAL bundle locally before tagging
tauri dev loads the SPA from http://localhost:5173, a plain http origin. The shipped bundle
loads it from tauri://localhost, a secure custom-scheme origin — and every desktop-only bug
found in the field on 2026-09-03/04 (relative-URL DOMException, mixed content, missing WS
Origin, the reqwest-vs-webview cookie split, the WS handshake that can't carry the cookie)
depends on that difference. Dev mode cannot reproduce any of them, so "works in tauri dev"
carries no information about a release. Before pushing a vX.Y.Z tag:
pnpm --filter @parking/server dev(local backend;.envmust haveCOOKIE_SECURE=0andtauri://localhostinWS_ALLOWED_ORIGINS).pnpm --filter @parking/desktop bundleand run the produced AppImage fromsrc-tauri/target/release/bundle/appimage/(WSLg is enough).- On the ConnectScreen enter
127.0.0.1:3000, Test must say reachable, then Save. - Log in. The booth header must show LIVE (not "JASHTË LINJË") within a few seconds.
- Perform one mutation (e.g. change your UI language) — it must succeed (proves CSRF).
- Open Setup → Logs and confirm a
frontend-sourced row from this desktop session exists (proves the desktop log channel; historically it was silently 403'd).
Only then tag. If a release still fails in the field, the gap is in this list — fix the list.
Not here (deliberately)
Kiosk lockdown (fullscreen/no-decorations) and launching Fastify from the shell are out of scope for the scaffold — on the appliance Fastify runs as its own service and this shell connects to it.