toknmtr — self-hostable Claude Code usage & analytics dashboard

Server (SvelteKit + SQLite Docker container) + per-machine agent that parses
Claude Code JSONL transcripts. docker-compose one-command deploy; raw transcript
view + search gated behind SHOW_TRANSCRIPTS (hidden by default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-07-08 19:33:55 +01:00
commit 71a60ab054
74 changed files with 15613 additions and 0 deletions

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { SvelteURLSearchParams } from 'svelte/reactivity';
import { RANGE_PRESETS, type ResolvedRange } from '$lib/ranges';
let { range }: { range: ResolvedRange } = $props();
// local overrides only kick in once the user edits/opens; otherwise everything
// tracks the resolved range from the server load (so navigation re-seeds it).
let manualCustom = $state(false);
let userFrom = $state<string | null>(null);
let userTo = $state<string | null>(null);
const showCustom = $derived(manualCustom || range.custom);
const from = $derived(
userFrom ?? range.from ?? (range.since ?? new Date().toISOString()).slice(0, 10)
);
const to = $derived(userTo ?? range.to ?? range.until.slice(0, 10));
// Navigate on the CURRENT route (dashboard, /sessions, …), preserving any other
// query params (e.g. sort/dir) and only rewriting range/from/to. resolve() only
// rewrites pathnames, not query strings, so the disable is required.
function navWith(updates: Record<string, string | null>) {
const params = new SvelteURLSearchParams(page.url.search);
for (const [k, v] of Object.entries(updates)) {
if (v === null) params.delete(k);
else params.set(k, v);
}
const qs = params.toString();
// eslint-disable-next-line svelte/no-navigation-without-resolve
goto(`${page.url.pathname}${qs ? `?${qs}` : ''}`, { keepFocus: true, noScroll: true });
}
function selectPreset(key: string) {
manualCustom = false;
navWith({ range: key, from: null, to: null });
}
function applyCustom(e: Event) {
e.preventDefault();
if (!from || !to) return;
navWith({ range: 'custom', from, to });
}
</script>
<div class="range">
<div class="seg" role="group" aria-label="Time range">
{#each RANGE_PRESETS as p (p.key)}
<button
type="button"
class:active={!showCustom && range.key === p.key}
onclick={() => selectPreset(p.key)}>{p.label}</button
>
{/each}
<button type="button" class:active={showCustom} onclick={() => (manualCustom = !manualCustom)}
>Custom</button
>
</div>
{#if showCustom}
<form class="custom" onsubmit={applyCustom}>
<input
type="date"
value={from}
oninput={(e) => (userFrom = e.currentTarget.value)}
aria-label="From date"
max={to}
/>
<span class="dash"></span>
<input
type="date"
value={to}
oninput={(e) => (userTo = e.currentTarget.value)}
aria-label="To date"
min={from}
/>
<button type="submit" class="apply">Apply</button>
</form>
{/if}
</div>
<style>
.range {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.seg {
display: inline-flex;
background: var(--bg-panel-2);
border: 1px solid var(--border-soft);
border-radius: 999px;
padding: 2px;
}
.seg button {
appearance: none;
border: none;
background: transparent;
color: var(--text-dim);
font: inherit;
font-size: 0.8rem;
padding: 0.32rem 0.7rem;
border-radius: 999px;
cursor: pointer;
transition:
background 0.12s ease,
color 0.12s ease;
}
.seg button:hover {
color: var(--text);
}
.seg button.active {
background: var(--grad-accent);
color: var(--on-accent);
font-weight: 600;
box-shadow: 0 4px 14px -4px var(--glow);
}
.custom {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.custom input[type='date'] {
background: var(--bg-raised);
border: 1px solid var(--border-soft);
color: var(--text);
border-radius: var(--radius-sm);
padding: 0.3rem 0.5rem;
font: inherit;
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-size: 0.8rem;
transition: border-color 0.12s ease;
}
.custom input[type='date']:hover,
.custom input[type='date']:focus-visible {
border-color: var(--accent);
outline: none;
}
.dash {
color: var(--text-faint);
font-size: 0.8rem;
}
.apply {
appearance: none;
border: 1px solid var(--border-soft);
background: var(--bg-raised);
color: var(--text);
font: inherit;
font-size: 0.8rem;
padding: 0.32rem 0.75rem;
border-radius: var(--radius-sm);
cursor: pointer;
transition:
border-color 0.12s ease,
background 0.12s ease;
}
.apply:hover {
border-color: var(--accent);
background: var(--bg-panel-2);
}
</style>