/* Eno.Health Console — Platform Administrators: manage console operators of the back-end */ const AD = window.ENO; function adminRoleLabel(id) { return (AD.ADMIN_ROLES.find((r) => r.id === id) || {}).label || id; } function adminRoleTone(id) { return id === "owner" ? "accent" : id === "admin" ? "purple" : "neutral"; } function adminStatusPill(s) { if (s === "active") return h(Pill, { tone: "ok", soft: true }, "active"); if (s === "invited") return h(Pill, { tone: "warn", soft: true }, "invited"); return h(Pill, { tone: "neutral", soft: true }, "disabled"); } function PlatformAdminsView() { const toast = useToast(); const [admins, setAdmins] = useState(() => AD.PLATFORM_ADMINS.map((a) => ({ ...a }))); const [q, setQ] = useState(""); const [roleFilter, setRoleFilter] = useState("all"); const [editing, setEditing] = useState(null); // { isNew, ...admin } const [removing, setRemoving] = useState(null); const ownerCount = admins.filter((a) => a.role === "owner" && a.status !== "disabled").length; const activeAdmins = admins.filter((a) => a.status === "active"); const mfaOn = activeAdmins.filter((a) => a.mfa).length; const mfaPct = activeAdmins.length ? Math.round((mfaOn / activeAdmins.length) * 100) : 100; const pending = admins.filter((a) => a.status === "invited").length; const rows = admins.filter((a) => (roleFilter === "all" || a.role === roleFilter) && (!q.trim() || (a.name + " " + a.email).toLowerCase().includes(q.trim().toLowerCase()))); const isLastOwner = (a) => a.role === "owner" && ownerCount <= 1; const save = (data) => { if (data.isNew) { const id = "adm_" + (data.email.split("@")[0] || Math.random().toString(16).slice(2, 8)).replace(/[^a-z0-9]+/gi, "_").toLowerCase(); const initials = data.name.trim().split(/\s+/).map((w) => w[0]).slice(0, 2).join("").toUpperCase() || "?"; setAdmins((xs) => [...xs, { id, name: data.name.trim(), email: data.email.trim(), role: data.role, cells: data.cells.trim() || "all", status: "invited", mfa: false, lastActive: null, createdMin: 0, initials }]); toast("Invitation sent to " + data.email.trim(), "ok"); } else { setAdmins((xs) => xs.map((a) => a.id === data.id ? { ...a, name: data.name.trim(), email: data.email.trim(), role: data.role, cells: data.cells.trim() || "all", mfa: data.mfa } : a)); toast("“" + data.name.trim() + "” updated", "ok"); } setEditing(null); }; const toggleStatus = (a) => { if (a.self) return toast("You can't disable your own account"); if (a.status === "active" && isLastOwner(a)) return toast("At least one active owner is required"); setAdmins((xs) => xs.map((x) => x.id === a.id ? { ...x, status: x.status === "disabled" ? "active" : "disabled" } : x)); toast(a.status === "disabled" ? "“" + a.name + "” re-enabled" : "“" + a.name + "” disabled"); }; const resend = (a) => toast("Invitation re-sent to " + a.email, "ok"); const toggleMfa = (a) => { setAdmins((xs) => xs.map((x) => x.id === a.id ? { ...x, mfa: !x.mfa } : x)); toast(a.mfa ? "2FA disabled for “" + a.name + "”" : "2FA enabled for “" + a.name + "”", a.mfa ? undefined : "ok"); }; const remove = (a) => { setAdmins((xs) => xs.filter((x) => x.id !== a.id)); toast("“" + a.name + "” removed from administrators"); setRemoving(null); }; const head = ["Administrator", "Role", "Cells", "2FA", "Last active", "Status", ""]; return h("div", { className: "view" }, h(PageHead, { title: "Platform Administrators", sub: "People who can sign in to this management console — not tenant users", right: h(Button, { variant: "primary", size: "sm", icon: "plus", onClick: () => setEditing({ isNew: true, name: "", email: "", role: "operator", cells: "all" }) }, "Invite administrator") }), h("div", { className: "ref-banner" }, h(Icon, { name: "shield", size: 16, className: "dim" }), h("div", null, "Administrators have owner-scoped access to the back-end platform. Grant the ", h("strong", null, "least privilege"), " needed, require 2FA, and keep at least one active ", h("strong", null, "Owner"), ".")), h("div", { className: "stat-grid stat-grid-4" }, h(Card, { pad: false }, h(Stat, { label: "Administrators", value: admins.length, icon: "tenants" })), h(Card, { pad: false }, h(Stat, { label: "Owners", value: ownerCount, icon: "key" })), h(Card, { pad: false }, h(Stat, { label: "2FA coverage", value: mfaPct, unit: "%", tone: mfaPct === 100 ? "ok" : "warn", icon: "lock" })), h(Card, { pad: false }, h(Stat, { label: "Pending invites", value: pending, icon: "clock" }))), h(Card, { pad: false, title: "Administrators", subtitle: "Console operators across all regional cells" }, h("div", { className: "toolbar" }, h("div", { className: "search" }, h(Icon, { name: "search", size: 16 }), h("input", { className: "search-input", placeholder: "Search by name or email…", value: q, onChange: (e) => setQ(e.target.value) })), h(Select, { value: roleFilter, onChange: setRoleFilter, options: [{ value: "all", label: "All roles" }].concat(AD.ADMIN_ROLES.map((r) => ({ value: r.id, label: r.label }))) }), h("span", { className: "toolbar-count" }, rows.length, " of ", admins.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-r" : "" }, c)))), h("tbody", null, rows.map((a) => h("tr", { key: a.id }, h("td", null, h("div", { className: "user-cell" }, h(Avatar, { initials: a.initials, size: 30, tone: a.status === "disabled" ? "neutral" : "accent" }), h("div", { className: "user-cell-meta" }, h("div", { className: "user-cell-name" }, a.name, a.self ? h("span", { className: "self-badge" }, "You") : null), h("div", { className: "dim mono small" }, a.email)))), h("td", null, h(Pill, { tone: adminRoleTone(a.role), soft: true }, adminRoleLabel(a.role))), h("td", null, a.cells === "all" ? h("span", { className: "tag" }, "All cells") : h("span", { className: "dim mono small" }, a.cells)), h("td", null, h("button", { className: "mfa-toggle" + (a.mfa ? " on" : ""), title: a.mfa ? "2FA enabled — click to disable" : "2FA disabled — click to enable", onClick: () => toggleMfa(a) }, h(Icon, { name: a.mfa ? "lock" : "alert", size: 13 }), a.mfa ? "On" : "Off")), h("td", { className: "dim" }, a.lastActive == null ? (a.status === "invited" ? "never" : "—") : fmtAgo(AD.ago(a.lastActive))), h("td", null, adminStatusPill(a.status)), h("td", { className: "ta-r" }, h("div", { className: "row-actions" }, a.status === "invited" && h("button", { className: "mini-btn", title: "Resend invitation", onClick: () => resend(a) }, h(Icon, { name: "refresh", size: 15 })), h("button", { className: "mini-btn", title: "Edit administrator", onClick: () => setEditing({ isNew: false, ...a }) }, h(Icon, { name: "edit", size: 15 })), h("button", { className: "mini-btn", title: a.self ? "You can't disable yourself" : (a.status === "disabled" ? "Re-enable" : "Disable"), disabled: a.self, onClick: () => toggleStatus(a) }, h(Icon, { name: a.status === "disabled" ? "check" : "lock", size: 15 })), h("button", { className: "mini-btn danger", title: a.self ? "You can't remove yourself" : (isLastOwner(a) ? "At least one owner is required" : "Remove"), disabled: a.self || isLastOwner(a), onClick: () => setRemoving(a) }, h(Icon, { name: "trash", size: 15 })))))), !rows.length && h("tr", null, h("td", { colSpan: 7 }, h(Empty, { icon: "tenants", title: "No administrators", sub: "Adjust your search or filter." })))))), h(RoleLegend, null)), h(AdminEditorModal, { data: editing, roles: AD.ADMIN_ROLES, onClose: () => setEditing(null), onSave: save }), h(Modal, { open: !!removing, onClose: () => setRemoving(null), title: "Remove administrator", width: 460, footer: removing && h(React.Fragment, null, h(Button, { variant: "ghost", onClick: () => setRemoving(null) }, "Cancel"), h(Button, { variant: "danger", icon: "trash", onClick: () => remove(removing) }, "Remove access")) }, removing && h("div", { className: "mform" }, h("p", { style: { margin: 0 } }, "Revoke console access for ", h("strong", null, removing.name), " (", h("span", { className: "mono" }, removing.email), ")? They will be signed out immediately and can only return by re-invitation."), h("p", { className: "mform-note" }, h(Icon, { name: "info", size: 14 }), "This does not affect any tenant or patient data.")))); } function RoleLegend() { return h("div", { className: "role-legend" }, AD.ADMIN_ROLES.map((r) => h("div", { className: "role-legend-item", key: r.id }, h(Pill, { tone: adminRoleTone(r.id), soft: true }, r.label), h("span", { className: "dim small" }, r.desc)))); } function AdminEditorModal({ data, roles, onClose, onSave }) { const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [role, setRole] = useState("operator"); const [cells, setCells] = useState("all"); const [mfa, setMfa] = useState(false); useEffect(() => { if (data) { setName(data.name || ""); setEmail(data.email || ""); setRole(data.role || "operator"); setCells(data.cells || "all"); setMfa(!!data.mfa); } }, [data]); if (!data) return null; const badEmail = !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim()); const valid = name.trim() && !badEmail; const roleDesc = (roles.find((r) => r.id === role) || {}).desc; const submit = () => { if (valid) onSave({ isNew: data.isNew, id: data.id, name, email, role, cells, mfa }); }; return h(Modal, { open: true, onClose, title: data.isNew ? "Invite administrator" : "Edit administrator", width: 520, 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 ? "Send invitation" : "Save changes")) }, h("div", { className: "mform" }, h("div", { className: "mform-2" }, h(Field, { label: "Full name" }, h("input", { className: "input", autoFocus: true, value: name, placeholder: "e.g. Lukas Vermeulen", onChange: (e) => setName(e.target.value) })), h(Field, { label: "Work email", hint: badEmail && email ? "Enter a valid email address." : "" }, h("input", { className: "input mono", type: "email", value: email, placeholder: "name@eno.health", onChange: (e) => setEmail(e.target.value) }))), h(Field, { label: "Role", hint: roleDesc }, h(Select, { value: role, onChange: setRole, options: roles.map((r) => ({ value: r.id, label: r.label })) })), h(Field, { label: "Cell access", hint: "Which regional cells this administrator can operate (e.g. eu-be, eu-nl) — or all cells" }, h("input", { className: "input", value: cells, placeholder: "all", onChange: (e) => setCells(e.target.value) })), !data.isNew && h(Field, { label: "Two-factor authentication", hint: mfa ? "Required at every sign-in for this administrator." : "Not enforced — this account can sign in with a password alone." }, h("div", { className: "mfa-field" }, h(Toggle, { on: mfa, onChange: setMfa, label: "Two-factor authentication" }), h("span", { className: "mfa-field-state" + (mfa ? " on" : "") }, mfa ? "Enabled" : "Disabled"))), data.isNew && h("p", { className: "mform-note" }, h(Icon, { name: "lock", size: 14 }), "The administrator sets a password and enables 2FA when they accept the invitation."))); } Object.assign(window, { PlatformAdminsView, SettingsView }); /* ======================================================================== */ /* SETTINGS — platform configuration (LLM, …) */ /* ======================================================================== */ const SETTINGS_LS = "eno_settings"; function loadSettings() { try { return JSON.parse(localStorage.getItem(SETTINGS_LS)) || {}; } catch { return {}; } } function saveSettings(s) { try { localStorage.setItem(SETTINGS_LS, JSON.stringify(s)); } catch {} } const SETTINGS_DEFAULTS = { bmLoincMinConfidence: 85 }; function SettingsView() { const [tab, setTab] = useState("llm"); const tabs = [{ key: "llm", label: "LLM" }]; return h("div", { className: "view" }, h(PageHead, { title: "Settings", sub: "Platform configuration" }), h(Tabs, { tabs, active: tab, onChange: setTab }), h("div", { className: "tabbody" }, tab === "llm" ? h(LlmSettings, null) : null)); } function LlmSettings() { const toast = useToast(); const [stored, setStored] = useState(() => ({ ...SETTINGS_DEFAULTS, ...loadSettings() })); const [minConf, setMinConf] = useState(stored.bmLoincMinConfidence); const dirty = minConf !== stored.bmLoincMinConfidence; const clamp = (v) => Math.max(50, Math.min(100, Math.round(v) || 0)); const setVal = (v) => setMinConf(clamp(v)); const belowCount = AD.BIOMARKERS.filter((b) => (b.confidence ?? 100) < minConf).length; const total = AD.BIOMARKERS.length; const save = () => { const next = { ...stored, bmLoincMinConfidence: minConf }; saveSettings(next); setStored(next); toast("Minimum confidence saved · " + minConf + "%", "ok"); }; const reset = () => setMinConf(stored.bmLoincMinConfidence); return h(React.Fragment, null, h("div", { className: "ref-banner" }, h(Icon, { name: "sparkles", size: 16, className: "dim" }), h("div", null, "Thresholds that govern how the extraction model maps observations onto the knowledge base. ", "Mappings at or above the threshold are accepted automatically; anything below is routed to manual review.")), h(Card, { pad: false, title: "Biomarker → LOINC mapping", subtitle: "Minimum confidence the model must reach to auto-accept a mapping" }, h("div", { className: "set-block" }, h("div", { className: "set-row" }, h("div", { className: "set-row-label" }, h("div", { className: "set-row-title" }, "Minimum confidence score"), h("div", { className: "set-row-desc" }, "When the model links an extracted biomarker to a LOINC code with confidence below this value, the mapping is held for an administrator to confirm.")), h("div", { className: "set-conf" }, h("input", { className: "set-range", type: "range", min: 50, max: 100, step: 1, value: minConf, onChange: (e) => setVal(e.target.value) }), h("div", { className: "set-conf-val" }, h("input", { className: "input set-num", type: "number", min: 50, max: 100, value: minConf, onChange: (e) => setVal(e.target.value) }), h("span", { className: "set-conf-pct" }, "%")))), h("div", { className: "set-scale" }, h("span", null, "50% · permissive"), h("span", null, "100% · strict")), h("div", { className: "set-impact" }, h(Icon, { name: "info", size: 14 }), h("div", null, "At ", h("strong", null, minConf + "%"), ", ", h("strong", null, belowCount), " of ", total, " current biomarkers would fall below the threshold and route to review.")), h("div", { className: "set-actions" }, h(Button, { variant: "ghost", disabled: !dirty, onClick: reset }, "Reset"), h(Button, { variant: "primary", icon: "check", disabled: !dirty, onClick: save }, "Save changes"))))); }