// 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
{}} />
cwd sessions tok
last top tool activity
{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
)}
date tool host session
turns tok
{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} />;
})}
00 06 12 18 24
{/* 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
host tool source
lag outbox
last ok events 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 (
{sections.map(s => (
setSection(s)}>{s}
))}
{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
tool path
poll events
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
module sourcecraft.dev/bigbes/lethe
bind {AUTH_CONFIG.bind} · loopback-only, behind reverse proxy
db ~/.local/share/lethe/store.sqlite (412 MB) · WAL · busy_timeout=5s
fts turns_fts · tool_outputs_fts · 24,118 turns indexed
migrations 0001_init · applied on startup via embed.FS
api /api/v1 · /healthz · /readyz · /metrics
uptime 14d 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 source Caddy → Authelia forward-auth
used by browser 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
jwks cached · last fetch {a.oidc.jwksLastFetch}
used by collector, 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
time user via path code note
{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,
});