/* ============================================================
app.jsx — LIVE PWA shell. Fetches /api/dashboard (the window.OCC
contract computed by the backend), gates behind the Argon2id
session, and shows a data-source banner (sample / live / building).
The design-time demo-scenario switcher and tweaks panel are removed:
the dashboard now reflects real computed values only.
Screens (Home/Recovery/Sleep/Trends/Clinical) and primitives are
loaded unchanged from the design files.
============================================================ */
(function () {
const { useState, useEffect, useCallback } = React;
const ACCENT = { good: 132, attn: 352, acute: 18 }; // fixed "Sage / Plum"
function api(path, opts = {}) {
return fetch(path, Object.assign(
{ credentials: "same-origin", headers: { "Content-Type": "application/json" } }, opts));
}
// ---- device chrome (unchanged from the design) ----------
function StatusBar() {
return (
6:42
);
}
function AppBar({ route, go, theme, toggleTheme, onLogout }) {
const detail = route.screen === "recovery" || route.screen === "sleep";
return (
{detail ? (
) : (
openclawclinic
)}
);
}
function TabBar({ route, go, alert }) {
const tabs = [["home", "Home", "home"], ["trends", "Trends", "trend"], ["clinical", "Clinical", "clinical"]];
const activeTab = ["recovery", "sleep"].includes(route.screen) ? "home" : route.screen;
return (
);
}
// ---- data-source banner (sample / live / building) ------
function SourceBanner({ ds }) {
if (!ds) return null;
const tone = ds.mode === "live" ? "var(--good)" : ds.mode === "building" ? "var(--attn)" : "var(--ink-3)";
const label = { live: "LIVE", building: `BASELINE BUILDING · ${ds.nights_valid}/${ds.baseline_target}`,
sample: "SAMPLE DATA", none: "NO DATA" }[ds.mode] || ds.mode;
return (
{label}
{ds.note}
);
}
// ---- login (styled with tokens) -------------------------
function Login({ onLogin }) {
const [u, setU] = useState(""); const [p, setP] = useState("");
const [err, setErr] = useState(null); const [busy, setBusy] = useState(false);
const submit = async (e) => {
e.preventDefault(); setBusy(true); setErr(null);
try {
const r = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ username: u, password: p }) });
if (r.ok) { onLogin(); return; }
const d = await r.json().catch(() => ({}));
setErr(r.status === 429 ? "Too many attempts — locked out briefly." : (d.detail || "Invalid credentials"));
} catch (e) { setErr("Network error"); } finally { setBusy(false); }
};
return (
);
}
const Center = ({ children }) => (
{children}
);
function App() {
const [theme, setTheme] = useState(() => localStorage.getItem("occ-theme") || "light");
const [authed, setAuthed] = useState(null); // null=checking
const [dash, setDash] = useState(null);
const [err, setErr] = useState(null);
const [route, setRoute] = useState({ screen: "home", param: null });
useEffect(() => { document.documentElement.dataset.theme = theme; localStorage.setItem("occ-theme", theme); }, [theme]);
useEffect(() => {
const r = document.documentElement.style;
r.setProperty("--good-h", ACCENT.good); r.setProperty("--attn-h", ACCENT.attn); r.setProperty("--acute-h", ACCENT.acute);
}, []);
const loadDash = useCallback(async () => {
try {
const r = await api("/api/dashboard");
if (r.status === 401) { setAuthed(false); return; }
setDash(await r.json());
} catch (e) { setErr(String(e)); }
}, []);
useEffect(() => {
(async () => {
try { const r = await api("/api/auth/me"); if (r.ok) { setAuthed(true); loadDash(); } else setAuthed(false); }
catch (e) { setAuthed(false); }
})();
}, [loadDash]);
const go = (screen, param = null) => { setRoute({ screen, param }); document.querySelector(".screen")?.scrollTo(0, 0); };
const logout = async () => { await api("/api/auth/logout", { method: "POST" }); setAuthed(false); setDash(null); setRoute({ screen: "home", param: null }); };
const toggleTheme = () => setTheme((x) => x === "dark" ? "light" : "dark");
const frame = (children) => (
);
if (authed === null) return frame(<>Loading…>);
if (authed === false) return frame(<> { setAuthed(true); loadDash(); }} />>);
if (err) return frame(<>Couldn’t load data.
{err}>);
if (!dash) return frame(<>Loading your data…>);
if (!dash.available) {
return frame(<>
No data yet
{dash.data_source?.note}
>);
}
// expose the live contract for screens that read globals
window.OCC = { trends: dash.trends, clinicalFor: () => dash.clinical };
const data = dash.scenario;
const alert = (data.acute || []).length ? "acute" : (data.subacute || []).length ? "attn" : null;
let body;
if (route.screen === "home") body = ;
else if (route.screen === "recovery") body = ;
else if (route.screen === "sleep") body = ;
else if (route.screen === "trends") body = ;
else if (route.screen === "clinical") body = ;
return (
);
}
// small style helpers
function btn() { return { display: "flex", alignItems: "center", gap: 7, background: "none", border: "none", color: "var(--ink)", padding: 0 }; }
function iconBtn() { return { width: 34, height: 34, borderRadius: 999, border: "1px solid var(--line)", background: "var(--surface)", display: "grid", placeItems: "center" }; }
function inp() { return { padding: "11px 13px", borderRadius: "var(--r-sm, 10px)", border: "1px solid var(--line)", background: "var(--surface)", color: "var(--ink)", fontSize: 14, fontFamily: "var(--font-ui)" }; }
window.OCCApp = App;
})();