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>
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
import { json, error } from '@sveltejs/kit';
|
|
import { env } from '$env/dynamic/private';
|
|
import { db } from '$lib/server/db';
|
|
import type { RequestHandler } from './$types';
|
|
|
|
/**
|
|
* Usage-gauge ingest. The agent PTY-drives `claude /usage` (see agent/usage.ts) and POSTs the
|
|
* three subscription bars here. Separate from /api/ingest because the shape and cadence differ
|
|
* (a slow, ~5-9s scrape done once per run, not per JSONL sweep).
|
|
*
|
|
* Auth: `Authorization: Bearer $API_TOKEN` (same token as /api/ingest).
|
|
*
|
|
* Accepted body — either a single gauge object or a batch:
|
|
* { host, ts_utc, session_pct?, week_all_pct?, week_sonnet_pct? }
|
|
* { host?, gauges: GaugeRow[] } // host is a fallback for rows that omit their own
|
|
*
|
|
* Rows upsert by (host, ts_utc) — re-posting the same scrape is idempotent.
|
|
*/
|
|
|
|
interface IncomingGauge {
|
|
host?: unknown;
|
|
ts_utc?: unknown;
|
|
session_pct?: unknown;
|
|
week_all_pct?: unknown;
|
|
week_sonnet_pct?: unknown;
|
|
}
|
|
|
|
interface UsageBody extends IncomingGauge {
|
|
gauges?: unknown;
|
|
}
|
|
|
|
function str(v: unknown): string | null {
|
|
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
}
|
|
|
|
function num(v: unknown): number | null {
|
|
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
}
|
|
|
|
// Lazily prepared on first request so importing this module (e.g. the build's
|
|
// analyse pass, which has no writable DB dir) never opens the database.
|
|
type Stmt = import('better-sqlite3').Statement<unknown[]>;
|
|
|
|
let prepared = false;
|
|
let upsertGaugeStmt!: Stmt;
|
|
let insertMany!: (rows: IncomingGauge[], fallbackHost: string | null) => number;
|
|
|
|
function ensurePrepared() {
|
|
if (prepared) return;
|
|
const conn = db();
|
|
upsertGaugeStmt = conn.prepare(`
|
|
INSERT INTO usage_gauges (host, ts_utc, session_pct, week_all_pct, week_sonnet_pct)
|
|
VALUES (@host, @ts_utc, @session_pct, @week_all_pct, @week_sonnet_pct)
|
|
ON CONFLICT(host, ts_utc) DO UPDATE SET
|
|
session_pct = COALESCE(excluded.session_pct, session_pct),
|
|
week_all_pct = COALESCE(excluded.week_all_pct, week_all_pct),
|
|
week_sonnet_pct = COALESCE(excluded.week_sonnet_pct, week_sonnet_pct)
|
|
`);
|
|
|
|
insertMany = conn.transaction((rows: IncomingGauge[], fallbackHost: string | null) => {
|
|
let count = 0;
|
|
for (const g of rows) {
|
|
const host = str(g.host) ?? fallbackHost;
|
|
const ts_utc = str(g.ts_utc);
|
|
if (!host || !ts_utc) continue; // incomplete PK — skip
|
|
upsertGaugeStmt.run({
|
|
host,
|
|
ts_utc,
|
|
session_pct: num(g.session_pct),
|
|
week_all_pct: num(g.week_all_pct),
|
|
week_sonnet_pct: num(g.week_sonnet_pct)
|
|
});
|
|
count++;
|
|
}
|
|
return count;
|
|
});
|
|
prepared = true;
|
|
}
|
|
|
|
export const POST: RequestHandler = async ({ request }) => {
|
|
const token = env.API_TOKEN;
|
|
if (!token) throw error(500, 'server missing API_TOKEN');
|
|
if (request.headers.get('authorization') !== `Bearer ${token}`) throw error(401, 'unauthorized');
|
|
|
|
const body = (await request.json().catch(() => null)) as UsageBody | null;
|
|
if (!body || typeof body !== 'object') throw error(400, 'invalid JSON body');
|
|
|
|
ensurePrepared();
|
|
const rows = Array.isArray(body.gauges) ? (body.gauges as IncomingGauge[]) : [body];
|
|
const count = insertMany(rows, str(body.host));
|
|
return json({ ok: true, gauges: count });
|
|
};
|
|
|
|
export const GET: RequestHandler = async () => json({ ok: true, service: 'toknmtr usage' });
|