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
Fixing VITE_API_BASE got login to build a correct absolute URL, but it still
failed with WebKit's generic "Load failed" — WebKitGTK treats tauri://localhost
as a secure origin, so http://127.0.0.1:3000 (and ws://) from inside it is
blocked as mixed content, a WebKit limitation CSP's connect-src can't override.
Added tauri-plugin-http (genuine fetch() drop-in, wired via a new
platformFetch() in origin.ts, used by api.ts + logger.ts) and
tauri-plugin-websocket (not a drop-in — adapted behind a native-WebSocket-
shaped interface in the new platform-ws.ts so use-live-feed.ts needed no
changes). Both route through Tauri's Rust side instead of the webview's own
fetch/WebSocket. Capabilities scoped to 127.0.0.1:3000/localhost:3000, matching
the existing CSP allowlist.
Add a third data stream (app_logs), distinct from the signed ledger and device
telemetry, for operational/diagnostic logs — an offline appliance has no Sentry to
ship to, so the host is the log store.
Backend: a pino stream tees warn/error/fatal into app_logs (info/debug stay
stdout-only) with no call-site change; the DB is built before Fastify so the logger
has its sink. Frontend (lib/logger.ts): ships failed API requests (minus 401 churn),
window.onerror, unhandledrejection, and a top-level React ErrorBoundary; console
warn/error forwarded only at debug/trace. Batched/throttled POST, sendBeacon on
pagehide, loop-safe (never logs the /api/logs call), best-effort everywhere.
POST /api/logs (any signed-in user, CSRF, tolerant) + GET /api/logs gated by a new
log:read permission (new `log` RBAC resource; admin holds it). Retention: pruned by
age + row cap, hourly + at startup. UI: a Logs screen under /setup (filter
level/source/since, expand to context+stack), sq+en. Migration 0009_app_logs.
Verified end-to-end via app.inject: login -> POST 204 -> GET 200 with the record;
backend warn/error persisted, info dropped; non-admin GET 403 / POST 204.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V