Improve umbrella graph visualization
Some checks failed
plugin-tests / test (push) Failing after 5s

This commit is contained in:
Svrnty 2026-05-26 06:15:27 -04:00
parent 4b1f2075ae
commit 28ffa92f6f
4 changed files with 598 additions and 20 deletions

View File

@ -45,10 +45,21 @@
padding: 6px 10px; font-size: 12px; cursor: pointer;
}
.umbrella-controls button:hover { border-color: var(--accent); color: var(--accent); }
.umbrella-controls button.active {
border-color: var(--accent-2);
color: var(--accent-2);
background: rgba(251,191,36,0.08);
}
.umbrella-filters {
grid-column: 1 / -1;
display: flex; flex-wrap: wrap; gap: 6px;
}
.umbrella-disclosure {
grid-column: 1 / -1;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chip {
background: var(--bg); color: var(--fg-dim);
border: 1px solid #2a2f3a; border-radius: 999px;
@ -56,9 +67,51 @@
font-family: var(--mono);
}
.chip.active { color: var(--accent); border-color: var(--accent); background: rgba(110,231,183,0.08); }
.chip.disclosure-chip { border-style: dashed; }
.chip.disclosure-chip.active { border-style: solid; }
.umbrella-canvas { position: relative; display: grid; grid-template-columns: 1fr; }
.umbrella-canvas { position: relative; display: grid; grid-template-columns: 1fr; overflow: hidden; }
.umbrella-cy { position: absolute; inset: 0; }
.umbrella-layer-overlay {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 1;
}
.umbrella-cy { z-index: 2; }
.layer-band {
position: absolute;
border: 1px solid rgba(231,234,240,0.10);
border-radius: 8px;
background: rgba(21,24,31,0.42);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
}
.layer-band.collapsed {
opacity: 0.76;
}
.layer-band-label {
position: absolute;
left: 12px;
top: 8px;
display: flex;
gap: 8px;
align-items: baseline;
font-family: var(--mono);
color: var(--fg);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
background: rgba(15,17,21,0.72);
border-radius: 4px;
padding: 2px 6px;
}
.layer-band-label .count {
color: var(--fg-dim);
font-size: 10px;
}
.layer-band.collapsed .layer-band-label .count {
color: var(--accent-2);
}
.umbrella-side {
position: absolute; top: 0; right: 0; bottom: 0;
width: 420px; background: var(--bg-2);

View File

@ -16,13 +16,16 @@
<button data-layout="cose">force</button>
<button data-layout="breadthfirst">tier</button>
<button data-layout="concentric">center</button>
<button data-view="spine">spine</button>
<button data-view="org">org chart</button>
<button data-view="mindmap">mindmap</button>
<button id="reset">reset</button>
</div>
<div class="umbrella-filters" id="filters">
<!-- filter chips injected by JS based on node types -->
</div>
<div class="umbrella-filters" id="filters"></div>
<div class="umbrella-disclosure" id="disclosure"></div>
</header>
<main class="umbrella-canvas">
<div id="layerOverlay" class="umbrella-layer-overlay" aria-hidden="true"></div>
<div id="cy" class="umbrella-cy"></div>
<aside id="side" class="umbrella-side" data-open="false">
<button class="close" id="closeSide" title="close">×</button>

View File

@ -6,14 +6,14 @@
"use strict";
const TYPE_COLOR = {
doc: "#6ee7b7",
profile: "#fbbf24",
doc: "#64748b",
profile: "#6ee7b7",
skill: "#a78bfa",
mcp_server: "#60a5fa",
mcp_server: "#f97316",
sovereign_api: "#f97316",
cortex_tool: "#94a3b8",
cortex_tool: "#60a5fa",
external_dep: "#f87171",
credential: "#475569",
credential: "#be647a",
};
const TYPE_SHAPE = {
doc: "round-rectangle",
@ -34,11 +34,24 @@
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();
window.__svrntyUmbrella = { ready: false, error: null, nodes: 0, edges: 0 };
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" });
@ -59,18 +72,58 @@
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";
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 = `${t} (${g.nodes.filter(n => n.type === t).length})`;
chip.textContent = `${quietTypes.has(t) ? "show " : ""}${t} (${g.nodes.filter(n => n.type === t).length})`;
chip.addEventListener("click", () => toggleType(t, chip));
wrap.appendChild(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) {
@ -83,7 +136,9 @@
if (!cy) return;
cy.batch(() => {
cy.nodes().forEach(n => {
const visible = activeTypes.has(n.data("type"));
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 => {
@ -96,7 +151,14 @@
function buildCyElements(g) {
const nodes = g.nodes.map(n => ({
data: { id: n.id, label: n.label || n.id, type: n.type, raw: 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 },
@ -115,21 +177,27 @@
style: {
"background-color": ele => TYPE_COLOR[ele.data("type")] || "#475569",
"shape": ele => TYPE_SHAPE[ele.data("type")] || "ellipse",
"label": "data(label)",
"label": ele => ele.data("important") ? ele.data("shortLabel") : "",
"color": "#e7eaf0",
"text-valign": "bottom",
"text-margin-y": 4,
"font-size": 10,
"font-size": ele => ele.data("fontSize") || 10,
"font-family": "ui-monospace, monospace",
"width": 28,
"height": 28,
"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" },
style: { "border-width": 3, "border-color": "#fbbf24", "label": "data(label)" },
},
{
selector: "node.hover",
style: { "label": "data(label)", "z-index": 20 },
},
{
selector: "edge",
@ -150,8 +218,406 @@
],
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) =>
'<div class="layer-band' + (band.collapsed ? ' collapsed' : '') + '" data-layer="' + band.key + '">' +
'<div class="layer-band-label">' +
'<span>' + band.label + '</span>' +
'<span class="count">' + band.count + (band.collapsed ? ' hidden' : ' nodes') + '</span>' +
'</div>' +
'</div>'
).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) {
@ -240,14 +706,20 @@
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();
@ -268,11 +740,14 @@
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;

View File

@ -0,0 +1,47 @@
"""Static checks for the umbrella graph browser assets."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
HTML = ROOT / "static" / "umbrella.html"
JS = ROOT / "static" / "umbrella.js"
def test_umbrella_exposes_spine_org_and_mindmap_controls():
html = HTML.read_text()
assert 'data-view="spine"' in html
assert 'data-view="org"' in html
assert 'data-view="mindmap"' in html
assert 'id="disclosure"' in html
def test_umbrella_implements_semantic_preset_views():
js = JS.read_text()
html = HTML.read_text()
assert 'id="layerOverlay"' in html
assert "const SPINE_LAYERS" in js
assert "Governance / Protocols" in js
assert "function spineLayout" in js
assert "function spinePositions" in js
assert "function orgPositions" in js
assert "function mindmapPositions" in js
assert 'name: "preset"' in js
assert "positions: (node) => positions[node.id()] || node.position()" in js
assert "window.__svrntyUmbrella.view" in js
def test_umbrella_has_collision_reduction_rules():
js = JS.read_text()
assert "function _estimatedLabelWidth" in js
assert "function _wrapGroupRows" in js
assert "function applyLabelDensity" in js
assert "function renderLayerOverlay" in js
assert "function applyDefaultDisclosure" in js
assert 'new Set(["credential"])' in js
assert 'new Set(["knowledge"])' in js
assert "function toggleLayerDisclosure" in js
assert "function resetGraphEmphasis" in js
assert "function focusNode" in js
assert 'selector: "node.hover"' in js
assert 'cy.on("zoom", () => applyLabelDensity())' in js
assert "window.cy = cy" in js