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 `
<div class="delegations-layout">
<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 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>`;
}

View File

@ -73,44 +73,35 @@ class FolkTrustSankey extends HTMLElement {
private async loadData() {
const authBase = this.getAuthBase();
const headers = this.getAuthHeaders();
const apiBase = this.getApiBase();
try {
// Fetch outbound delegations for all users in the space
// We use the trust scores endpoint to get the flow data
const [outRes, inRes] = await Promise.all([
fetch(`${authBase}/api/delegations/from?space=${encodeURIComponent(this.space)}`, { headers }),
fetch(`${authBase}/api/delegations/to?space=${encodeURIComponent(this.space)}`, { headers }),
// Fetch all space-level delegations and user directory in parallel
const [delegRes, usersRes] = await Promise.all([
fetch(`${authBase}/api/delegations/space?space=${encodeURIComponent(this.space)}`),
fetch(`${apiBase}/api/users?space=${encodeURIComponent(this.space)}`),
]);
const allFlows: DelegationFlow[] = [];
const seen = new Set<string>();
for (const res of [outRes, inRes]) {
if (res.ok) {
const data = await res.json();
for (const d of data.delegations || []) {
if (seen.has(d.id)) continue;
seen.add(d.id);
allFlows.push({
id: d.id,
fromDid: d.delegatorDid,
fromName: d.delegatorDid.slice(0, 12) + "...",
toDid: d.delegateDid,
toName: d.delegateDid.slice(0, 12) + "...",
authority: d.authority,
weight: d.weight,
state: d.state,
createdAt: d.createdAt,
});
}
if (delegRes.ok) {
const data = await delegRes.json();
for (const d of data.delegations || []) {
allFlows.push({
id: d.id,
fromDid: d.from,
fromName: d.from.slice(0, 12) + "...",
toDid: d.to,
toName: d.to.slice(0, 12) + "...",
authority: d.authority,
weight: d.weight,
state: "active",
createdAt: Date.now(),
});
}
}
this.flows = allFlows;
// Load user names
const apiBase = this.getApiBase();
const usersRes = await fetch(`${apiBase}/api/users?space=${encodeURIComponent(this.space)}`);
// Resolve user display names
if (usersRes.ok) {
const userData = await usersRes.json();
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 token = getTokenForSpace(dataSpace);
// Check per-space cache
const cached = graphCaches.get(dataSpace);
// Check per-space cache (keyed by space + trust params)
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) {
c.header("Cache-Control", "public, max-age=60");
return c.json(cached.data);
@ -289,15 +292,13 @@ routes.get("/api/graph", async (c) => {
}
// 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) {
const ENCRYPTID_URL = process.env.ENCRYPTID_INTERNAL_URL || "http://encryptid:3000";
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/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) {
@ -318,23 +319,31 @@ routes.get("/api/graph", async (c) => {
if (scoresRes.ok) {
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>();
for (const s of scoreData.scores || []) {
trustMap.set(s.did, s.totalScore);
}
// Annotate existing nodes with trust scores
for (const node of nodes) {
if (trustMap.has(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 };
graphCaches.set(dataSpace, { data: result, ts: Date.now() });
graphCaches.set(cacheKey, { data: result, ts: Date.now() });
c.header("Cache-Control", "public, max-age=60");
return c.json(result);
} catch (e) {

View File

@ -7081,6 +7081,25 @@ app.delete('/api/delegations/:id', async (c) => {
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
app.get('/api/trust/scores', async (c) => {
const authority = c.req.query('authority') || 'voting';