Files
Codeman/src/web/routes/status-telemetry-routes.ts
T
arkonandClaude Opus 4.8 95fb5fc226 feat(usage): replay last-known plan usage in the SSE init snapshot
The header chip previously only repopulated on reload from per-browser
localStorage, so a fresh browser (or cleared storage) stayed blank until a
session next rendered telemetry. Store the latest broadcast telemetry
process-wide (plan-usage-latest.ts) and include it as `planUsage` in
getLightState — the per-connection SSE init snapshot — so handleInit paints
the chip immediately on every fresh load / reconnect, authoritative over the
localStorage restore. Null until the first telemetry of the process.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 06:15:34 +02:00

72 lines
2.9 KiB
TypeScript

/**
* @fileoverview Status-telemetry route.
*
* Receives Claude Code statusline payloads POSTed by the Codeman-managed
* statusLine exporter (see `hooks-config.generateStatusLineCommand`) and
* broadcasts the parsed plan-usage limits (5-hour + weekly) to SSE clients for
* the header "Plan Usage Limits" chip. Auth-exempt like `/api/hook-event`
* (localhost-only; hook-secret-gated while a tunnel runs — see middleware/auth).
*
* Returns a compact plain-text status string for the exporter to print as the
* in-terminal footer (print-through), so injecting our statusLine doesn't leave
* the terminal footer blank.
*/
import { FastifyInstance } from 'fastify';
import { StatusTelemetrySchema } from '../schemas.js';
import { parseBody } from '../route-helpers.js';
import {
parseStatusTelemetry,
parseSessionStatus,
formatSessionStatusText,
telemetrySignature,
type RawStatuslinePayload,
} from '../../usage-telemetry.js';
import { SessionStatusTelemetry } from '../sse-events.js';
import { setLatestPlanUsage } from '../plan-usage-latest.js';
import type { SessionPort, EventPort } from '../ports/index.js';
export function registerStatusTelemetryRoutes(app: FastifyInstance, ctx: SessionPort & EventPort): void {
// Last broadcast telemetry signature per session — the statusline fires on
// every assistant message, so we only rebroadcast when the value changes.
const lastSig = new Map<string, string>();
app.post('/api/status-telemetry', async (req, reply) => {
const { sessionId, data } = parseBody(StatusTelemetrySchema, req.body);
reply.type('text/plain; charset=utf-8');
// Unknown session — minimal footer, no broadcast.
if (!ctx.sessions.has(sessionId)) {
lastSig.delete(sessionId);
return 'codeman';
}
const payload = data as RawStatuslinePayload | undefined;
// Plan-usage limits (account-wide) → broadcast to the header chip, when
// present and changed (the statusline fires on every assistant message).
const telemetry = parseStatusTelemetry(payload);
if (telemetry) {
const sig = telemetrySignature(telemetry);
if (lastSig.get(sessionId) !== sig) {
lastSig.set(sessionId, sig);
// Bound the map across long multi-session runs: prune dead sessions.
if (lastSig.size > 256) {
for (const id of [...lastSig.keys()]) {
if (!ctx.sessions.has(id)) lastSig.delete(id);
}
}
const payload = { sessionId, ...telemetry };
setLatestPlanUsage(payload); // replayed in the SSE init snapshot for fresh loads
ctx.broadcast(SessionStatusTelemetry, payload);
}
}
// In-terminal statusline footer → CURRENT SESSION status (model / tokens /
// context %), NOT the plan limits. Available from the first render, even
// before rate_limits appears.
return formatSessionStatusText(parseSessionStatus(payload));
});
}