fix(rnetwork): trust data rendering in graph and delegation views

- Fix graph cache keying: include trust/authority params so cached
  non-trust responses don't shadow trust-enriched requests
- Add /api/delegations/space endpoint to EncryptID for space-level
  delegation listing (no auth required, for graph/sankey)
- Fetch and include delegates_to edges in graph API response
- Pass auth-url attribute to delegation manager and sankey components
- Rewrite sankey loadData to use space-level delegation endpoint
  instead of per-user endpoints (shows all flows, not just current user)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-11 20:03:23 -07:00
parent 42c6dea091
commit 913f9aa4d0
4 changed files with 60 additions and 41 deletions

View File

@ -657,10 +657,10 @@ class FolkCrmView extends HTMLElement {
return ` return `
<div class="delegations-layout"> <div class="delegations-layout">
<div class="delegations-col"> <div class="delegations-col">
<folk-delegation-manager space="${this.space}"></folk-delegation-manager> <folk-delegation-manager space="${this.space}" auth-url="https://auth.rspace.online"></folk-delegation-manager>
</div> </div>
<div class="delegations-col"> <div class="delegations-col">
<folk-trust-sankey space="${this.space}"></folk-trust-sankey> <folk-trust-sankey space="${this.space}" auth-url="https://auth.rspace.online"></folk-trust-sankey>
</div> </div>
</div>`; </div>`;
} }

View File

@ -73,44 +73,35 @@ class FolkTrustSankey extends HTMLElement {
private async loadData() { private async loadData() {
const authBase = this.getAuthBase(); const authBase = this.getAuthBase();
const headers = this.getAuthHeaders(); const apiBase = this.getApiBase();
try { try {
// Fetch outbound delegations for all users in the space // Fetch all space-level delegations and user directory in parallel
// We use the trust scores endpoint to get the flow data const [delegRes, usersRes] = await Promise.all([
const [outRes, inRes] = await Promise.all([ fetch(`${authBase}/api/delegations/space?space=${encodeURIComponent(this.space)}`),
fetch(`${authBase}/api/delegations/from?space=${encodeURIComponent(this.space)}`, { headers }), fetch(`${apiBase}/api/users?space=${encodeURIComponent(this.space)}`),
fetch(`${authBase}/api/delegations/to?space=${encodeURIComponent(this.space)}`, { headers }),
]); ]);
const allFlows: DelegationFlow[] = []; const allFlows: DelegationFlow[] = [];
const seen = new Set<string>(); if (delegRes.ok) {
const data = await delegRes.json();
for (const res of [outRes, inRes]) { for (const d of data.delegations || []) {
if (res.ok) { allFlows.push({
const data = await res.json(); id: d.id,
for (const d of data.delegations || []) { fromDid: d.from,
if (seen.has(d.id)) continue; fromName: d.from.slice(0, 12) + "...",
seen.add(d.id); toDid: d.to,
allFlows.push({ toName: d.to.slice(0, 12) + "...",
id: d.id, authority: d.authority,
fromDid: d.delegatorDid, weight: d.weight,
fromName: d.delegatorDid.slice(0, 12) + "...", state: "active",
toDid: d.delegateDid, createdAt: Date.now(),
toName: d.delegateDid.slice(0, 12) + "...", });
authority: d.authority,
weight: d.weight,
state: d.state,
createdAt: d.createdAt,
});
}
} }
} }
this.flows = allFlows; this.flows = allFlows;
// Load user names // Resolve user display names
const apiBase = this.getApiBase();
const usersRes = await fetch(`${apiBase}/api/users?space=${encodeURIComponent(this.space)}`);
if (usersRes.ok) { if (usersRes.ok) {
const userData = await usersRes.json(); const userData = await usersRes.json();
const nameMap = new Map<string, string>(); const nameMap = new Map<string, string>();

View File

@ -189,8 +189,11 @@ routes.get("/api/graph", async (c) => {
const dataSpace = (c.get("effectiveSpace" as any) as string) || space; const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
const token = getTokenForSpace(dataSpace); const token = getTokenForSpace(dataSpace);
// Check per-space cache // Check per-space cache (keyed by space + trust params)
const cached = graphCaches.get(dataSpace); const includeTrust = c.req.query("trust") === "true";
const authority = c.req.query("authority") || "voting";
const cacheKey = includeTrust ? `${dataSpace}:trust:${authority}` : dataSpace;
const cached = graphCaches.get(cacheKey);
if (cached && Date.now() - cached.ts < CACHE_TTL) { if (cached && Date.now() - cached.ts < CACHE_TTL) {
c.header("Cache-Control", "public, max-age=60"); c.header("Cache-Control", "public, max-age=60");
return c.json(cached.data); return c.json(cached.data);
@ -289,15 +292,13 @@ routes.get("/api/graph", async (c) => {
} }
// If trust=true, merge EncryptID user nodes + delegation edges // If trust=true, merge EncryptID user nodes + delegation edges
const includeTrust = c.req.query("trust") === "true";
const authority = c.req.query("authority") || "voting";
if (includeTrust) { if (includeTrust) {
const ENCRYPTID_URL = process.env.ENCRYPTID_INTERNAL_URL || "http://encryptid:3000"; const ENCRYPTID_URL = process.env.ENCRYPTID_INTERNAL_URL || "http://encryptid:3000";
try { try {
const [usersRes, scoresRes] = await Promise.all([ const [usersRes, scoresRes, delegRes] = await Promise.all([
fetch(`${ENCRYPTID_URL}/api/users/directory?space=${encodeURIComponent(dataSpace)}`, { signal: AbortSignal.timeout(5000) }), fetch(`${ENCRYPTID_URL}/api/users/directory?space=${encodeURIComponent(dataSpace)}`, { signal: AbortSignal.timeout(5000) }),
fetch(`${ENCRYPTID_URL}/api/trust/scores?space=${encodeURIComponent(dataSpace)}&authority=${encodeURIComponent(authority)}`, { signal: AbortSignal.timeout(5000) }), fetch(`${ENCRYPTID_URL}/api/trust/scores?space=${encodeURIComponent(dataSpace)}&authority=${encodeURIComponent(authority)}`, { signal: AbortSignal.timeout(5000) }),
fetch(`${ENCRYPTID_URL}/api/delegations/space?space=${encodeURIComponent(dataSpace)}&authority=${encodeURIComponent(authority)}`, { signal: AbortSignal.timeout(5000) }),
]); ]);
if (usersRes.ok) { if (usersRes.ok) {
@ -318,23 +319,31 @@ routes.get("/api/graph", async (c) => {
if (scoresRes.ok) { if (scoresRes.ok) {
const scoreData = await scoresRes.json() as { scores: Array<{ did: string; totalScore: number }> }; const scoreData = await scoresRes.json() as { scores: Array<{ did: string; totalScore: number }> };
// Build trust score lookup for node sizing
const trustMap = new Map<string, number>(); const trustMap = new Map<string, number>();
for (const s of scoreData.scores || []) { for (const s of scoreData.scores || []) {
trustMap.set(s.did, s.totalScore); trustMap.set(s.did, s.totalScore);
} }
// Annotate existing nodes with trust scores
for (const node of nodes) { for (const node of nodes) {
if (trustMap.has(node.id)) { if (trustMap.has(node.id)) {
(node.data as any).trustScore = trustMap.get(node.id); (node.data as any).trustScore = trustMap.get(node.id);
} }
} }
} }
} catch { /* trust enrichment is best-effort */ }
// Add delegation edges
if (delegRes.ok) {
const delegData = await delegRes.json() as { delegations: Array<{ from: string; to: string; authority: string; weight: number }> };
for (const d of delegData.delegations || []) {
if (nodeIds.has(d.from) && nodeIds.has(d.to)) {
edges.push({ source: d.from, target: d.to, type: "delegates_to", weight: d.weight } as any);
}
}
}
} catch (e) { console.error("[Network] Trust enrichment error:", e); }
} }
const result = { nodes, edges, demo: false }; const result = { nodes, edges, demo: false };
graphCaches.set(dataSpace, { data: result, ts: Date.now() }); graphCaches.set(cacheKey, { data: result, ts: Date.now() });
c.header("Cache-Control", "public, max-age=60"); c.header("Cache-Control", "public, max-age=60");
return c.json(result); return c.json(result);
} catch (e) { } catch (e) {

View File

@ -7081,6 +7081,25 @@ app.delete('/api/delegations/:id', async (c) => {
return c.json({ success: true }); return c.json({ success: true });
}); });
// GET /api/delegations/space — all active delegations for a space (internal use, no auth)
app.get('/api/delegations/space', async (c) => {
const spaceSlug = c.req.query('space');
if (!spaceSlug) return c.json({ error: 'space query param required' }, 400);
const authority = c.req.query('authority');
const delegations = await listActiveDelegations(spaceSlug, authority || undefined);
return c.json({
delegations: delegations.map(d => ({
id: d.id,
from: d.delegatorDid,
to: d.delegateDid,
authority: d.authority,
weight: d.weight,
})),
space: spaceSlug,
authority: authority || 'all',
});
});
// GET /api/trust/scores — aggregated trust scores for visualization // GET /api/trust/scores — aggregated trust scores for visualization
app.get('/api/trust/scores', async (c) => { app.get('/api/trust/scores', async (c) => {
const authority = c.req.query('authority') || 'voting'; const authority = c.req.query('authority') || 'voting';