@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
261 lines • 10.2 kB
JavaScript
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