diff --git a/web/src/lib/components/EntityGraph.svelte b/web/src/lib/components/EntityGraph.svelte index b3747f9..028987e 100644 --- a/web/src/lib/components/EntityGraph.svelte +++ b/web/src/lib/components/EntityGraph.svelte @@ -66,8 +66,11 @@ let sim: Simulation | null = null // type → browsing category, so the graph can be scoped client-side (the - // graph endpoint itself has no category/domain param). - let typeCategory = $state>(new Map()) + // graph endpoint itself has no category/domain param). Value is undefined + // for types deliberately excluded from every category (e.g. execution/ + // check/task — see categories.ts); the key is still present so inCategory + // can tell "excluded on purpose" apart from "not in the ontology at all." + let typeCategory = $state>(new Map()) let hoveredId = $state(null) @@ -108,11 +111,13 @@ return typeof end === 'object' ? end.id : end } - // Node belongs to the active category? (Unknown types fall back to visible - // so a missing ontology entry never blanks the graph.) + // Node belongs to the active category? Types the ontology never returned + // at all fall back to visible (so a missing entry never blanks the + // graph); types the ontology returned but categories.ts deliberately + // excludes (key present, value undefined) do not. function inCategory(type: string): boolean { - const c = typeCategory.get(type) - return c === undefined || c === category + if (!typeCategory.has(type)) return true + return typeCategory.get(type) === category } async function load() { @@ -162,9 +167,7 @@ onMount(() => { fetchEntityTypes().then((types) => { - typeCategory = new Map( - types.map((t) => [t.name, typeToCategory(t.name, t.domain)]).filter((e): e is [string, Category] => e[1] !== undefined) - ) + typeCategory = new Map(types.map((t) => [t.name, typeToCategory(t.name, t.domain)])) // Re-derive the active node types now that category membership is known. activeNodeTypes = new Set(nodes.filter((n) => inCategory(n.type)).map((n) => n.type)) }) @@ -259,11 +262,17 @@ // Real infra relationships mostly cross category lines (a service sits on // a network, uses storage, runs on an lxc — different categories under // this taxonomy). Hard-hiding any edge whose other end isn't in-category - // left focus nodes looking like disconnected dots. Instead, pull in their - // 1-hop neighbors (any category) so the edges — and what they connect - // to — stay visible, just visually secondary (see nodeOpacity). + // left focus nodes looking like disconnected dots. Rooted views (the user + // is exploring out from one entity) pull in 1-hop neighbors of any + // category, dimmed, so the edges — and what they connect to — stay + // visible. Unscoped "browse the whole category" views (no root) skip + // this: with ~50 focus nodes that touch nearly everything, 1-hop + // expansion floods in most of the graph (measured: 417 of 479 total + // entities for an unrooted Fleet view) — worse than the isolated-dot + // problem it was meant to fix. There, same-category-only edges stay. const neighborNodeIds = $derived.by(() => { const neighbors = new Set() + if (!root.trim()) return neighbors for (const l of links) { if (!activeRelTypes.has(l.type)) continue const s = endpointId(l.source)