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:
commit
71a60ab054
74 changed files with 15613 additions and 0 deletions
174
src/lib/components/RangePicker.svelte
Normal file
174
src/lib/components/RangePicker.svelte
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue