/* ============================================================
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.
Retry
);
}
// 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}
{ window.location.href = `/api/switch/wearables?next=/connections`; }}
className="occ-tap"
style={{
padding: "6px 12px",
borderRadius: 999,
border: "1px solid var(--line)",
background: "var(--surface)",
color: "var(--ink)",
fontSize: 11,
fontWeight: 600,
cursor: "pointer"
}}
>
{isConnected ? "Manage" : "Connect"}
);
})}
{/* Informative connection footer */}
Connections are synced and managed through the secure Open Wearables core service.
);
}
Object.assign(window, { Wearables });
})();