UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

319 lines (277 loc) 9.46 kB
import express from 'express'; import { createServer } from 'http'; import { Server as SocketIOServer } from 'socket.io'; import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; import { PerformanceMonitor } from '../utils/performance-monitor.js'; import { SecurityManager } from '../utils/security-manager.js'; import { ErrorHandler } from '../utils/error-handler.js'; import { setupMetricsAPI } from './api/metrics.js'; import { setupAgileAPI } from './api/agile.js'; import { setupSecurityAPI } from './api/security.js'; import { setupErrorsAPI } from './api/errors.js'; import { setupApprovalsAPI } from './api/approvals.js'; import { setupWebSocketHandlers } from './utils/websocket.js'; import { ensureDatabaseReady } from '../storage/sqlite-manager.js'; import { setSocketIOInstance } from './utils/database-events.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export interface DashboardConfig { enabled?: boolean; port?: number; host?: string; autoOpen?: boolean; features?: { performance?: boolean; security?: boolean; agile?: boolean; errors?: boolean; }; realTimeUpdates?: boolean; exportEnabled?: boolean; } export class DashboardServer { private app: express.Application; private server: any; private io: SocketIOServer; private config: DashboardConfig; private isRunning: boolean = false; constructor( config: DashboardConfig, private performanceMonitor: PerformanceMonitor, private securityManager: SecurityManager, private errorHandler: ErrorHandler, private agileManager?: any ) { this.config = config; this.app = express(); this.server = createServer(this.app); this.io = new SocketIOServer(this.server, { cors: { origin: `http://${config.host}:${config.port}`, methods: ['GET', 'POST'] } }); this.setupMiddleware(); this.setupRoutes(); this.setupStaticFiles(); this.setupWebSocket(); } private setupMiddleware(): void { this.app.use(cors()); this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); // Request logging middleware - only log API errors this.app.use((req, res, next) => { // Skip logging for static file requests if (!req.path.startsWith('/api/')) { return next(); } // Store the original res.json to intercept responses const originalJson = res.json; res.json = function(data) { // Only log if there's an error or non-2xx status if (res.statusCode >= 400 || (data && data.error)) { console.error(`📊 Dashboard API Error: ${req.method} ${req.path} - Status: ${res.statusCode}`); } return originalJson.call(this, data); }; next(); }); } private setupStaticFiles(): void { // Serve static files from public directory const publicPath = path.join(__dirname, 'public'); this.app.use(express.static(publicPath)); // Serve main dashboard page this.app.get('/', (req, res) => { res.sendFile(path.join(publicPath, 'index.html')); }); } private setupRoutes(): void { // API Routes if (this.config.features.performance) { setupMetricsAPI(this.app, this.performanceMonitor, this.config.exportEnabled); } if (this.config.features.agile && this.agileManager) { setupAgileAPI(this.app, this.agileManager); // Add advanced agile analytics routes import('./api/agile-analytics.js').then(module => { this.app.use('/api/agile', module.default); }).catch(err => { console.error('Failed to load agile analytics API:', err); }); } if (this.config.features.security) { setupSecurityAPI(this.app, this.securityManager); } if (this.config.features.errors) { setupErrorsAPI(this.app, this.errorHandler); } // Always enable approvals API for human interaction setupApprovalsAPI(this.app); // Health check endpoint this.app.get('/api/health', async (req, res) => { try { const db = await ensureDatabaseReady(1, 100); // Quick check const dbInfo = db.getConnectionInfo(); res.json({ status: 'healthy', timestamp: new Date().toISOString(), features: this.config.features, version: process.env.npm_package_version || '1.0.0', database: { status: dbInfo.isInitialized ? 'ready' : 'not_ready', path: dbInfo.dbPath } }); } catch (error) { res.status(503).json({ status: 'unhealthy', timestamp: new Date().toISOString(), error: 'Database not available', database: { status: 'not_ready' } }); } }); // 404 handler for API routes this.app.use('/api/*', (req, res) => { res.status(404).json({ error: 'API endpoint not found', path: req.path, method: req.method }); }); } private setupWebSocket(): void { if (this.config.realTimeUpdates) { // Set Socket.IO instance for database events setSocketIOInstance(this.io); setupWebSocketHandlers( this.io, this.performanceMonitor, this.securityManager, this.errorHandler, this.agileManager ); } } async start(): Promise<void> { if (this.isRunning) { console.error('📊 Dashboard server is already running'); return; } // Wait for database to be ready before starting the server try { console.error('📊 Waiting for database to be ready...'); await ensureDatabaseReady(5, 2000); // 5 retries, 2s delay each console.error('✅ Database is ready for dashboard'); } catch (error) { console.error('❌ Database not ready for dashboard:', error); throw error; } return new Promise((resolve, reject) => { this.server.listen(this.config.port, this.config.host, () => { this.isRunning = true; const url = `http://${this.config.host}:${this.config.port}`; console.error(`📊 Atlas Dashboard available at ${url}`); if (this.config.realTimeUpdates) { console.error('📡 Real-time updates enabled via WebSocket'); } // Start real-time data collection this.startDataCollection(); resolve(); }); this.server.on('error', (error: any) => { if (error.code === 'EADDRINUSE') { console.error(`❌ Dashboard port ${this.config.port} is already in use`); console.error(`💡 Try changing the port in your Atlas configuration`); } else { console.error(`❌ Dashboard server error:`, error.message); } reject(error); }); }); } async stop(): Promise<void> { if (!this.isRunning) { return; } return new Promise((resolve) => { this.stopDataCollection(); // Close all socket.io connections first this.io.close(() => { // Then close the HTTP server this.server.close(() => { this.isRunning = false; console.error('📊 Dashboard server stopped'); resolve(); }); }); // Force close any remaining connections after a timeout setTimeout(() => { this.server.closeAllConnections?.(); resolve(); }, 1000); }); } private dataCollectionInterval?: NodeJS.Timeout; private startDataCollection(): void { if (!this.config.realTimeUpdates) { return; } // Collect and broadcast data every 5 seconds this.dataCollectionInterval = setInterval(async () => { try { // Broadcast performance metrics if (this.config.features.performance) { const metrics = await this.performanceMonitor.getSystemMetrics(); this.io.emit('performance_update', { timestamp: new Date().toISOString(), metrics }); } // Broadcast security status if (this.config.features.security) { const securityStatus = await this.securityManager.getSecurityStatus(); this.io.emit('security_update', { timestamp: new Date().toISOString(), status: securityStatus }); } // Broadcast error patterns (less frequent - every 30 seconds) if (this.config.features.errors && Math.random() < 0.16) { // ~1/6 chance = every 30s const errorPatterns = await this.errorHandler.analyzeErrorPatterns({ timeRange: '1h', minOccurrences: 1, groupBy: 'type' }); this.io.emit('errors_update', { timestamp: new Date().toISOString(), patterns: errorPatterns }); } } catch (error) { console.error('📊 Dashboard data collection error:', error); } }, 5000); } private stopDataCollection(): void { if (this.dataCollectionInterval) { clearInterval(this.dataCollectionInterval); this.dataCollectionInterval = undefined; } } getUrl(): string { return `http://${this.config.host}:${this.config.port}`; } isHealthy(): boolean { return this.isRunning; } getConfig(): DashboardConfig { return { ...this.config }; } }