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
158
src/lib/components/dashboard/Punchcard.svelte
Normal file
158
src/lib/components/dashboard/Punchcard.svelte
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
<script lang="ts">
|
||||
import { fmtInt, fmtCompact, fmtUsd } from '$lib/format';
|
||||
import type { Punchcard } from '$lib/server/stats/punchcard';
|
||||
|
||||
interface Props {
|
||||
punchcard: Punchcard;
|
||||
metric?: 'events' | 'tokens';
|
||||
}
|
||||
|
||||
let { punchcard, metric = 'events' }: Props = $props();
|
||||
|
||||
// Row labels, Monday-first to match the server's dow index (0=Mon..6=Sun).
|
||||
const DAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
const hours = Array.from({ length: 24 }, (_, h) => h);
|
||||
const tickHours = [0, 6, 12, 18, 23];
|
||||
|
||||
const cells = $derived(punchcard.cells);
|
||||
const maxValue = $derived(
|
||||
metric === 'events'
|
||||
? Math.max(1, punchcard.maxEventCount)
|
||||
: Math.max(1, punchcard.maxTotalTokens)
|
||||
);
|
||||
const hasData = $derived(punchcard.totalEvents > 0);
|
||||
|
||||
function cellValue(dow: number, hour: number): number {
|
||||
const c = cells[dow][hour];
|
||||
return metric === 'events' ? c.eventCount : c.totalTokens;
|
||||
}
|
||||
|
||||
// Intensity 0..1 → opacity of the accent fill. sqrt gives lighter cells more
|
||||
// visible contrast at the low end.
|
||||
function intensity(dow: number, hour: number): number {
|
||||
const v = cellValue(dow, hour);
|
||||
if (v <= 0) return 0;
|
||||
return Math.sqrt(v / maxValue);
|
||||
}
|
||||
|
||||
function hourLabel(hour: number): string {
|
||||
return `${String(hour).padStart(2, '0')}:00`;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !hasData}
|
||||
<p class="empty">No data in this window.</p>
|
||||
{:else}
|
||||
<div class="punch">
|
||||
<div class="grid" role="img" aria-label="Activity by day of week and hour of day (UTC)">
|
||||
{#each DAY_LABELS as day, dow (day)}
|
||||
<span class="day-label mono">{day}</span>
|
||||
{#each hours as hour (hour)}
|
||||
{@const cell = cells[dow][hour]}
|
||||
{@const op = intensity(dow, hour)}
|
||||
<span
|
||||
class="cell"
|
||||
class:zero={op === 0}
|
||||
style={`--op:${op}`}
|
||||
title={`${day} ${hourLabel(hour)} UTC · ${fmtInt(cell.eventCount)} events · ${fmtCompact(cell.totalTokens)} tok · ${fmtUsd(cell.costUsd)}`}
|
||||
></span>
|
||||
{/each}
|
||||
{/each}
|
||||
<span class="corner"></span>
|
||||
{#each hours as hour (hour)}
|
||||
<span class="hour-tick mono">{tickHours.includes(hour) ? hour : ''}</span>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span>Less</span>
|
||||
<span class="swatch" style="--op:0.1"></span>
|
||||
<span class="swatch" style="--op:0.4"></span>
|
||||
<span class="swatch" style="--op:0.7"></span>
|
||||
<span class="swatch" style="--op:1"></span>
|
||||
<span>More</span>
|
||||
<span class="utc-note mono">UTC</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.punch {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
/* day label column + 24 hour columns */
|
||||
grid-template-columns: auto repeat(24, 1fr);
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.day-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-faint);
|
||||
text-align: right;
|
||||
padding-right: 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cell {
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: 3px;
|
||||
background: var(--accent);
|
||||
opacity: var(--op);
|
||||
min-height: 12px;
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
|
||||
.cell:hover {
|
||||
transform: scale(1.18);
|
||||
}
|
||||
|
||||
.cell.zero {
|
||||
background: var(--bg-raised);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.corner {
|
||||
/* aligns with the day-label column under the grid */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hour-tick {
|
||||
font-size: 0.62rem;
|
||||
color: var(--text-faint);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.swatch {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
background: var(--accent);
|
||||
opacity: var(--op);
|
||||
}
|
||||
|
||||
.utc-note {
|
||||
margin-left: auto;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--text-dim);
|
||||
font-size: 13px;
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue