// Web dashboard for monitoring account rotation status import { readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import type { IncomingMessage, ServerResponse } from "node:http"; import type { Config } from "./types.js"; import type { AccountRotator } from "./rotator.js"; import { readLimitedBody } from "./body-limit.js"; import { generateVirtualKey, listVirtualKeys, getVirtualKeyByHash, updateVirtualKey, deleteVirtualKey, } from "./virtual-keys.js"; import { getSpendLogs, getDailySpendSummary, getSpendByKey } from "./spend-logger.js"; import { logger } from "./logger.js"; const dashboardLogger = logger.child("dashboard"); const __dirname = dirname(fileURLToPath(import.meta.url)); // Static assets are read once at startup and served via dedicated routes. const DASHBOARD_CSS = readFileSync( join(__dirname, "static", "dashboard.css"), "utf-8", ); const DASHBOARD_JS = readFileSync( join(__dirname, "static", "dashboard.js"), "utf-8", ); const DASHBOARD_KEYS_JS = readFileSync( join(__dirname, "static", "dashboard-keys.js"), "utf-8", ); const DASHBOARD_LOGS_JS = readFileSync( join(__dirname, "static", "dashboard-logs.js"), "utf-8", ); export function serveDashboard(res: ServerResponse): void { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(DASHBOARD_HTML); } export function serveStaticCss(res: ServerResponse): void { res.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "public, max-age=3600", }); res.end(DASHBOARD_CSS); } export function serveStaticJs(res: ServerResponse): void { res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "public, max-age=3600", }); res.end(DASHBOARD_JS); } export function serveStaticKeysJs(res: ServerResponse): void { res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "public, max-age=3600", }); res.end(DASHBOARD_KEYS_JS); } export function serveStaticLogsJs(res: ServerResponse): void { res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "public, max-age=3600", }); res.end(DASHBOARD_LOGS_JS); } export function serveDashboardKeys(res: ServerResponse): void { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(DASHBOARD_KEYS_HTML); } export function serveDashboardLogs(res: ServerResponse): void { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(DASHBOARD_LOGS_HTML); } export function serveStatusApi( res: ServerResponse, rotator: AccountRotator, ): void { res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", }); res.end(JSON.stringify(rotator.getStatus())); } export function serveConfigApi( res: ServerResponse, rotator: AccountRotator, ): void { res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", }); res.end(JSON.stringify(rotator.getConfig())); } export function serveConfigExportApi( res: ServerResponse, rotator: AccountRotator, ): void { res.writeHead(200, { "Content-Type": "application/json", "Content-Disposition": 'attachment; filename="pi-antigravity-rotator-config.json"', }); res.end(JSON.stringify(rotator.getConfig(), null, 2)); } export function serveConfigImportApi( res: ServerResponse, rotator: AccountRotator, config: Config, ): void { rotator.replaceConfig(config); res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ ok: true, importedAccounts: config.accounts.length }), ); } export function serveEnableApi( res: ServerResponse, rotator: AccountRotator, email: string, ): void { const ok = rotator.enableAccount(email); res.writeHead(ok ? 200 : 409, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok, email })); } export function serveDisableApi( res: ServerResponse, rotator: AccountRotator, email: string, ): void { const ok = rotator.disableAccount(email); res.writeHead(ok ? 200 : 404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok, email })); } export function serveQuarantineApi( res: ServerResponse, rotator: AccountRotator, email: string, ): void { const ok = rotator.quarantineAccount(email); res.writeHead(ok ? 200 : 404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok, email })); } export function serveRestoreApi( res: ServerResponse, rotator: AccountRotator, email: string, ): void { const ok = rotator.restoreAccount(email); res.writeHead(ok ? 200 : 404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok, email })); } export function serveRemoveAccountApi( res: ServerResponse, rotator: AccountRotator, email: string, ): void { const ok = rotator.removeAccount(email); res.writeHead(ok ? 200 : 400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok, email })); } export function serveSetTierApi( res: ServerResponse, rotator: AccountRotator, email: string, tier: string, ): void { const ok = rotator.setAccountTier(email, tier); res.writeHead(ok ? 200 : 400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok, email, tier })); } export function serveFreshWindowStartsApi( res: ServerResponse, rotator: AccountRotator, enabled: boolean, ): void { const changed = rotator.setAllowFreshWindowStarts(enabled); res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ ok: true, changed, allowFreshWindowStarts: enabled }), ); } export function serveAccountFreshWindowStartsApi( res: ServerResponse, rotator: AccountRotator, email: string, enabled: boolean, ): void { const ok = rotator.setAccountAllowFreshWindowStartsOverride(email, enabled); res.writeHead(ok ? 200 : 404, { "Content-Type": "application/json" }); res.end( JSON.stringify({ ok, email, allowFreshWindowStartsOverride: enabled }), ); } export function serveClearInFlightApi( res: ServerResponse, rotator: AccountRotator, email: string, modelKey?: string, ): void { const ok = rotator.clearInFlightRequests(email, modelKey); res.writeHead(ok ? 200 : 404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok, email, modelKey })); } export function serveClearBreakerApi( res: ServerResponse, rotator: AccountRotator, modelKey?: string, ): void { if (modelKey) { rotator.clearModelBreaker(modelKey); } else { rotator.clearAllBreakers(); } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true })); } export function serveKickstartApi( res: ServerResponse, rotator: AccountRotator, email: string, modelKey?: string, ): void { if (modelKey) { rotator.kickstartTimerForAccount(email, modelKey).then((result) => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(result)); }).catch((err) => { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: String(err) })); }); } else { rotator.kickstartAllFreshTimers(email).then((result) => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(result)); }).catch((err) => { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: String(err), results: [] })); }); } } export function serveAutoWarmupApi( res: ServerResponse, rotator: AccountRotator, enabled: boolean, ): void { const changed = rotator.setAutoWarmup(enabled); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, changed, autoWarmupEnabled: enabled })); } // ── Virtual Keys & Spend Logging REST API ──────────────────────────── export async function serveGenerateVirtualKeyApi( req: IncomingMessage, res: ServerResponse, ): Promise { try { const rawBody = await readLimitedBody(req); const parsed = rawBody.length > 0 ? JSON.parse(rawBody.toString("utf-8")) : {}; if (!parsed.alias || typeof parsed.alias !== "string") { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Field 'alias' is required" })); return; } const created = await generateVirtualKey({ alias: parsed.alias, userId: parsed.userId, models: parsed.models, metadata: parsed.metadata, createdBy: parsed.createdBy || "admin", }); res.writeHead(201, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, ...created })); } catch (err) { dashboardLogger.error(`Failed to generate virtual key: ${err}`); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Internal server error" })); } } export async function serveListVirtualKeysApi( res: ServerResponse, ): Promise { try { const keys = await listVirtualKeys(); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, keys })); } catch (err) { dashboardLogger.error(`Failed to list virtual keys: ${err}`); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Internal server error" })); } } export async function serveGetVirtualKeyApi( res: ServerResponse, tokenHash: string, ): Promise { try { const key = await getVirtualKeyByHash(tokenHash); if (!key) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Virtual key not found" })); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, key })); } catch (err) { dashboardLogger.error(`Failed to get virtual key: ${err}`); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Internal server error" })); } } export async function serveUpdateVirtualKeyApi( req: IncomingMessage, res: ServerResponse, tokenHash: string, ): Promise { try { const rawBody = await readLimitedBody(req); const updates = rawBody.length > 0 ? JSON.parse(rawBody.toString("utf-8")) : {}; const updated = await updateVirtualKey(tokenHash, updates); if (!updated) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Virtual key not found" })); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, key: updated })); } catch (err) { dashboardLogger.error(`Failed to update virtual key: ${err}`); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Internal server error" })); } } export async function serveDeleteVirtualKeyApi( res: ServerResponse, tokenHash: string, ): Promise { try { const deleted = await deleteVirtualKey(tokenHash); if (!deleted) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Virtual key not found" })); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, message: "Virtual key deleted" })); } catch (err) { dashboardLogger.error(`Failed to delete virtual key: ${err}`); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Internal server error" })); } } export async function serveGetSpendLogsApi( req: IncomingMessage, res: ServerResponse, ): Promise { try { const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`); const keyHash = url.searchParams.get("keyHash") || undefined; const model = url.searchParams.get("model") || undefined; const status = url.searchParams.get("status") || undefined; const startDate = url.searchParams.get("startDate") || undefined; const endDate = url.searchParams.get("endDate") || undefined; const limit = url.searchParams.has("limit") ? parseInt(url.searchParams.get("limit")!, 10) : 50; const offset = url.searchParams.has("offset") ? parseInt(url.searchParams.get("offset")!, 10) : 0; const result = await getSpendLogs({ apiKeyHash: keyHash, model, status, startDate, endDate, limit, offset, }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, ...result })); } catch (err) { dashboardLogger.error(`Failed to get spend logs: ${err}`); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Internal server error" })); } } export async function serveGetSpendSummaryApi( req: IncomingMessage, res: ServerResponse, ): Promise { try { const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`); const keyHash = url.searchParams.get("keyHash") || undefined; const startDate = url.searchParams.get("startDate") || undefined; const endDate = url.searchParams.get("endDate") || undefined; const summary = await getDailySpendSummary({ apiKeyHash: keyHash, startDate, endDate, }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, summary })); } catch (err) { dashboardLogger.error(`Failed to get spend summary: ${err}`); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Internal server error" })); } } export async function serveGetSpendByKeyApi( req: IncomingMessage, res: ServerResponse, ): Promise { try { const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`); const startDate = url.searchParams.get("startDate") || undefined; const endDate = url.searchParams.get("endDate") || undefined; const byKey = await getSpendByKey({ startDate, endDate }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, byKey })); } catch (err) { dashboardLogger.error(`Failed to get spend by key: ${err}`); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "Internal server error" })); } } function renderAppShell(opts: { title: string; activeTab: "accounts" | "keys" | "logs"; contentHtml: string; scriptSrc: string; }): string { const pageTitle = opts.title === "Pi Antigravity Rotator" ? opts.title : (opts.title + " — Pi Antigravity Rotator"); return ` ${pageTitle}
NEW

Pi Antigravity Rotator

v--
Uptime: -- | Port: -- | Rotation: -- reqs | Updated: -- | Requests: 0
${opts.contentHtml} `; } const DASHBOARD_HTML = renderAppShell({ title: "Pi Antigravity Rotator", activeTab: "accounts", scriptSrc: "/static/dashboard.js", contentHtml: `
Token Usage
`, }); const DASHBOARD_KEYS_HTML = renderAppShell({ title: "Virtual Keys", activeTab: "keys", scriptSrc: "/static/dashboard-keys.js", contentHtml: `

Virtual Keys & Access Control

Manage API credentials, agent assignments, and per-model authorization rules

Total Credentials
0
Registered virtual keys
Active Keys
0
Authorized for requests
Blocked Keys
0
Revoked access
Supported Models
12
Gemini, Claude, GPT-OSS
Virtual Keys
Key Alias & Name User ID Allowed Models Status Last Active Actions
`, }); const DASHBOARD_LOGS_HTML = renderAppShell({ title: "Spend Logs", activeTab: "logs", scriptSrc: "/static/dashboard-logs.js", contentHtml: `

Spend Logs & Usage Analytics

Real-time audit trail of requests, prompt/completion tokens, latency metrics, and payload inspector

Total Requests
0
Logged requests
Prompt Tokens
0
Input tokens processed
Completion Tokens
0
Output tokens generated
Est. Cost
$0.000000
Estimated USD cost
Avg Latency
--
Average round-trip duration
Time Key / Agent Model Call Type Status Tokens (In / Out) Est. Cost Duration TTFB IP
`, });