/* Eno.Health Console — Streams: wearable & smart-device signal ingestion (scaffold) */ const S = window.ENO; function StreamsView({ onSelectCell, onOpenDevice }) { const [tab, setTab] = useState("fleet"); const tabs = [ { key: "fleet", label: "Device fleet", count: S.DEVICES.length }, { key: "signals", label: "Signal streams" }, { key: "integrations", label: "Integrations" }, ]; return h("div", { className: "view" }, h(PageHead, { title: "Streams", sub: "Wearable and smart-device signal ingestion — time-series telemetry from connected devices" }), h("div", { className: "rbac-banner" }, h(Icon, { name: "runs", size: 15 }), h("div", null, h("strong", null, "New ingestion modality."), " Signals are continuous time-series — they run a parallel pre-processing path (decode → ", h(Mono, null, "normalize units"), " → ", h(Mono, null, "range check"), " → de-identify → time-series store). The device fleet below is live; stream drill-down and integration management are next.")), h(StreamKpis, null), h(Tabs, { tabs, active: tab, onChange: setTab }), h("div", { className: "tabbody" }, tab === "fleet" && h(DeviceFleet, { onOpenDevice }), tab === "signals" && h(SignalStreamsComingSoon, null), tab === "integrations" && h(IntegrationsComingSoon, null))); } function StreamKpis() { const k = S.streamKpis(); return h("div", { className: "stat-grid stat-grid-4" }, h(Card, { pad: false }, h(Stat, { label: "Active devices", value: k.activeDevices, unit: "/ " + k.totalDevices, tone: "ok", icon: "tenants" })), h(Card, { pad: false }, h(Stat, { label: "Readings / min", value: fmtNum(k.readingsPerMin), spark: S.series(2, 16, 30, 9), tone: "info", icon: "runs" })), h(Card, { pad: false }, h(Stat, { label: "Median sync lag", value: k.syncLag, unit: "s", tone: "ok", icon: "clock" })), h(Card, { pad: false }, h(Stat, { label: "Needs attention", value: k.needsAttention, tone: k.needsAttention ? "bad" : "muted", delta: k.needsAttention ? "stale / expired / offline" : "all healthy", icon: "alert" }))); } const DEV_STATUS_FILTERS = [ { value: "all", label: "All statuses" }, { value: "connected", label: "Connected" }, { value: "syncing", label: "Syncing" }, { value: "stale", label: "Stale" }, { value: "token_expired", label: "Token expired" }, { value: "disconnected", label: "Disconnected" }, ]; function DeviceFleet({ onOpenDevice }) { const [status, setStatus] = useState("all"); const [source, setSource] = useState("all"); const [q, setQ] = useState(""); const rows = S.DEVICES.filter((d) => { if (status !== "all" && d.status !== status) return false; if (source !== "all" && d.source !== source) return false; if (q && !(d.id + d.model + d.patient + S.sourceName(d.source)).toLowerCase().includes(q.toLowerCase())) return false; return true; }); const head = ["Device", "Patient", "Tenant", "Source", "Streams", "Battery", "Last sync", "Status"]; const body = rows.map((d) => { const st = S.DEVICE_STATUS[d.status]; return h("tr", { key: d.id, className: "row-click", onClick: () => onOpenDevice(d) }, h("td", null, h("div", { className: "cell-doc" }, h("span", { className: "cell-file" }, d.model), h(Mono, { className: "cell-id" }, d.id))), h("td", null, h(Mono, null, d.patient)), h("td", null, S.tenantName(d.tenant)), h("td", null, h("span", { className: "src-cell" }, h("span", { className: "src-glyph" }, S.STREAM_SOURCES.find((s) => s.key === d.source).glyph || "♥"), S.sourceName(d.source))), h("td", null, h("span", { className: "tags" }, d.streams.slice(0, 4).map((s) => h("span", { className: "tag", key: s }, S.signalLabel(s))), d.streams.length > 4 ? h("span", { className: "tag" }, "+" + (d.streams.length - 4)) : null)), h("td", null, d.battery == null ? h("span", { className: "dim" }, "mains") : h(Battery, { pct: d.battery })), h("td", { className: "dim" }, d.status === "syncing" ? h("span", { className: "sync-now" }, h(Icon, { name: "spinner", size: 12, className: "spin" }), "now") : fmtAgo(d.lastSync)), h("td", null, h(Pill, { tone: st.tone, soft: true }, st.label))); }); return h(Card, { pad: false }, h("div", { className: "toolbar" }, h("div", { className: "search" }, h(Icon, { name: "search", size: 16 }), h("input", { className: "search-input", placeholder: "Search device, model, patient, source…", value: q, onChange: (e) => setQ(e.target.value) })), h(Select, { value: status, onChange: setStatus, options: DEV_STATUS_FILTERS }), h(Select, { value: source, onChange: setSource, options: [{ value: "all", label: "All sources" }, ...S.STREAM_SOURCES.map((s) => ({ value: s.key, label: s.name }))] }), h("span", { className: "toolbar-count" }, rows.length, " of ", S.DEVICES.length)), h("div", { className: "table-wrap" }, h("table", { className: "table" }, h("thead", null, h("tr", null, head.map((c) => h("th", { key: c }, c)))), h("tbody", null, body, !rows.length && h("tr", null, h("td", { colSpan: 8 }, h(Empty, { title: "No devices match", sub: "Try clearing filters." }))))))); } function Battery({ pct }) { const tone = pct <= 15 ? "bad" : pct <= 35 ? "warn" : "ok"; return h("span", { className: "battery" }, h("span", { className: "battery-shell" }, h("span", { className: "battery-fill", style: { width: pct + "%", background: `var(--${tone})` } })), h("span", { className: "battery-pct mono" }, pct + "%")); } function SignalStreamsComingSoon() { return h(Card, null, h("div", { className: "soon-head" }, h("h3", { className: "card-title" }, "Signal stream explorer"), h(Pill, { tone: "accent" }, "Coming soon")), h("p", { className: "soon-lead" }, "Per-metric stream drill-down — the signal equivalent of the document debugger. Each stream will expose:"), h("div", { className: "soon-grid" }, [ ["runs", "Sync timeline & gaps", "Reading cadence with dropout windows highlighted, so you can see exactly when a device stopped streaming."], ["database", "Unit normalization", "Raw → canonical units (e.g. mg/dL ↔ mmol/L, °F ↔ °C) with the mapping that was applied."], ["shield", "Range & plausibility checks", "Out-of-range and implausible readings flagged before they reach storage."], ["lock", "De-identification", "Same PHI/PII posture as documents, plus location-from-GPS and pattern-based re-identification guards."], ].map(([icon, title, body]) => h("div", { className: "soon-card", key: title }, h("div", { className: "soon-icon" }, h(Icon, { name: icon, size: 18 })), h("div", null, h("div", { className: "soon-title" }, title), h("div", { className: "soon-sub" }, body))))), h("div", { className: "soon-sample" }, h("div", { className: "soon-sample-head" }, h(Mono, null, "hr"), " · Heart rate · ", h("span", { className: "dim" }, "Apple Watch S9 · pt_a91f")), h(MiniBars, { data: S.series(7, 40, 62, 16), tone: "info", height: 54 }), h("div", { className: "dim small" }, "bpm · last 40 min · preview of the stream sparkline"))); } function IntegrationsComingSoon() { return h(Card, null, h("div", { className: "soon-head" }, h("h3", { className: "card-title" }, "Integrations" ), h(Pill, { tone: "accent" }, "Coming soon")), h("p", { className: "soon-lead" }, "Connection health for each device-data provider — OAuth token status, API rate-limit headroom, and per-source ingest volume."), h("div", { className: "integ-grid" }, S.STREAM_SOURCES.map((s) => { const devs = S.DEVICES.filter((d) => d.source === s.key); const issues = devs.filter((d) => d.status === "token_expired" || d.status === "disconnected").length; return h("div", { className: "integ-card", key: s.key }, h("div", { className: "integ-top" }, h("span", { className: "src-glyph lg" }, s.glyph || "♥"), h("div", null, h("div", { className: "integ-name" }, s.name), h("div", { className: "dim small" }, s.kind))), h("div", { className: "integ-meta" }, h("span", null, h("strong", null, devs.length), " devices"), issues ? h(Pill, { tone: "warn", soft: true }, issues + " need auth") : h(Pill, { tone: "ok", soft: true }, "healthy"))); }))); } /* ======================================================================== */ /* DEVICE / SIGNAL STREAM DRILL-DOWN */ /* ======================================================================== */ const SIG_BASE = { hr: [64, 18], hrv: [55, 22], spo2: [97, 2], sleep: [78, 12], steps: [42, 30], temp: [36.6, 0.5], bp: [120, 12], ecg: [60, 22], glucose: [112, 34] }; const SIG_CAD = { hr: "1 / min · 5s on workout", hrv: "nightly + spot", spo2: "1 / min", sleep: "nightly summary", steps: "1 / min rollup", temp: "every 5 min", bp: "on demand", ecg: "on demand · 30s", glucose: "every 5 min" }; const SIG_NORM = { hr: "bpm (canonical)", hrv: "ms (canonical)", spo2: "fraction → %", sleep: "vendor stages → canonical", steps: "cumulative → delta", temp: "°F → °C", bp: "mmHg → systolic / diastolic", ecg: "250 Hz → samples", glucose: "mg/dL ↔ mmol/L" }; function streamStat(dev, key) { const [b, a] = SIG_BASE[key] || [50, 15]; const seed = (dev.id.charCodeAt(4) || 5) + key.length; const data = S.series(seed, 32, b, a); const offline = ["stale", "token_expired", "disconnected"].includes(dev.status); const primary = dev.streams[0] === key; const gap = offline ? (primary ? dev.gap : Math.min(100, +(dev.gap * 0.7).toFixed(1))) : +((seed % 5) * 0.35).toFixed(1); const inRange = Math.max(88, 100 - gap * 0.25 - (seed % 7) * 0.3); const flagged = Math.round((100 - inRange) / 100 * data.length * 4); const meta = S.SIGNAL_TYPES.find((s) => s.key === key) || { label: key, unit: "" }; const last = data[data.length - 1]; const lastDisp = key === "bp" ? `${Math.round(last)}/${Math.round(last * 0.64)}` : key === "temp" ? last.toFixed(1) : Math.round(last); return { meta, data, lastDisp, gap, inRange: inRange.toFixed(1), flagged, cad: SIG_CAD[key], norm: SIG_NORM[key], offline }; } function syncEvents(dev) { const out = []; if (dev.status === "token_expired") out.push({ label: "OAuth token expired", service: S.sourceName(dev.source), at: dev.lastSync, state: "failed", note: "Refresh token rejected by provider — patient must reconnect the integration." }); else if (dev.status === "disconnected") out.push({ label: "Device unreachable", service: S.sourceName(dev.source), at: dev.lastSync, state: "failed", note: "No successful sync in over 24h — battery depleted or app uninstalled." }); else if (dev.status === "stale") out.push({ label: "Sync overdue", service: "bucket-watcher", at: dev.lastSync, state: "active", note: "Last batch is older than the freshness threshold — gap accumulating." }); const n = dev.offline ? 2 : 5; const offsets = [dev.lastMin + 6, dev.lastMin + 13, dev.lastMin + 22, dev.lastMin + 34, dev.lastMin + 49]; for (let i = 0; i < n; i++) out.push({ label: `Synced ${12 + (i * 9) % 41} readings`, service: S.sourceName(dev.source), at: S.ago(offsets[i] * S.MIN), state: "done" }); return out; } function DeviceDrawer({ device, onClose }) { const [tab, setTab] = useState("signals"); const toast = useToast(); useEffect(() => { setTab("signals"); }, [device && device.id]); if (!device) return null; const st = S.DEVICE_STATUS[device.status]; const footer = h("div", { className: "drawer-actions" }, h(Button, { variant: "ghost", size: "sm", icon: "copy", onClick: () => { navigator.clipboard?.writeText(device.id); toast("Device ID copied"); } }, "Copy ID"), h(Button, { variant: "default", size: "sm", icon: "refresh", onClick: () => toast("Re-sync requested for " + device.id) }, "Force sync"), (device.status === "token_expired" || device.status === "disconnected") && h(Button, { variant: "primary", size: "sm", icon: "webhooks", onClick: () => toast("Reconnect link sent to patient") }, "Reconnect"), h(Button, { variant: "danger-ghost", size: "sm", icon: "trash", onClick: () => toast("Device unlinked from patient") }, "Unlink")); const tabs = [ { key: "signals", label: "Signals", count: device.streams.length }, { key: "sync", label: "Sync timeline" }, { key: "validation", label: "Validation" }, { key: "deid", label: "De-identification" }, { key: "meta", label: "Metadata" }, ]; return h(Drawer, { open: true, onClose, width: 780, footer, title: device.model, sub: h("div", { className: "drawer-subline" }, h(Mono, { copy: true }, device.id), h(Pill, { tone: st.tone }, st.label), h("span", { className: "src-cell" }, h("span", { className: "src-glyph" }, S.STREAM_SOURCES.find((s) => s.key === device.source).glyph || "♥"), S.sourceName(device.source))), }, h(Tabs, { tabs, active: tab, onChange: setTab }), h("div", { className: "drawer-tabbody" }, tab === "signals" && h(SignalsTab, { device }), tab === "sync" && h(SyncTab, { device }), tab === "validation" && h(ValidationTab, { device }), tab === "deid" && h(DeidTab, { device }), tab === "meta" && h(MetaDeviceTab, { device }))); } function SignalsTab({ device }) { return h("div", { className: "stream-list" }, device.streams.map((key) => { const s = streamStat(device, key); const gapTone = s.gap > 20 ? "bad" : s.gap > 5 ? "warn" : "ok"; return h("div", { className: "stream-card", key }, h("div", { className: "stream-head" }, h("div", null, h("div", { className: "stream-name" }, s.meta.label, h("span", { className: "stream-unit mono" }, s.meta.unit)), h("div", { className: "stream-cad dim" }, s.cad)), h("div", { className: "stream-last" }, h("span", { className: "stream-val" }, s.lastDisp), h("span", { className: "stream-val-unit dim" }, s.meta.unit))), s.offline && s.gap >= 100 ? h("div", { className: "stream-offline" }, h(Icon, { name: "alert", size: 13 }), "No readings — stream halted") : h(MiniBars, { data: s.data, tone: s.offline ? "warn" : "info", height: 42 }), h("div", { className: "stream-meta" }, h("span", { className: "stream-chip" }, h("span", { className: "dim" }, "Normalize"), h(Mono, null, s.norm)), h("span", { className: "stream-chip" }, h("span", { className: "dim" }, "Range"), h(Pill, { tone: +s.inRange > 98 ? "ok" : +s.inRange > 95 ? "warn" : "bad", soft: true }, s.inRange + "% in range")), h("span", { className: "stream-chip" }, h("span", { className: "dim" }, "Gap"), h(Pill, { tone: gapTone, soft: true }, s.gap + "%")))); })); } function SyncTab({ device }) { const ev = syncEvents(device); return h("div", { className: "timeline" }, ev.map((t, i) => h("div", { className: "tl-row", key: i }, h("div", { className: `tl-marker tl-${t.state}` }, h(Icon, { name: t.state === "failed" ? "x" : t.state === "active" ? "dot" : "check", size: 13 })), h("div", { className: "tl-main" }, h("div", { className: "tl-label" }, t.label, h("span", { className: "tl-service mono" }, t.service)), h("div", { className: "tl-time" }, fmtTime(t.at), " · ", fmtAgo(t.at)), t.note && h("div", { className: "tl-note" }, h(Icon, { name: "alert", size: 13 }), t.note)), i < ev.length - 1 && h("div", { className: "tl-line" })))); } function ValidationTab({ device }) { const head = ["Stream", "In-range", "Flagged", "Normalization"]; const body = device.streams.map((key) => { const s = streamStat(device, key); return h("tr", { key }, h("td", null, s.meta.label), h("td", null, h(Pill, { tone: +s.inRange > 98 ? "ok" : +s.inRange > 95 ? "warn" : "bad", soft: true }, s.inRange + "%")), h("td", { className: "ta-c mono" }, s.flagged), h("td", null, h(Mono, { className: "dim" }, s.norm))); }); return h("div", null, h("div", { className: "audit-banner" }, h(Icon, { name: "shield", size: 14 }), "Out-of-range and implausible readings are flagged before the time-series store. Flagged values are kept but marked, never silently dropped."), h("div", { className: "table-wrap flush" }, h("table", { className: "table table-tight" }, h("thead", null, h("tr", null, head.map((c) => h("th", { key: c }, c)))), h("tbody", null, body)))); } function DeidTab({ device }) { const items = [ ["Device serial / IDFV", "pseudonymized", "up"], ["GPS / location", "stripped", "up"], ["Patient identifiers", device.patient + " pseudonym", "up"], ["Re-identification pattern guard", "active", "up"], ["Timestamps", "jittered ±60s", "up"], ]; return h("div", null, h("div", { className: "audit-banner" }, h(Icon, { name: "lock", size: 14 }), "Signals carry the same PHI/PII posture as documents, plus location and pattern-based re-identification guards."), h("div", { className: "engine-grid deid-grid" }, items.map(([label, state, dot]) => h("div", { className: "engine", key: label }, h("span", { className: `svc-dot svc-${dot}` }), h("span", { className: "engine-name" }, label), h("span", { className: "engine-state" }, state))))); } function MetaDeviceTab({ device }) { const rows = [ ["Device ID", device.id, true], ["Patient", device.patient + " (pseudonymous)"], ["Tenant", S.tenantName(device.tenant) + " (" + device.tenant + ")"], ["Source", S.sourceName(device.source)], ["Model", device.model], ["Firmware", device.fw], ["Battery", device.battery == null ? "Mains powered" : device.battery + "%"], ["Cell", device.cell], ["Connection", S.DEVICE_STATUS[device.status].label], ["Streams", device.streams.map(S.signalLabel).join(", ")], ["Readings / min", device.rpm], ["Last sync", fmtTime(device.lastSync) + " · " + fmtAgo(device.lastSync)], ]; return h("div", { className: "meta-grid" }, rows.map(([k, v, mono], i) => h("div", { className: "meta-row", key: i }, h("span", { className: "meta-k" }, k), h("span", { className: "meta-v" }, mono ? h(Mono, { copy: true }, v) : v)))); } Object.assign(window, { StreamsView, DeviceDrawer });