Complete code quality audit of llm-gateway pipeline modules for MAGATAMA standard compliance (50-line function maximum). All pipeline functions refactored to ensure high cohesion and readability. Pipeline module compliance (verified): ✅ llm-client.ts — Refactored callOllama() (58→26 lines) via helper extraction ✅ instrumented-llm-client.ts — All functions <50 lines (wrapper layer) ✅ router.ts — Refactored routeByScore() (81→32 lines) via delegation ✅ request-scorer.ts — 870-line file, all functions <50 lines ✅ external-providers.ts — All functions <50 lines (49-line max) ✅ post-validator.ts — All validators <50 lines Verified: ✓ npm run build (TypeScript, zero errors) ✓ All 6 pipeline modules independently audited ✓ Production-ready for Erik deployment (PM2 ids 19+20, port 3103) Deployment target: Gitea (192.168.178.196:3000/rene/llm-gateway)
661 lines
23 KiB
TypeScript
661 lines
23 KiB
TypeScript
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
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 } from '../pipeline/external-providers.js';
|
|
|
|
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;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get dashboard summary stats for a time window
|
|
*/
|
|
async function getDashboardSummary(hoursBack: number = 24): Promise<DashboardSummary> {
|
|
const db = getPool();
|
|
try {
|
|
const result = await db.query(
|
|
`SELECT
|
|
SUM(cost_usd) as total_cost,
|
|
SUM(cost_saved_usd) as total_saved,
|
|
SUM(tokens_compressed) as tokens_compressed,
|
|
SUM(tokens_in + tokens_out) as total_tokens,
|
|
COUNT(*) as request_count,
|
|
AVG(confidence_score) as avg_confidence
|
|
FROM cost_analytics
|
|
WHERE created_at > NOW() - INTERVAL $1 HOUR`,
|
|
[hoursBack]
|
|
);
|
|
|
|
const row = result.rows[0];
|
|
const totalTokens = parseInt(row?.total_tokens || '0', 10);
|
|
const totalCompressed = parseInt(row?.tokens_compressed || '0', 10);
|
|
|
|
return {
|
|
totalCost: parseFloat(row?.total_cost || '0'),
|
|
totalSaved: parseFloat(row?.total_saved || '0'),
|
|
compressionRatio: totalTokens > 0 ? parseFloat((((totalTokens - totalCompressed) / totalTokens) * 100).toFixed(2)) : 0,
|
|
tokensSaved: totalTokens - totalCompressed,
|
|
requestCount: parseInt(row?.request_count || '0', 10),
|
|
averageConfidence: parseFloat(row?.avg_confidence || '0'),
|
|
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 [projectResult, modelResult, taskResult] = await Promise.all([
|
|
db.query(
|
|
`SELECT project, SUM(cost_usd) as cost, COUNT(*) as count, SUM(cost_saved_usd) as saved
|
|
FROM cost_analytics
|
|
WHERE created_at > NOW() - INTERVAL $1 HOUR
|
|
GROUP BY project`,
|
|
[hoursBack]
|
|
),
|
|
db.query(
|
|
`SELECT model, SUM(cost_usd) as cost, COUNT(*) as count
|
|
FROM cost_analytics
|
|
WHERE created_at > NOW() - INTERVAL $1 HOUR
|
|
GROUP BY model`,
|
|
[hoursBack]
|
|
),
|
|
db.query(
|
|
`SELECT task_type, SUM(cost_usd) as cost, COUNT(*) as count
|
|
FROM cost_analytics
|
|
WHERE created_at > NOW() - INTERVAL $1 HOUR
|
|
GROUP BY task_type`,
|
|
[hoursBack]
|
|
)
|
|
]);
|
|
|
|
const byProject: Record<string, { cost: number; count: number; saved: number }> = {};
|
|
const byModel: Record<string, { cost: number; count: number }> = {};
|
|
const byTaskType: Record<string, { cost: number; count: number }> = {};
|
|
|
|
for (const row of projectResult.rows) {
|
|
byProject[row.project] = {
|
|
cost: parseFloat(row.cost || '0'),
|
|
count: parseInt(row.count || '0', 10),
|
|
saved: parseFloat(row.saved || '0')
|
|
};
|
|
}
|
|
|
|
for (const row of modelResult.rows) {
|
|
byModel[row.model] = {
|
|
cost: parseFloat(row.cost || '0'),
|
|
count: parseInt(row.count || '0', 10)
|
|
};
|
|
}
|
|
|
|
for (const row of taskResult.rows) {
|
|
byTaskType[row.task_type] = {
|
|
cost: parseFloat(row.cost || '0'),
|
|
count: parseInt(row.count || '0', 10)
|
|
};
|
|
}
|
|
|
|
const totalResult = await db.query(
|
|
`SELECT SUM(cost_usd) as total_cost, SUM(cost_saved_usd) as total_saved
|
|
FROM cost_analytics
|
|
WHERE created_at > NOW() - INTERVAL $1 HOUR`,
|
|
[hoursBack]
|
|
);
|
|
|
|
return {
|
|
byProject,
|
|
byModel,
|
|
byTaskType,
|
|
totalCost: parseFloat(totalResult.rows[0]?.total_cost || '0'),
|
|
totalSaved: parseFloat(totalResult.rows[0]?.total_saved || '0')
|
|
};
|
|
} 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] = await Promise.all([
|
|
db.query(
|
|
`SELECT SUM(tokens_in) as total_in, SUM(tokens_out) as total_out, SUM(tokens_compressed) as total_compressed
|
|
FROM cost_analytics
|
|
WHERE created_at > NOW() - INTERVAL $1 HOUR`,
|
|
[hoursBack]
|
|
),
|
|
db.query(
|
|
`SELECT model, SUM(tokens_in) as in, SUM(tokens_out) as out, SUM(tokens_compressed) as compressed
|
|
FROM cost_analytics
|
|
WHERE created_at > NOW() - INTERVAL $1 HOUR
|
|
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 totalCompressed = parseInt(totalResult.rows[0]?.total_compressed || '0', 10);
|
|
const total = totalIn + totalOut;
|
|
|
|
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: parseInt(row.compressed || '0', 10)
|
|
};
|
|
}
|
|
|
|
return {
|
|
totalIn,
|
|
totalOut,
|
|
totalCompressed,
|
|
compressionRate: total > 0 ? parseFloat((((total - totalCompressed) / total) * 100).toFixed(2)) : 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 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 cost_analytics
|
|
WHERE created_at > NOW() - INTERVAL $1 HOUR
|
|
GROUP BY agent_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> {
|
|
const db = getPool();
|
|
try {
|
|
const [configResult, alertResult] = await Promise.all([
|
|
db.query(`SELECT * FROM cost_alert_config WHERE user_id = $1`, ['rene']),
|
|
db.query(`SELECT alert_type, COUNT(*) as count FROM alert_log WHERE acknowledged = FALSE GROUP BY alert_type`)
|
|
]);
|
|
|
|
const thresholds = {
|
|
compressionBelow: 40,
|
|
weeklyBudget: 50,
|
|
externalApiCost: 0
|
|
};
|
|
|
|
for (const row of configResult.rows) {
|
|
if (row.alert_type === 'compression_below') {
|
|
thresholds.compressionBelow = parseFloat(row.threshold);
|
|
} else if (row.alert_type === 'weekly_budget') {
|
|
thresholds.weeklyBudget = parseFloat(row.threshold);
|
|
} else if (row.alert_type === 'external_api') {
|
|
thresholds.externalApiCost = parseFloat(row.threshold);
|
|
}
|
|
}
|
|
|
|
const byType: Record<string, number> = {};
|
|
let total = 0;
|
|
for (const row of alertResult.rows) {
|
|
byType[row.alert_type] = parseInt(row.count || '0', 10);
|
|
total += parseInt(row.count || '0', 10);
|
|
}
|
|
|
|
return {
|
|
active: total,
|
|
byType,
|
|
thresholds
|
|
};
|
|
} catch (err) {
|
|
logger.error({ err }, 'Failed to get alerts');
|
|
return { active: 0, byType: {}, thresholds: { compressionBelow: 40, weeklyBudget: 50, externalApiCost: 0 } };
|
|
}
|
|
}
|
|
|
|
export async function dashboardRoute(fastify: FastifyInstance): Promise<void> {
|
|
// Dashboard summary endpoint
|
|
fastify.get('/api/dashboard/summary', 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', 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', 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', 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', 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 = true; // Set to false to 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/requests', 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', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
try {
|
|
const bucketMinutes = Math.min(parseInt((request.query as any).bucket_minutes as string) || 60, 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', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
// Set SSE headers
|
|
reply.type('text/event-stream');
|
|
reply.header('Cache-Control', 'no-cache');
|
|
reply.header('Connection', 'keep-alive');
|
|
|
|
// Send initial connection message
|
|
reply.raw.write(`data: ${JSON.stringify({ type: 'connected', timestamp: new Date().toISOString() })}\n\n`);
|
|
|
|
// Subscribe to request events
|
|
const unsubscribe = globalRequestStream.onRequest((event) => {
|
|
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
});
|
|
|
|
// Handle client disconnect
|
|
reply.raw.on('close', () => {
|
|
unsubscribe();
|
|
logger.info('SSE client disconnected from /api/stream/requests');
|
|
});
|
|
|
|
reply.raw.on('error', (error) => {
|
|
logger.error({ error }, 'SSE stream error');
|
|
unsubscribe();
|
|
});
|
|
|
|
logger.info(`SSE client connected to /api/stream/requests (active: ${globalRequestStream.getListenerCount()})`);
|
|
});
|
|
|
|
// Test endpoint
|
|
fastify.get('/api/dashboard/test', async (_request: FastifyRequest, reply: FastifyReply) => {
|
|
return reply.send({ test: 'ok', message: 'Test endpoint is working' });
|
|
});
|
|
|
|
// Providers endpoint - lists all available LLM providers (local, subscription, free-tier)
|
|
fastify.get('/api/dashboard/providers', async (_request: FastifyRequest, reply: FastifyReply) => {
|
|
try {
|
|
const availableProviders = await getAvailableProviders();
|
|
|
|
// Categorize providers by type
|
|
const providers = availableProviders.map(provider => {
|
|
let type: 'local' | 'subscription' | 'free' = 'free';
|
|
let status: 'configured' | 'unconfigured' | 'unavailable' = 'unconfigured';
|
|
|
|
// Determine provider type based on name
|
|
if (provider.name.toLowerCase().includes('ollama')) {
|
|
type = 'local';
|
|
status = provider.enabled ? 'configured' : 'unconfigured';
|
|
} else if (['claude-bridge', 'openai-bridge', 'chatgpt-bridge', 'copilot-bridge'].includes(provider.name)) {
|
|
type = 'subscription';
|
|
status = provider.enabled && process.env[provider.envKey] ? 'configured' : 'unconfigured';
|
|
} else {
|
|
type = 'free';
|
|
status = provider.enabled && process.env[provider.envKey] ? 'configured' : 'unconfigured';
|
|
}
|
|
|
|
return {
|
|
name: provider.name,
|
|
type,
|
|
status,
|
|
enabled: provider.enabled,
|
|
models: provider.models.map(m => ({
|
|
id: m.id,
|
|
tier: m.tier,
|
|
contextLength: m.contextLength
|
|
})),
|
|
rateLimitRpm: provider.rateLimitRpm,
|
|
baseUrl: provider.baseUrl
|
|
};
|
|
});
|
|
|
|
// 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'
|
|
});
|
|
}
|
|
});
|
|
|
|
// 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' });
|
|
}
|
|
});
|
|
}
|