UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

261 lines 10.2 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 { 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 class DashboardServer { performanceMonitor; securityManager; errorHandler; agileManager; app; server; io; config; isRunning = false; constructor(config, performanceMonitor, securityManager, errorHandler, agileManager) { this.performanceMonitor = performanceMonitor; this.securityManager = securityManager; this.errorHandler = errorHandler; this.agileManager = agileManager; 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(); } setupMiddleware() { 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(); }); } setupStaticFiles() { // 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')); }); } setupRoutes() { // 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 }); }); } setupWebSocket() { 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() { 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) => { 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() { 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); }); } dataCollectionInterval; startDataCollection() { 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); } stopDataCollection() { if (this.dataCollectionInterval) { clearInterval(this.dataCollectionInterval); this.dataCollectionInterval = undefined; } } getUrl() { return `http://${this.config.host}:${this.config.port}`; } isHealthy() { return this.isRunning; } getConfig() { return { ...this.config }; } } //# sourceMappingURL=server.js.map