/* Cortex-OS Umbrella panel — Cytoscape.js render of /api/umbrella graph. * Consumes UI-stable schema v1.0 per CORTEX-OS-UMBRELLA-VIZ-PRD.md. * Curator-maintained graph artifact at graph/umbrella.json. */ (function () { "use strict"; const TYPE_COLOR = { doc: "#64748b", profile: "#6ee7b7", skill: "#a78bfa", mcp_server: "#f97316", sovereign_api: "#f97316", cortex_tool: "#60a5fa", external_dep: "#f87171", credential: "#be647a", }; const TYPE_SHAPE = { doc: "round-rectangle", profile: "hexagon", skill: "ellipse", mcp_server: "diamond", sovereign_api: "rectangle", cortex_tool: "round-rectangle", external_dep: "triangle", credential: "vee", }; const EDGE_STYLE = { depends_on: { color: "#475569", style: "solid" }, governs: { color: "#fbbf24", style: "solid" }, consumes: { color: "#6ee7b7", style: "solid" }, produces: { color: "#a78bfa", style: "dashed" }, supersedes: { color: "#f87171", style: "dashed" }, cites: { color: "#38bdf8", style: "dotted" }, derives_from: { color: "#f59e0b", style: "dashed" }, }; const SPINE_LAYERS = [ { key: "system", label: "System Spine", color: "#e7eaf0", baseWidth: 920 }, { key: "governance", label: "Governance / Protocols", color: "#fbbf24", baseWidth: 1240 }, { key: "runtime", label: "Runtime / WebUI / Services", color: "#60a5fa", baseWidth: 880 }, { key: "profiles", label: "Agents / Profiles", color: "#6ee7b7", baseWidth: 680 }, { key: "capabilities", label: "Skills / Tools / APIs", color: "#a78bfa", baseWidth: 2200 }, { key: "projects", label: "Projects / Domain Systems", color: "#14b8a6", baseWidth: 760 }, { key: "knowledge", label: "SOT / Evidence / Outputs", color: "#94a3b8", baseWidth: 2200 }, ]; let cy = null; let graph = null; let layerOverlayState = null; const activeTypes = new Set(); const quietTypes = new Set(["credential"]); const quietLayers = new Set(["knowledge"]); const disclosedLayers = new Set(); window.__svrntyUmbrella = { ready: false, error: null, nodes: 0, edges: 0, view: "force" }; async function loadGraph() { const res = await fetch("/api/umbrella", { cache: "no-store" }); if (!res.ok) throw new Error("graph load failed: " + res.status); return await res.json(); } function renderStats(g) { const s = g.stats || {}; const byType = s.by_type || {}; document.getElementById("stats").textContent = `nodes ${s.total_nodes || g.nodes.length} · edges ${s.total_edges || g.edges.length} · ` + Object.entries(byType).map(([k, v]) => `${k}:${v}`).join(" "); document.getElementById("genInfo").textContent = `generated ${g.generated_at || "?"} by ${g.generated_by || "?"}`; } function renderFilters(g) { const types = [...new Set(g.nodes.map(n => n.type))].sort(); const wrap = document.getElementById("filters"); const disclosure = document.getElementById("disclosure"); wrap.innerHTML = ""; disclosure.innerHTML = ""; types.forEach(t => { activeTypes.add(t); const chip = document.createElement("button"); chip.className = "chip active" + (quietTypes.has(t) ? " disclosure-chip" : ""); chip.dataset.type = t; chip.style.borderColor = TYPE_COLOR[t] || "#475569"; chip.style.color = TYPE_COLOR[t] || "#e7eaf0"; chip.textContent = `${quietTypes.has(t) ? "show " : ""}${t} (${g.nodes.filter(n => n.type === t).length})`; chip.addEventListener("click", () => toggleType(t, chip)); (quietTypes.has(t) ? disclosure : wrap).appendChild(chip); }); quietLayers.forEach((layer) => { const count = g.nodes.filter((n) => _spineLayerKey(n) === layer).length; const chip = document.createElement("button"); chip.className = "chip disclosure-chip"; chip.dataset.layerDisclosure = layer; chip.textContent = `show ${_layerLabel(layer)} (${count})`; chip.addEventListener("click", () => toggleLayerDisclosure(layer, chip)); disclosure.appendChild(chip); }); } function applyDefaultDisclosure() { quietTypes.forEach((t) => activeTypes.delete(t)); disclosedLayers.clear(); document.querySelectorAll(".chip[data-type]").forEach((chip) => { chip.classList.toggle("active", activeTypes.has(chip.dataset.type)); }); document.querySelectorAll(".chip[data-layer-disclosure]").forEach((chip) => { chip.classList.toggle("active", disclosedLayers.has(chip.dataset.layerDisclosure)); }); applyFilter(); } function _layerLabel(layer) { if (layer === "knowledge") return "SOT / evidence"; return layer; } function toggleLayerDisclosure(layer, chipEl) { if (disclosedLayers.has(layer)) { disclosedLayers.delete(layer); chipEl.classList.remove("active"); } else { disclosedLayers.add(layer); chipEl.classList.add("active"); } applyFilter(); if (window.__svrntyUmbrella.view === "spine") runPresetView("spine"); } function toggleType(t, chipEl) { if (activeTypes.has(t)) { activeTypes.delete(t); chipEl.classList.remove("active"); } else { activeTypes.add(t); chipEl.classList.add("active"); } applyFilter(); } function applyFilter() { if (!cy) return; cy.batch(() => { cy.nodes().forEach(n => { const layer = n.data("layer"); const visible = activeTypes.has(n.data("type")) && (!quietLayers.has(layer) || disclosedLayers.has(layer)); n.style("display", visible ? "element" : "none"); }); cy.edges().forEach(e => { const s = e.source().style("display"); const t = e.target().style("display"); e.style("display", (s === "element" && t === "element") ? "element" : "none"); }); }); } function buildCyElements(g) { const nodes = g.nodes.map(n => ({ data: { id: n.id, label: n.label || n.id, shortLabel: _shortLabel(n.label || n.id), type: n.type, raw: n, ..._visualMeta(n), }, })); const edges = g.edges.map((e, i) => ({ data: { id: `e${i}`, source: e.source, target: e.target, type: e.type, raw: e }, })); return [...nodes, ...edges]; } function renderGraph(g) { cy = cytoscape({ container: document.getElementById("cy"), elements: buildCyElements(g), wheelSensitivity: 0.2, style: [ { selector: "node", style: { "background-color": ele => TYPE_COLOR[ele.data("type")] || "#475569", "shape": ele => TYPE_SHAPE[ele.data("type")] || "ellipse", "label": ele => ele.data("important") ? ele.data("shortLabel") : "", "color": "#e7eaf0", "text-valign": "bottom", "text-margin-y": 4, "font-size": ele => ele.data("fontSize") || 10, "font-family": "ui-monospace, monospace", "width": ele => ele.data("nodeWidth") || 28, "height": ele => ele.data("nodeHeight") || 28, "border-width": 1, "border-color": "#0f1115", "text-wrap": "wrap", "text-max-width": ele => Math.max(90, (ele.data("nodeWidth") || 28) + 44), }, }, { selector: "node:selected", style: { "border-width": 3, "border-color": "#fbbf24", "label": "data(label)" }, }, { selector: "node.hover", style: { "label": "data(label)", "z-index": 20 }, }, { selector: "edge", style: { "width": 1, "line-color": ele => (EDGE_STYLE[ele.data("type")] || {}).color || "#475569", "line-style": ele => (EDGE_STYLE[ele.data("type")] || {}).style || "solid", "curve-style": "bezier", "target-arrow-shape": "triangle", "target-arrow-color": ele => (EDGE_STYLE[ele.data("type")] || {}).color || "#475569", "opacity": 0.5, }, }, { selector: "edge:selected", style: { "opacity": 1, "width": 2 }, }, ], layout: { name: "cose", animate: false, idealEdgeLength: 80, nodeRepulsion: 12000 }, }); window.cy = cy; cy.on("tap", "node", (evt) => openSidePanel(evt.target.data("raw"))); cy.on("tap", (evt) => { if (evt.target === cy) closeSidePanel(); }); cy.on("mouseover", "node", (evt) => focusNode(evt.target)); cy.on("mouseout", "node", (evt) => { evt.target.removeClass("hover"); resetGraphEmphasis(); }); cy.on("zoom", () => applyLabelDensity()); cy.on("pan zoom render", () => updateLayerOverlay()); } function _shortLabel(value) { const label = String(value || ""); if (label.length <= 24) return label; return label.slice(0, 21) + "..."; } function _estimatedLabelWidth(node) { const label = String(node.label || node.id || ""); const layer = node.layer || _spineLayerKey(node); const priority = node.labelPriority || _labelPriority(node, layer); const size = node.nodeWidth ? node : _nodeSize(node, layer, priority); return Math.min(320, Math.max(size.nodeWidth || size.width || 92, label.length * 7 + 44)); } function _isImportantNode(node) { const id = node.id || ""; return node.type === "profile" || ["readme", "architecture", "cortex-os-framework", "cortex-os-roadmap"].includes(id); } function _visualMeta(node) { const layer = _spineLayerKey(node); const priority = _labelPriority(node, layer); const size = _nodeSize(node, layer, priority); return { layer, labelPriority: priority, important: priority <= 1, nodeWidth: size.width, nodeHeight: size.height, fontSize: size.fontSize, }; } function _labelPriority(node, layer) { if (layer === "system") return 0; if (node.type === "profile") return 0; if (layer === "runtime" || layer === "projects") return 1; if (["skill", "mcp_server", "cortex_tool", "sovereign_api", "external_dep"].includes(node.type)) return 2; return 3; } function _nodeSize(node, layer, priority) { if (layer === "system") return { width: 148, height: 54, fontSize: 12 }; if (node.type === "profile") return { width: 118, height: 48, fontSize: 11 }; if (layer === "runtime" || layer === "projects") return { width: 108, height: 42, fontSize: 10 }; if (priority === 2) return { width: 82, height: 34, fontSize: 9 }; return { width: 34, height: 28, fontSize: 9 }; } function applyLabelDensity() { if (!cy) return; const zoom = cy.zoom(); cy.batch(() => { cy.nodes().forEach((node) => { if (node.selected() || node.hasClass("hover")) return; const priority = node.data("labelPriority"); const label = priority <= 1 || (priority === 2 && zoom >= 0.85) || zoom >= 1.25 ? node.data("shortLabel") : ""; node.style("label", label); }); }); } function resetGraphEmphasis() { if (!cy) return; const inSpine = window.__svrntyUmbrella.view === "spine"; cy.batch(() => { cy.nodes().style("opacity", 1); cy.edges().style({ "opacity": inSpine ? 0.14 : 0.5, "width": 1 }); }); applyLabelDensity(); } function focusNode(node) { if (!cy) return; node.addClass("hover"); const neighborhood = node.closedNeighborhood(); cy.batch(() => { cy.elements().not(neighborhood).style("opacity", 0.16); neighborhood.nodes().style("opacity", 1); cy.edges().style({ "opacity": 0.05, "width": 1 }); node.connectedEdges().style({ "opacity": 0.9, "width": 2 }); }); } function _sourcePath(node) { return String((node && node.source_path) || ""); } function _docCategory(node) { const path = _sourcePath(node); if (path.includes("/01-ROADMAP/")) return "roadmap"; if (path.includes("/02-FRAMEWORK/")) return "framework"; if (path.includes("/03-PROTOCOLS/")) return "protocols"; if (path.includes("/04-STANDARDS/")) return "standards"; if (path.includes("/05-RECIPES/")) return "recipes"; if (path.includes("/06-REGISTRY/")) return "registry"; if (path.includes("/07-BRAND/")) return "brand"; if (path.includes("/08-OUTPUTS/")) return "outputs"; if (path.includes("/99-RAW/")) return "raw"; return "docs"; } function _spineLayerKey(node) { const id = node.id || ""; const source = _sourcePath(node); if (["readme", "architecture", "cortex-os-framework", "cortex-os-roadmap"].includes(id)) return "system"; if (["hermes-webui", "hermes-agent", "llm-gateway"].includes(id) || source.startsWith("hermes-webui/")) return "runtime"; if (node.type === "profile") return "profiles"; if (node.type === "skill") return "capabilities"; if (["mcp_server", "cortex_tool", "sovereign_api", "external_dep", "credential"].includes(node.type)) return "capabilities"; if (["bte", "bte-mcp", "adwright", "adwright-mcp", "svrnty-vision", "deep-research", "sandcastle", "pi-code"].includes(id)) return "projects"; if (source && !source.startsWith("sot/")) return "projects"; if (node.type === "doc") { const cat = _docCategory(node); if (["framework", "protocols", "standards"].includes(cat)) return "governance"; return "knowledge"; } return "knowledge"; } function _groupRows(nodes, keyFn) { const groups = new Map(); nodes.forEach((node) => { const key = keyFn(node); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(node); }); groups.forEach((items) => items.sort((a, b) => String(a.label || a.id).localeCompare(String(b.label || b.id)))); return [...groups.entries()].sort((a, b) => String(a[0]).localeCompare(String(b[0]))); } function _wrapGroupRows(groups, width, minGap) { const rows = []; groups.forEach(([group, items]) => { let row = []; let used = 0; items.forEach((node) => { const need = _estimatedLabelWidth(node) + minGap; if (row.length && used + need > width) { rows.push([group, row]); row = []; used = 0; } row.push(node); used += need; }); if (row.length) rows.push([group, row]); }); return rows; } function _rowPositions(groups, opts) { const positions = {}; const width = Math.max(opts.width || 1200, 900); const top = opts.top || 80; const rowGap = opts.rowGap || 120; const minGap = opts.minGap || 78; const rows = _wrapGroupRows(groups, Math.max(width - 180, 720), minGap); rows.forEach(([_, items], rowIndex) => { const totalLabelWidth = items.reduce((sum, node) => sum + _estimatedLabelWidth(node), 0); const rowWidth = Math.max(width - 180, totalLabelWidth + Math.max(0, items.length - 1) * minGap); const startX = (width - rowWidth) / 2; const availableGap = items.length > 1 ? (rowWidth - totalLabelWidth) / (items.length - 1) : 0; let cursor = startX; items.forEach((node, i) => { const labelWidth = _estimatedLabelWidth(node); positions[node.id] = { x: items.length === 1 ? width / 2 : cursor + labelWidth / 2, y: top + rowIndex * rowGap, }; cursor += labelWidth + Math.max(minGap, availableGap); }); }); return positions; } function _layerRows(items, layerWidth) { const innerWidth = Math.max(layerWidth - 120, 360); const rows = []; let row = []; let used = 0; items.forEach((node) => { const need = _estimatedLabelWidth(node) + 56; if (row.length && used + need > innerWidth) { rows.push(row); row = []; used = 0; } row.push(node); used += need; }); if (row.length) rows.push(row); return rows; } function spineLayout(g) { const layerMap = new Map(); SPINE_LAYERS.forEach((layer) => layerMap.set(layer.key, [])); g.nodes.forEach((node) => { const layer = _spineLayerKey(node); if (!layerMap.has(layer)) layerMap.set("knowledge", []); layerMap.get(layerMap.has(layer) ? layer : "knowledge").push(node); }); layerMap.forEach((items) => { items.sort((a, b) => { const av = _labelPriority(a, _spineLayerKey(a)); const bv = _labelPriority(b, _spineLayerKey(b)); return av - bv || String(a.label || a.id).localeCompare(String(b.label || b.id)); }); }); const positions = {}; const bands = []; let y = 80; SPINE_LAYERS.forEach((layer, layerIndex) => { const allItems = layerMap.get(layer.key) || []; if (!allItems.length) return; const collapsed = quietLayers.has(layer.key) && !disclosedLayers.has(layer.key); const items = collapsed ? [] : allItems; if (collapsed) { const layerWidth = Math.max(900, Math.min(1800, layer.baseWidth || 900)); const x1 = -layerWidth / 2; const x2 = layerWidth / 2; const y1 = y; const y2 = y + 52; bands.push({ key: layer.key, label: layer.label, count: allItems.length, color: layer.color, x1, x2, y1, y2, collapsed: true }); y = y2 + 42; return; } const density = items.reduce((sum, node) => sum + _estimatedLabelWidth(node), 0); const layerWidth = Math.min(4200, Math.max(layer.baseWidth || 900, density * 0.72)); const rows = _layerRows(items, layerWidth); const bandHeight = Math.max(138, rows.length * 92 + 74); const x1 = -layerWidth / 2; const x2 = layerWidth / 2; const y1 = y; const y2 = y + bandHeight; bands.push({ key: layer.key, label: layer.label, count: items.length, color: layer.color, x1, x2, y1, y2, collapsed: false }); rows.forEach((row, rowIndex) => { const rowWidth = row.reduce((sum, node) => sum + _estimatedLabelWidth(node), 0) + Math.max(0, row.length - 1) * 58; let cursor = -rowWidth / 2; row.forEach((node) => { const w = _estimatedLabelWidth(node); positions[node.id] = { x: cursor + w / 2, y: y1 + 78 + rowIndex * 92, }; cursor += w + 58; }); }); y = y2 + 70; }); return { positions, bands }; } function spinePositions(g) { return spineLayout(g).positions; } function orgPositions(g) { const bounds = document.getElementById("cy").getBoundingClientRect(); const profiles = g.nodes.filter((n) => n.type === "profile"); const profileIds = new Set(profiles.map((p) => p.id.replace(/-planb$/, ""))); const groups = [["system", g.nodes.filter((n) => ["readme", "architecture", "cortex-os-framework"].includes(n.id))]]; groups.push(["profiles", profiles]); profiles.forEach((profile) => { const shortId = profile.id.replace(/-planb$/, ""); groups.push([profile.id + "-skills", g.nodes.filter((n) => n.type === "skill" && (n.id.startsWith(profile.id + "::") || n.id.startsWith(shortId + "-")) )]); }); groups.push(["tools", g.nodes.filter((n) => ["mcp_server", "cortex_tool", "sovereign_api", "external_dep"].includes(n.type))]); groups.push(["credentials", g.nodes.filter((n) => n.type === "credential")]); groups.push(["sot", g.nodes.filter((n) => n.type === "doc" && !["readme", "architecture", "cortex-os-framework"].includes(n.id))]); return _rowPositions(groups.filter(([, items]) => items.length), { width: bounds.width, top: 70, rowGap: 120, minGap: 86, }); } function mindmapPositions(g) { const bounds = document.getElementById("cy").getBoundingClientRect(); const width = Math.max(bounds.width, 900); const height = Math.max(bounds.height, 700); const center = { x: width / 2, y: height / 2 }; const positions = {}; const rootIds = new Set(["readme", "architecture", "cortex-os-framework"]); const roots = g.nodes.filter((n) => rootIds.has(n.id)); roots.forEach((node, i) => { positions[node.id] = { x: center.x - 80 + i * 80, y: center.y }; }); const groups = _groupRows(g.nodes.filter((n) => !rootIds.has(n.id)), (n) => n.type || "other"); const maxRadius = Math.min(width, height) * 0.42; groups.forEach(([_, items], groupIndex) => { const angleStart = (Math.PI * 2 * groupIndex) / groups.length; const angleEnd = (Math.PI * 2 * (groupIndex + 1)) / groups.length; items.forEach((node, i) => { const t = (i + 1) / (items.length + 1); const angle = angleStart + (angleEnd - angleStart) * t; const ring = Math.floor(i / Math.max(1, Math.ceil(items.length / 4))); const radius = maxRadius * (0.35 + 0.16 * Math.min(ring, 4)); positions[node.id] = { x: center.x + Math.cos(angle) * radius, y: center.y + Math.sin(angle) * radius, }; }); }); return positions; } function clearLayerOverlay() { layerOverlayState = null; const overlay = document.getElementById("layerOverlay"); if (overlay) overlay.innerHTML = ""; } function renderLayerOverlay(bands) { const overlay = document.getElementById("layerOverlay"); if (!overlay || !cy) return; layerOverlayState = bands; overlay.innerHTML = bands.map((band) => '
' + '
' + '' + band.label + '' + '' + band.count + (band.collapsed ? ' hidden' : ' nodes') + '' + '
' + '
' ).join(""); updateLayerOverlay(); } function updateLayerOverlay() { if (!layerOverlayState || !cy) return; const overlay = document.getElementById("layerOverlay"); if (!overlay) return; const z = cy.zoom(); const pan = cy.pan(); overlay.querySelectorAll(".layer-band").forEach((el, index) => { const band = layerOverlayState[index]; if (!band) return; const left = band.x1 * z + pan.x; const top = band.y1 * z + pan.y; const width = (band.x2 - band.x1) * z; const height = (band.y2 - band.y1) * z; el.style.left = left + "px"; el.style.top = top + "px"; el.style.width = width + "px"; el.style.height = height + "px"; el.style.borderColor = band.color + "44"; el.style.background = band.collapsed ? "linear-gradient(90deg, " + band.color + "0f, rgba(21,24,31,0.20))" : "linear-gradient(90deg, " + band.color + "14, rgba(21,24,31,0.34))"; }); } function setActiveView(name) { document.querySelectorAll("button[data-layout], button[data-view]").forEach((button) => { button.classList.toggle("active", button.dataset.layout === name || button.dataset.view === name); }); window.__svrntyUmbrella.view = name; } function runPresetView(name) { if (!cy || !graph) return; let positions = null; if (name === "spine") { const layout = spineLayout(graph); positions = layout.positions; renderLayerOverlay(layout.bands); } else { clearLayerOverlay(); positions = name === "org" ? orgPositions(graph) : mindmapPositions(graph); } setActiveView(name); cy.layout({ name: "preset", positions: (node) => positions[node.id()] || node.position(), animate: true, fit: true, padding: 48, }).run(); setTimeout(resetGraphEmphasis, 250); setTimeout(updateLayerOverlay, 260); } async function openSidePanel(node) { document.getElementById("side").dataset.open = "true"; document.getElementById("sideTitle").textContent = node.label || node.id; const meta = document.getElementById("sideMeta"); meta.innerHTML = ""; const metaFields = ["type", "tier", "status", "category", "owner", "role", "pin"]; metaFields.forEach(f => { if (node[f] != null) { const d = document.createElement("div"); d.innerHTML = `${f}: ${node[f]}`; meta.appendChild(d); } }); if (node.governance) { Object.entries(node.governance).forEach(([k, v]) => { if (v != null) { const d = document.createElement("div"); d.innerHTML = `gov.${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`; meta.appendChild(d); } }); } if (node.description) { const d = document.createElement("div"); d.style.marginTop = "8px"; d.style.color = "#e7eaf0"; d.textContent = node.description; meta.appendChild(d); } // Body — fetch markdown if source_path const body = document.getElementById("sideBody"); if (node.source_path) { body.textContent = "loading…"; try { const r = await fetch("/api/umbrella/doc?path=" + encodeURIComponent(node.source_path), { cache: "no-store" }); if (r.ok) { const j = await r.json(); body.textContent = j.body || "(empty)"; } else { body.textContent = "(no doc — " + r.status + ")"; } } catch (e) { body.textContent = "(fetch error: " + e + ")"; } } else { body.textContent = "(no source_path)"; } // Edges const edges = document.getElementById("sideEdges"); edges.innerHTML = ""; if (!cy) return; const cyNode = cy.getElementById(node.id); const outgoing = cyNode.connectedEdges().filter(e => e.source().id() === node.id); const incoming = cyNode.connectedEdges().filter(e => e.target().id() === node.id); if (outgoing.length) { edges.innerHTML += `

outgoing (${outgoing.length})

`; } if (incoming.length) { edges.innerHTML += `

incoming (${incoming.length})

`; } edges.querySelectorAll("button[data-id]").forEach(b => { b.addEventListener("click", () => { const id = b.dataset.id; const n = graph.nodes.find(n => n.id === id); if (n) { openSidePanel(n); cy.getElementById(id).select(); } }); }); } function closeSidePanel() { document.getElementById("side").dataset.open = "false"; if (cy) cy.elements().unselect(); } function bindControls() { document.getElementById("closeSide").addEventListener("click", closeSidePanel); document.getElementById("reset").addEventListener("click", () => cy.fit(null, 30)); document.querySelectorAll("button[data-layout]").forEach(b => { b.addEventListener("click", () => { const name = b.dataset.layout; clearLayerOverlay(); setActiveView(name); const opts = name === "cose" ? { name, animate: true, idealEdgeLength: 80, nodeRepulsion: 12000 } : name === "breadthfirst" ? { name, animate: true, directed: true, padding: 10 } : { name, animate: true }; cy.layout(opts).run(); setTimeout(resetGraphEmphasis, 250); }); }); document.querySelectorAll("button[data-view]").forEach(b => { b.addEventListener("click", () => runPresetView(b.dataset.view)); }); const search = document.getElementById("search"); search.addEventListener("input", () => { const q = search.value.trim().toLowerCase(); cy.batch(() => { cy.nodes().forEach(n => { const hit = !q || n.data("id").toLowerCase().includes(q) || (n.data("raw").description || "").toLowerCase().includes(q); n.style("opacity", hit ? 1 : 0.15); }); }); }); } async function init() { try { graph = await loadGraph(); renderStats(graph); renderFilters(graph); renderGraph(graph); bindControls(); runPresetView("spine"); applyDefaultDisclosure(); window.__svrntyUmbrella = { ready: true, error: null, nodes: graph.nodes.length, edges: graph.edges.length, view: window.__svrntyUmbrella.view || "spine", }; } catch (e) { document.getElementById("stats").textContent = "load failed: " + e.message; window.__svrntyUmbrella = { ready: false, error: String(e.message || e), nodes: 0, edges: 0 }; console.error("[umbrella]", e); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();