/* 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: "#6ee7b7", profile: "#fbbf24", skill: "#a78bfa", mcp_server: "#60a5fa", sovereign_api: "#f97316", cortex_tool: "#94a3b8", external_dep: "#f87171", credential: "#475569", }; 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" }, }; let cy = null; let graph = null; const activeTypes = new Set(); 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"); wrap.innerHTML = ""; types.forEach(t => { activeTypes.add(t); const chip = document.createElement("button"); chip.className = "chip active"; chip.dataset.type = t; chip.style.borderColor = TYPE_COLOR[t] || "#475569"; chip.style.color = TYPE_COLOR[t] || "#e7eaf0"; chip.textContent = `${t} (${g.nodes.filter(n => n.type === t).length})`; chip.addEventListener("click", () => toggleType(t, chip)); wrap.appendChild(chip); }); } 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 visible = activeTypes.has(n.data("type")); 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, type: n.type, raw: 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": "data(label)", "color": "#e7eaf0", "text-valign": "bottom", "text-margin-y": 4, "font-size": 10, "font-family": "ui-monospace, monospace", "width": 28, "height": 28, "border-width": 1, "border-color": "#0f1115", }, }, { selector: "node:selected", style: { "border-width": 3, "border-color": "#fbbf24" }, }, { 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 }, }); cy.on("tap", "node", (evt) => openSidePanel(evt.target.data("raw"))); cy.on("tap", (evt) => { if (evt.target === cy) closeSidePanel(); }); } 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; 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(); }); }); 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(); } catch (e) { document.getElementById("stats").textContent = "load failed: " + e.message; console.error("[umbrella]", e); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();