// Tiny fetch helper used by app.jsx for every HTTP call. // Reads base URL + token from window globals injected by index.html. // // Errors: throws on non-2xx. Callers can .catch() to show a friendly // failure (e.g. the undo bar's "Couldn't send that" copy). // // 401 special case: with indefinite-lifetime JWTs, the only realistic // way to get a 401 is operator-side action (server-side jwt_secret // rotation, account revoked from tenant, manual localStorage wipe). In // all those cases the right UX is "drop the dead token and re-route // through Google". We do that here so every caller doesn't have to // reimplement it. The fetch still throws so in-flight requests fail // cleanly — but the redirect kicks off before the .catch fires, so // callers won't see anything render. async function kxLogout({ redirect = true } = {}) { try { localStorage.removeItem("kx_token"); localStorage.removeItem("kx_jwt"); } catch (_) { /* private mode / disabled storage — ignore */ } window.KX_TOKEN = null; if (redirect) { // Send the operator back through Google so they get a fresh token. // /auth/google/start sets the OAuth state cookie and 302s to Google; // the callback redirects back to the PWA root with ?token=... which // the bootstrap in index.html stores + strips from the URL. window.location.href = window.KX_API_BASE.replace(/\/api\/v1$/, '') + "/auth/google/start"; } } window.kxLogout = kxLogout; async function kxApi(path, opts = {}) { // FormData uploads must let the browser set the Content-Type // (multipart boundary). Only force JSON for non-FormData bodies. const isFormData = typeof FormData !== 'undefined' && opts.body instanceof FormData; const headers = { ...(isFormData ? {} : { "Content-Type": "application/json" }), ...(opts.headers || {}), }; if (window.KX_TOKEN) { headers["Authorization"] = "Bearer " + window.KX_TOKEN; } const res = await fetch(window.KX_API_BASE + path, { ...opts, headers }); if (res.status === 401 && window.KX_TOKEN) { // Token is dead. Don't silently render mock data — clear it and // re-route through Google. Throw so the in-flight call still // resolves to its catch branch (the redirect will arrive a tick // later; callers won't see anything render meanwhile). kxLogout({ redirect: true }); throw new Error("API 401 " + path + " — re-authenticating"); } if (!res.ok) throw new Error("API " + res.status + " " + path); // Some endpoints (e.g. /pause action acks) return empty body — guard. const ctype = res.headers.get("content-type") || ""; if (!ctype.includes("application/json")) return null; return res.json(); } window.kxApi = kxApi;