// Desktop-only: the operator-configured backend origin (host:port of the // Fastify server this install talks to), persisted across restarts. // // The desktop shell is a generic .deb/.AppImage — it is NOT built for one // specific booth, so the backend address can't be baked in at build time // (that was the old VITE_API_BASE approach; a rebuild was needed to point the // same installer at a different appliance). Instead the operator enters it // once in the ConnectScreen (shown before login whenever nothing usable is // stored yet) and it's saved to a JSON file in the OS config dir via // tauri-plugin-store, read back on every launch before any API call. // // Browser build: this module is never reached (inTauri() gates every call // site — see origin.ts), so there is no browser equivalent or fallback here. import type { Store } from "@tauri-apps/plugin-store"; const STORE_FILE = "backend-config.json"; const KEY = "backendUrl"; let storeHandle: Store | null = null; async function getStore(): Promise { if (!storeHandle) { const { load } = await import("@tauri-apps/plugin-store"); storeHandle = await load(STORE_FILE, { autoSave: true }); } return storeHandle; } /** The saved backend origin (no trailing slash), or null if never configured. * Desktop only — throws if called from a browser build. */ export async function loadBackendUrl(): Promise { const store = await getStore(); const v = await store.get(KEY); return typeof v === "string" && v.length > 0 ? v.replace(/\/$/, "") : null; } /** Persist a new backend origin (validated + reachable — call testBackendUrl * first). Takes effect immediately for future platformFetch/wsUrl calls. */ export async function saveBackendUrl(url: string): Promise { const store = await getStore(); await store.set(KEY, url.replace(/\/$/, "")); await store.save(); } /** Clear the saved backend (forces the ConnectScreen back up next launch). */ export async function clearBackendUrl(): Promise { const store = await getStore(); await store.delete(KEY); await store.save(); } export interface BackendCheck { ok: boolean; /** "unreachable" (network/DNS/refused) | "bad_response" (reachable, not our API). */ reason?: "unreachable" | "bad_response"; detail?: string; } /** Probe a candidate origin via GET /health — the server's one unauthenticated * route (server.ts), which answers `{status:"ok", app:"parking-system"}`. We * require BOTH a 2xx and that `app` value: the previous probe hit an * auth-guarded route and accepted 401/403 as "ours", which any password- * protected service on the LAN would also have passed. Uses the same * tauri-plugin-http path platformFetch does (raw fetch from the webview can't * reach an arbitrary LAN host — mixed content, see origin.ts). */ export async function testBackendUrl(url: string): Promise { const origin = url.replace(/\/$/, ""); try { const { fetch: tauriFetch } = await import("@tauri-apps/plugin-http"); const res = await tauriFetch(`${origin}/health`, { method: "GET", signal: AbortSignal.timeout(5000), }); if (!res.ok) { return { ok: false, reason: "bad_response", detail: `HTTP ${res.status}` }; } const body = (await res.json().catch(() => null)) as { app?: unknown } | null; if (body?.app !== "parking-system") { return { ok: false, reason: "bad_response", detail: "unexpected /health body" }; } return { ok: true }; } catch (err) { return { ok: false, reason: "unreachable", detail: err instanceof Error ? err.message : String(err), }; } }