// Projects index, single project, stats, health, settings const ProjectsScreen = () => { const { go } = useRouter(); const cols = '1fr 90px 90px 110px 110px 90px'; return ( <>
{PROJECTS.length} projects · ranked by recent activity {}} />
cwdsessionstok lasttop toolactivity
{PROJECTS.map((p, i) => (
go({ name: 'project', cwd: p.cwd })}> {p.cwd} {p.sessions} {p.tok} {p.last}
))}
); }; const ProjectScreen = () => { const { go, route } = useRouter(); const cwd = route.cwd || '~/code/tt-bundle'; const meta = PROJECTS.find(p => p.cwd === cwd) || { sessions: 0, tok: '0', topTool: 'claude-code' }; const lastSeg = cwd.split('/').filter(Boolean).pop() || cwd; const parent = cwd.slice(0, cwd.length - lastSeg.length); const sessionList = SESSIONS.filter(s => s.cwd === cwd); const cols = '90px 110px 70px 1fr 50px 60px'; return ( <>
{parent}
{lastSeg} {meta.sessions} sessions {meta.tok} tok {Array.from(new Set(sessionList.map(s => s.host))).map(h => )}
{sessionList.length > 0 && (
★ saved: {SAVED_SEARCHES.map(s => ( ★ {s} ))} + new
)}
datetoolhostsession turnstok
{sessionList.length === 0 ? ( ) : sessionList.map((s) => (
go({ name: 'session', id: s.id })}> {s.when.replace(/^Today /, '').replace(/^Yest\. /, 'Yest ')} {s.q} {s.turns} {s.tok}
))}
); }; const StatsScreen = () => { const { go } = useRouter(); const [range, setRange] = React.useState('30d'); const [groupBy, setGroupBy] = React.useState('tool'); return ( <>
range: {['7d', '30d', '90d', 'all'].map(r => ( setRange(r)}>{r} ))} group by: {['tool', 'host', 'project', 'model'].map(g => ( setGroupBy(g)}>{g} ))}
{/* per-tool strip */}
per tool · last {range}
{TOOL_ROLLUPS.map((r, i) => (
{r.tool} {r.turns.toLocaleString()} {r.ktok}k tok {r.cost > 0 ? `$${r.cost.toFixed(2)}` : '—'} {(r.share * 100).toFixed(0)}%
))}
{/* turns/day stacked */}
turns/day · stacked by tool claude-code opencode crush
{/* Y axis grid */} {[0, 30, 60, 90, 120].map(y => ( ))} {Array.from({ length: 60 }).map((_, i) => { const claude = 8 + Math.abs(Math.sin(i * 0.5)) * 60; const oc = 2 + Math.abs(Math.cos(i * 0.9)) * 7; const crush = 1 + Math.abs(Math.sin(i * 1.2)) * 3; const x = i * 10 + 2; return ( ); })}
60d ago today
{/* heatmap */}
activity · 12 weeks
{Array.from({ length: 12 * 7 }).map((_, i) => { const v = (Math.sin(i * 0.6) + Math.cos(i * 0.3) + 1.5) / 3; const w = Math.floor(i / 7), d = i % 7; const op = 0.1 + v * 0.85; return ; })}
less {[0.15, 0.35, 0.55, 0.8, 1].map(o => ( ))} more
{/* top cwd */}
top cwd
{[ ['~/code/tt-bundle', 41, 1.0], ['~/work/scarlet-svc', 28, 0.68], ['~/work/atelier/migrations', 14, 0.34], ['~/code/dotfiles', 7, 0.17], ['~/notes', 5, 0.12], ].map(([p, n, r]) => (
go({ name: 'project', cwd: p })}> {p}
{n}
))}
{/* hour-of-day */}
turns by hour
{Array.from({ length: 24 }).map((_, i) => { const v = i < 7 ? 4 + Math.sin(i) * 3 : i < 12 ? 30 + Math.sin(i * 0.7) * 18 : i < 18 ? 45 + Math.cos(i * 0.6) * 14 : 18 + Math.sin(i * 1.1) * 10; const h = Math.max(2, v); return 19 ? 0.45 : 0.85} />; })}
0006121824
{/* host split */}
by host
{[ ['laptop', 1240, 0.72, '#3b6e3b'], ['workpc', 480, 0.28, 'var(--accent)'], ].map(([h, n, r, c]) => (
{n.toLocaleString()} ({(r * 100).toFixed(0)}%)
))}
1,720 turns · 30d
); }; const HealthScreen = () => { const okCount = COLLECTORS.filter(c => c.status === 'ok').length; const warnCount = COLLECTORS.filter(c => c.status === 'warn').length; const staleCount = COLLECTORS.filter(c => c.status === 'stale').length; const cols = '20px 90px 130px 1fr 60px 70px 80px 80px'; return ( <>
collectors ● {okCount} ok {warnCount > 0 && ● {warnCount} warn} {staleCount > 0 && ● {staleCount} stale} poll: 30s · ingesting as {ME.user} via {ME.via} → 127.0.0.1:8401
hosttoolsource lagoutbox last okevents 24h
{COLLECTORS.map((r, i) => (
{r.host} {r.src} {r.lag} 0 ? '' : 'muted')} style={r.out > 0 ? { color: 'var(--warn)', fontWeight: 600 } : {}}>{r.out} {r.last} {r.ev}
))}
backfill: claude-code/workpc 6/9 files
67% ~3m ● last error: 09:08 crush "tool_call_v2" → metadata fallback
); }; const SettingsScreen = () => { const [section, setSection] = React.useState('Sources'); const sections = ['Sources', 'Display', 'Auth', 'Backup', 'Export', 'Tags', 'Saved searches']; return (
{section === 'Sources' && } {section === 'Display' && } {section === 'Auth' && } {section === 'Backup' && } {section === 'Export' && } {section === 'Tags' && } {section === 'Saved searches' && }
); }; const SettingsSources = () => { const cols = '14px 110px 1fr 60px 90px 70px 50px'; return ( <>
Sources
~/.config/assistant-log/config.toml · per-host
toolpath pollevents last ok
{[ ['ok', 'claude-code', '~/.claude/projects', '30s', 12418, 'now'], ['ok', 'opencode', '~/.local/share/opencode', '30s', 312, '12s'], ['ok', 'crush', '~/.cache/crush', '60s', 88, '28s'], ['ok', 'pi', '~/.config/pi/history', '60s', 47, '1m'], ['warn', 'kimi', '~/.kimi-cli/history.jsonl', '60s', 12, '2m'], ].map(([s, t, p, poll, ev, last], i, arr) => (
{p} {poll} {ev.toLocaleString()} {last} edit
))}
+ add source
server
modulesourcecraft.dev/bigbes/lethe bind{AUTH_CONFIG.bind} · loopback-only, behind reverse proxy db~/.local/share/lethe/store.sqlite (412 MB) · WAL · busy_timeout=5s ftsturns_fts · tool_outputs_fts · 24,118 turns indexed migrations0001_init · applied on startup via embed.FS api/api/v1 · /healthz · /readyz · /metrics uptime14d 03:12 · since boot
); }; const SettingsAuth = () => { const a = AUTH_CONFIG; return ( <>
Auth
Two independent paths, both gated by the same allowlist. Server binds 127.0.0.1 only — a reverse proxy on phoebe terminates TLS and forwards. Editing requires rewriting config.yaml and restarting.
allowlist · auth.allowed_users
{a.allowedUsers.map(u => ( {u}{a.admins.includes(u) && ADMIN} ))} + add
forward-auth (header trust) {a.forwardAuth.enabled ? '● enabled' : '○ disabled'}
user header{a.forwardAuth.userHeader} trust sourceCaddy → Authelia forward-auth used bybrowser sessions w/ Authelia cookie
caddy snippet
{`forward_auth authelia.internal:9091 {
  uri /api/verify?rd=https://auth/
  copy_headers Remote-User Remote-Email
}
reverse_proxy 127.0.0.1:8401`}
oidc bearer {a.oidc.enabled ? '● enabled' : '○ disabled'}
issuer{a.oidc.issuer} audience{a.oidc.audience} claim{a.oidc.usernameClaim} → fallback sub jwkscached · last fetch {a.oidc.jwksLastFetch} used bycollector, scripted clients
resolution order
1. Authorization: Bearer … validated → user from JWT
2. else {a.forwardAuth.userHeader} taken from proxy
3. else 401 · invalid bearer never falls back (fail-closed)
recent auth events
timeuserviapathcodenote
{AUTH_EVENTS.map((e, i) => (
= 400 ? 'var(--err-bg)' : 'transparent', }}> {e.t} {e.user} {e.via} {e.path} = 400 ? 'var(--err)' : 'var(--ok)' }}>{e.status} {e.note || ''}
))}
trust model
owner is server-derived from the authenticated user on every ingest write. The wire format in internal/shared/wire/ has no owner field — collectors cannot impersonate. Read endpoints filter to owner = current_user; admins ({a.admins.join(', ')}) may pass ?owner=<user> or ?owner=* to override. Non-admins passing ?owner= at all → 403. A session belonging to another owner returns 404 (existence is never leaked).
); }; const SettingsDisplay = () => ( <>
Display
Mirror of the Tweaks panel — these settings sync.
{[ ['density', ['compact', 'comfortable']], ['tool calls', ['expanded', 'collapsed']], ['accent color', ['on', 'off']], ['hour format', ['24h', '12h']], ].map(([l, opts]) => (
{l}
{opts.map((o, j) => ( {o} ))}
))}
); const SettingsStub = ({ title, subtitle }) => ( <>
{title}
{subtitle}
Detailed config UI · stubbed for prototype
); Object.assign(window, { ProjectsScreen, ProjectScreen, StatsScreen, HealthScreen, SettingsScreen, });