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; 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' });