-
-
{signal.kind}
-
- {signal.severity}
- {signal.state}
-
+
0}>
+
+ {#each signals as signal (signal.id)}
+
+
+
{signal.kind}
+
+ {signal.severity}
+ {signal.state}
- {#if ['raised', 'acknowledged', 'acting'].includes(signal.state)}
-
- {#if signal.state === 'raised'}
-
- {/if}
+
+ {#if ['raised', 'acknowledged', 'acting'].includes(signal.state)}
+
+ {#if signal.state === 'raised'}
ackOpenSignal(signal.id)}>Ack
-
-
- {/if}
-
- {:else}
-
None.
- {/each}
-
-
-
-
-
- Executions
-
-
- {#each executions as execution (execution.id)}
-
- {execution.action}
- {execution.status}
-
- {:else}
- None.
- {/each}
-
-
-
-
-
- Knowledge
-
-
- {#each knowledge as hit (hit.id)}
-
- {hit.type}{hit.title}
-
- {:else}
- None linked.
- {/each}
-
-
-
-
-
-
-
- Recent events
-
-
- {#each events as ev (ev.id)}
-
- {new Date(ev.ts).toLocaleString()}
- {ev.type}
-
- {:else}
- No events yet.
- {/each}
-
-
-
-
-
- Agent activity
-
-
- {#each agentActivity as activity (activity.id)}
-
-
- {new Date(activity.ts).toLocaleString()}
- {activity.activity_type}
+ {/if}
+
+
-
{activity.agent_id}{activity.tool_name ? ` · ${activity.tool_name}` : ''}
-
- {:else}
- No agent activity.
- {/each}
-
-
+ {/if}
+
+ {:else}
+
None.
+ {/each}
+
+
-
-
- Audit trail
-
-
- {#each auditEntries as entry (entry.id)}
-
-
- {new Date(entry.ts).toLocaleString()}
- {entry.actor_type}
-
-
{entry.actor_id ?? '—'} · {entry.action}
+
0}>
+
+ {#each executions as execution (execution.id)}
+
+ {execution.action}
+ {execution.status}
+
+ {:else}
+
None.
+ {/each}
+
+
+
+
0}>
+
+ {#each knowledge as hit (hit.id)}
+
+ {hit.type}{hit.title}
+
+ {:else}
+
None linked.
+ {/each}
+
+
+
+
0}>
+
+ {#each events as ev (ev.id)}
+
+ {new Date(ev.ts).toLocaleString()}
+ {ev.type}
+
+ {:else}
+
No events yet.
+ {/each}
+
+
+
+
0}>
+
+ {#each agentActivity as activity (activity.id)}
+
+
+ {new Date(activity.ts).toLocaleString()}
+ {activity.activity_type}
- {:else}
-
No audit entries.
- {/each}
-
-
-
+
{activity.agent_id}{activity.tool_name ? ` · ${activity.tool_name}` : ''}
+
+ {:else}
+ No agent activity.
+ {/each}
+
+
+
+ 0}>
+
+ {#each auditEntries as entry (entry.id)}
+
+
+ {new Date(entry.ts).toLocaleString()}
+ {entry.actor_type}
+
+
{entry.actor_id ?? '—'} · {entry.action}
+
+ {:else}
+
No audit entries.
+ {/each}
+
+
{/if}
diff --git a/web/src/lib/components/EntityGraph.svelte b/web/src/lib/components/EntityGraph.svelte
new file mode 100644
index 0000000..5013952
--- /dev/null
+++ b/web/src/lib/components/EntityGraph.svelte
@@ -0,0 +1,476 @@
+
+
+{#if loading && !nodes.length}
+
+{:else}
+
+
+
+ scroll to zoom · drag background to pan · drag nodes · click to inspect · double-click to re-root
+
+
+{/if}
diff --git a/web/src/lib/components/EntitySheet.svelte b/web/src/lib/components/EntitySheet.svelte
index f74aaf1..f981160 100644
--- a/web/src/lib/components/EntitySheet.svelte
+++ b/web/src/lib/components/EntitySheet.svelte
@@ -3,16 +3,26 @@
import * as Sheet from '$lib/components/ui/sheet'
let { slug, open = $bindable(false) }: { slug: string | null; open?: boolean } = $props()
+
+ // Lets a relation click inside the sheet drill into that entity in place,
+ // without closing/reopening. Resets to the externally-requested slug
+ // whenever the caller opens the sheet on a different entity.
+ let currentSlug = $state(slug)
+ $effect(() => {
+ currentSlug = slug
+ })
- {slug ?? 'Entity detail'}
+ {currentSlug ?? 'Entity detail'}
Entity detail panel
- {#if slug}
-
+ {#if currentSlug}
+ {#key currentSlug}
+ (currentSlug = s)} />
+ {/key}
{/if}
diff --git a/web/src/lib/components/EntityTable.svelte b/web/src/lib/components/EntityTable.svelte
new file mode 100644
index 0000000..fbf30cc
--- /dev/null
+++ b/web/src/lib/components/EntityTable.svelte
@@ -0,0 +1,178 @@
+
+
+{#if loading}
+
+
+
+
+ Slug
+ Type
+ Name
+ State
+ Health
+
+
+
+ {#each skeletonSlugWidths as slugWidth, i}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/each}
+
+
+
+{:else}
+ {#snippet sortHead(key: SortKey, label: string)}
+
+
+
+ {/snippet}
+
+
+
+
+ {@render sortHead('slug', 'Slug')}
+ {@render sortHead('type', 'Type')}
+ {@render sortHead('name', 'Name')}
+ {@render sortHead('state', 'State')}
+ {@render sortHead('health', 'Health')}
+
+
+
+ {#each sortedEntities as entity (entity.id)}
+ onSelect(entity.slug)}
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(entity.slug) } }}
+ >
+ {entity.slug}
+ {entity.type}
+ {entity.name}
+
+ {#if entity.state}
+ {entity.state}
+ {:else}
+ —
+ {/if}
+
+
+ {#if entity.health}
+
+
+ {relativeTime(entity.last_check_at)}
+
+ {:else}
+ —
+ {/if}
+
+
+ {:else}
+
+ No entities in this layer match the filter.
+
+ {/each}
+
+
+
+{/if}
diff --git a/web/src/pages/Entities.svelte b/web/src/pages/Entities.svelte
deleted file mode 100644
index 533d8df..0000000
--- a/web/src/pages/Entities.svelte
+++ /dev/null
@@ -1,159 +0,0 @@
-
-
-
-
-
Entities
- {filtered.length} of {entities.length}
-
-
-
-
-
-
- {typeFilter === 'all' ? 'All types' : typeFilter}
-
-
- All types
- {#each types as type}
- {type}
- {/each}
-
-
-
-
- {#if loading}
-
- {#each Array(8) as _}
-
- {/each}
-
- {:else}
-
-
-
-
- Slug
- Type
- Name
- State
- Health
-
-
-
- {#each filtered as entity (entity.id)}
- openEntity(entity.slug)}
- onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openEntity(entity.slug) } }}
- >
- {entity.slug}
- {entity.type}
- {entity.name}
-
- {#if entity.state}
- {entity.state}
- {:else}
- —
- {/if}
-
-
- {#if entity.health}
-
-
- {relativeTime(entity.last_check_at)}
-
- {:else}
- —
- {/if}
-
-
- {:else}
-
- No entities match this filter.
-
- {/each}
-
-
-
- {/if}
-
-
-
diff --git a/web/src/pages/Graph.svelte b/web/src/pages/Graph.svelte
deleted file mode 100644
index 59dd9c8..0000000
--- a/web/src/pages/Graph.svelte
+++ /dev/null
@@ -1,517 +0,0 @@
-
-
-
-
-
Graph
-
-
-
-
-
-
- {nodes.length} nodes · {links.length} edges{graph?.truncated ? ' · truncated' : ''} · {Math.round(view.k * 100)}%
-
-
-
-
- {#if allNodeTypes.length}
-
- Nodes
- {#each allNodeTypes as type}
-
- {/each}
-
- {/if}
- {#if allRelTypes.length}
-
- Edges
- {#each allRelTypes as type}
-
- {/each}
-
- {/if}
-
-
- {#if loading && !nodes.length}
-
- {:else}
-
-
-
- scroll to zoom · drag background to pan · drag nodes · click to inspect · double-click to re-root
-
-
- {/if}
-
-
-
{ if (!open) selected = null }}>
-
- {#if selected}
-
- {selected.slug}
- {selected.type}{selected.state ? ` — ${selected.state}` : ''}
-
-
-
-
-
-
-
-
Attributes
-
{JSON.stringify(selected.attributes, null, 2)}
-
-
-
Relationships
-
- {#each links.filter((l) => endpointId(l.source) === selected!.id || endpointId(l.target) === selected!.id) as link}
- {@const sId = endpointId(link.source)}
- {@const other = endpoint(sId === selected!.id ? link.target : link.source)}
-
-
- {sId === selected!.id ? '→' : '←'} {link.type}
-
- {other?.slug ?? '—'}
-
- {:else}
-
No relationships in this view.
- {/each}
-
-
-
-
Blast radius ({blastRadius.length})
-
- {#each blastRadius as item (item.entity.id)}
-
- {item.entity.slug}
- depth {item.depth}
-
- {:else}
-
No downstream dependents.
- {/each}
-
-
-
- {/if}
-
-
-
-
diff --git a/web/src/pages/KnowledgeBase.svelte b/web/src/pages/KnowledgeBase.svelte
new file mode 100644
index 0000000..25be65a
--- /dev/null
+++ b/web/src/pages/KnowledgeBase.svelte
@@ -0,0 +1,288 @@
+
+
+
+
+
+
(layer = v as Layer)}>
+
+ {#each layers as l}
+ {l.label}
+ {/each}
+
+
+
+
+
+
+
+
+
+ {#if view === 'table'}
+
+
+
+
+ {typeFilter === 'all' ? 'All types' : typeFilter}
+
+
+ All types
+ {#each tableTypes as type}
+ {type}
+ {/each}
+
+
+ {filteredEntities.length} of {tableEntities.length}
+
+ {:else}
+
+
+
+
+
+
+ {graphInfo.visibleCount} nodes{graphInfo.truncated ? ' · truncated' : ''} · {graphInfo.zoomPct}%
+
+
+
+ {#if graphInfo.allNodeTypes.length || graphInfo.allRelTypes.length}
+
+ {#if graphInfo.allNodeTypes.length}
+
+ Nodes
+ {#each graphInfo.allNodeTypes as type}
+
+ {/each}
+
+ {/if}
+ {#if graphInfo.allRelTypes.length}
+
+ Edges
+ {#each graphInfo.allRelTypes as type}
+ {@const color = graphInfo.relColors.get(type) ?? '#30363d'}
+
+ {/each}
+
+ {/if}
+
+ {/if}
+ {/if}
+
+
+
+
+ {#if view === 'graph'}
+
+ {:else}
+
+ {/if}
+
+
+
+
+
+ {#if selectedSlug}
+ {#key selectedSlug}
+
+ {/key}
+ {:else}
+
+ Select an entity to see its detail.
+
+ {/if}
+
+
+