feat(rnetwork): enhanced trust flow viz + rename authorities to gov/fin/dev-ops
- Compute delegatedWeight per node from delegation edges (both directions) - Animated directional particles on delegation edges (count/size ~ weight) - Wider delegation edges (1+weight*8) with 0.15 curvature - "All" authority overlay mode with per-authority colored edges - Rename 5 authorities (voting/moderation/curation/treasury/membership) to 3 verticals: gov-ops, fin-ops, dev-ops - DB migration: update CHECK constraint + migrate existing data - Update all frontend components + backend defaults Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f2d575d1a2
commit
4c44eb9941
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* <folk-delegation-manager> — per-vertical delegation management UI.
|
||||
*
|
||||
* Shows bars for each authority vertical (voting, moderation, curation, treasury, membership)
|
||||
* Shows bars for each authority vertical (gov-ops, fin-ops, dev-ops)
|
||||
* with percentage allocated and delegate avatars. Supports create, edit, revoke.
|
||||
* Weight sum validated client-side before submission.
|
||||
*/
|
||||
|
|
@ -30,13 +30,11 @@ interface SpaceUser {
|
|||
role: string;
|
||||
}
|
||||
|
||||
const AUTHORITIES = ["voting", "moderation", "curation", "treasury", "membership"] as const;
|
||||
const AUTHORITIES = ["gov-ops", "fin-ops", "dev-ops"] as const;
|
||||
const AUTHORITY_ICONS: Record<string, string> = {
|
||||
voting: "\u{1F5F3}\uFE0F",
|
||||
moderation: "\u{1F6E1}\uFE0F",
|
||||
curation: "\u2728",
|
||||
treasury: "\u{1F4B0}",
|
||||
membership: "\u{1F465}",
|
||||
"gov-ops": "\u{1F3DB}\uFE0F",
|
||||
"fin-ops": "\u{1F4B0}",
|
||||
"dev-ops": "\u{1F528}",
|
||||
};
|
||||
|
||||
class FolkDelegationManager extends HTMLElement {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface GraphNode {
|
|||
location?: string;
|
||||
description?: string;
|
||||
trustScore?: number;
|
||||
delegatedWeight?: number; // 0-1 normalized, computed from delegation edges
|
||||
// 3d-force-graph internal properties
|
||||
x?: number;
|
||||
y?: number;
|
||||
|
|
@ -27,10 +28,19 @@ interface GraphEdge {
|
|||
type: string;
|
||||
label?: string;
|
||||
weight?: number;
|
||||
authority?: string;
|
||||
}
|
||||
|
||||
const DELEGATION_AUTHORITIES = ["voting", "moderation", "curation", "treasury", "membership"] as const;
|
||||
const DELEGATION_AUTHORITIES = ["gov-ops", "fin-ops", "dev-ops"] as const;
|
||||
type DelegationAuthority = typeof DELEGATION_AUTHORITIES[number];
|
||||
type AuthoritySelection = "all" | DelegationAuthority;
|
||||
|
||||
// Per-authority edge colors for "all" overlay mode (governance, economics, technology)
|
||||
const AUTHORITY_COLORS: Record<string, string> = {
|
||||
"gov-ops": "#a78bfa", // purple — governance decisions
|
||||
"fin-ops": "#fbbf24", // amber — economic/financial decisions
|
||||
"dev-ops": "#34d399", // green — technical decisions
|
||||
};
|
||||
|
||||
// Node colors by type
|
||||
const NODE_COLORS: Record<string, number> = {
|
||||
|
|
@ -63,7 +73,7 @@ class FolkGraphViewer extends HTMLElement {
|
|||
private error = "";
|
||||
private selectedNode: GraphNode | null = null;
|
||||
private trustMode = false;
|
||||
private authority: DelegationAuthority = "voting";
|
||||
private authority: AuthoritySelection = "gov-ops";
|
||||
|
||||
// 3D graph instance
|
||||
private graph: any = null;
|
||||
|
|
@ -102,7 +112,7 @@ class FolkGraphViewer extends HTMLElement {
|
|||
private async loadData() {
|
||||
const base = this.getApiBase();
|
||||
try {
|
||||
const trustParam = this.trustMode ? `?trust=true&authority=${this.authority}` : "";
|
||||
const trustParam = this.trustMode ? `?trust=true&authority=${encodeURIComponent(this.authority)}` : "";
|
||||
const [wsRes, infoRes, graphRes] = await Promise.all([
|
||||
fetch(`${base}/api/workspaces`),
|
||||
fetch(`${base}/api/info`),
|
||||
|
|
@ -121,7 +131,7 @@ class FolkGraphViewer extends HTMLElement {
|
|||
this.updateGraphData();
|
||||
}
|
||||
|
||||
private async reloadWithAuthority(authority: DelegationAuthority) {
|
||||
private async reloadWithAuthority(authority: AuthoritySelection) {
|
||||
this.authority = authority;
|
||||
this.trustMode = true;
|
||||
await this.loadData();
|
||||
|
|
@ -169,8 +179,27 @@ class FolkGraphViewer extends HTMLElement {
|
|||
type: edgeTypeMap[e.type] || e.type,
|
||||
label: e.label,
|
||||
weight: e.weight,
|
||||
authority: e.authority,
|
||||
} as GraphEdge));
|
||||
|
||||
// Compute delegatedWeight for every node from delegation edges
|
||||
const nodeWeights = new Map<string, number>();
|
||||
for (const e of this.edges) {
|
||||
if (e.type !== "delegates_to") continue;
|
||||
const sid = typeof e.source === "string" ? e.source : e.source.id;
|
||||
const tid = typeof e.target === "string" ? e.target : e.target.id;
|
||||
const w = e.weight || 0.5;
|
||||
nodeWeights.set(sid, (nodeWeights.get(sid) || 0) + w);
|
||||
nodeWeights.set(tid, (nodeWeights.get(tid) || 0) + w);
|
||||
}
|
||||
const maxWeight = Math.max(...nodeWeights.values(), 1);
|
||||
for (const node of this.nodes) {
|
||||
const w = nodeWeights.get(node.id);
|
||||
if (w != null) {
|
||||
node.delegatedWeight = w / maxWeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign company colors
|
||||
const companies = this.nodes.filter(n => n.type === "company");
|
||||
this.companyColors.clear();
|
||||
|
|
@ -215,8 +244,14 @@ class FolkGraphViewer extends HTMLElement {
|
|||
|
||||
private getNodeRadius(node: GraphNode): number {
|
||||
if (node.type === "company") return 22;
|
||||
if (node.trustScore != null && this.trustMode) {
|
||||
return 8 + (node.trustScore * 22);
|
||||
if (this.trustMode) {
|
||||
// Prefer edge-computed delegatedWeight, fall back to trustScore
|
||||
if (node.delegatedWeight != null) {
|
||||
return 6 + node.delegatedWeight * 24;
|
||||
}
|
||||
if (node.trustScore != null) {
|
||||
return 6 + node.trustScore * 24;
|
||||
}
|
||||
}
|
||||
return 12;
|
||||
}
|
||||
|
|
@ -308,7 +343,7 @@ class FolkGraphViewer extends HTMLElement {
|
|||
.authority-btn:hover { border-color: var(--rs-border-strong); }
|
||||
.authority-btn.active { border-color: #a78bfa; color: #a78bfa; background: rgba(167, 139, 250, 0.1); }
|
||||
|
||||
.legend { display: flex; gap: 16px; margin-top: 12px; }
|
||||
.legend { display: flex; gap: 16px; margin-top: 12px; flex-wrap: wrap; }
|
||||
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--rs-text-muted); }
|
||||
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.dot-person { background: #3b82f6; }
|
||||
|
|
@ -387,6 +422,7 @@ class FolkGraphViewer extends HTMLElement {
|
|||
</div>
|
||||
|
||||
<div class="authority-bar" id="authority-bar">
|
||||
<button class="authority-btn" data-authority="all">All</button>
|
||||
${DELEGATION_AUTHORITIES.map(a => `<button class="authority-btn" data-authority="${a}">${a}</button>`).join("")}
|
||||
</div>
|
||||
|
||||
|
|
@ -409,6 +445,11 @@ class FolkGraphViewer extends HTMLElement {
|
|||
<div class="legend-item"><svg width="20" height="10"><line x1="0" y1="5" x2="20" y2="5" stroke="#888" stroke-width="2"></line></svg> Works at</div>
|
||||
<div class="legend-item"><svg width="20" height="10"><line x1="0" y1="5" x2="20" y2="5" stroke="#c084fc" stroke-width="2" stroke-dasharray="4 2"></line></svg> Point of contact</div>
|
||||
<div class="legend-item" id="legend-delegates" style="display:none"><svg width="20" height="10"><line x1="0" y1="5" x2="20" y2="5" stroke="#a78bfa" stroke-width="2"></line></svg> Delegates to</div>
|
||||
<span id="legend-authority-colors" style="display:none">
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#a78bfa"></span> Gov-Ops</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#fbbf24"></span> Fin-Ops</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#34d399"></span> Dev-Ops</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="workspace-section"></div>
|
||||
|
|
@ -449,7 +490,7 @@ class FolkGraphViewer extends HTMLElement {
|
|||
// Authority buttons
|
||||
this.shadow.querySelectorAll("[data-authority]").forEach(el => {
|
||||
el.addEventListener("click", () => {
|
||||
const authority = (el as HTMLElement).dataset.authority as DelegationAuthority;
|
||||
const authority = (el as HTMLElement).dataset.authority as AuthoritySelection;
|
||||
this.reloadWithAuthority(authority);
|
||||
});
|
||||
});
|
||||
|
|
@ -514,21 +555,45 @@ class FolkGraphViewer extends HTMLElement {
|
|||
.linkSource("source")
|
||||
.linkTarget("target")
|
||||
.linkColor((link: GraphEdge) => {
|
||||
if (link.type === "delegates_to") {
|
||||
if (this.authority === "all" && link.authority) {
|
||||
return AUTHORITY_COLORS[link.authority] || EDGE_STYLES.delegates_to.color;
|
||||
}
|
||||
return EDGE_STYLES.delegates_to.color;
|
||||
}
|
||||
const style = EDGE_STYLES[link.type] || EDGE_STYLES.default;
|
||||
return style.color;
|
||||
})
|
||||
.linkWidth((link: GraphEdge) => {
|
||||
if (link.type === "delegates_to") {
|
||||
return 1 + (link.weight || 0.5) * 3;
|
||||
return 1 + (link.weight || 0.5) * 8;
|
||||
}
|
||||
const style = EDGE_STYLES[link.type] || EDGE_STYLES.default;
|
||||
return style.width;
|
||||
})
|
||||
.linkCurvature((link: GraphEdge) =>
|
||||
link.type === "delegates_to" ? 0.15 : 0
|
||||
)
|
||||
.linkCurveRotation("rotation")
|
||||
.linkOpacity(0.6)
|
||||
.linkDirectionalArrowLength((link: GraphEdge) =>
|
||||
link.type === "delegates_to" ? 4 : 0
|
||||
)
|
||||
.linkDirectionalArrowRelPos(1)
|
||||
.linkDirectionalParticles((link: GraphEdge) =>
|
||||
link.type === "delegates_to" ? Math.ceil((link.weight || 0.5) * 4) : 0
|
||||
)
|
||||
.linkDirectionalParticleSpeed(0.004)
|
||||
.linkDirectionalParticleWidth((link: GraphEdge) =>
|
||||
link.type === "delegates_to" ? 1 + (link.weight || 0.5) * 2 : 0
|
||||
)
|
||||
.linkDirectionalParticleColor((link: GraphEdge) => {
|
||||
if (link.type !== "delegates_to") return null;
|
||||
if (this.authority === "all" && link.authority) {
|
||||
return AUTHORITY_COLORS[link.authority] || "#c4b5fd";
|
||||
}
|
||||
return "#c4b5fd";
|
||||
})
|
||||
.linkLineDash((link: GraphEdge) => {
|
||||
const style = EDGE_STYLES[link.type] || EDGE_STYLES.default;
|
||||
return style.dashed ? [4, 2] : null;
|
||||
|
|
@ -745,8 +810,10 @@ class FolkGraphViewer extends HTMLElement {
|
|||
// Update legend visibility for trust mode
|
||||
const membersLegend = this.shadow.getElementById("legend-members");
|
||||
const delegatesLegend = this.shadow.getElementById("legend-delegates");
|
||||
const authorityColors = this.shadow.getElementById("legend-authority-colors");
|
||||
if (membersLegend) membersLegend.style.display = this.trustMode ? "" : "none";
|
||||
if (delegatesLegend) delegatesLegend.style.display = this.trustMode ? "" : "none";
|
||||
if (delegatesLegend) delegatesLegend.style.display = (this.trustMode && this.authority !== "all") ? "" : "none";
|
||||
if (authorityColors) authorityColors.style.display = (this.trustMode && this.authority === "all") ? "" : "none";
|
||||
|
||||
// Fit view after data settles
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@ interface TrustEvent {
|
|||
createdAt: number;
|
||||
}
|
||||
|
||||
const SANKEY_AUTHORITIES = ["voting", "moderation", "curation", "treasury", "membership"] as const;
|
||||
const SANKEY_AUTHORITIES = ["gov-ops", "fin-ops", "dev-ops"] as const;
|
||||
const FLOW_COLOR = "#a78bfa";
|
||||
|
||||
class FolkTrustSankey extends HTMLElement {
|
||||
private shadow: ShadowRoot;
|
||||
private space = "";
|
||||
private authority = "voting";
|
||||
private authority = "gov-ops";
|
||||
private flows: DelegationFlow[] = [];
|
||||
private events: TrustEvent[] = [];
|
||||
private loading = true;
|
||||
|
|
@ -50,7 +50,7 @@ class FolkTrustSankey extends HTMLElement {
|
|||
|
||||
connectedCallback() {
|
||||
this.space = this.getAttribute("space") || "demo";
|
||||
this.authority = this.getAttribute("authority") || "voting";
|
||||
this.authority = this.getAttribute("authority") || "gov-ops";
|
||||
this.render();
|
||||
this.loadData();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1727,7 +1727,7 @@ export async function updatePushSubscriptionLastUsed(id: string): Promise<void>
|
|||
// DELEGATIONS (person-to-person liquid democracy)
|
||||
// ============================================================================
|
||||
|
||||
export type DelegationAuthority = 'voting' | 'moderation' | 'curation' | 'treasury' | 'membership' | 'custom';
|
||||
export type DelegationAuthority = 'gov-ops' | 'fin-ops' | 'dev-ops' | 'custom';
|
||||
export type DelegationState = 'active' | 'paused' | 'revoked';
|
||||
|
||||
export interface StoredDelegation {
|
||||
|
|
|
|||
|
|
@ -376,7 +376,7 @@ CREATE TABLE IF NOT EXISTS delegations (
|
|||
id TEXT PRIMARY KEY,
|
||||
delegator_did TEXT NOT NULL,
|
||||
delegate_did TEXT NOT NULL,
|
||||
authority TEXT NOT NULL CHECK (authority IN ('voting', 'moderation', 'curation', 'treasury', 'membership', 'custom')),
|
||||
authority TEXT NOT NULL CHECK (authority IN ('gov-ops', 'fin-ops', 'dev-ops', 'custom')),
|
||||
weight REAL NOT NULL CHECK (weight > 0 AND weight <= 1),
|
||||
max_depth INTEGER NOT NULL DEFAULT 3,
|
||||
retain_authority BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
|
@ -452,3 +452,26 @@ CREATE TABLE IF NOT EXISTS legacy_identities (
|
|||
|
||||
CREATE INDEX IF NOT EXISTS idx_legacy_identities_user ON legacy_identities(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_legacy_identities_pubkey ON legacy_identities(legacy_public_key_hash);
|
||||
|
||||
-- ============================================================================
|
||||
-- MIGRATION: Rename authority verticals (voting/moderation/curation/treasury/membership → gov-ops/fin-ops/dev-ops)
|
||||
-- ============================================================================
|
||||
|
||||
-- Migrate existing delegation data
|
||||
UPDATE delegations SET authority = 'gov-ops' WHERE authority IN ('voting', 'moderation');
|
||||
UPDATE delegations SET authority = 'fin-ops' WHERE authority = 'treasury';
|
||||
UPDATE delegations SET authority = 'dev-ops' WHERE authority IN ('curation', 'membership');
|
||||
|
||||
-- Migrate existing trust_scores data
|
||||
UPDATE trust_scores SET authority = 'gov-ops' WHERE authority IN ('voting', 'moderation');
|
||||
UPDATE trust_scores SET authority = 'fin-ops' WHERE authority = 'treasury';
|
||||
UPDATE trust_scores SET authority = 'dev-ops' WHERE authority IN ('curation', 'membership');
|
||||
|
||||
-- Migrate existing trust_events data
|
||||
UPDATE trust_events SET authority = 'gov-ops' WHERE authority IN ('voting', 'moderation');
|
||||
UPDATE trust_events SET authority = 'fin-ops' WHERE authority = 'treasury';
|
||||
UPDATE trust_events SET authority = 'dev-ops' WHERE authority IN ('curation', 'membership');
|
||||
|
||||
-- Update CHECK constraint on delegations table
|
||||
ALTER TABLE delegations DROP CONSTRAINT IF EXISTS delegations_authority_check;
|
||||
ALTER TABLE delegations ADD CONSTRAINT delegations_authority_check CHECK (authority IN ('gov-ops', 'fin-ops', 'dev-ops', 'custom'));
|
||||
|
|
|
|||
|
|
@ -7122,7 +7122,7 @@ app.get('/', (c) => {
|
|||
// DELEGATION ROUTES (person-to-person liquid democracy)
|
||||
// ============================================================================
|
||||
|
||||
const VALID_AUTHORITIES = ['voting', 'moderation', 'curation', 'treasury', 'membership', 'custom'];
|
||||
const VALID_AUTHORITIES = ['gov-ops', 'fin-ops', 'dev-ops', 'custom'];
|
||||
|
||||
// POST /api/delegations — create a new delegation
|
||||
app.post('/api/delegations', async (c) => {
|
||||
|
|
@ -7359,7 +7359,7 @@ app.get('/api/delegations/space', async (c) => {
|
|||
|
||||
// GET /api/trust/scores — aggregated trust scores for visualization
|
||||
app.get('/api/trust/scores', async (c) => {
|
||||
const authority = c.req.query('authority') || 'voting';
|
||||
const authority = c.req.query('authority') || 'gov-ops';
|
||||
const spaceSlug = c.req.query('space');
|
||||
if (!spaceSlug) return c.json({ error: 'space query parameter required' }, 400);
|
||||
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ export function computeTrustScores(
|
|||
|
||||
// ── Background Recomputation ──
|
||||
|
||||
const AUTHORITIES: DelegationAuthority[] = ['voting', 'moderation', 'curation', 'treasury', 'membership', 'custom'];
|
||||
const AUTHORITIES: DelegationAuthority[] = ['gov-ops', 'fin-ops', 'dev-ops', 'custom'];
|
||||
|
||||
/**
|
||||
* Recompute all trust scores for a single space.
|
||||
|
|
|
|||
Loading…
Reference in New Issue