Create Dashboard service main application
This commit is contained in:
672
services/dashboard/src/app.js
Normal file
672
services/dashboard/src/app.js
Normal file
@ -0,0 +1,672 @@
|
||||
import express from 'express';
|
||||
import { Server as SocketIOServer } from 'socket.io';
|
||||
import { createServer } from 'http';
|
||||
import { createClient } from 'redis';
|
||||
import { Client as PgClient } from 'pg';
|
||||
import winston from 'winston';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import compression from 'compression';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
// ES modules compatibility
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Environment variables
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const POSTGRES_URL = process.env.POSTGRES_URL;
|
||||
const REDIS_URL = process.env.REDIS_URL || 'redis://redis:6379';
|
||||
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
|
||||
const NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
// Language configuration
|
||||
const LANGUAGE_INFO = {
|
||||
'en': { name: 'English', flag: '🇺🇸', color: '#3498db' },
|
||||
'de': { name: 'German', flag: '🇩🇪', color: '#e74c3c' },
|
||||
'ko': { name: 'Korean', flag: '🇰🇷', color: '#f39c12' },
|
||||
'es': { name: 'Spanish', flag: '🇪🇸', color: '#9b59b6' },
|
||||
'fr': { name: 'French', flag: '🇫🇷', color: '#2ecc71' },
|
||||
'it': { name: 'Italian', flag: '🇮🇹', color: '#e67e22' },
|
||||
'pt': { name: 'Portuguese', flag: '🇵🇹', color: '#1abc9c' },
|
||||
'ru': { name: 'Russian', flag: '🇷🇺', color: '#34495e' },
|
||||
'ja': { name: 'Japanese', flag: '🇯🇵', color: '#e91e63' },
|
||||
'zh': { name: 'Chinese', flag: '🇨🇳', color: '#ff5722' }
|
||||
};
|
||||
|
||||
// Logging configuration
|
||||
const logger = winston.createLogger({
|
||||
level: LOG_LEVEL,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({ filename: '/app/logs/dashboard-error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: '/app/logs/dashboard.log' })
|
||||
]
|
||||
});
|
||||
|
||||
class DashboardService {
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.server = createServer(this.app);
|
||||
this.io = new SocketIOServer(this.server, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
this.redis_client = null;
|
||||
this.pg_client = null;
|
||||
this.activeConnections = new Map();
|
||||
this.realtimeStats = {
|
||||
totalTranscriptions: 0,
|
||||
totalSpeakingTime: 0,
|
||||
activeUsers: 0,
|
||||
languageBreakdown: {},
|
||||
recentActivity: []
|
||||
};
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
// Initialize database connection
|
||||
this.pg_client = new PgClient({ connectionString: POSTGRES_URL });
|
||||
await this.pg_client.connect();
|
||||
|
||||
// Initialize Redis connection
|
||||
this.redis_client = createClient({ url: REDIS_URL });
|
||||
await this.redis_client.connect();
|
||||
|
||||
// Set up Express middleware
|
||||
this.setupMiddleware();
|
||||
|
||||
// Set up routes
|
||||
this.setupRoutes();
|
||||
|
||||
// Set up Socket.IO
|
||||
this.setupSocketIO();
|
||||
|
||||
// Start listening for Redis events
|
||||
this.listenForEvents();
|
||||
|
||||
logger.info('Dashboard service initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize dashboard service:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
setupMiddleware() {
|
||||
// Security and performance middleware
|
||||
this.app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
this.app.use(compression());
|
||||
this.app.use(cors());
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100 // limit each IP to 100 requests per windowMs
|
||||
});
|
||||
this.app.use(limiter);
|
||||
|
||||
// Static files and view engine
|
||||
this.app.use(express.static(path.join(__dirname, '../public')));
|
||||
this.app.set('view engine', 'ejs');
|
||||
this.app.set('views', path.join(__dirname, 'views'));
|
||||
|
||||
// Body parsing
|
||||
this.app.use(express.json({ limit: '10mb' }));
|
||||
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Logging middleware
|
||||
this.app.use((req, res, next) => {
|
||||
logger.info(`${req.method} ${req.url}`, {
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent')
|
||||
});
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
// Main dashboard page
|
||||
this.app.get('/', async (req, res) => {
|
||||
try {
|
||||
const dashboardData = await this.getDashboardData();
|
||||
res.render('dashboard', {
|
||||
data: dashboardData,
|
||||
moment,
|
||||
languages: LANGUAGE_INFO
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error rendering dashboard:', error);
|
||||
res.status(500).render('error', { error: 'Failed to load dashboard' });
|
||||
}
|
||||
});
|
||||
|
||||
// API endpoints
|
||||
this.app.get('/api/stats', async (req, res) => {
|
||||
try {
|
||||
const stats = await this.getSystemStats();
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
logger.error('Error getting stats:', error);
|
||||
res.status(500).json({ error: 'Failed to get stats' });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.get('/api/recent-activity', async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const activity = await this.getRecentActivity(limit);
|
||||
res.json(activity);
|
||||
} catch (error) {
|
||||
logger.error('Error getting recent activity:', error);
|
||||
res.status(500).json({ error: 'Failed to get recent activity' });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.get('/api/language-stats', async (req, res) => {
|
||||
try {
|
||||
const days = parseInt(req.query.days) || 7;
|
||||
const stats = await this.getLanguageStats(days);
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
logger.error('Error getting language stats:', error);
|
||||
res.status(500).json({ error: 'Failed to get language stats' });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.get('/api/performance-metrics', async (req, res) => {
|
||||
try {
|
||||
const hours = parseInt(req.query.hours) || 24;
|
||||
const metrics = await this.getPerformanceMetrics(hours);
|
||||
res.json(metrics);
|
||||
} catch (error) {
|
||||
logger.error('Error getting performance metrics:', error);
|
||||
res.status(500).json({ error: 'Failed to get performance metrics' });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.get('/api/user-activity', async (req, res) => {
|
||||
try {
|
||||
const days = parseInt(req.query.days) || 7;
|
||||
const activity = await this.getUserActivity(days);
|
||||
res.json(activity);
|
||||
} catch (error) {
|
||||
logger.error('Error getting user activity:', error);
|
||||
res.status(500).json({ error: 'Failed to get user activity' });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check
|
||||
this.app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
version: '2.0.0'
|
||||
});
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
this.app.use((req, res) => {
|
||||
res.status(404).render('error', { error: 'Page not found' });
|
||||
});
|
||||
|
||||
// Error handler
|
||||
this.app.use((error, req, res, next) => {
|
||||
logger.error('Express error:', error);
|
||||
res.status(500).render('error', { error: 'Internal server error' });
|
||||
});
|
||||
}
|
||||
|
||||
setupSocketIO() {
|
||||
this.io.on('connection', (socket) => {
|
||||
logger.info('Client connected to dashboard', { socketId: socket.id });
|
||||
|
||||
// Send initial data
|
||||
socket.emit('stats-update', this.realtimeStats);
|
||||
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', () => {
|
||||
logger.info('Client disconnected from dashboard', { socketId: socket.id });
|
||||
});
|
||||
|
||||
// Handle client requests for specific data
|
||||
socket.on('request-stats', async () => {
|
||||
try {
|
||||
const stats = await this.getSystemStats();
|
||||
socket.emit('stats-update', stats);
|
||||
} catch (error) {
|
||||
logger.error('Error sending stats to client:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getDashboardData() {
|
||||
try {
|
||||
const [
|
||||
systemStats,
|
||||
recentActivity,
|
||||
languageStats,
|
||||
performanceMetrics,
|
||||
userActivity
|
||||
] = await Promise.all([
|
||||
this.getSystemStats(),
|
||||
this.getRecentActivity(10),
|
||||
this.getLanguageStats(7),
|
||||
this.getPerformanceMetrics(24),
|
||||
this.getUserActivity(7)
|
||||
]);
|
||||
|
||||
return {
|
||||
systemStats,
|
||||
recentActivity,
|
||||
languageStats,
|
||||
performanceMetrics,
|
||||
userActivity,
|
||||
activeConnections: Array.from(this.activeConnections.values())
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error getting dashboard data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getSystemStats() {
|
||||
try {
|
||||
const queries = [
|
||||
// Total transcriptions
|
||||
`SELECT COUNT(*) as total_transcriptions FROM transcriptions WHERE status = 'completed'`,
|
||||
|
||||
// Total speaking time
|
||||
`SELECT COALESCE(SUM(duration_seconds), 0) as total_speaking_time FROM recordings WHERE status IN ('completed', 'processed')`,
|
||||
|
||||
// Active connections count
|
||||
`SELECT COUNT(*) as active_connections FROM connections WHERE ended_at IS NULL`,
|
||||
|
||||
// Today's activity
|
||||
`SELECT COUNT(*) as today_transcriptions FROM transcriptions
|
||||
WHERE DATE(created_at) = CURRENT_DATE AND status = 'completed'`,
|
||||
|
||||
// Language breakdown (last 7 days)
|
||||
`SELECT detected_language, COUNT(*) as count
|
||||
FROM transcriptions
|
||||
WHERE created_at >= NOW() - INTERVAL '7 days' AND status = 'completed'
|
||||
GROUP BY detected_language
|
||||
ORDER BY count DESC`,
|
||||
|
||||
// Average processing times
|
||||
`SELECT
|
||||
AVG(processing_time_ms) as avg_transcription_time,
|
||||
AVG(t.processing_time_ms) as avg_translation_time
|
||||
FROM transcriptions tr
|
||||
LEFT JOIN translations t ON tr.id = t.transcription_id
|
||||
WHERE tr.created_at >= NOW() - INTERVAL '24 hours'`
|
||||
];
|
||||
|
||||
const results = await Promise.all(
|
||||
queries.map(query => this.pg_client.query(query))
|
||||
);
|
||||
|
||||
const languageBreakdown = {};
|
||||
if (results[4].rows) {
|
||||
results[4].rows.forEach(row => {
|
||||
const langInfo = LANGUAGE_INFO[row.detected_language] || { name: row.detected_language, flag: '🏳️' };
|
||||
languageBreakdown[row.detected_language] = {
|
||||
count: parseInt(row.count),
|
||||
name: langInfo.name,
|
||||
flag: langInfo.flag,
|
||||
color: langInfo.color || '#95a5a6'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
totalTranscriptions: parseInt(results[0].rows[0].total_transcriptions),
|
||||
totalSpeakingTime: parseFloat(results[1].rows[0].total_speaking_time || 0),
|
||||
activeConnections: parseInt(results[2].rows[0].active_connections),
|
||||
todayTranscriptions: parseInt(results[3].rows[0].today_transcriptions),
|
||||
languageBreakdown,
|
||||
avgTranscriptionTime: parseFloat(results[5].rows[0].avg_transcription_time || 0),
|
||||
avgTranslationTime: parseFloat(results[5].rows[0].avg_translation_time || 0),
|
||||
lastUpdated: new Date().toISOString()
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error getting system stats:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getRecentActivity(limit = 20) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
t.id as transcription_id,
|
||||
t.detected_language,
|
||||
t.transcription_text,
|
||||
t.created_at,
|
||||
t.processing_time_ms,
|
||||
r.speaker_nickname,
|
||||
r.speaker_username,
|
||||
r.duration_seconds,
|
||||
c.channel_name,
|
||||
c.guild_id
|
||||
FROM transcriptions t
|
||||
JOIN recordings r ON t.recording_id = r.id
|
||||
JOIN connections c ON r.connection_id = c.id
|
||||
WHERE t.status = 'completed'
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT $1
|
||||
`;
|
||||
|
||||
const result = await this.pg_client.query(query, [limit]);
|
||||
|
||||
return result.rows.map(row => ({
|
||||
...row,
|
||||
languageInfo: LANGUAGE_INFO[row.detected_language] || { name: row.detected_language, flag: '🏳️' },
|
||||
timeAgo: moment(row.created_at).fromNow(),
|
||||
transcriptPreview: row.transcription_text.length > 100
|
||||
? row.transcription_text.substring(0, 100) + '...'
|
||||
: row.transcription_text
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting recent activity:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getLanguageStats(days = 7) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
detected_language,
|
||||
COUNT(*) as transcription_count,
|
||||
AVG(processing_time_ms) as avg_processing_time,
|
||||
SUM(r.duration_seconds) as total_speaking_time
|
||||
FROM transcriptions t
|
||||
JOIN recordings r ON t.recording_id = r.id
|
||||
WHERE t.created_at >= NOW() - INTERVAL '${days} days'
|
||||
AND t.status = 'completed'
|
||||
GROUP BY detected_language
|
||||
ORDER BY transcription_count DESC
|
||||
`;
|
||||
|
||||
const result = await this.pg_client.query(query);
|
||||
|
||||
return result.rows.map(row => ({
|
||||
...row,
|
||||
languageInfo: LANGUAGE_INFO[row.detected_language] || { name: row.detected_language, flag: '🏳️' },
|
||||
transcription_count: parseInt(row.transcription_count),
|
||||
avg_processing_time: parseFloat(row.avg_processing_time || 0),
|
||||
total_speaking_time: parseFloat(row.total_speaking_time || 0)
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting language stats:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getPerformanceMetrics(hours = 24) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
service_name,
|
||||
operation,
|
||||
AVG(duration_ms) as avg_duration,
|
||||
COUNT(*) as operation_count,
|
||||
COUNT(CASE WHEN success = false THEN 1 END) as error_count
|
||||
FROM processing_metrics
|
||||
WHERE created_at >= NOW() - INTERVAL '${hours} hours'
|
||||
GROUP BY service_name, operation
|
||||
ORDER BY service_name, operation
|
||||
`;
|
||||
|
||||
const result = await this.pg_client.query(query);
|
||||
|
||||
return result.rows.map(row => ({
|
||||
...row,
|
||||
avg_duration: parseFloat(row.avg_duration || 0),
|
||||
operation_count: parseInt(row.operation_count),
|
||||
error_count: parseInt(row.error_count),
|
||||
success_rate: row.operation_count > 0
|
||||
? ((row.operation_count - row.error_count) / row.operation_count * 100).toFixed(2)
|
||||
: 0
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting performance metrics:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getUserActivity(days = 7) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
speaker_id,
|
||||
speaker_nickname,
|
||||
speaker_username,
|
||||
COUNT(*) as transcription_count,
|
||||
SUM(duration_seconds) as total_speaking_time,
|
||||
MAX(started_at) as last_activity
|
||||
FROM recordings r
|
||||
JOIN transcriptions t ON r.id = t.recording_id
|
||||
WHERE r.started_at >= NOW() - INTERVAL '${days} days'
|
||||
AND t.status = 'completed'
|
||||
GROUP BY speaker_id, speaker_nickname, speaker_username
|
||||
ORDER BY transcription_count DESC
|
||||
LIMIT 20
|
||||
`;
|
||||
|
||||
const result = await this.pg_client.query(query);
|
||||
|
||||
return result.rows.map(row => ({
|
||||
...row,
|
||||
transcription_count: parseInt(row.transcription_count),
|
||||
total_speaking_time: parseFloat(row.total_speaking_time || 0),
|
||||
last_activity_ago: moment(row.last_activity).fromNow()
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting user activity:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async listenForEvents() {
|
||||
logger.info('Starting to listen for dashboard events');
|
||||
|
||||
const subscriber = this.redis_client.duplicate();
|
||||
await subscriber.connect();
|
||||
await subscriber.subscribe('voice-translator-events', (message) => {
|
||||
this.processRedisMessage(message);
|
||||
});
|
||||
|
||||
logger.info('Subscribed to voice-translator-events channel for dashboard');
|
||||
}
|
||||
|
||||
async processRedisMessage(message) {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
const { event, data: eventData } = data;
|
||||
|
||||
switch (event) {
|
||||
case 'connection_started':
|
||||
await this.handleConnectionStarted(eventData);
|
||||
break;
|
||||
|
||||
case 'connection_ended':
|
||||
await this.handleConnectionEnded(eventData);
|
||||
break;
|
||||
|
||||
case 'transcription_completed':
|
||||
await this.handleTranscriptionCompleted(eventData);
|
||||
break;
|
||||
|
||||
case 'translations_completed':
|
||||
await this.handleTranslationsCompleted(eventData);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.debug('Unhandled event type for dashboard:', event);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
logger.error('Invalid JSON in Redis message:', message);
|
||||
} else {
|
||||
logger.error('Error processing Redis message in dashboard:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleConnectionStarted(eventData) {
|
||||
const { connectionId, channelName, guildId } = eventData;
|
||||
|
||||
this.activeConnections.set(connectionId, {
|
||||
...eventData,
|
||||
startedAt: new Date()
|
||||
});
|
||||
|
||||
// Broadcast to all connected clients
|
||||
this.io.emit('connection-started', {
|
||||
connectionId,
|
||||
channelName,
|
||||
guildId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
logger.info('Dashboard: Connection started', { connectionId, channelName });
|
||||
}
|
||||
|
||||
async handleConnectionEnded(eventData) {
|
||||
const { connectionId } = eventData;
|
||||
|
||||
this.activeConnections.delete(connectionId);
|
||||
|
||||
// Broadcast to all connected clients
|
||||
this.io.emit('connection-ended', {
|
||||
connectionId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
logger.info('Dashboard: Connection ended', { connectionId });
|
||||
}
|
||||
|
||||
async handleTranscriptionCompleted(eventData) {
|
||||
const { transcriptionId, recordingId, text, language } = eventData;
|
||||
|
||||
// Update realtime stats
|
||||
this.realtimeStats.totalTranscriptions++;
|
||||
this.realtimeStats.languageBreakdown[language] =
|
||||
(this.realtimeStats.languageBreakdown[language] || 0) + 1;
|
||||
|
||||
// Broadcast to all connected clients
|
||||
this.io.emit('transcription-completed', {
|
||||
transcriptionId,
|
||||
recordingId,
|
||||
text: text.length > 100 ? text.substring(0, 100) + '...' : text,
|
||||
language,
|
||||
languageInfo: LANGUAGE_INFO[language] || { name: language, flag: '🏳️' },
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
logger.info('Dashboard: Transcription completed', { transcriptionId, language });
|
||||
}
|
||||
|
||||
async handleTranslationsCompleted(eventData) {
|
||||
const { transcriptionId, translations } = eventData;
|
||||
|
||||
// Broadcast to all connected clients
|
||||
this.io.emit('translations-completed', {
|
||||
transcriptionId,
|
||||
translationCount: Object.keys(translations).length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
logger.info('Dashboard: Translations completed', { transcriptionId });
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.redis_client) {
|
||||
await this.redis_client.quit();
|
||||
}
|
||||
if (this.pg_client) {
|
||||
await this.pg_client.end();
|
||||
}
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
this.server.listen(PORT, () => {
|
||||
logger.info(`Dashboard server running on port ${PORT}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Main application
|
||||
async function main() {
|
||||
const dashboard = new DashboardService();
|
||||
|
||||
try {
|
||||
await dashboard.initialize();
|
||||
dashboard.start();
|
||||
|
||||
logger.info('Dashboard service started successfully');
|
||||
|
||||
// Graceful shutdown handling
|
||||
process.on('SIGINT', async () => {
|
||||
logger.info('Received SIGINT, shutting down dashboard gracefully...');
|
||||
await dashboard.cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
logger.info('Received SIGTERM, shutting down dashboard gracefully...');
|
||||
await dashboard.cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to start dashboard service:', error);
|
||||
await dashboard.cleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle unhandled errors
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
logger.error('Uncaught Exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Start the application
|
||||
main().catch((error) => {
|
||||
logger.error('Fatal error in dashboard main:', error);
|
||||
process.exit(1);
|
||||
});
|
Reference in New Issue
Block a user