import { HttpServer } from '@shardix/http'; import { createServer } from 'node:http'; import type { Server } from 'node:http'; export interface DashboardApiOptions { port?: number; token?: string; } export interface LiveMetricsSnapshot { timestamp: string; uptime: number; cpuUsagePercent: number; memoryHeapUsedMB: number; memoryRssMB: number; eventLoopDelayMs: number; interactionsTotal: number; interactionsPerSec: number; commandsTotal: number; commandsPerSec: number; eventsTotal: number; errorsTotal: number; errorsPerSec: number; avgCommandDurationMs: number; p95CommandDurationMs: number; gatewayPingMs: number; gatewayStatus: 'connected' | 'disconnected' | 'unknown'; cacheHitRatio: number; activeTraces: number; } export interface CommandMetric { name: string; executions: number; avgMs: number; p95Ms: number; errors: number; } export interface LogEntry { timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'fatal'; message: string; data?: Record; } class MetricsRegistry { private metrics: LiveMetricsSnapshot = this.defaultMetrics(); private commands: Map = new Map(); private logs: LogEntry[] = []; private wsClients: Set = new Set(); private defaultMetrics(): LiveMetricsSnapshot { return { timestamp: new Date().toISOString(), uptime: process.uptime(), cpuUsagePercent: 0, memoryHeapUsedMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), memoryRssMB: Math.round(process.memoryUsage().rss / 1024 / 1024), eventLoopDelayMs: 0, interactionsTotal: 0, interactionsPerSec: 0, commandsTotal: 0, commandsPerSec: 0, eventsTotal: 0, errorsTotal: 0, errorsPerSec: 0, avgCommandDurationMs: 0, p95CommandDurationMs: 0, gatewayPingMs: 0, gatewayStatus: 'unknown', cacheHitRatio: 0, activeTraces: 0, }; } /** Called by ObservabilityProvider to update live metrics */ public pushMetrics(metrics: Partial): void { this.metrics = { ...this.metrics, ...metrics, timestamp: new Date().toISOString(), uptime: process.uptime(), memoryRssMB: Math.round(process.memoryUsage().rss / 1024 / 1024), memoryHeapUsedMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), }; // Broadcast to WebSocket clients this.broadcastToWs({ type: 'metrics', data: this.metrics }); } /** Called by command handlers to record command stats */ public recordCommand(name: string, durationMs: number, isError = false): void { const existing = this.commands.get(name) ?? { name, executions: 0, avgMs: 0, p95Ms: 0, errors: 0, }; existing.executions++; existing.avgMs = (existing.avgMs * (existing.executions - 1) + durationMs) / existing.executions; if (isError) existing.errors++; this.commands.set(name, existing); } /** Add a log entry (ring buffer, max 500) */ public addLog(entry: LogEntry): void { this.logs.push(entry); if (this.logs.length > 500) this.logs.shift(); this.broadcastToWs({ type: 'log', data: entry }); } public getMetrics(): LiveMetricsSnapshot { return { ...this.metrics, uptime: process.uptime() }; } public getCommands(): CommandMetric[] { return [...this.commands.values()].sort((a, b) => b.executions - a.executions); } public getLogs(limit = 100): LogEntry[] { return this.logs.slice(-limit); } public addWsClient(client: any): void { this.wsClients.add(client); } public removeWsClient(client: any): void { this.wsClients.delete(client); } private broadcastToWs(message: unknown): void { const payload = JSON.stringify(message); for (const client of this.wsClients) { try { if (client.readyState === 1 /* OPEN */) { client.send(payload); } } catch { this.wsClients.delete(client); } } } } /** Global metrics registry — providers push data here */ export const metricsRegistry = new MetricsRegistry(); class MetricsWebSocketServer { private wss: any = null; private broadcastInterval: ReturnType | null = null; async start(port: number): Promise { try { const { WebSocketServer } = await import('ws' as any); this.wss = new WebSocketServer({ port }); this.wss.on('connection', (ws: any) => { metricsRegistry.addWsClient(ws); // Send current metrics immediately on connect ws.send(JSON.stringify({ type: 'metrics', data: metricsRegistry.getMetrics() })); ws.on('close', () => metricsRegistry.removeWsClient(ws)); ws.on('error', () => metricsRegistry.removeWsClient(ws)); }); // Broadcast live metrics every second this.broadcastInterval = setInterval(() => { metricsRegistry.pushMetrics({}); }, 1000); console.log(`[Shardix Dashboard] WebSocket metrics server on ws://localhost:${port}`); } catch { // ws package not installed — skip WebSocket server silently } } async stop(): Promise { if (this.broadcastInterval) clearInterval(this.broadcastInterval); if (this.wss) this.wss.close(); } } export class DashboardApiServer { private server: HttpServer; private token: string; private wsServer = new MetricsWebSocketServer(); private startTime = Date.now(); constructor(options: DashboardApiOptions = {}) { this.token = options.token || process.env.SHARDIX_DASHBOARD_TOKEN || 'shardix_secret_token'; this.server = new HttpServer({ port: options.port || 3005 }); this.registerEndpoints(); } private timingSafeEqual(a: string, b: string): boolean { if (a.length !== b.length) return false; let result = 0; for (let i = 0; i < a.length; i++) { result |= a.charCodeAt(i) ^ b.charCodeAt(i); } return result === 0; } private registerEndpoints(): void { const fastify = this.server.getFastify(); // CORS headers fastify.addHook('onSend', async (request, reply, payload) => { reply.header('Access-Control-Allow-Origin', '*'); reply.header('Access-Control-Allow-Headers', 'Authorization, Content-Type'); reply.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); return payload; }); fastify.options('*', async (request, reply) => { reply.status(200).send(); }); // Authentication middleware — skip for public routes fastify.addHook('onRequest', async (request, reply) => { if (request.method === 'OPTIONS') return; if (request.url === '/' || request.url.startsWith('/dashboard') || request.url === '/ping') return; const authHeader = request.headers.authorization; const queryToken = (request.query as any)?.token; const providedToken = authHeader?.replace('Bearer ', '').trim() || queryToken; if (!providedToken || !this.timingSafeEqual(providedToken, this.token)) { reply.status(401).send({ error: 'Unauthorized: Invalid Shardix Dashboard Token' }); return; } }); fastify.get('/ping', async () => ({ pong: true, uptime: process.uptime() })); fastify.get('/api/application', async () => { let version = '0.9.0'; try { version = require('../../core/package.json').version; } catch {} return { name: process.env.npm_package_name || 'Shardix Application', version, shardixVersion: '0.9.0', status: 'ONLINE', uptime: Math.floor(process.uptime()), startedAt: new Date(Date.now() - process.uptime() * 1000).toISOString(), pid: process.pid, nodeVersion: process.version, platform: process.platform, arch: process.arch, }; }); fastify.get('/api/metrics/live', async () => metricsRegistry.getMetrics()); fastify.get('/api/metrics/commands', async () => ({ commands: metricsRegistry.getCommands(), totalExecutions: metricsRegistry.getCommands().reduce((s, c) => s + c.executions, 0), })); fastify.get('/api/runtime', async () => ({ runtime: 'GatewayRuntime', adapter: process.env.SHARDIX_ADAPTER || 'DiscordJSAdapter', transports: ['GatewayWebSocket'], status: 'ONLINE', restLatencyMs: 0, gatewayLatencyMs: metricsRegistry.getMetrics().gatewayPingMs, })); fastify.get('/api/workers', async () => [ { id: `worker_${process.pid}`, status: 'healthy', memoryUsageMB: Math.round(process.memoryUsage().rss / 1024 / 1024), pid: process.pid, uptime: Math.floor(process.uptime()), }, ]); fastify.get('/api/cluster', async () => ({ mode: 'standalone', workers: 1, shards: 'auto', })); fastify.get('/api/health', async () => { const mem = process.memoryUsage(); const heapMB = mem.heapUsed / 1024 / 1024; return { status: heapMB > 1024 ? 'fail' : heapMB > 512 ? 'warn' : 'pass', uptime: Math.floor(process.uptime()), checks: { process: [{ componentName: 'process', status: 'pass', observedValue: Math.floor(process.uptime()), observedUnit: 's' }], memory: [{ componentName: 'memory', status: heapMB > 512 ? 'warn' : 'pass', observedValue: Math.round(heapMB), observedUnit: 'MB' }], }, }; }); fastify.get('/api/events', async () => ({ eventsTotal: metricsRegistry.getMetrics().eventsTotal, recentEvents: [], })); fastify.get('/api/logs', async (req) => { const limit = parseInt((req.query as any)?.limit ?? '100'); const level = (req.query as any)?.level; let logs = metricsRegistry.getLogs(limit); if (level) logs = logs.filter((l) => l.level === level); return logs; }); fastify.get('/api/providers', async () => [ { name: 'HealthProvider', status: 'active', version: '0.9.0' }, { name: 'ObservabilityProvider', status: 'active', version: '0.9.0' }, { name: 'SchedulerProvider', status: 'active', version: '0.9.0' }, { name: 'QueueProvider', status: 'active', version: '0.9.0' }, ]); fastify.get('/api/cache', async () => ({ hitRatio: metricsRegistry.getMetrics().cacheHitRatio, hits: metricsRegistry.getMetrics().cacheHitRatio * 1000, misses: (1 - metricsRegistry.getMetrics().cacheHitRatio) * 1000, driver: 'memory', })); fastify.get('/', async (request, reply) => { // Try to serve the dashboard-web index.html const { existsSync, readFileSync } = await import('node:fs'); const { join } = await import('node:path'); const dashboardHtmlPaths = [ join(__dirname, '../../dashboard-web/index.html'), join(process.cwd(), 'node_modules/@shardix/dashboard-web/index.html'), ]; for (const p of dashboardHtmlPaths) { if (existsSync(p)) { return reply.type('text/html').send(readFileSync(p, 'utf-8')); } } return reply.type('text/html').send(this.renderMinimalDashboard()); }); } private renderMinimalDashboard(): string { return ` Shardix Dashboard v0.9
v0.9 Dashboard API Online — Port 3005
Status
ONLINE
Uptime
—
Memory RSS
—
Heap Used
—
Interactions/s
—
Gateway Ping
—

REST API Endpoints

GET /api/application — Application overview
GET /api/metrics/live — Live metrics snapshot
GET /api/metrics/commands — Command performance data
GET /api/runtime — Runtime information
GET /api/health — Health check status
GET /api/logs?limit=100&level=info — Structured logs
GET /api/workers — Worker processes
GET /api/cache — Cache statistics

Install @shardix/dashboard-web for the full interactive dashboard UI.
Authenticate with: Authorization: Bearer ${this.token}

`; } public async start(): Promise { await this.server.start(); await this.wsServer.start(9090); } public async stop(): Promise { await this.server.stop(); await this.wsServer.stop(); } }