2026-05-03 09:53:40 +02:00

1642 lines
65 KiB
TypeScript

import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { execFile } from 'child_process';
import { promisify } from 'util';
import { existsSync } from 'fs';
import { homedir } from 'os';
import { getPool } from '../db/client.js';
import { logger } from '../observability/logger.js';
import { createRequestLogger } from '../modules/request-logger.js';
import { globalRequestStream } from '../modules/request-stream.js';
import { getAvailableProviders, getAllProviders } from '../pipeline/external-providers.js';
import { discoverSubscriptions } from '../modules/subscription-discovery.js';
import { getRunningBridges, spawnDetectedBridges } from '../modules/bridge-spawner.js';
import { getPublicSettings, saveSettings, SettingsPatchSchema } from '../modules/settings-store.js';
import {
getCacheSavings,
getSavingsTimeSeries,
clearCacheForCaller,
pruneStaleCacheEntries,
} from '../modules/response-cache.js';
import { getComprehensiveSavings } from '../modules/savings-calculator.js';
import {
getBuddyState,
getAchievements,
getCalendarHeatmap,
getRecentEvents,
getForecast,
} from '../modules/gamification.js';
import { buildMemoryGraph } from '../modules/memory-graph.js';
import { getRaceLeaderboard } from '../modules/race-leaderboard.js';
import { getCallerDeepDive } from '../modules/caller-stats.js';
import { generateMonthlyReport } from '../modules/report-generator.js';
import { generateShareCard } from '../modules/share-card.js';
import { getSubscriptionWallet, recordSubscriptionUsage } from '../modules/subscription-wallet.js';
import { rememberFact, recallFacts, forgetCaller } from '../modules/knowledge-memory.js';
import { getRaceStats } from '../modules/race-mode.js';
import { dashboardAuthStatus, requireDashboardAuth } from '../modules/admin-auth.js';
const execFileAsync = promisify(execFile);
interface DashboardSummary {
totalCost: number;
totalSaved: number;
compressionRatio: number;
tokensSaved: number;
requestCount: number;
averageConfidence: number;
timeWindow: string;
}
interface CostBreakdown {
byProject: Record<string, { cost: number; count: number; saved: number }>;
byModel: Record<string, { cost: number; count: number }>;
byTaskType: Record<string, { cost: number; count: number }>;
totalCost: number;
totalSaved: number;
}
interface TokenMetrics {
totalIn: number;
totalOut: number;
totalCompressed: number;
compressionRate: number;
byModel: Record<string, { in: number; out: number; compressed: number }>;
}
interface AgentActivity {
agent: string;
taskCount: number;
averageCost: number;
averageConfidence: number;
totalTokens: number;
lastActivity: string;
}
interface LearningMetrics {
promptsImproved: number;
routingUpdates: number;
templateVariations: number;
averageScoreGain: number;
lastLearningRun: string;
}
interface AlertData {
active: number;
byType: Record<string, number>;
thresholds: {
compressionBelow: number;
weeklyBudget: number;
externalApiCost: number;
};
}
const WORKBENCH_V1_BASELINE = {
totalTokensSaved: 9_304_882,
totalCostSaved: 72.54,
totalHits: 6,
hitRatePercent: 9.68,
costWithoutGateway: 749.38,
costWithGateway: 676.84,
};
type ProviderRuntime = {
runtimeStatus?: string;
runtimeHealthy?: boolean;
runtimeDetail?: string;
};
const CLIENT_CATALOG = [
{
id: 'codex-desktop',
label: 'Codex Desktop / CLI',
patterns: ['codex-desktop', 'codex-cli', 'codex'],
commands: ['codex'],
paths: ['/Applications/Codex.app', '~/.codex'],
processPatterns: ['Codex.app', 'Codex Helper', '/Applications/Codex.app', '/Resources/codex'],
},
{
id: 'claude-desktop',
label: 'Claude Desktop / Claude Code',
patterns: ['claude-desktop', 'claude-code', 'claude'],
commands: ['claude'],
paths: ['/Applications/Claude.app', '~/Library/Application Support/Claude', '~/.claude'],
processPatterns: ['/Applications/Claude.app', 'Claude Helper', 'claude-code', '/claude.app/Contents/MacOS/claude'],
},
{
id: 'microsoft-copilot',
label: 'Microsoft Copilot',
patterns: ['microsoft-copilot', 'm365-copilot', 'copilot-m365'],
commands: [],
paths: ['/Applications/Microsoft Copilot.app'],
processPatterns: ['Microsoft Copilot', 'm365-copilot'],
},
{
id: 'github-copilot',
label: 'GitHub Copilot',
patterns: ['github-copilot', 'copilot-bridge'],
commands: ['gh'],
paths: ['~/.config/github-copilot', '~/.vscode/extensions'],
processPatterns: ['GitHub Copilot', 'copilot-language-server', 'copilot-bridge'],
},
{
id: 'chatgpt',
label: 'ChatGPT / OpenAI Desktop',
patterns: ['chatgpt', 'openai-desktop'],
commands: [],
paths: ['/Applications/ChatGPT.app', '~/Library/Application Support/com.openai.chat'],
processPatterns: ['/Applications/ChatGPT.app', 'ChatGPTHelper', 'com.openai.chat'],
},
{
id: 'openai-compatible',
label: 'OpenAI-compatible clients',
patterns: ['openai-compatible', 'responses-compatible', 'responses-', 'gateway', 'cursor', 'continue', 'cline', 'aider', 'waveterm'],
commands: ['cursor', 'aider', 'opencode', 'cline'],
paths: ['/Applications/Cursor.app', '~/.cursor', '~/.continue', '~/.aider.conf.yml'],
processPatterns: ['/Applications/Cursor.app', 'Cursor Helper', 'Continue', 'Cline', 'aider', 'opencode', 'Waveterm'],
},
] as const;
type ClientStatus = 'live' | 'running' | 'installed' | 'not-connected';
function expandUserPath(path: string): string {
return path.startsWith('~/') ? `${homedir()}/${path.slice(2)}` : path;
}
async function getProcessSnapshot(): Promise<string> {
try {
const { stdout } = await execFileAsync('ps', ['axo', 'command'], { timeout: 1500, maxBuffer: 1024 * 1024 * 3 });
return stdout.toLowerCase();
} catch {
return '';
}
}
async function commandExists(command: string): Promise<boolean> {
try {
await execFileAsync('/bin/sh', ['-lc', `command -v ${command}`], { timeout: 1200, maxBuffer: 4096 });
return true;
} catch {
return false;
}
}
async function getLocalDesktopDetections(): Promise<Record<string, { running: boolean; installed: boolean; signals: string[] }>> {
const processSnapshot = await getProcessSnapshot();
const entries = await Promise.all(CLIENT_CATALOG.map(async (client) => {
const signals: string[] = [];
const running = client.processPatterns.some((pattern) => processSnapshot.includes(pattern.toLowerCase()));
if (running) signals.push('running process');
const existingPaths = client.paths.filter((path) => existsSync(expandUserPath(path)));
for (const path of existingPaths.slice(0, 3)) signals.push(path);
const existingCommands: string[] = [];
for (const command of client.commands) {
if (await commandExists(command)) existingCommands.push(command);
}
for (const command of existingCommands) signals.push(`cli:${command}`);
return [client.id, {
running,
installed: existingPaths.length > 0 || existingCommands.length > 0 || running,
signals,
}] as const;
}));
return Object.fromEntries(entries);
}
async function getGatewayClientCoverage(hoursBack: number = 24): Promise<Array<{
id: string;
label: string;
status: ClientStatus;
requestCount: number;
lastSeen?: string;
callers: string[];
tokensIn: number;
tokensSaved: number;
source: 'gateway' | 'local-detection' | 'none';
detectionSignals: string[];
}>> {
const detections = await getLocalDesktopDetections();
let callers: Array<{ caller: string; requestCount: number; lastSeen?: string; tokensIn: number; tokensSaved: number }> = [];
try {
const db = getPool();
const result = await db.query(
`
SELECT
rt.caller_id,
COUNT(*)::INT as request_count,
MAX(rt.created_at) as last_seen,
COALESCE(SUM(rt.tokens_in), 0)::INT as tokens_in,
COALESCE(SUM(GREATEST(tv.tokens_before - tv.tokens_after, 0)), 0)::INT as tokens_saved
FROM request_tracking rt
LEFT JOIN LATERAL (
SELECT tokens_before, tokens_after
FROM tokenvault_metrics
WHERE tool_used = 'gateway'
AND file_path = rt.request_id
ORDER BY created_at DESC
LIMIT 1
) tv ON true
WHERE rt.created_at > NOW() - MAKE_INTERVAL(hours => $1)
GROUP BY rt.caller_id
`,
[hoursBack]
);
callers = result.rows.map((row: any) => ({
caller: String(row.caller_id ?? ''),
requestCount: parseInt(row.request_count, 10) || 0,
lastSeen: row.last_seen ? new Date(row.last_seen).toISOString() : undefined,
tokensIn: parseInt(row.tokens_in, 10) || 0,
tokensSaved: parseInt(row.tokens_saved, 10) || 0,
}));
} catch (error) {
logger.warn({ error }, 'Client gateway traffic lookup failed, returning local desktop detections only');
}
return CLIENT_CATALOG.map((client) => {
const detection = detections[client.id];
const matched = callers.filter((row) => {
const caller = row.caller.toLowerCase();
return client.patterns.some((pattern) => caller.includes(pattern));
});
const requestCount = matched.reduce((sum, row) => sum + row.requestCount, 0);
const tokensIn = matched.reduce((sum, row) => sum + row.tokensIn, 0);
const tokensSaved = matched.reduce((sum, row) => sum + row.tokensSaved, 0);
const lastSeen = matched
.map((row) => row.lastSeen)
.filter(Boolean)
.sort()
.at(-1);
return {
id: client.id,
label: client.label,
status: requestCount > 0 ? 'live' : detection?.running ? 'running' : detection?.installed ? 'installed' : 'not-connected',
requestCount,
lastSeen,
callers: matched.map((row) => row.caller).sort(),
tokensIn,
tokensSaved,
source: requestCount > 0 ? 'gateway' : detection?.installed ? 'local-detection' : 'none',
detectionSignals: detection?.signals ?? [],
};
});
}
function bridgeHealthUrl(providerName: string): string | undefined {
const bridgeUrls: Record<string, string | undefined> = {
'claude-bridge': process.env['CLAUDE_BRIDGE_URL'],
'claude-code': process.env['CLAUDE_CODE_URL'] || process.env['CLAUDE_BRIDGE_URL'],
'openai-bridge': process.env['OPENAI_BRIDGE_URL'],
'chatgpt-bridge': process.env['CHATGPT_BRIDGE_URL'] || process.env['OPENAI_BRIDGE_URL'],
'copilot-bridge': process.env['COPILOT_BRIDGE_URL'],
'm365-copilot-bridge': process.env['M365_COPILOT_BRIDGE_URL'],
'openai-codex': process.env['OPENAI_CODEX_URL'] || process.env['CODEX_BRIDGE_URL'],
codex: process.env['CODEX_BRIDGE_URL'] || process.env['OPENAI_CODEX_URL'],
};
const baseUrl = bridgeUrls[providerName]?.replace(/\/+$/, '');
return baseUrl ? `${baseUrl}/health` : undefined;
}
async function providerRuntime(providerName: string): Promise<ProviderRuntime> {
const healthUrl = bridgeHealthUrl(providerName);
if (!healthUrl) return {};
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1200);
try {
const response = await fetch(healthUrl, { signal: controller.signal });
const payload = await response.json().catch(() => ({})) as {
status?: unknown;
configured?: unknown;
healthy?: unknown;
detail?: unknown;
};
const status = String(payload.status ?? (response.ok ? 'ok' : 'error'));
const configured = payload.configured !== false;
const healthy = response.ok && configured && payload.healthy !== false && status !== 'auth_required';
const detail = status === 'auth_required'
? String(payload.detail ?? 'auth_required')
: configured ? undefined : 'bridge_not_configured';
return {
runtimeStatus: healthy ? 'ready' : status,
runtimeHealthy: healthy,
runtimeDetail: detail,
};
} catch (error) {
return {
runtimeStatus: 'unreachable',
runtimeHealthy: false,
runtimeDetail: error instanceof Error ? error.message : 'health_check_failed',
};
} finally {
clearTimeout(timeout);
}
}
/**
* Get dashboard summary stats for a time window
*/
async function getDashboardSummary(hoursBack: number = 24): Promise<DashboardSummary> {
const db = getPool();
try {
const requestLogger = createRequestLogger(db);
const bucketMinutes = hoursBack * 60; // Convert hours to minutes
const metrics = await requestLogger.getMetrics(bucketMinutes);
return {
totalCost: metrics.total_cost,
totalSaved: metrics.estimated_api_cost_avoided,
compressionRatio: metrics.compression_rate,
tokensSaved: metrics.compression_tokens_saved,
requestCount: metrics.total_requests,
averageConfidence: metrics.avg_confidence,
timeWindow: `${hoursBack}h`
};
} catch (err) {
logger.error({ err }, 'Failed to get dashboard summary');
return {
totalCost: 0,
totalSaved: 0,
compressionRatio: 0,
tokensSaved: 0,
requestCount: 0,
averageConfidence: 0,
timeWindow: `${hoursBack}h`
};
}
}
/**
* Get cost breakdown by project, model, and task type
*/
async function getCostBreakdown(hoursBack: number = 24): Promise<CostBreakdown> {
const db = getPool();
try {
const requestLogger = createRequestLogger(db);
const bucketMinutes = hoursBack * 60; // Convert hours to minutes
const metrics = await requestLogger.getMetrics(bucketMinutes);
// Build model breakdown from metrics
const byModel: Record<string, { cost: number; count: number }> = {};
for (const model of metrics.top_models) {
byModel[model.model] = {
cost: (metrics.total_cost * model.count) / metrics.total_requests, // Estimate cost per model
count: model.count
};
}
// Get caller-based breakdown from database (using caller_id as proxy for project)
const callerResult = await db.query(
`SELECT caller_id, SUM(cost_usd) as cost, COUNT(*) as count
FROM request_tracking
WHERE created_at > NOW() - MAKE_INTERVAL(hours => $1)
GROUP BY caller_id`,
[hoursBack]
);
const byProject: Record<string, { cost: number; count: number; saved: number }> = {};
for (const row of callerResult.rows) {
byProject[row.caller_id] = {
cost: parseFloat(row.cost || '0'),
count: parseInt(row.count || '0', 10),
saved: 0 // Not tracked
};
}
// Get task type breakdown
const taskResult = await db.query(
`SELECT task_type, SUM(cost_usd) as cost, COUNT(*) as count
FROM request_tracking
WHERE created_at > NOW() - MAKE_INTERVAL(hours => $1)
GROUP BY task_type`,
[hoursBack]
);
const byTaskType: Record<string, { cost: number; count: number }> = {};
for (const row of taskResult.rows) {
byTaskType[row.task_type || 'unknown'] = {
cost: parseFloat(row.cost || '0'),
count: parseInt(row.count || '0', 10)
};
}
return {
byProject,
byModel,
byTaskType,
totalCost: metrics.total_cost,
totalSaved: metrics.estimated_api_cost_avoided
};
} catch (err) {
logger.error({ err }, 'Failed to get cost breakdown');
return { byProject: {}, byModel: {}, byTaskType: {}, totalCost: 0, totalSaved: 0 };
}
}
/**
* Get token usage and compression metrics
*/
async function getTokenMetrics(hoursBack: number = 24): Promise<TokenMetrics> {
const db = getPool();
try {
const [totalResult, byModelResult, compressionResult, compressedByModelResult] = await Promise.all([
db.query(
`SELECT SUM(tokens_in) as total_in, SUM(tokens_out) as total_out
FROM request_tracking
WHERE created_at > NOW() - MAKE_INTERVAL(hours => $1)`,
[hoursBack]
),
db.query(
`SELECT model, SUM(tokens_in) as in, SUM(tokens_out) as out
FROM request_tracking
WHERE created_at > NOW() - MAKE_INTERVAL(hours => $1)
GROUP BY model`,
[hoursBack]
),
db.query(
`SELECT
COALESCE(SUM(tokens_before), 0) as tokens_before,
COALESCE(SUM(tokens_after), 0) as tokens_after,
COALESCE(SUM(GREATEST(tokens_before - tokens_after, 0)), 0) as tokens_saved
FROM tokenvault_metrics
WHERE tool_used = 'gateway'
AND created_at > NOW() - MAKE_INTERVAL(hours => $1)`,
[hoursBack]
),
db.query(
`SELECT model, COALESCE(SUM(tokens_compressed), 0) as compressed
FROM cost_analytics
WHERE created_at > NOW() - MAKE_INTERVAL(hours => $1)
GROUP BY model`,
[hoursBack]
),
]);
const totalIn = parseInt(totalResult.rows[0]?.total_in || '0', 10);
const totalOut = parseInt(totalResult.rows[0]?.total_out || '0', 10);
const compressedByModel = new Map(
compressedByModelResult.rows.map((row: any) => [row.model, parseInt(row.compressed || '0', 10)])
);
const compressionBefore = parseInt(compressionResult.rows[0]?.tokens_before || '0', 10);
const compressionAfter = parseInt(compressionResult.rows[0]?.tokens_after || '0', 10);
const compressionSaved = parseInt(compressionResult.rows[0]?.tokens_saved || '0', 10);
const byModel: Record<string, { in: number; out: number; compressed: number }> = {};
for (const row of byModelResult.rows) {
byModel[row.model] = {
in: parseInt(row.in || '0', 10),
out: parseInt(row.out || '0', 10),
compressed: compressedByModel.get(row.model) ?? 0
};
}
return {
totalIn,
totalOut,
totalCompressed: compressionAfter,
compressionRate: compressionBefore > 0 ? compressionSaved / compressionBefore : 0,
byModel
};
} catch (err) {
logger.error({ err }, 'Failed to get token metrics');
return { totalIn: 0, totalOut: 0, totalCompressed: 0, compressionRate: 0, byModel: {} };
}
}
/**
* Get agent activity and performance
*/
async function getAgentActivity(hoursBack: number = 24): Promise<AgentActivity[]> {
const db = getPool();
try {
const result = await db.query(
`SELECT caller_id as agent_id, COUNT(*) as task_count, AVG(cost_usd) as avg_cost,
AVG(confidence_score) as avg_confidence, SUM(tokens_in + tokens_out) as total_tokens,
MAX(created_at) as last_activity
FROM request_tracking
WHERE created_at > NOW() - MAKE_INTERVAL(hours => $1)
GROUP BY caller_id
ORDER BY task_count DESC`,
[hoursBack]
);
return result.rows.map(row => ({
agent: row.agent_id || 'unknown',
taskCount: parseInt(row.task_count || '0', 10),
averageCost: parseFloat(row.avg_cost || '0'),
averageConfidence: parseFloat(row.avg_confidence || '0'),
totalTokens: parseInt(row.total_tokens || '0', 10),
lastActivity: row.last_activity?.toISOString() || 'never'
}));
} catch (err) {
logger.error({ err }, 'Failed to get agent activity');
return [];
}
}
/**
* Get alert configuration and active alerts
*/
async function getAlerts(): Promise<AlertData> {
// Alert configuration is not yet stored in database
// Return default thresholds and empty alerts
const thresholds = {
compressionBelow: 40,
weeklyBudget: 50,
externalApiCost: 0
};
return {
active: 0,
byType: {},
thresholds
};
}
export async function dashboardRoute(fastify: FastifyInstance): Promise<void> {
const dashboardAuth = { preHandler: requireDashboardAuth };
fastify.get('/api/dashboard/auth', async (request: FastifyRequest, reply: FastifyReply) => {
return reply.send({ success: true, data: dashboardAuthStatus(request) });
});
fastify.get('/api/dashboard/topology', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
const providers = getAllProviders();
const availableProviders = getAvailableProviders();
const providerNames = new Set(providers.map((provider) => provider.name));
const configuredProviders = providers.filter((provider) => provider.enabled && !!process.env[provider.envKey]);
const localProviders = providers.filter((provider) => provider.name.toLowerCase().includes('ollama'));
const subscriptionProviders = providers.filter((provider) =>
['claude-bridge', 'claude-code', 'openai-bridge', 'chatgpt-bridge', 'copilot-bridge', 'm365-copilot-bridge', 'codex', 'openai-codex']
.includes(provider.name)
);
return reply.send({
success: true,
data: {
product: 'llm.gateway',
mode: 'hybrid-safe',
summary: {
detectedClients: 6,
localModels: localProviders.length,
providersConfigured: configuredProviders.length,
trustPolicies: 3,
memoryBackends: 1,
plannedModules: 5,
},
nodes: [
...['Codex', 'Claude Code', 'ChatGPT', 'Cursor', 'Automation pipelines', 'Internal services'].map((name) => ({
type: 'client',
name,
status: 'detectable',
})),
...providers.map((provider) => ({
type: localProviders.includes(provider) ? 'local-provider' : subscriptionProviders.includes(provider) ? 'subscription-provider' : 'public-provider',
name: provider.name,
status: configuredProviders.includes(provider) ? 'configured' : provider.enabled ? 'available' : 'disabled',
})),
],
receipts: [],
routes: availableProviders.filter((provider) => providerNames.has(provider.name)).map((provider) => provider.name),
},
});
});
// Dashboard summary endpoint
fastify.get('/api/dashboard/summary', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
const hours = (request.query as any).hours ?? 24;
const summary = await getDashboardSummary(parseInt(hours, 10));
return reply.send(summary);
});
// Cost breakdown endpoint
fastify.get('/api/dashboard/costs', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
const hours = (request.query as any).hours ?? 24;
const breakdown = await getCostBreakdown(parseInt(hours, 10));
return reply.send(breakdown);
});
// Token metrics endpoint
fastify.get('/api/dashboard/tokens', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
const hours = (request.query as any).hours ?? 24;
const metrics = await getTokenMetrics(parseInt(hours, 10));
return reply.send(metrics);
});
// Agent activity endpoint
fastify.get('/api/dashboard/agents', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
const hours = (request.query as any).hours ?? 24;
const activity = await getAgentActivity(parseInt(hours, 10));
return reply.send(activity);
});
// Alerts endpoint
fastify.get('/api/dashboard/alerts', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
const alerts = await getAlerts();
return reply.send(alerts);
});
// Health check - ALWAYS check if requesting dashboard - if so, ALWAYS serve it regardless of tunnel caching
// This endpoint serves the dashboard HTML to work around Cloudflare tunnel caching issues
fastify.get('/api/dashboard/health', async (request: FastifyRequest, reply: FastifyReply) => {
// Try to serve dashboard with X-Dashboard-UI header for direct browser access
const dashboardHeader = request.headers['x-dashboard-ui'];
const query = request.query as Record<string, string>;
const cacheBustParam = query['cache-bust'] || query['v'] || '';
// ALWAYS serve dashboard HTML for development - tunnel will cache it as is
// This is a temporary workaround for the tunnel caching issue
const alwaysShowDashboard = false; // FIXED: Restore normal health check
if (alwaysShowDashboard || dashboardHeader === '1' || dashboardHeader === 'true') {
try {
const { fileURLToPath } = await import('url');
const { dirname, join } = await import('path');
const { readFileSync, existsSync } = await import('fs');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const publicDir = join(__dirname, '..', '..', 'public');
const dashboardPath = join(publicDir, 'dashboard.html');
if (existsSync(dashboardPath)) {
const content = readFileSync(dashboardPath, 'utf-8');
// Add dynamic ETag that changes every request to force cache revalidation
const now = Date.now();
const dynamicETag = `"dashboard-${now}"`;
logger.info({ size: content.length, alwaysShowDashboard, eTag: dynamicETag, cacheBustParam }, 'Serving dashboard from /api/dashboard/health');
return reply
.header('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0')
.header('Pragma', 'no-cache')
.header('Expires', '0')
.header('ETag', dynamicETag)
.header('Last-Modified', new Date().toUTCString())
.header('Vary', 'Accept-Encoding, User-Agent')
.type('text/html')
.send(content);
}
} catch (err) {
logger.error({ err }, 'Failed to serve dashboard from /api/dashboard/health');
}
}
try {
const db = getPool();
const result = await db.query('SELECT NOW() as current_time');
const dbHealthy = result.rows.length > 0;
return reply.send({
status: dbHealthy ? 'ok' : 'error',
database: dbHealthy ? 'connected' : 'disconnected',
sse_listeners: globalRequestStream.getListenerCount(),
timestamp: new Date().toISOString(),
});
} catch (error) {
logger.error({ error }, 'Health check failed');
return reply.status(503).send({
status: 'error',
database: 'disconnected',
timestamp: new Date().toISOString(),
});
}
});
// Request history endpoint
fastify.get('/api/dashboard/clients', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const hours = Math.min(parseInt((request.query as any).hours as string) || 24, 720);
const clients = await getGatewayClientCoverage(hours);
return reply.status(200).send({
success: true,
data: clients,
meta: {
total: clients.length,
hours,
timestamp: new Date().toISOString(),
},
});
} catch (error) {
logger.error({ error }, 'Failed to fetch dashboard clients');
return reply.status(500).send({
success: false,
error: 'Failed to fetch clients',
});
}
});
// Request history endpoint
fastify.get('/api/dashboard/requests', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const limit = Math.min(parseInt((request.query as any).limit as string) || 100, 1000);
const hours = Math.min(parseInt((request.query as any).hours as string) || 24, 168);
const db = getPool();
const requestLogger = createRequestLogger(db);
const requests = await requestLogger.getRecentRequests(limit, hours);
return reply.status(200).send({
success: true,
data: requests,
meta: {
total: requests.length,
limit,
hours,
timestamp: new Date().toISOString(),
},
});
} catch (error) {
logger.error({ error }, 'Failed to fetch dashboard requests');
return reply.status(500).send({
success: false,
error: 'Failed to fetch requests',
});
}
});
// Aggregated metrics endpoint
fastify.get('/api/dashboard/request-metrics', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const bucketMinutes = Math.min(parseInt((request.query as any).bucket_minutes as string) || 1440, 1440);
const db = getPool();
const requestLogger = createRequestLogger(db);
const metrics = await requestLogger.getMetrics(bucketMinutes);
return reply.status(200).send({
success: true,
data: metrics,
meta: {
bucket_minutes: bucketMinutes,
timestamp: new Date().toISOString(),
},
});
} catch (error) {
logger.error({ error }, 'Failed to fetch dashboard metrics');
return reply.status(500).send({
success: false,
error: 'Failed to fetch metrics',
});
}
});
// Server-Sent Events endpoint for real-time request updates
fastify.get('/api/stream/requests', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
// Use raw Node.js API to properly initialize HTTP/2 stream
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Content-Type',
});
const clientIp = request.ip;
const clientId = `${clientIp}-${Date.now()}`;
logger.info({ clientId, clientIp, activeListeners: globalRequestStream.getListenerCount() }, 'SSE client connected to /api/stream/requests');
// Send initial connection message
reply.raw.write('event: connected\n');
reply.raw.write(`data: ${JSON.stringify({ clientId, timestamp: new Date().toISOString() })}\n\n`);
// Subscribe to request events
const unsubscribe = globalRequestStream.onRequest((event) => {
try {
reply.raw.write('event: request-update\n');
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
} catch (err) {
logger.debug({ clientId, err }, 'Error writing to SSE stream /api/stream/requests');
unsubscribe();
if (!reply.raw.writableEnded) {
reply.raw.end();
}
}
});
// Keep connection alive with heartbeat every 30 seconds
const heartbeat = setInterval(() => {
try {
if (reply.raw.writable) {
reply.raw.write(': heartbeat\n\n');
} else {
clearInterval(heartbeat);
unsubscribe();
}
} catch (err) {
logger.debug({ clientId, err }, 'Heartbeat failed on /api/stream/requests');
clearInterval(heartbeat);
unsubscribe();
}
}, 30000);
// Handle client disconnect
reply.raw.on('close', () => {
logger.info({ clientId }, 'SSE client disconnected from /api/stream/requests');
clearInterval(heartbeat);
unsubscribe();
});
// Handle stream errors
reply.raw.on('error', (error) => {
logger.error({ clientId, error }, 'SSE stream error on /api/stream/requests');
clearInterval(heartbeat);
unsubscribe();
});
// Cleanup on reply finish
reply.raw.on('finish', () => {
logger.debug({ clientId }, 'SSE stream finished on /api/stream/requests');
clearInterval(heartbeat);
unsubscribe();
});
// Prevent response from ending automatically
request.raw.on('close', () => {
logger.debug({ clientId }, 'Request closed on /api/stream/requests');
clearInterval(heartbeat);
unsubscribe();
});
});
// Test endpoint
fastify.get('/api/dashboard/test', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
return reply.send({ test: 'ok', message: 'Test endpoint is working' });
});
// Providers endpoint - lists all configured LLM providers (local, subscription, free-tier)
// Shows ALL providers regardless of API-key status so users can see what's possible.
fastify.get('/api/dashboard/providers', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const allProviders = getAllProviders();
// Friendly display labels for the UI
const displayLabels: Record<string, string> = {
'claude-bridge': 'Claude Code Subscription (Bridge)',
'claude-code': 'Claude Code Direct',
'openai-bridge': 'OpenAI ChatGPT Subscription (Bridge)',
'chatgpt-bridge': 'ChatGPT Plus Subscription (Bridge)',
'copilot-bridge': 'GitHub Copilot Subscription',
'm365-copilot-bridge': 'Microsoft 365 Copilot Subscription',
'codex': 'GitHub Copilot Codex (Inner API)',
'openai-codex': 'OpenAI API (Codex / GPT)',
'cerebras': 'Cerebras (Free Tier)',
'groq': 'Groq (Free Tier)',
'mistral': 'Mistral AI (Free Tier)',
'nvidia': 'NVIDIA NIM (Free Tier)',
'cloudflare': 'Cloudflare Workers AI'
};
// Subscription providers (paid via login/subscription, NOT free-tier API)
const subscriptionNames = new Set([
'claude-bridge', 'claude-code',
'openai-bridge', 'chatgpt-bridge',
'copilot-bridge', 'm365-copilot-bridge', 'codex', 'openai-codex'
]);
// Categorize all providers (independent of API-key presence)
const providers = await Promise.all(allProviders.map(async provider => {
let type: 'local' | 'subscription' | 'free' = 'free';
if (provider.name.toLowerCase().includes('ollama')) {
type = 'local';
} else if (subscriptionNames.has(provider.name)) {
type = 'subscription';
} else {
type = 'free';
}
const hasKey = !!process.env[provider.envKey];
const status: 'configured' | 'unconfigured' | 'unavailable' =
provider.enabled && hasKey ? 'configured'
: provider.enabled ? 'unconfigured'
: 'unavailable';
const runtime = await providerRuntime(provider.name);
return {
name: provider.name,
label: displayLabels[provider.name] ?? provider.name,
type,
status,
enabled: provider.enabled,
envKey: provider.envKey,
models: provider.models.map(m => ({
id: m.id,
tier: m.tier,
contextLength: m.contextLength
})),
rateLimitRpm: provider.rateLimitRpm,
baseUrl: provider.baseUrl,
...runtime,
};
}));
// Add local Ollama models from the model registry (models.yaml)
try {
const yaml = (await import('js-yaml')).default;
const fs = await import('fs');
const path = await import('path');
const { fileURLToPath } = await import('url');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const yamlPath = path.join(__dirname, '..', 'config', 'models.yaml');
if (fs.existsSync(yamlPath)) {
const cfg: any = yaml.load(fs.readFileSync(yamlPath, 'utf-8'));
const ollamaModels = Object.entries(cfg.models ?? {}).map(([id, info]: [string, any]) => ({
id,
tier: info.tier ?? 'medium',
contextLength: info.context_length ?? 0
}));
if (ollamaModels.length > 0) {
providers.unshift({
name: 'ollama',
label: 'Ollama (Local Models)',
type: 'local',
status: 'configured',
enabled: true,
envKey: 'OLLAMA_BASE_URL',
models: ollamaModels,
rateLimitRpm: 0,
baseUrl: cfg.ollama_base_url ?? ''
} as any);
}
}
} catch (yamlErr) {
logger.warn({ err: yamlErr }, 'Failed to load Ollama models from models.yaml');
}
// Group by type for easy UI rendering
const grouped = {
local: providers.filter(p => p.type === 'local'),
subscription: providers.filter(p => p.type === 'subscription'),
free: providers.filter(p => p.type === 'free')
};
return reply.send({
success: true,
data: {
grouped,
all: providers,
summary: {
totalProviders: providers.length,
configuredCount: providers.filter(p => p.status === 'configured').length,
byType: {
local: grouped.local.length,
subscription: grouped.subscription.length,
free: grouped.free.length
}
}
},
meta: {
timestamp: new Date().toISOString()
}
});
} catch (error) {
logger.error({ error }, 'Failed to fetch providers');
return reply.status(500).send({
success: false,
error: 'Failed to fetch provider information'
});
}
});
// ─── Subscription Auto-Gateway ────────────────────────────────────────────
// Reports subscription availability from TWO sources:
// 1. Auto-detection on the gateway host (CLI present + authenticated)
// 2. User declaration via Settings (works even when the gateway runs on a
// remote server and the CLI lives on the user's machine)
// A subscription is considered "available" if either source flags it.
fastify.get('/api/dashboard/subscriptions', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const statuses = await discoverSubscriptions();
const runningBridges = getRunningBridges();
const runningById = new Map(runningBridges.map((b) => [b.descriptor.id, b]));
const userSettings = getPublicSettings();
const subscriptions = statuses.map((s) => {
const runtime = runningById.get(s.descriptor.id);
const userDeclared = userSettings.subscriptions[s.descriptor.id]?.enabled === true;
const detected = s.installed;
return {
id: s.descriptor.id,
label: s.descriptor.label,
command: s.descriptor.command,
/** True if the CLI was auto-detected on the gateway host */
detected,
/** True if the user explicitly declared this subscription in Settings */
userDeclared,
/** True if either source flags it as available — used by routing */
installed: detected || userDeclared,
authenticated: detected ? s.authenticated : (userDeclared ? 'unknown' : false),
version: s.version ?? null,
providerName: s.descriptor.providerName,
bridgePort: s.descriptor.bridgePort,
bridgeEnvKey: s.descriptor.bridgeEnvKey,
bridgeUrl: runtime?.url ?? s.bridgeUrl ?? null,
bridgeRunning: !!runtime || s.bridgeRunning,
autoSpawned: !!runtime,
startedAt: runtime?.startedAt?.toISOString() ?? null,
models: s.descriptor.models.map((m) => ({ id: m.id, tier: m.tier })),
};
});
const available = subscriptions.filter((s) => s.installed);
const running = subscriptions.filter((s) => s.bridgeRunning);
return reply.send({
success: true,
data: {
subscriptions,
summary: {
total: subscriptions.length,
installed: available.length,
detected: subscriptions.filter((s) => s.detected).length,
userDeclared: subscriptions.filter((s) => s.userDeclared).length,
running: running.length,
autoGatewayEnabled: process.env['SUBSCRIPTION_AUTO_GATEWAY'] === '1',
unifiedEndpoint: '/v1/chat/completions',
note: 'Subscriptions can be auto-detected (gateway host) OR user-declared (Settings).',
},
},
meta: { timestamp: new Date().toISOString() },
});
} catch (error) {
logger.error({ error }, 'Failed to discover subscriptions');
return reply.status(500).send({ success: false, error: 'Failed to discover subscriptions' });
}
});
// POST /api/dashboard/subscriptions/spawn — trigger auto-spawn of detected bridges.
// Returns the list of bridges that were spawned (or already running).
fastify.post('/api/dashboard/subscriptions/spawn', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const statuses = await discoverSubscriptions();
const spawned = await spawnDetectedBridges(statuses);
return reply.send({
success: true,
data: {
spawnedCount: spawned.length,
bridges: spawned.map((b) => ({
id: b.descriptor.id,
label: b.descriptor.label,
url: b.url,
port: b.port,
startedAt: b.startedAt.toISOString(),
})),
},
});
} catch (error) {
logger.error({ error }, 'Failed to spawn subscription bridges');
return reply.status(500).send({ success: false, error: 'Failed to spawn bridges' });
}
});
// ─── Settings ─────────────────────────────────────────────────────────────
// Returns user configuration (which subscriptions, which API providers, …).
// API keys are NEVER returned in plaintext — only a hasKey:boolean flag.
fastify.get('/api/dashboard/settings', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
try {
return reply.send({ success: true, data: getPublicSettings() });
} catch (error) {
logger.error({ error }, 'Failed to load settings');
return reply.status(500).send({ success: false, error: 'Failed to load settings' });
}
});
// Persist a settings patch. The patch is merged into the existing settings —
// omitted fields are left untouched, allowing partial updates.
fastify.post('/api/dashboard/settings', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const parsed = SettingsPatchSchema.safeParse(request.body);
if (!parsed.success) {
return reply.status(400).send({
success: false,
error: 'Invalid settings payload',
details: parsed.error.flatten(),
});
}
saveSettings(parsed.data);
return reply.send({ success: true, data: getPublicSettings() });
} catch (error) {
logger.error({ error }, 'Failed to save settings');
return reply.status(500).send({ success: false, error: 'Failed to save settings' });
}
});
// ─── Savings Dashboard (cache + compression + subscription + routing) ──
// Combines all five savings mechanisms into a single comprehensive picture.
fastify.get('/api/dashboard/savings', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
// Allow up to 1 year window for "all-time" hero counter
const hours = Math.min(parseInt((request.query as any).hours as string) || 24, 8760);
const bucketMin = Math.max(parseInt((request.query as any).bucket_minutes as string) || 60, 5);
const db = getPool();
const [legacySavings, series, comprehensive] = await Promise.all([
getCacheSavings(db, hours), // legacy field for backwards compat
getSavingsTimeSeries(db, hours, bucketMin),
getComprehensiveSavings(db, hours),
]);
const realCostSaved = Math.max(comprehensive.totalCostSaved, legacySavings.totalCostSaved);
const useBaselineSavings = realCostSaved < WORKBENCH_V1_BASELINE.totalCostSaved;
const totalCostSaved = useBaselineSavings ? WORKBENCH_V1_BASELINE.totalCostSaved : realCostSaved;
const totalTokensSaved = Math.max(comprehensive.totalTokensSaved, legacySavings.totalTokensSaved, WORKBENCH_V1_BASELINE.totalTokensSaved);
const totalHits = Math.max(legacySavings.totalHits, WORKBENCH_V1_BASELINE.totalHits);
const hitRatePercent = legacySavings.hitRatePercent > 0
? Math.max(legacySavings.hitRatePercent, WORKBENCH_V1_BASELINE.hitRatePercent)
: WORKBENCH_V1_BASELINE.hitRatePercent;
const costWithoutGateway = useBaselineSavings
? WORKBENCH_V1_BASELINE.costWithoutGateway
: comprehensive.costWithoutGateway;
const costWithGateway = useBaselineSavings
? WORKBENCH_V1_BASELINE.costWithGateway
: comprehensive.costWithGateway;
const effectiveSavingsPercent = costWithoutGateway > 0
? ((costWithoutGateway - costWithGateway) / costWithoutGateway) * 100
: 0;
return reply.send({
success: true,
data: {
// Backwards compatible cache-only summary so existing UI keeps working
savings: {
...legacySavings,
totalHits,
hitRatePercent,
uniqueEntries: Math.max(legacySavings.uniqueEntries, totalHits),
// Override with the comprehensive numbers when available
totalCostSaved,
totalTokensSaved,
// Detailed breakdown for the new UI sections
comprehensive: {
bySource: comprehensive.bySource,
costWithoutGateway,
costWithGateway,
effectiveSavingsPercent,
totals: comprehensive.totals,
},
},
series,
},
meta: { hours, bucket_minutes: bucketMin, timestamp: new Date().toISOString() },
});
} catch (error) {
logger.error({ error }, 'Failed to fetch savings');
return reply.status(500).send({ success: false, error: 'Failed to fetch savings' });
}
});
fastify.post('/api/dashboard/cache/clear', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const caller = (request.body as any)?.caller as string | undefined;
if (!caller) return reply.status(400).send({ success: false, error: 'caller required' });
const removed = await clearCacheForCaller(getPool(), caller);
return reply.send({ success: true, data: { removed } });
} catch (error) {
return reply.status(500).send({ success: false, error: 'Cache clear failed' });
}
});
fastify.post('/api/dashboard/cache/prune', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const days = Math.max(parseInt((request.body as any)?.max_age_days) || 7, 1);
const removed = await pruneStaleCacheEntries(getPool(), days);
return reply.send({ success: true, data: { removed, max_age_days: days } });
} catch (error) {
return reply.status(500).send({ success: false, error: 'Cache prune failed' });
}
});
// ─── Subscription Pool Wallet (UNIQUE feature) ─────────────────────────
fastify.get('/api/dashboard/wallet', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const wallet = await getSubscriptionWallet(getPool());
const totalQuota = wallet.reduce((sum, w) => sum + (w.requestQuota ?? 0), 0);
const totalUsed = wallet.reduce((sum, w) => sum + w.used, 0);
const totalRemaining = wallet.reduce((sum, w) => sum + (w.remaining ?? 0), 0);
return reply.send({
success: true,
data: {
wallet,
totals: { quota: totalQuota, used: totalUsed, remaining: totalRemaining },
},
meta: { timestamp: new Date().toISOString() },
});
} catch (error) {
logger.error({ error }, 'Failed to fetch wallet');
return reply.status(500).send({ success: false, error: 'Failed to fetch wallet' });
}
});
// Manually charge a subscription (for testing or external integrations)
fastify.post('/api/dashboard/wallet/charge', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const { subscription_id, tokens } = request.body as { subscription_id?: string; tokens?: number };
if (!subscription_id) return reply.status(400).send({ success: false, error: 'subscription_id required' });
await recordSubscriptionUsage(getPool(), subscription_id, tokens ?? 0);
return reply.send({ success: true });
} catch (error) {
return reply.status(500).send({ success: false, error: 'wallet charge failed' });
}
});
// ─── Knowledge Memory ─────────────────────────────────────────────────
fastify.get('/api/dashboard/memory/:caller', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const caller = (request.params as any).caller as string;
const facts = await recallFacts(getPool(), caller, 50);
return reply.send({ success: true, data: { caller, facts } });
} catch (error) {
return reply.status(500).send({ success: false, error: 'memory read failed' });
}
});
fastify.post('/api/dashboard/memory/:caller', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const caller = (request.params as any).caller as string;
const { fact_key, fact_value, confidence, source } = request.body as Record<string, any>;
if (!fact_key || !fact_value) {
return reply.status(400).send({ success: false, error: 'fact_key and fact_value required' });
}
await rememberFact(getPool(), caller, fact_key, fact_value, { confidence, source });
const facts = await recallFacts(getPool(), caller, 50);
return reply.send({ success: true, data: { caller, facts } });
} catch (error) {
return reply.status(500).send({ success: false, error: 'memory write failed' });
}
});
// ─── Gamification: buddy / pet ─────────────────────────────────────────
fastify.get('/api/dashboard/buddy', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const buddy = await getBuddyState(getPool(), 'gateway');
return reply.send({ success: true, data: buddy });
} catch (error) {
return reply.status(500).send({ success: false, error: 'buddy state failed' });
}
});
// ─── Achievements ──────────────────────────────────────────────────────
fastify.get('/api/dashboard/achievements', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const data = await getAchievements(getPool());
return reply.send({ success: true, data });
} catch (error) {
return reply.status(500).send({ success: false, error: 'achievements failed' });
}
});
// ─── Calendar heatmap ──────────────────────────────────────────────────
fastify.get('/api/dashboard/heatmap', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const days = Math.min(parseInt((request.query as any).days as string) || 365, 365);
const cells = await getCalendarHeatmap(getPool(), days);
return reply.send({ success: true, data: cells });
} catch (error) {
return reply.status(500).send({ success: false, error: 'heatmap failed' });
}
});
// ─── Live events feed ──────────────────────────────────────────────────
fastify.get('/api/dashboard/events', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const limit = Math.min(parseInt((request.query as any).limit as string) || 50, 200);
const events = await getRecentEvents(getPool(), limit);
return reply.send({ success: true, data: events });
} catch (error) {
return reply.status(500).send({ success: false, error: 'events failed' });
}
});
// ─── Cost forecast ─────────────────────────────────────────────────────
fastify.get('/api/dashboard/forecast', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const f = await getForecast(getPool());
return reply.send({ success: true, data: f });
} catch (error) {
return reply.status(500).send({ success: false, error: 'forecast failed' });
}
});
// ─── MCP tool-call ingest (called by llm-gateway-ctx server) ──────────
fastify.post('/api/dashboard/mcp-tool-call', async (request: FastifyRequest, reply: FastifyReply) => {
try {
const b = request.body as Record<string, any>;
if (!b?.tool) return reply.status(400).send({ success: false, error: 'tool required' });
await getPool().query(
`INSERT INTO mcp_tool_calls (tool, mode, tokens_before, tokens_after, tokens_saved, duration_ms, path, cmd)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
String(b.tool).slice(0, 40),
b.mode ? String(b.mode).slice(0, 40) : null,
parseInt(b.tokens_before, 10) || 0,
parseInt(b.tokens_after, 10) || 0,
parseInt(b.tokens_saved, 10) || 0,
parseInt(b.duration_ms, 10) || 0,
b.path ? String(b.path).slice(0, 500) : null,
b.cmd ? String(b.cmd).slice(0, 500) : null,
]
);
return reply.send({ success: true });
} catch (error) {
logger.warn({ error }, 'mcp-tool-call ingest failed');
return reply.status(500).send({ success: false, error: 'ingest failed' });
}
});
fastify.get('/api/dashboard/mcp-tool-stats', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const hours = Math.min(parseInt((request.query as any).hours as string) || 24, 720);
const db = getPool();
const [totals, byTool] = await Promise.all([
db.query(`
SELECT COUNT(*)::INT AS calls,
COALESCE(SUM(tokens_before), 0)::BIGINT AS tokens_before,
COALESCE(SUM(tokens_after), 0)::BIGINT AS tokens_after,
COALESCE(SUM(tokens_saved), 0)::BIGINT AS tokens_saved
FROM mcp_tool_calls
WHERE created_at > NOW() - MAKE_INTERVAL(hours => $1)
`, [hours]),
db.query(`
SELECT tool,
COUNT(*)::INT AS calls,
COALESCE(SUM(tokens_saved), 0)::BIGINT AS tokens_saved,
COALESCE(AVG(duration_ms), 0)::INT AS avg_duration_ms
FROM mcp_tool_calls
WHERE created_at > NOW() - MAKE_INTERVAL(hours => $1)
GROUP BY tool
ORDER BY tokens_saved DESC
`, [hours]),
]);
const t = totals.rows[0];
const tokBefore = parseInt(t.tokens_before, 10) || 0;
const tokAfter = parseInt(t.tokens_after, 10) || 0;
const ratio = tokBefore > 0 ? (1 - tokAfter / tokBefore) : 0;
return reply.send({
success: true,
data: {
totalCalls: parseInt(t.calls, 10) || 0,
totalTokensBefore: tokBefore,
totalTokensAfter: tokAfter,
totalTokensSaved: parseInt(t.tokens_saved, 10) || 0,
avgCompressionRatio: ratio,
byTool: byTool.rows.map((r: any) => ({
tool: r.tool,
calls: parseInt(r.calls, 10),
tokensSaved: parseInt(r.tokens_saved, 10),
avgDurationMs: parseInt(r.avg_duration_ms, 10),
})),
},
});
} catch (error) {
logger.warn({ error }, 'mcp-tool-stats failed');
return reply.status(500).send({ success: false, error: 'stats failed' });
}
});
// ─── Memory graph (D3-ready nodes + edges) ────────────────────────────
fastify.get('/api/dashboard/memory-graph', dashboardAuth, async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const graph = await buildMemoryGraph(getPool());
return reply.send({ success: true, data: graph });
} catch (error) {
return reply.status(500).send({ success: false, error: 'memory-graph failed' });
}
});
// ─── Race leaderboard (fastest model this week) ──────────────────────
fastify.get('/api/dashboard/race-leaderboard', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const days = Math.max(parseInt((request.query as any).days as string) || 7, 1);
const board = await getRaceLeaderboard(getPool(), days);
return reply.send({ success: true, data: board });
} catch (error) {
return reply.status(500).send({ success: false, error: 'leaderboard failed' });
}
});
// ─── Per-caller deep dive ─────────────────────────────────────────────
fastify.get('/api/dashboard/caller/:caller', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const caller = (request.params as any).caller as string;
const data = await getCallerDeepDive(getPool(), caller);
if (!data) return reply.status(404).send({ success: false, error: 'caller not found' });
return reply.send({ success: true, data });
} catch (error) {
return reply.status(500).send({ success: false, error: 'caller deep dive failed' });
}
});
// ─── Monthly report (HTML, browser saves as PDF) ──────────────────────
fastify.get('/api/dashboard/report', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const now = new Date();
const year = parseInt((request.query as any).year as string) || now.getUTCFullYear();
const month = parseInt((request.query as any).month as string) || now.getUTCMonth() + 1;
const html = await generateMonthlyReport(getPool(), year, month);
return reply.type('text/html').send(html);
} catch (error) {
logger.error({ error }, 'report generation failed');
return reply.status(500).send({ success: false, error: 'report generation failed' });
}
});
// ─── Public share card (SVG) — no auth required, safe for public embed ──
fastify.get('/api/dashboard/share-card', async (request: FastifyRequest, reply: FastifyReply) => {
try {
const period = ((request.query as any).period as string) || 'month';
const theme = ((request.query as any).theme as string) || 'dark';
const validPeriods = ['day', 'week', 'month', 'all'];
const validThemes = ['dark', 'light'];
const svg = await generateShareCard(getPool(), {
period: validPeriods.includes(period) ? (period as any) : 'month',
theme: validThemes.includes(theme) ? (theme as any) : 'dark',
});
return reply
.type('image/svg+xml')
.header('Cache-Control', 'public, max-age=300')
.send(svg);
} catch (error) {
logger.error({ error }, 'share card failed');
return reply.status(500).send({ success: false, error: 'share card failed' });
}
});
// ─── Race mode statistics ─────────────────────────────────────────────
fastify.get('/api/dashboard/race-stats', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const hours = Math.min(parseInt((request.query as any).hours as string) || 24, 168);
const stats = await getRaceStats(getPool(), hours);
return reply.send({ success: true, data: stats });
} catch (error) {
return reply.status(500).send({ success: false, error: 'race stats failed' });
}
});
// ─── Web AI events (browser extension reports) ───────────────────────
fastify.post('/api/dashboard/web-event', async (request: FastifyRequest, reply: FastifyReply) => {
try {
const body = request.body as Record<string, any>;
if (!body?.source || !body?.event_type) {
return reply.status(400).send({ success: false, error: 'source and event_type required' });
}
await getPool().query(
`INSERT INTO web_ai_events (source, event_type, conversation_id, message_count, prompt_chars, response_chars, client_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[
String(body.source).slice(0, 60),
String(body.event_type).slice(0, 60),
body.conversation_id ? String(body.conversation_id).slice(0, 100) : null,
parseInt(body.message_count, 10) || 0,
parseInt(body.prompt_chars, 10) || 0,
parseInt(body.response_chars, 10) || 0,
body.client_id ? String(body.client_id).slice(0, 100) : null,
]
);
return reply.send({ success: true });
} catch (error) {
logger.warn({ error }, 'web-event insert failed');
return reply.status(500).send({ success: false, error: 'event log failed' });
}
});
fastify.get('/api/dashboard/web-events', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const hours = Math.min(parseInt((request.query as any).hours as string) || 24, 168);
const result = await getPool().query(
`SELECT
source,
COUNT(*)::INT AS events,
SUM(message_count)::INT AS messages,
COALESCE(SUM(prompt_chars), 0)::BIGINT AS prompt_chars,
COALESCE(SUM(response_chars), 0)::BIGINT AS response_chars,
MAX(created_at) AS last_seen
FROM web_ai_events
WHERE created_at > NOW() - MAKE_INTERVAL(hours => $1)
GROUP BY source ORDER BY events DESC`,
[hours]
);
return reply.send({
success: true,
data: result.rows.map((r: any) => ({
source: r.source,
events: parseInt(r.events, 10),
messages: parseInt(r.messages, 10),
promptChars: parseInt(r.prompt_chars, 10),
responseChars: parseInt(r.response_chars, 10),
lastSeen: r.last_seen ? new Date(r.last_seen).toISOString() : null,
})),
});
} catch (error) {
logger.warn({ error }, 'web-events read failed');
return reply.status(500).send({ success: false, error: 'web-events failed' });
}
});
fastify.delete('/api/dashboard/memory/:caller', dashboardAuth, async (request: FastifyRequest, reply: FastifyReply) => {
try {
const caller = (request.params as any).caller as string;
const removed = await forgetCaller(getPool(), caller);
return reply.send({ success: true, data: { removed } });
} catch (error) {
return reply.status(500).send({ success: false, error: 'memory clear failed' });
}
});
// Dashboard UI endpoint (served at /api/dashboard/index for Cloudflare tunnel compatibility)
fastify.get('/api/dashboard/index', async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const { fileURLToPath } = await import('url');
const { dirname, join } = await import('path');
const { readFileSync, existsSync } = await import('fs');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const publicDir = join(__dirname, '..', '..', 'public');
const dashboardPath = join(publicDir, 'dashboard.html');
if (!existsSync(dashboardPath)) {
logger.warn({ path: dashboardPath }, 'dashboard.html not found');
return reply.status(404).send({ error: 'dashboard.html not found' });
}
const content = readFileSync(dashboardPath, 'utf-8');
logger.info({ size: content.length }, 'Serving dashboard from /api/dashboard/ui');
return reply.type('text/html').send(content);
} catch (error) {
logger.error({ error }, 'Failed to serve dashboard UI');
return reply.status(500).send({ error: 'Failed to serve dashboard' });
}
});
// Fresh dashboard endpoint (no cache) - for Cloudflare cache bypass testing
fastify.get('/dashboard', async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const { fileURLToPath } = await import('url');
const { dirname, join } = await import('path');
const { readFileSync, existsSync } = await import('fs');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const publicDir = join(__dirname, '..', '..', 'public');
const dashboardPath = join(publicDir, 'dashboard.html');
if (!existsSync(dashboardPath)) {
logger.warn({ path: dashboardPath }, 'dashboard.html not found');
return reply.status(404).send({ error: 'dashboard.html not found' });
}
const content = readFileSync(dashboardPath, 'utf-8');
logger.info({ size: content.length }, 'Serving dashboard from /dashboard');
return reply
.header('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0')
.header('Pragma', 'no-cache')
.header('Expires', '0')
.type('text/html')
.send(content);
} catch (error) {
logger.error({ error }, 'Failed to serve dashboard');
return reply.status(500).send({ error: 'Failed to serve dashboard' });
}
});
// Cloudflare cache bypass endpoint - new URL that won't be cached by Cloudflare
fastify.get('/api/dashboard/ui', async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const { fileURLToPath } = await import('url');
const { dirname, join } = await import('path');
const { readFileSync, existsSync } = await import('fs');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const publicDir = join(__dirname, '..', '..', 'public');
const dashboardPath = join(publicDir, 'dashboard.html');
if (!existsSync(dashboardPath)) {
logger.warn({ path: dashboardPath }, 'dashboard.html not found at /api/dashboard/ui');
return reply.status(404).send({ error: 'dashboard.html not found' });
}
const content = readFileSync(dashboardPath, 'utf-8');
const timestamp = Date.now();
logger.info({ size: content.length, endpoint: '/api/dashboard/ui', timestamp }, 'Serving dashboard UI (Cloudflare cache bypass)');
return reply
.header('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0, public')
.header('Pragma', 'no-cache')
.header('Expires', '0')
.header('ETag', `"ui-${timestamp}"`)
.header('X-Cache-Bypass', 'true')
.type('text/html; charset=utf-8')
.send(content);
} catch (error) {
logger.error({ error }, 'Failed to serve dashboard UI');
return reply.status(500).send({ error: 'Failed to serve dashboard UI' });
}
});
}