// Metagraph table — only renders columns we actually have data for. // As central exposes more per-miner fields, just stop excluding them in // the column-presence checks below; the table will start rendering them. window.Metagraph = function Metagraph({ navigate }) { // Re-render when data.js finishes its fetch. Until then, show a loading // shimmer rather than mock data. const [tick, setTick] = React.useState(0); React.useEffect(() => { const onLoaded = () => setTick((t) => t + 1); window.addEventListener('miners:loaded', onLoaded); // If data.js already finished before this component mounted, kick once. if (window.__MINER_DATA) setTick((t) => t + 1); return () => window.removeEventListener('miners:loaded', onLoaded); }, []); const miners = window.__MINER_DATA || []; const summary = window.__MINER_SUMMARY || { total: 0, live: 0, contributions: 0 }; // Detect which optional columns to render based on what fields the data // actually carries. If central later starts returning hotkeys / sparklines / // scores, the columns appear automatically with no UI change. const hasHotkey = miners.some((m) => !!m.hotkey); const hasSparkline = miners.some((m) => Array.isArray(m.sparkline) && m.sparkline.length > 0); const hasScore = miners.some((m) => typeof m.score === 'number'); const [filter, setFilter] = React.useState('all'); // all | live | offline const [q, setQ] = React.useState(''); const filtered = React.useMemo(() => { const ql = q.trim().toLowerCase(); return miners.filter((m) => { if (filter === 'live' && !m.live) return false; if (filter === 'offline' && m.live) return false; if (ql) { const searchable = String(m.uid) + (m.hotkey ? ' ' + m.hotkey.toLowerCase() : ''); return searchable.includes(ql); } return true; }); }, [q, filter, miners, tick]); const maxContrib = miners[0] ? miners[0].contributions : 1; function formatLastSeen(h) { if (h === null || h === undefined) return 'never'; if (h < 1) return 'just now'; if (h < 24) return h + 'h ago'; const d = Math.floor(h / 24); return d + 'd ago'; } // Build column list dynamically. Rank + UID + Contributions + Status are // always shown. The rest are conditional on data presence. const cols = [ { key: 'rank', label: 'RANK' }, { key: 'uid', label: hasHotkey ? 'HOTKEY' : 'UID' }, { key: 'contributions', label: 'CONTRIBUTIONS' }, ]; if (hasSparkline) cols.push({ key: 'spark', label: 'ACTIVITY · 14D', className: 'col-spark' }); cols.push({ key: 'lastSeen', label: 'LAST CONTRIBUTION', style: { textAlign: 'center' } }); if (hasScore) cols.push({ key: 'score', label: 'LAST SCORE' }); cols.push({ key: 'status', label: 'STATUS', style: { textAlign: 'right' } }); const gridTemplate = cols .map((c) => { if (c.key === 'rank') return '60px'; if (c.key === 'uid') return hasHotkey ? '1fr' : '120px'; if (c.key === 'contributions') return '1.4fr'; if (c.key === 'spark') return '180px'; if (c.key === 'lastSeen') return '160px'; if (c.key === 'score') return '110px'; if (c.key === 'status') return '120px'; return '1fr'; }) .join(' '); return (

Metagraph

Independent contributors expanding and verifying the Financial Knowledge Graph. Ranked by total accepted contributions; a contributor is Live if their most recent contribution landed in the last 7 days.

{summary.total}
Total contributors
{summary.live}
Live (last 7d)
{(summary.contributions || 0).toLocaleString()}
Total contributions
setQ(e.target.value)} />
{cols.map((c) => (
{c.label}
))}
{filtered.map((m) => (
{m.rank <= 3 ? ( {m.rank} ) : ( #{m.rank} )}
{hasHotkey ? ( <> {m.hotkeyShort} uid:{m.uid} ) : ( UID {m.uid} )}
{(m.contributions || 0).toLocaleString()}
{hasSparkline && (
)}
{formatLastSeen(m.lastSeenHoursAgo)}
{hasScore && (
{m.live ? m.score.toFixed(4) : }
)}
{m.live ? ( LIVE ) : m.lastSeenHoursAgo !== null ? ( IDLE ) : ( )}
))} {filtered.length === 0 && (
{miners.length === 0 ? 'No contributions recorded yet.' : 'No contributors match.'}
)}
); }; // Kept for forward compat — when central starts shipping per-miner sparklines, // the column re-appears and uses this component automatically. window.Sparkline = function Sparkline({ data, live }) { const w = 160, h = 28; const max = Math.max(0.0001, ...data); const pts = data.map((v, i) => { const x = (i / (data.length - 1)) * (w - 2) + 1; const y = h - 2 - (v / max) * (h - 6); return [x, y]; }); const path = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' '); const areaPath = path + ` L ${w-1} ${h-1} L 1 ${h-1} Z`; const color = live ? '#7c7cff' : '#4a4a5a'; const id = 'sg' + Math.random().toString(36).slice(2, 8); return ( ); };