// 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 (
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.