fix(web): render full document content, keep graph connected under categories
Two fixes to the new category taxonomy:
- Knowledge Base couldn't show a document/investigation/runbook's own
markdown body — knowledge_entities.content was never exposed by any
endpoint (GetEntityKnowledge answers "what knowledge references this
entity", not "what is this entity's content"). Add GET
/api/v1/knowledge/content/{id} and render it with the existing
marked+DOMPurify pipeline in a new Content section.
- The graph hid any edge whose other endpoint wasn't in the active
category, so nodes with only cross-category neighbors rendered as
disconnected dots. Queried the real relationship table: ~70% of infra
edges cross Fleet/Network/Services/Storage lines (compute+network+
software+storage+physical used to be one "infrastructure" layer).
EntityGraph now keeps 1-hop neighbors visible but dimmed instead of
hiding them, so the edges — and what they connect to — stay visible.
- categories.ts: `cognition` domain conflated true knowledge (document/
investigation/runbook, 58 entities) with operational telemetry
(execution/check/task/signal/approval/pattern/skill/classification/
feedback, 300+ entities with their own Operations/Signals/Learning
pages). Mapping the whole domain to Knowledge pulled in 245 execution
entities fanning out from ~17 compute nodes via `targets` edges — the
single biggest source of graph clutter. Knowledge now maps by type
(document/investigation/runbook only); the rest of cognition is
excluded from Knowledge Base browsing entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { forceSimulation, forceLink, forceManyBody, forceCenter, forceCollide, forceX, forceY, type Simulation } from 'd3-force'
|
||||
import { fetchGraph, fetchEntityTypes, type GraphView, type Entity, type Health } from '$lib/api'
|
||||
import { liveEvents, subscribeEvents } from '$lib/stores/events'
|
||||
import { domainToCategory, type Category } from '$lib/categories'
|
||||
import { typeToCategory, type Category } from '$lib/categories'
|
||||
import { Skeleton } from '$lib/components/ui/skeleton'
|
||||
|
||||
export interface GraphInfo {
|
||||
@@ -163,7 +163,7 @@
|
||||
onMount(() => {
|
||||
fetchEntityTypes().then((types) => {
|
||||
typeCategory = new Map(
|
||||
types.map((t) => [t.name, domainToCategory(t.domain)]).filter((e): e is [string, Category] => e[1] !== undefined)
|
||||
types.map((t) => [t.name, typeToCategory(t.name, t.domain)]).filter((e): e is [string, Category] => e[1] !== undefined)
|
||||
)
|
||||
// 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))
|
||||
@@ -252,8 +252,29 @@
|
||||
return new Set(nodes.filter((n) => n.slug.toLowerCase().includes(q) || n.name.toLowerCase().includes(q)).map((n) => n.id))
|
||||
})
|
||||
|
||||
// Visible = in the active category AND its node-type toggle is on.
|
||||
const visibleNodeIds = $derived(new Set(nodes.filter((n) => inCategory(n.type) && activeNodeTypes.has(n.type)).map((n) => n.id)))
|
||||
// Focus = in the active category AND its node-type toggle is on — these
|
||||
// are what the category tab is "about."
|
||||
const focusNodeIds = $derived(new Set(nodes.filter((n) => inCategory(n.type) && activeNodeTypes.has(n.type)).map((n) => n.id)))
|
||||
|
||||
// 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).
|
||||
const neighborNodeIds = $derived.by(() => {
|
||||
const neighbors = new Set<string>()
|
||||
for (const l of links) {
|
||||
if (!activeRelTypes.has(l.type)) continue
|
||||
const s = endpointId(l.source)
|
||||
const t = endpointId(l.target)
|
||||
if (focusNodeIds.has(s) && !focusNodeIds.has(t)) neighbors.add(t)
|
||||
else if (focusNodeIds.has(t) && !focusNodeIds.has(s)) neighbors.add(s)
|
||||
}
|
||||
return neighbors
|
||||
})
|
||||
|
||||
const visibleNodeIds = $derived(new Set([...focusNodeIds, ...neighborNodeIds]))
|
||||
|
||||
const selectedId = $derived(nodes.find((n) => n.slug === selectedSlug)?.id ?? null)
|
||||
|
||||
@@ -279,9 +300,10 @@
|
||||
})
|
||||
|
||||
function nodeOpacity(node: Node): number {
|
||||
if (matchedIds !== null) return matchedIds.has(node.id) ? 1 : 0.15
|
||||
if (focusIds !== null) return focusIds.has(node.id) ? 1 : 0.15
|
||||
return 1
|
||||
const base = focusNodeIds.has(node.id) ? 1 : 0.4
|
||||
if (matchedIds !== null) return matchedIds.has(node.id) ? base : 0.1
|
||||
if (focusIds !== null) return focusIds.has(node.id) ? 1 : Math.min(base, 0.15)
|
||||
return base
|
||||
}
|
||||
|
||||
function linkVisualState(link: Link): { opacity: number; emphasized: boolean } {
|
||||
|
||||
Reference in New Issue
Block a user