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>
174 lines
4.2 KiB
Svelte
174 lines
4.2 KiB
Svelte
<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>
|