Merge branch 'dev'
CI/CD / deploy (push) Failing after 3m5s Details

This commit is contained in:
Jeff Emmett 2026-04-11 12:41:45 -04:00
commit 9b05134ae5
5 changed files with 651 additions and 1 deletions

592
lib/folk-holon-explorer.ts Normal file
View File

@ -0,0 +1,592 @@
/**
* folk-holon-explorer Zoomable hexagonal hierarchy navigator.
*
* Dual navigation:
* - H3 Mode: Navigate H3 resolution levels via h3-js hierarchy
* - Space Mode: Navigate nested rSpace spaces via /api/spaces/:slug/nest
*
* Features:
* - Circular SVG layout (Holons/Zircle arc placement)
* - MetatronGrid sacred geometry background
* - Appreciation weight normalization (sum 100%)
* - Endorsement logging to trust engine on weight save
*/
import * as h3 from 'h3-js';
import { FolkShape } from './folk-shape';
import { css, html } from './tags';
import { getHolonHierarchy, getResolutionName } from './holon-service';
// ── Types ──
interface ExplorerNode {
id: string;
label: string;
weight: number;
ownerDID?: string;
}
type NavMode = 'h3' | 'space';
// ── Appreciation normalization (from Holons project) ──
function normalize(values: number[], changedIndex: number): number[] {
const total = values.reduce((s, v) => s + v, 0);
if (total <= 100) return values;
const excess = total - 100;
const othersSum = total - values[changedIndex];
if (othersSum === 0) return values.map((v, i) => i === changedIndex ? 100 : 0);
return values.map((v, i) =>
i === changedIndex ? v : Math.max(0, v - excess * v / othersSum)
);
}
// ── MetatronGrid SVG ──
function metatronGrid(size: number): string {
const cx = size / 2, cy = size / 2, r = size * 0.42, ri = r * 0.5;
const hex = (radius: number) => {
const pts: string[] = [];
for (let i = 0; i < 6; i++) {
const a = (Math.PI / 3) * i - Math.PI / 2;
pts.push(`${cx + radius * Math.cos(a)},${cy + radius * Math.sin(a)}`);
}
return pts.join(' ');
};
// Star of David: two overlapping triangles
const tri = (offset: number) => {
const pts: string[] = [];
for (let i = 0; i < 3; i++) {
const a = (2 * Math.PI / 3) * i + offset - Math.PI / 2;
pts.push(`${cx + r * Math.cos(a)},${cy + r * Math.sin(a)}`);
}
return pts.join(' ');
};
return `<polygon points="${hex(r)}" fill="none" stroke="currentColor" opacity="0.12"/>
<polygon points="${hex(ri)}" fill="none" stroke="currentColor" opacity="0.08"/>
<polygon points="${tri(0)}" fill="none" stroke="currentColor" opacity="0.1"/>
<polygon points="${tri(Math.PI / 3)}" fill="none" stroke="currentColor" opacity="0.1"/>`;
}
// ── Styles ──
const styles = css`
:host {
background: #0f172a;
color: #e2e8f0;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.3);
min-width: 420px;
min-height: 400px;
}
.header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px; background: #0e7490; color: white;
border-radius: 8px 8px 0 0; font-size: 12px; font-weight: 600; cursor: move;
}
.header-left { display: flex; align-items: center; gap: 6px; min-width: 0; }
.header-left span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.header-right { display: flex; gap: 4px; flex-shrink: 0; }
.header-right button, .mode-toggle {
background: transparent; border: none; color: white; cursor: pointer;
padding: 2px 6px; border-radius: 4px; font-size: 12px;
}
.header-right button:hover, .mode-toggle:hover { background: rgba(255,255,255,0.2); }
.mode-toggle.active { background: rgba(255,255,255,0.25); font-weight: 700; }
.breadcrumb {
display: flex; gap: 4px; padding: 6px 12px; font-size: 11px;
color: #94a3b8; background: #1e293b; flex-wrap: wrap; align-items: center;
}
.breadcrumb span { cursor: pointer; }
.breadcrumb span:hover { color: #06b6d4; text-decoration: underline; }
.breadcrumb .sep { color: #475569; cursor: default; }
.breadcrumb .sep:hover { color: #475569; text-decoration: none; }
.breadcrumb .current { color: #e2e8f0; font-weight: 600; cursor: default; }
.breadcrumb .current:hover { color: #e2e8f0; text-decoration: none; }
.content {
display: flex; flex-direction: column; overflow: hidden;
height: calc(100% - 66px); /* header + breadcrumb */
}
.svg-wrap { flex: 1; min-height: 0; position: relative; }
.svg-wrap svg { width: 100%; height: 100%; }
.slider-panel {
padding: 8px 12px; max-height: 140px; overflow-y: auto;
border-top: 1px solid rgba(255,255,255,0.08);
}
.slider-row {
display: flex; align-items: center; gap: 8px; font-size: 11px; margin-bottom: 4px;
}
.slider-row label { width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #94a3b8; }
.slider-row input[type=range] { flex: 1; accent-color: #10b981; }
.slider-row .val { width: 30px; text-align: right; color: #10b981; font-weight: 600; }
.sum-row { font-size: 11px; color: #64748b; text-align: right; padding: 2px 0; }
.connect-form {
display: flex; flex-direction: column; align-items: center;
justify-content: center; height: 100%; gap: 12px; padding: 16px; text-align: center;
}
.connect-form input {
padding: 8px 12px; border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px; font-size: 13px; font-family: monospace;
background: #1e293b; color: #e2e8f0; outline: none; width: 100%; max-width: 320px;
}
.connect-form input:focus { border-color: #06b6d4; box-shadow: 0 0 0 2px rgba(6,182,212,0.2); }
.connect-form button {
padding: 8px 20px; background: #0e7490; color: white; border: none;
border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;
}
.connect-form button:hover { background: #0891b2; }
.hint { font-size: 12px; color: #64748b; }
.empty { display: flex; align-items: center; justify-content: center; height: 100%; color: #64748b; font-size: 13px; }
.more-label { font-size: 10px; fill: #64748b; }
`;
declare global {
interface HTMLElementTagNameMap {
'folk-holon-explorer': FolkHolonExplorer;
}
}
export class FolkHolonExplorer extends FolkShape {
static override tagName = 'folk-holon-explorer';
static {
const sheet = new CSSStyleSheet();
const parentRules = Array.from(FolkShape.styles.cssRules).map((r) => r.cssText).join('\n');
const childRules = Array.from(styles.cssRules).map((r) => r.cssText).join('\n');
sheet.replaceSync(`${parentRules}\n${childRules}`);
this.styles = sheet;
}
// ── State ──
#mode: NavMode = 'space';
#rootId = '';
#spaceSlug = '';
#breadcrumb: { id: string; label: string }[] = [];
#children: ExplorerNode[] = [];
#loading = false;
#contentEl: HTMLElement | null = null;
#saveTimer: ReturnType<typeof setTimeout> | null = null;
override createRenderRoot() {
const root = super.createRenderRoot();
this.#mode = (this.getAttribute('mode') as NavMode) || 'space';
this.#rootId = this.getAttribute('root-id') || '';
this.#spaceSlug = this.getAttribute('space-slug') || (window as any).__rspaceSpace || '';
const wrapper = document.createElement('div');
wrapper.style.cssText = 'width:100%;height:100%;display:flex;flex-direction:column;';
wrapper.innerHTML = html`
<div class="header">
<div class="header-left">
<span>🔮</span>
<span>Holon Explorer</span>
</div>
<div class="header-right">
<button class="mode-toggle ${this.#mode === 'space' ? 'active' : ''}" data-mode="space" title="Space Mode">Spaces</button>
<button class="mode-toggle ${this.#mode === 'h3' ? 'active' : ''}" data-mode="h3" title="H3 Hex Mode">H3</button>
<button class="close-btn" title="Close">×</button>
</div>
</div>
<div class="breadcrumb"></div>
<div class="content"></div>
`;
const slot = root.querySelector('slot');
const containerDiv = slot?.parentElement as HTMLElement;
if (containerDiv) containerDiv.replaceWith(wrapper);
this.#contentEl = wrapper.querySelector('.content');
// Wire mode toggles
wrapper.querySelectorAll('.mode-toggle').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const m = (btn as HTMLElement).dataset.mode as NavMode;
if (m !== this.#mode) {
this.#mode = m;
this.#breadcrumb = [];
this.#children = [];
wrapper.querySelectorAll('.mode-toggle').forEach((b) =>
b.classList.toggle('active', (b as HTMLElement).dataset.mode === m));
this.#render();
}
});
});
wrapper.querySelector('.close-btn')!.addEventListener('click', (e) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent('close'));
});
// Auto-load
if (this.#mode === 'space' && this.#spaceSlug) {
this.#breadcrumb = [{ id: this.#spaceSlug, label: this.#spaceSlug }];
this.#loadSpaceChildren(this.#spaceSlug);
} else if (this.#mode === 'h3' && this.#rootId) {
this.#breadcrumb = [{ id: this.#rootId, label: getResolutionName(h3.getResolution(this.#rootId)) }];
this.#loadH3Children(this.#rootId);
}
this.#render();
return root;
}
disconnectedCallback() {
if (this.#saveTimer) clearTimeout(this.#saveTimer);
}
// ── Data loading ──
async #loadSpaceChildren(slug: string) {
this.#loading = true;
this.#render();
try {
const res = await fetch(`/api/spaces/${encodeURIComponent(slug)}/nest`, { signal: AbortSignal.timeout(5000) });
if (!res.ok) { this.#children = []; this.#loading = false; this.#render(); return; }
const data = await res.json();
const nested: any[] = data.nestedSpaces || [];
this.#children = nested.map((s: any) => ({
id: s.slug || s.id,
label: s.name || s.slug || s.id,
weight: 100 / Math.max(nested.length, 1),
ownerDID: s.ownerDID,
}));
this.#loadSavedWeights();
} catch { this.#children = []; }
this.#loading = false;
this.#render();
}
#loadH3Children(cellId: string) {
this.#loading = true;
this.#render();
try {
const { children } = getHolonHierarchy(cellId);
// H3 can produce many children — show first 12
const shown = children.slice(0, 12);
this.#children = shown.map((c) => ({
id: c,
label: `${getResolutionName(h3.getResolution(c))} ${c.slice(-6)}`,
weight: 100 / Math.max(shown.length, 1),
}));
this.#loadSavedWeights();
} catch { this.#children = []; }
this.#loading = false;
this.#render();
}
#drillInto(node: ExplorerNode) {
this.#breadcrumb.push({ id: node.id, label: node.label });
this.#children = [];
if (this.#mode === 'space') {
this.#loadSpaceChildren(node.id);
} else {
this.#loadH3Children(node.id);
}
}
#navigateTo(index: number) {
if (index >= this.#breadcrumb.length - 1) return;
const target = this.#breadcrumb[index];
this.#breadcrumb = this.#breadcrumb.slice(0, index + 1);
this.#children = [];
if (this.#mode === 'space') {
this.#loadSpaceChildren(target.id);
} else {
this.#loadH3Children(target.id);
}
}
// ── Appreciation persistence ──
#weightKey(): string {
const current = this.#breadcrumb[this.#breadcrumb.length - 1];
return current ? `holon-explorer:weights:${current.id}` : '';
}
#loadSavedWeights() {
const key = this.#weightKey();
if (!key) return;
try {
const raw = localStorage.getItem(key);
if (!raw) return;
const saved: Record<string, number> = JSON.parse(raw);
for (const child of this.#children) {
if (saved[child.id] != null) child.weight = saved[child.id];
}
} catch { /* ignore */ }
}
#saveWeights() {
const key = this.#weightKey();
if (!key) return;
const map: Record<string, number> = {};
for (const c of this.#children) map[c.id] = c.weight;
try { localStorage.setItem(key, JSON.stringify(map)); } catch { /* ignore */ }
}
#scheduleEndorse() {
if (this.#saveTimer) clearTimeout(this.#saveTimer);
this.#saveTimer = setTimeout(() => this.#fireEndorsements(), 1500);
}
async #fireEndorsements() {
const space = this.#spaceSlug || (window as any).__rspaceSpace;
const token = (window as any).__rspaceAuthToken;
if (!space || !token) return;
for (const child of this.#children) {
if (!child.ownerDID) continue;
try {
await fetch(`/${encodeURIComponent(space)}/rnetwork/api/endorse`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({
targetDid: child.ownerDID,
authority: 'gov-ops',
weight: child.weight / 100,
space,
}),
});
} catch { /* silent */ }
}
}
// ── Rendering ──
#render() {
if (!this.#contentEl) return;
// Breadcrumb
const bc = this.renderRoot.querySelector('.breadcrumb') as HTMLElement;
if (bc) {
if (this.#breadcrumb.length === 0) {
bc.innerHTML = '';
} else {
bc.innerHTML = this.#breadcrumb.map((b, i) => {
const isLast = i === this.#breadcrumb.length - 1;
const sep = i > 0 ? '<span class="sep"></span>' : '';
return isLast
? `${sep}<span class="current">${esc(b.label)}</span>`
: `${sep}<span data-idx="${i}">${esc(b.label)}</span>`;
}).join('');
bc.querySelectorAll('span[data-idx]').forEach((el) => {
el.addEventListener('click', (e) => {
e.stopPropagation();
this.#navigateTo(parseInt((el as HTMLElement).dataset.idx!, 10));
});
});
}
}
// Content
if (this.#mode === 'h3' && this.#breadcrumb.length === 0) {
this.#renderH3Form();
} else if (this.#loading) {
this.#contentEl.innerHTML = '<div class="empty">Loading...</div>';
} else if (this.#children.length === 0 && this.#breadcrumb.length > 0) {
this.#contentEl.innerHTML = '<div class="empty">No children found</div>';
} else if (this.#children.length === 0) {
this.#contentEl.innerHTML = '<div class="empty">Enter a space or H3 cell to explore</div>';
} else {
this.#renderExplorer();
}
}
#renderH3Form() {
this.#contentEl!.innerHTML = html`
<div class="connect-form">
<div style="font-size:36px"></div>
<div style="font-weight:600">H3 Hex Explorer</div>
<div class="hint">Enter an H3 cell ID to explore its hierarchy</div>
<input type="text" class="h3-input" placeholder="e.g. 872a1070bffffff" />
<button class="go-btn">Explore</button>
</div>
`;
const input = this.#contentEl!.querySelector('.h3-input') as HTMLInputElement;
const btn = this.#contentEl!.querySelector('.go-btn') as HTMLButtonElement;
const go = () => {
const val = input.value.trim();
if (!val) return;
try {
if (!h3.isValidCell(val)) return;
this.#rootId = val;
this.#breadcrumb = [{ id: val, label: getResolutionName(h3.getResolution(val)) }];
this.#loadH3Children(val);
} catch { /* invalid */ }
};
input.addEventListener('pointerdown', (e) => e.stopPropagation());
input.addEventListener('keydown', (e) => { e.stopPropagation(); if (e.key === 'Enter') go(); });
btn.addEventListener('click', (e) => { e.stopPropagation(); go(); });
}
#renderExplorer() {
const N = this.#children.length;
const MAX_VISIBLE = 12;
const visible = this.#children.slice(0, MAX_VISIBLE);
const overflow = N > MAX_VISIBLE ? N - MAX_VISIBLE : 0;
// SVG dimensions (use fixed viewBox, scales with container)
const W = 400, H = 300;
const cx = W / 2, cy = H / 2;
let orbit = Math.min(W, H) * 0.35;
if (N > 8) orbit *= 0.85;
// MetatronGrid
const gridSvg = metatronGrid(Math.min(W, H));
// Build node positions
const nodesSvg: string[] = [];
const linesSvg: string[] = [];
for (let i = 0; i < visible.length; i++) {
const child = visible[i];
const angle = -110 + (220 / (visible.length + 1)) * (i + 1);
const rad = (angle - 90) * Math.PI / 180;
const nx = cx + orbit * Math.cos(rad);
const ny = cy + orbit * Math.sin(rad);
const r = Math.min(36, Math.max(16, 16 + child.weight * 0.3));
// Connection line
linesSvg.push(
`<line x1="${cx}" y1="${cy}" x2="${nx}" y2="${ny}" stroke="#10b981" stroke-opacity="0.3" stroke-width="${1 + child.weight * 0.03}"/>`
);
// Node circle
const fill = `rgba(16,185,129,${(0.1 + child.weight * 0.005).toFixed(3)})`;
nodesSvg.push(`
<g class="node" data-idx="${i}" style="cursor:pointer">
<circle cx="${nx}" cy="${ny}" r="${r}" fill="${fill}" stroke="#10b981" stroke-width="1.5"/>
<text x="${nx}" y="${ny + r + 12}" text-anchor="middle" fill="#94a3b8" font-size="9">${esc(child.label.length > 14 ? child.label.slice(0, 12) + '…' : child.label)}</text>
</g>
`);
}
// Center node
const centerSvg = `
<circle cx="${cx}" cy="${cy}" r="24" fill="rgba(6,182,212,0.2)" stroke="#06b6d4" stroke-width="2"/>
<text x="${cx}" y="${cy + 4}" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">${esc(this.#breadcrumb[this.#breadcrumb.length - 1]?.label.slice(0, 8) || '?')}</text>
`;
// Overflow indicator
const overflowSvg = overflow > 0
? `<text x="${cx}" y="${cy + orbit + 30}" text-anchor="middle" class="more-label">+${overflow} more</text>`
: '';
// Slider panel
const sliders = visible.map((child, i) => `
<div class="slider-row">
<label title="${esc(child.label)}">${esc(child.label)}</label>
<input type="range" min="0" max="100" value="${Math.round(child.weight)}" data-idx="${i}" />
<span class="val">${Math.round(child.weight)}</span>
</div>
`).join('');
const sum = this.#children.reduce((s, c) => s + c.weight, 0);
this.#contentEl!.innerHTML = html`
<div class="svg-wrap">
<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet">
${gridSvg}
${linesSvg.join('')}
${centerSvg}
${nodesSvg.join('')}
${overflowSvg}
</svg>
</div>
<div class="slider-panel">
${sliders}
<div class="sum-row">Total: ${Math.round(sum)}%</div>
</div>
`;
// Wire node clicks
this.#contentEl!.querySelectorAll('.node').forEach((g) => {
g.addEventListener('click', (e) => {
e.stopPropagation();
const idx = parseInt((g as HTMLElement).dataset.idx!, 10);
if (idx >= 0 && idx < visible.length) this.#drillInto(visible[idx]);
});
});
// Wire sliders
this.#contentEl!.querySelectorAll('input[type=range]').forEach((el) => {
el.addEventListener('pointerdown', (e) => e.stopPropagation());
el.addEventListener('input', (e) => {
e.stopPropagation();
const idx = parseInt((el as HTMLElement).dataset.idx!, 10);
const val = parseFloat((el as HTMLInputElement).value);
const weights = this.#children.map((c) => c.weight);
weights[idx] = val;
const normalized = normalize(weights, idx);
for (let i = 0; i < this.#children.length; i++) this.#children[i].weight = normalized[i];
// Update slider UI without full re-render
this.#contentEl!.querySelectorAll('.slider-row').forEach((row, ri) => {
const inp = row.querySelector('input') as HTMLInputElement;
const span = row.querySelector('.val') as HTMLElement;
if (inp && span && ri < this.#children.length) {
inp.value = String(Math.round(this.#children[ri].weight));
span.textContent = String(Math.round(this.#children[ri].weight));
}
});
const sumEl = this.#contentEl!.querySelector('.sum-row');
if (sumEl) sumEl.textContent = `Total: ${Math.round(this.#children.reduce((s, c) => s + c.weight, 0))}%`;
this.#saveWeights();
this.#scheduleEndorse();
});
});
}
// ── Serialization ──
override toJSON() {
return {
...super.toJSON(),
type: 'folk-holon-explorer',
mode: this.#mode,
rootId: this.#rootId,
spaceSlug: this.#spaceSlug,
breadcrumb: this.#breadcrumb,
};
}
static override fromData(data: Record<string, any>): FolkHolonExplorer {
const shape = FolkShape.fromData(data) as FolkHolonExplorer;
if (data.mode) shape.setAttribute('mode', data.mode);
if (data.rootId) shape.setAttribute('root-id', data.rootId);
if (data.spaceSlug) shape.setAttribute('space-slug', data.spaceSlug);
return shape;
}
override applyData(data: Record<string, any>): void {
super.applyData(data);
const modeChanged = data.mode && data.mode !== this.#mode;
const rootChanged = data.rootId && data.rootId !== this.#rootId;
const spaceChanged = data.spaceSlug && data.spaceSlug !== this.#spaceSlug;
if (data.mode) this.#mode = data.mode;
if (data.rootId) this.#rootId = data.rootId;
if (data.spaceSlug) this.#spaceSlug = data.spaceSlug;
if (data.breadcrumb) this.#breadcrumb = data.breadcrumb;
if (modeChanged || rootChanged || spaceChanged) {
if (this.#mode === 'space' && this.#spaceSlug) {
if (this.#breadcrumb.length === 0)
this.#breadcrumb = [{ id: this.#spaceSlug, label: this.#spaceSlug }];
this.#loadSpaceChildren(this.#breadcrumb[this.#breadcrumb.length - 1].id);
} else if (this.#mode === 'h3' && this.#rootId) {
if (this.#breadcrumb.length === 0)
this.#breadcrumb = [{ id: this.#rootId, label: getResolutionName(h3.getResolution(this.#rootId)) }];
this.#loadH3Children(this.#breadcrumb[this.#breadcrumb.length - 1].id);
}
}
this.#render();
}
}
function esc(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

View File

@ -109,6 +109,7 @@ export * from "./folk-spider-3d";
// Holon Shapes (H3 geospatial)
export * from "./folk-holon";
export * from "./folk-holon-browser";
export * from "./folk-holon-explorer";
export * from "./holon-service";
// Nested Space Shape

View File

@ -191,6 +191,24 @@ routes.get("/api/delegations", async (c) => {
}
});
// ── API: Endorse — proxy to EncryptID trust engine ──
routes.post("/api/endorse", async (c) => {
const auth = c.req.header("Authorization");
if (!auth) return c.json({ error: "Unauthorized" }, 401);
try {
const body = await c.req.json();
const res = await fetch(`${ENCRYPTID_URL}/api/trust/endorse`, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": auth },
body: JSON.stringify(body),
signal: AbortSignal.timeout(5000),
});
return c.json(await res.json(), res.status as any);
} catch {
return c.json({ error: "EncryptID unreachable" }, 502);
}
});
// ── API: Graph — transform entities to node/edge format ──
routes.get("/api/graph", async (c) => {
const space = c.req.param("space") || "demo";
@ -770,6 +788,7 @@ export const networkModule: RSpaceModule = {
icon: "🌐",
description: "Community relationship graph visualization with CRM sync",
scoping: { defaultScope: 'global', userConfigurable: false },
canvasShapes: ["folk-holon-explorer"],
routes,
landingPage: renderLanding,
standaloneDomain: "rnetwork.online",

View File

@ -9174,6 +9174,36 @@ app.get('/api/delegations/space', async (c) => {
});
});
// POST /api/trust/endorse — log an endorsement trust event
app.post('/api/trust/endorse', async (c) => {
const claims = await verifyTokenFromRequest(c.req.header('Authorization'));
if (!claims) return c.json({ error: 'Unauthorized' }, 401);
const body = await c.req.json().catch(() => null);
if (!body) return c.json({ error: 'Invalid JSON body' }, 400);
const { targetDid, authority, weight, space } = body;
if (!targetDid || typeof targetDid !== 'string')
return c.json({ error: 'targetDid required (string)' }, 400);
if (!space || typeof space !== 'string')
return c.json({ error: 'space required (string)' }, 400);
const w = typeof weight === 'number' ? Math.max(0, Math.min(1, weight)) : 0.5;
const auth = typeof authority === 'string' ? authority : 'gov-ops';
const event = {
id: crypto.randomUUID(),
sourceDid: claims.did || `did:key:${claims.sub.slice(0, 32)}`,
targetDid,
eventType: 'endorsement' as const,
authority: auth,
weightDelta: w,
spaceSlug: space,
};
await logTrustEvent(event);
return c.json({ success: true, event });
});
// GET /api/trust/scores — aggregated trust scores for visualization
app.get('/api/trust/scores', async (c) => {
const authority = c.req.query('authority') || 'gov-ops';

View File

@ -1195,6 +1195,7 @@
#canvas.feed-mode folk-booking,
#canvas.feed-mode folk-holon,
#canvas.feed-mode folk-holon-browser,
#canvas.feed-mode folk-holon-explorer,
#canvas.feed-mode folk-feed {
position: relative !important;
transform: none !important;
@ -1601,6 +1602,7 @@
folk-rapp,
folk-holon,
folk-holon-browser,
folk-holon-explorer,
folk-multisig-email {
position: absolute;
}
@ -2134,6 +2136,7 @@
<button id="new-piano" title="Piano">🎹 Piano</button>
<button id="new-holon" title="Holon">🌐 Holon</button>
<button id="new-holon-browser" title="Holon Browser">🔍 Holon Browser</button>
<button id="new-holon-explorer" title="Holon Explorer">🔮 Explorer</button>
<button id="new-record" title="Record" class="toolbar-disabled">🔴 Record</button>
<button id="new-stream" title="Stream" class="toolbar-disabled">📡 Stream</button>
</div>
@ -2497,6 +2500,7 @@
FolkFeed,
FolkHolon,
FolkHolonBrowser,
FolkHolonExplorer,
CommunitySync,
PresenceManager,
generatePeerId,
@ -2780,6 +2784,7 @@
FolkFeed.define();
FolkHolon.define();
FolkHolonBrowser.define();
FolkHolonExplorer.define();
// Register all shapes with the shape registry
shapeRegistry.register("folk-shape", FolkShape);
@ -2841,6 +2846,7 @@
shapeRegistry.register("folk-feed", FolkFeed);
shapeRegistry.register("folk-holon", FolkHolon);
shapeRegistry.register("folk-holon-browser", FolkHolonBrowser);
shapeRegistry.register("folk-holon-explorer", FolkHolonExplorer);
// Wire shape→module affiliations from module declarations
for (const mod of window.__rspaceAllModules || []) {
@ -3212,7 +3218,7 @@
"folk-splat", "folk-blender", "folk-drawfast", "folk-makereal",
"folk-freecad", "folk-kicad",
"folk-rapp",
"folk-holon", "folk-holon-browser",
"folk-holon", "folk-holon-browser", "folk-holon-explorer",
"folk-multisig-email",
"folk-feed"
].join(", ");
@ -4088,6 +4094,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
"folk-feed": { width: 280, height: 360 },
"folk-holon": { width: 500, height: 400 },
"folk-holon-browser": { width: 400, height: 450 },
"folk-holon-explorer": { width: 580, height: 540 },
"folk-transaction-builder": { width: 420, height: 520 },
};
@ -4809,6 +4816,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
});
document.getElementById("new-holon").addEventListener("click", () => setPendingTool("folk-holon"));
document.getElementById("new-holon-browser").addEventListener("click", () => setPendingTool("folk-holon-browser"));
document.getElementById("new-holon-explorer").addEventListener("click", () => setPendingTool("folk-holon-explorer"));
document.getElementById("new-image-gen").addEventListener("click", () => setPendingTool("folk-image-gen"));
document.getElementById("new-video-gen").addEventListener("click", () => setPendingTool("folk-video-gen"));
document.getElementById("new-zine-gen").addEventListener("click", () => setPendingTool("folk-zine-gen"));