fix(web): entity Relations panel was missing true incoming edges
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

fetchGraph({root, depth:1}) is backed by blast_radius, which only
walks outgoing edges — so it could never surface an edge some other
entity points at this one (e.g. host:hubris —hosts→ lxc:sophia) unless
that other entity happened to also be reachable going forward from
here. The Outgoing/Incoming split was filtering correctly, but
"Incoming" was starved of data by construction.

Switch to GET /entities/{id}/relations?direction=both — a dedicated
endpoint that matches on source_id OR target_id directly — via a new
fetchEntityRelations(). Simplifies the incoming/outgoing derivation
too, since every relation returned is now actually incident to the
entity (no more sibling-edge filtering needed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 08:13:19 +02:00
parent 258b14dcbc
commit 8615f2268f
2 changed files with 20 additions and 8 deletions

View File

@@ -494,6 +494,18 @@ export interface Relationship {
valid_to?: string | null valid_to?: string | null
} }
// Direct relationships of an entity, both directions — unlike fetchGraph's
// blast_radius (which only walks outgoing edges, so it can never surface an
// edge some other entity points at this one unless that entity is also
// reachable going forward from here), this hits a dedicated endpoint that
// matches on source_id OR target_id directly.
export async function fetchEntityRelations(id: string): Promise<Relationship[]> {
const res = await fetchWithAuth(`${API}/entities/${encodeURIComponent(id)}/relations?direction=both`)
if (!res.ok) return []
const data = await res.json()
return data.items ?? []
}
export type Health = 'healthy' | 'degraded' | 'down' | 'unknown' export type Health = 'healthy' | 'degraded' | 'down' | 'unknown'
export interface GraphView { export interface GraphView {

View File

@@ -6,7 +6,7 @@
import DOMPurify from 'dompurify' import DOMPurify from 'dompurify'
import { import {
fetchEntity, fetchEntity,
fetchGraph, fetchEntityRelations,
fetchMetrics, fetchMetrics,
fetchEntityEvents, fetchEntityEvents,
fetchEntitySignals, fetchEntitySignals,
@@ -58,10 +58,10 @@
let actingSignal = $state<string | null>(null) let actingSignal = $state<string | null>(null)
let chartContainers: Record<string, HTMLDivElement> = {} let chartContainers: Record<string, HTMLDivElement> = {}
// `relations` is the whole depth-1 neighborhood's edge set (any edge // fetchEntityRelations already scopes to edges incident to this entity
// between any two nodes in the subgraph, e.g. two sibling LXCs' shared // (source OR target = entity, both directions), so a plain split by which
// LAN), not just edges touching this entityso "incoming"/"outgoing" // side matches is enoughno risk of an unrelated sibling-to-sibling edge
// only make sense for the subset actually incident to it. // sneaking into either group.
const outgoingRelations = $derived(entity ? relations.filter((r) => r.source === entity!.slug) : []) const outgoingRelations = $derived(entity ? relations.filter((r) => r.source === entity!.slug) : [])
const incomingRelations = $derived( const incomingRelations = $derived(
entity ? relations.filter((r) => r.target === entity!.slug && r.source !== entity!.slug) : [] entity ? relations.filter((r) => r.target === entity!.slug && r.source !== entity!.slug) : []
@@ -74,8 +74,8 @@
loading = false loading = false
return return
} }
const [graphView, m, ev, sig, tk, kh, oc, ch, aa, au] = await Promise.all([ const [rel, m, ev, sig, tk, kh, oc, ch, aa, au] = await Promise.all([
fetchGraph({ root: entity.id, depth: 1 }), fetchEntityRelations(entity.id),
fetchMetrics(entity.id), fetchMetrics(entity.id),
fetchEntityEvents(entity.id), fetchEntityEvents(entity.id),
fetchEntitySignals(entity.id), fetchEntitySignals(entity.id),
@@ -86,7 +86,7 @@
fetchAgentActivity({ entity_id: entity.id, limit: 50 }), fetchAgentActivity({ entity_id: entity.id, limit: 50 }),
fetchAudit({ entity_id: entity.id, limit: 50 }) fetchAudit({ entity_id: entity.id, limit: 50 })
]) ])
relations = graphView?.edges ?? [] relations = rel
metrics = m metrics = m
events = ev events = ev
signals = sig signals = sig