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>
This commit is contained in:
arkon
2026-06-14 06:15:34 +02:00
co-authored by Claude Opus 4.8
parent eae225bf9a
commit 95fb5fc226
4 changed files with 32 additions and 1 deletions
+22
View File
@@ -0,0 +1,22 @@
/**
* @fileoverview Process-wide last-known plan-usage telemetry (account-global).
*
* The status-telemetry route writes the latest broadcast value here; the SSE
* init snapshot (`getLightState`) replays it so the header "Plan Usage Limits"
* chip shows immediately on a fresh page load / SSE reconnect — before any new
* statusline render arrives, and without relying on per-browser localStorage.
*
* Null until the first telemetry of the process; cleared naturally on restart.
*
* @module plan-usage-latest
*/
let latest: Record<string, unknown> | null = null;
export function setLatestPlanUsage(value: Record<string, unknown>): void {
latest = value;
}
export function getLatestPlanUsage(): Record<string, unknown> | null {
return latest;
}
+4
View File
@@ -2258,6 +2258,10 @@ class CodemanApp {
this._serverCjkOverride = data.inputCjkForm || false;
this._updateCjkInputState();
// Plan-usage chip: server's last-known telemetry, so it shows immediately on
// a fresh load / reconnect (authoritative; wins over the localStorage restore).
if (data.planUsage) this.updatePlanUsageChip(data.planUsage);
// Update version displays (header and toolbar)
if (data.version) {
const versionEl = this.$('versionDisplay');
+4 -1
View File
@@ -23,6 +23,7 @@ import {
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 {
@@ -56,7 +57,9 @@ export function registerStatusTelemetryRoutes(app: FastifyInstance, ctx: Session
if (!ctx.sessions.has(id)) lastSig.delete(id);
}
}
ctx.broadcast(SessionStatusTelemetry, { sessionId, ...telemetry });
const payload = { sessionId, ...telemetry };
setLatestPlanUsage(payload); // replayed in the SSE init snapshot for fresh loads
ctx.broadcast(SessionStatusTelemetry, payload);
}
}
+2
View File
@@ -124,6 +124,7 @@ import {
import type { EventLoopMonitorHandle } from '../utils/index.js';
import { MAX_CONCURRENT_SESSIONS, MAX_SSE_CLIENTS } from '../config/map-limits.js';
import { SseEvent } from './sse-events.js';
import { getLatestPlanUsage } from './plan-usage-latest.js';
import type { ScheduledRun } from './ports/index.js';
import { registerAuthMiddleware, registerSecurityHeaders, registerHostGuard } from './middleware/auth.js';
import { installRouteErrorHandler } from './route-error-handler.js';
@@ -1627,6 +1628,7 @@ export class WebServer extends EventEmitter {
subagents: subagentWatcher.getRecentSubagents(15), // 15 min to avoid stale agents
timestamp: now,
inputCjkForm: process.env.INPUT_CJK_FORM?.toUpperCase() === 'ON',
planUsage: getLatestPlanUsage(), // last-known plan-usage telemetry, for the header chip on fresh load
};
this.cachedLightState = { data: result, timestamp: now };