/* Eno.Health Console — Knowledge base: Wearables & Wearable Inputs. A Wearable Input (e.g. VO₂ Max) is a signal Eno can ingest. Each input is linked to the supported Wearables (devices / aggregators) that provide it, and becomes available to map onto Exposome factors. */ const W = window.ENO; const STREAM_MODES = { continuous: { label: "Continuous", tone: "ok" }, batched: { label: "Batched", tone: "info" }, manual: { label: "Manual", tone: "neutral" }, }; const streamMode = (k) => STREAM_MODES[k] || { label: k || "—", tone: "neutral" }; const hueFor = (k) => { let n = 0; for (const c of String(k)) n = (n * 31 + c.charCodeAt(0)) % 360; return n; }; function WearablesView() { const toast = useToast(); const [inputs, setInputs] = useState(() => W.SIGNAL_TYPES.map((s) => ({ ...s, sources: [...(s.sources || [])] }))); const [sources] = useState(() => W.STREAM_SOURCES.map((s) => ({ ...s }))); const [q, setQ] = useState(""); const [editing, setEditing] = useState(null); // input editor { isNew, key, label, unit, range, sources } const [selWearable, setSelWearable] = useState(null); const [removing, setRemoving] = useState(null); // input pending deletion (has active mappings) const usageOf = (key) => (W.EXPOSOME_FACTORS || []).filter((f) => (f.signals || []).includes(key)).length; const wearableUse = (srcKey) => inputs.filter((i) => i.sources.includes(srcKey)).length; const rows = inputs.filter((i) => !q || (i.label + " " + i.key + " " + i.unit).toLowerCase().includes(q.toLowerCase())); const saveInput = (data) => { if (data.isNew) { setInputs((xs) => [...xs, { key: data.key, label: data.label, category: data.category, unit: data.unit, range: data.range, sources: data.sources }]); toast("Wearable input “" + data.label + "” added", "ok"); } else { setInputs((xs) => xs.map((i) => i.key === data.key ? { ...i, label: data.label, category: data.category, unit: data.unit, range: data.range, sources: data.sources } : i)); toast("Wearable input “" + data.label + "” updated", "ok"); } setEditing(null); }; const removeInput = (key) => { const i = inputs.find((x) => x.key === key); setInputs((xs) => xs.filter((x) => x.key !== key)); toast("Wearable input “" + (i ? i.label : key) + "” deleted"); }; const head = ["Wearable input", "Signal type", "Identifier", "Unit", "Detected range", "Supported wearables", "Exposome use", ""]; return h("div", { className: "view" }, h(PageHead, { title: "Wearables", sub: "Wearable inputs Eno ingests and the supported devices that provide them — the signals available to map onto the Exposome" }), h("div", { className: "ref-banner" }, h(Icon, { name: "smartphone", size: 16, className: "dim" }), h("div", null, "A ", h("strong", null, "Wearable input"), " (e.g. ", h("strong", null, "VO₂ Max"), ") is a signal Eno can ingest. Each input is linked to the ", h("strong", null, "supported wearables"), " that provide it, and becomes available to map onto ", h("strong", null, "exposome factors"), ".")), h("div", { className: "stat-grid stat-grid-3" }, h(Card, { pad: false }, h(Stat, { label: "Wearable inputs", value: inputs.length, icon: "activity" })), h(Card, { pad: false }, h(Stat, { label: "Supported wearables", value: sources.length, icon: "smartphone" })), h(Card, { pad: false }, h(Stat, { label: "Mapped to exposome", value: inputs.filter((i) => usageOf(i.key) > 0).length, tone: "ok", icon: "globe" }))), h(Card, { pad: false, title: "Supported wearables", subtitle: "Devices and aggregators Eno can ingest from — click one to see its supported signals" }, h("div", { className: "wear-grid" }, sources.map((s) => { const n = wearableUse(s.key); const mode = streamMode(s.streamMode); return h("div", { className: "wear-card", key: s.key, onClick: () => setSelWearable(s) }, h("span", { className: "wear-glyph", style: { background: "oklch(0.62 0.13 " + hueFor(s.key) + ")", color: "#fff", borderColor: "transparent" } }, s.glyph || (s.name || "?")[0]), h("div", { className: "wear-main" }, h("span", { className: "wear-name" }, s.name), h("span", { className: "wear-kind" }, s.kind, " · ", n, " input", n === 1 ? "" : "s"), h("span", { className: "wear-mode" }, h(Pill, { tone: mode.tone, soft: true }, mode.label, " streaming")), s.refreshRate ? h("span", { className: "wear-refresh" }, h(Icon, { name: "refresh", size: 12 }), "Refresh: ", s.refreshRate) : null)); }))), h(Card, { pad: false, title: "Wearable inputs", subtitle: "Signals available to map onto exposome factors and biomarkers", actions: h(Button, { variant: "primary", size: "sm", icon: "plus", onClick: () => setEditing({ isNew: true, key: "", label: "", category: "", unit: "", range: "", sources: [] }) }, "New input") }, h("div", { className: "toolbar" }, h("div", { className: "search" }, h(Icon, { name: "search", size: 16 }), h("input", { className: "search-input", placeholder: "Search input, identifier or unit…", value: q, onChange: (e) => setQ(e.target.value) })), h("span", { className: "toolbar-count" }, rows.length, " of ", inputs.length)), h("div", { className: "table-wrap flush" }, h("table", { className: "table" }, h("thead", null, h("tr", null, head.map((c, i) => h("th", { key: i, className: i === 6 ? "ta-c" : (i === 7 ? "ta-r" : "") }, c)))), h("tbody", null, rows.map((i) => { const use = usageOf(i.key); const openEdit = () => setEditing({ isNew: false, key: i.key, label: i.label, category: i.category || "", unit: i.unit, range: i.range, sources: [...i.sources] }); return h("tr", { key: i.key, className: "row-click", onClick: openEdit }, h("td", null, h("span", { style: { fontWeight: 600 } }, i.label)), h("td", null, i.category ? h("span", { className: "tag" }, i.category) : h("span", { className: "dim" }, "—")), h("td", null, h("span", { className: "mono dim" }, i.key)), h("td", null, h("span", { className: "tag" }, i.unit)), h("td", { className: "mono dim" }, i.range), h("td", null, i.sources.length ? h("span", { className: "tags" }, i.sources.slice(0, 3).map((sk) => h("span", { className: "tag", key: sk }, W.sourceName(sk))), i.sources.length > 3 ? h("span", { className: "tag" }, "+" + (i.sources.length - 3)) : null) : h("span", { className: "dim" }, "—")), h("td", { className: "ta-c" }, use ? h("span", { className: "mono" }, use) : h("span", { className: "dim" }, "0")), h("td", { className: "ta-r" }, h("div", { className: "row-actions", onClick: (e) => e.stopPropagation() }, h("button", { className: "mini-btn", title: "Edit input", onClick: openEdit }, h(Icon, { name: "edit", size: 15 })), h("button", { className: "mini-btn danger", title: "Delete input", onClick: () => { if (usageOf(i.key) > 0) setRemoving(i); else removeInput(i.key); } }, h(Icon, { name: "trash", size: 15 }))))); }), !rows.length && h("tr", null, h("td", { colSpan: 8 }, h(Empty, { icon: "activity", title: "No wearable inputs", sub: "Adjust your search." }))))))), h(WearableInputModal, { data: editing, existing: inputs, sources, onClose: () => setEditing(null), onSave: saveInput }), h(WearableDrawer, { wearable: selWearable, inputs, onClose: () => setSelWearable(null) }), h(DeleteInputModal, { input: removing, onClose: () => setRemoving(null), onConfirm: () => { removeInput(removing.key); setRemoving(null); } })); } function DeleteInputModal({ input, onClose, onConfirm }) { if (!input) return null; const affected = (W.EXPOSOME_FACTORS || []).filter((f) => (f.signals || []).includes(input.key)); const n = affected.length; return h(Modal, { open: true, onClose, title: "Delete wearable input", width: 540, footer: h(React.Fragment, null, h(Button, { variant: "ghost", onClick: onClose }, "Cancel"), h(Button, { variant: "danger", icon: "trash", onClick: onConfirm }, "Unmap & delete")) }, h("div", { className: "mform" }, h("div", { className: "warn-banner" }, h(Icon, { name: "alert", size: 16 }), h("div", null, "Deleting the wearable input ", h("strong", null, "“" + input.label + "”"), " will unmap it from ", h("strong", null, n, " exposome factor", n === 1 ? "" : "s"), ". Those exposome mappings will be removed — this cannot be undone.")), h("div", { className: "refsec" }, h("p", { className: "refsec-title" }, "Affected exposome factors (" + n + ")"), h("div", { className: "tags" }, affected.map((f) => h("span", { className: "tag", key: f.id }, f.label)))), h("p", { className: "mform-note" }, h(Icon, { name: "info", size: 14 }), "The exposome factors themselves are kept — only their mapping to this wearable input is removed."))); } function WearableDrawer({ wearable, inputs, onClose }) { if (!wearable) return h(Drawer, { open: false, onClose }); const signals = inputs.filter((i) => i.sources.includes(wearable.key)); const mode = streamMode(wearable.streamMode); const head = ["Signal name", "Signal type", "Identifier", "Unit", "Detected range"]; return h(Drawer, { open: true, onClose, width: 660, title: wearable.name, sub: h(React.Fragment, null, h(Pill, { tone: "neutral", soft: true }, wearable.kind), h(Pill, { tone: mode.tone, soft: true }, mode.label, " streaming")) }, h("div", { className: "refbox" }, h("div", { className: "meta-grid" }, h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "Type"), h("span", { className: "meta-v" }, wearable.kind)), h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "Data streaming mode"), h("span", { className: "meta-v" }, mode.label)), h("div", { className: "meta-row" }, h("span", { className: "meta-k" }, "Data refresh rate"), h("span", { className: "meta-v" }, wearable.refreshRate || "—"))), h("div", { className: "refsec" }, h("div", { className: "refsec-head" }, h("p", { className: "refsec-title" }, "Supported signals"), h("span", { className: "toolbar-count" }, signals.length, " signal", signals.length === 1 ? "" : "s")), h("p", { className: "field-hint", style: { margin: "-2px 0 8px" } }, "Read-only — manage which signals a wearable provides from each input’s editor."), signals.length ? h("div", { className: "table-wrap", style: { border: "1px solid var(--line)", borderRadius: "var(--r-sm)" } }, h("table", { className: "table" }, h("thead", null, h("tr", null, head.map((c) => h("th", { key: c }, c)))), h("tbody", null, signals.map((s) => h("tr", { key: s.key }, h("td", null, h("span", { style: { fontWeight: 600 } }, s.label)), h("td", null, s.category ? h("span", { className: "tag" }, s.category) : h("span", { className: "dim" }, "—")), h("td", null, h("span", { className: "mono dim" }, s.key)), h("td", null, h("span", { className: "tag" }, s.unit)), h("td", { className: "mono dim" }, s.range)))))) : h(Empty, { icon: "activity", title: "No signals", sub: "No wearable inputs are linked to this device yet." })))); } function WearableInputModal({ data, existing, sources, onClose, onSave }) { const [label, setLabel] = useState(""); const [category, setCategory] = useState(""); const [unit, setUnit] = useState(""); const [range, setRange] = useState(""); const [srcs, setSrcs] = useState([]); useEffect(() => { if (data) { setLabel(data.label || ""); setCategory(data.category || ""); setUnit(data.unit || ""); setRange(data.range || ""); setSrcs([...(data.sources || [])]); } }, [data]); if (!data) return null; const dup = existing.some((i) => i.key !== data.key && i.label.trim().toLowerCase() === label.trim().toLowerCase()); const slug = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "").slice(0, 20) || "input"; const valid = label.trim() && !dup; const submit = () => { if (!valid) return; onSave({ isNew: data.isNew, key: data.isNew ? slug : data.key, label: label.trim(), category: category.trim(), unit: unit.trim() || "—", range: range.trim() || "—", sources: [...srcs] }); }; return h(Modal, { open: true, onClose, title: data.isNew ? "New wearable input" : "Edit wearable input", width: 560, footer: h(React.Fragment, null, h(Button, { variant: "ghost", onClick: onClose }, "Cancel"), h(Button, { variant: "primary", icon: data.isNew ? "plus" : "check", disabled: !valid, onClick: submit }, data.isNew ? "Create input" : "Save changes")) }, h("div", { className: "mform" }, h(Field, { label: "Input name", hint: dup ? "An input with this name already exists." : "e.g. VO₂ Max, Resting heart rate" }, h("input", { className: "input", autoFocus: true, value: label, placeholder: "e.g. VO₂ Max", onChange: (e) => setLabel(e.target.value) })), h("div", { className: "form-grid", style: { display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "14px" } }, h(Field, { label: "Signal type" }, h("input", { className: "input", value: category, placeholder: "e.g. Cardiac", onChange: (e) => setCategory(e.target.value) })), h(Field, { label: "Unit" }, h("input", { className: "input mono", value: unit, placeholder: "e.g. mL/kg/min", onChange: (e) => setUnit(e.target.value) })), h(Field, { label: "Detected range" }, h("input", { className: "input mono", value: range, placeholder: "e.g. 20–60", onChange: (e) => setRange(e.target.value) }))), !data.isNew && h(Field, { label: "Identifier" }, h("input", { className: "input mono", value: data.key, disabled: true })), data.isNew && label.trim() && h("p", { className: "mform-note" }, h(Icon, { name: "hash", size: 14 }), "Identifier: ", h("span", { className: "mono" }, slug)), h("div", { className: "refsec" }, h("div", { className: "refsec-head" }, h("p", { className: "refsec-title" }, "Supported wearables"), h("span", { className: "toolbar-count" }, srcs.length, " linked")), h("p", { className: "field-hint", style: { margin: "-2px 0 7px" } }, "Devices and aggregators that provide this input."), h(window.LinkCombo, { all: sources.map((s) => ({ id: s.key, label: s.name, meta: s.kind, search: s.name + " " + s.kind })), selected: srcs, placeholder: "Search wearable to link…", emptyText: "No wearables linked yet.", noMatchText: "No matching wearable", onAdd: (id) => setSrcs((xs) => xs.includes(id) ? xs : [...xs, id]), onRemove: (id) => setSrcs((xs) => xs.filter((x) => x !== id)) })))); } Object.assign(window, { WearablesView });