/* ============================================================ screens-wearables.jsx — Connected Wearables & Platforms. Fetches connection statuses from /api/wearables/connections and lists all available providers. If Withings scale is connected, it pulls and displays dynamic weight & body fat composition charts. ============================================================ */ (function () { const { useState, useEffect, useCallback } = React; // Render a simple SVG line chart for weight/body fat trends. function SimpleCompositionChart({ data, type, color, unit, w = 320, h = 130 }) { if (!data || data.length === 0) { return (
No data recorded in this period
); } const values = data.map(d => d.value); const minVal = Math.min(...values); const maxVal = Math.max(...values); const valRange = maxVal - minVal || 2; const lo = minVal - valRange * 0.15; const hi = maxVal + valRange * 0.15; const padL = 10, padR = 32, padT = 10, padB = 18; const x = (i) => padL + (i / Math.max(1, data.length - 1)) * (w - padL - padR); const y = (v) => h - padB - ((v - lo) / (hi - lo)) * (h - padT - padB); const pts = data.map((d, i) => `${x(i).toFixed(1)},${y(d.value).toFixed(1)}`).join(" "); const ticks = [lo, (lo + hi) / 2, hi]; return ( {ticks.map((t, i) => ( {t.toFixed(1)}{unit} ))} {data.map((d, i) => ( ))} ); } function Wearables() { const [connections, setConnections] = useState([]); const [providers, setProviders] = useState([]); const [bodyData, setBodyData] = useState(null); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const loadData = useCallback(async () => { setLoading(true); setErr(null); try { const [connRes, provRes] = await Promise.all([ fetch("/api/wearables/connections", { credentials: "same-origin" }), fetch("/api/wearables/providers", { credentials: "same-origin" }) ]); if (!connRes.ok || !provRes.ok) { throw new Error("Failed to fetch connection statuses."); } const connList = await connRes.json(); const provList = await provRes.json(); setConnections(connList); setProviders(provList); // Check if Withings scale is connected const isWithingsConnected = connList.some( c => c.provider === "withings" && c.status === "connected" ); if (isWithingsConnected) { const end = new Date().toISOString(); const start = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); const bodyRes = await fetch( `/api/wearables/body-composition?start_time=${start}&end_time=${end}`, { credentials: "same-origin" } ); if (bodyRes.ok) { setBodyData(await bodyRes.json()); } } } catch (e) { setErr(String(e)); } finally { setLoading(false); } }, []); useEffect(() => { loadData(); }, [loadData]); if (loading) { return (
Loading settings & connections…
); } if (err) { return (
Couldn't load connection state.
); } // Process Withings Weight & Body Fat timeseries let weightSeries = []; let fatSeries = []; let latestWeight = null; let latestFat = null; if (bodyData && bodyData.samples) { // Sort chronologically const sorted = [...bodyData.samples].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); weightSeries = sorted .filter(s => s.type === "weight") .map(s => ({ value: s.value, date: new Date(s.timestamp) })); fatSeries = sorted .filter(s => s.type === "body_fat_percentage") .map(s => ({ value: s.value, date: new Date(s.timestamp) })); if (weightSeries.length > 0) latestWeight = weightSeries[weightSeries.length - 1].value; if (fatSeries.length > 0) latestFat = fatSeries[fatSeries.length - 1].value; } return (
{/* Dynamic Scale Metrics: weight, fat percentage */} {weightSeries.length > 0 && (
Withings Smart Scale: Weight
)} {fatSeries.length > 0 && (
Withings Smart Scale: Body Fat
)} {/* Status list of active platforms */} Connected Services {providers.map((p) => { const conn = connections.find((c) => c.provider === p.id); const isConnected = conn && conn.status === "connected"; const tone = isConnected ? "var(--good)" : "var(--ink-4)"; const statusLabel = isConnected ? "CONNECTED" : "NOT CONNECTED"; return (
{p.name} {statusLabel}
{isConnected && conn.last_sync_at ? `Last synced: ${new Date(conn.last_sync_at).toLocaleString()}` : p.description}
); })}
{/* Informative connection footer */}
Connections are synced and managed through the secure Open Wearables core service.
); } Object.assign(window, { Wearables }); })();