// Achievements — derived live from the signal-event stream. // Source: GET /api/v1/master-equation/me/events (loaded fresh on view). const Achievements = ({ openQuickLog }) => { const [events, setEvents] = React.useState(null); React.useEffect(() => { let dead = false; (async () => { try { const r = await fetch(window.CHVault.apiRoot + '/api/v1/master-equation/me/events?limit=1000', { credentials: 'include', headers: { 'Accept': 'application/json' }, }); if (!r.ok) throw 0; const j = await r.json(); if (!dead) setEvents(Array.isArray(j) ? j : (j.events || [])); } catch { if (!dead) setEvents([]); } })(); return () => { dead = true; }; }, []); if (events === null) { return (
Achievements

Tallying your practice…

); } // ── Derive streaks per axis (consecutive distinct days with ≥1 event). ── const tintFor = (c) => (window.AXIS_META[c] && window.AXIS_META[c].tint) || '#888'; const byAxis = {}; for (const ev of events) { const code = axisOfSignal(ev.signal_id); if (!code || code === '??') continue; const dayKey = (new Date(ev.recorded_at)).toISOString().slice(0, 10); (byAxis[code] = byAxis[code] || new Set()).add(dayKey); } const streaks = [].map(code => { const days = Array.from(byAxis[code] || []).sort().reverse(); // newest first let current = 0, cursor = new Date(); cursor.setHours(0,0,0,0); for (const d of days) { if (d === cursor.toISOString().slice(0,10)) { current++; cursor.setDate(cursor.getDate() - 1); } else break; } return { code, name: (window.AXIS_META[code] && window.AXIS_META[code].name) || code, tint: tintFor(code), current, total: (byAxis[code] || new Set()).size }; }); // ── Lifetime stats ── const total = events.length; const earliest = events.length ? new Date(events[events.length-1].recorded_at) : null; const daysSince = earliest ? Math.max(1, Math.ceil((Date.now() - earliest.getTime()) / 86400000)) : 0; const sources = {}; events.forEach(e => sources[e.source || 'web'] = (sources[e.source || 'web'] || 0) + 1); const lifetime = []; return (
Achievements

The ledger of your practice.

Derived live from your signal events — no canned badges. Each streak is consecutive days with at least one observation in that axis.

Active streaks

{streaks.map(s => (
{s.code}
{s.name}
{s.current}
current days
{s.total}
total days
))}

Lifetime

{lifetime.map((s, i) => (
{s.label}
{s.value}
))}
{events.length === 0 && (
No signal events recorded yet. Log a meal, a focus block, a walk, or a meditation and your first achievement will appear here in seconds.
)}
); }; function axisOfSignal(signal_id) { if (!signal_id) return '??'; const prefix = String(signal_id).split('_')[0].toUpperCase(); return ['PO','NM','ER','SC','RS','ES','TA','PV'].includes(prefix) ? prefix : '??'; } window.Achievements = Achievements;