UNPKG

@kadi.build/local-remote-file-manager-ability

Version:

Local & Remote File Management System with S3-compatible container registry, HTTP server provider, file streaming, and comprehensive testing suite

793 lines (669 loc) 25 kB
/** * Monitoring Dashboard - Phase 4 Implementation * * Real-time console-based monitoring dashboard for S3HttpServer download progress, * server statistics, and auto-shutdown status. Provides a fancy text-based UI * with progress bars, live updates, and comprehensive monitoring information. * * Features: * - Real-time progress bars for individual and overall downloads * - Live server statistics and performance metrics * - Download speed monitoring and ETA calculations * - Auto-shutdown status and countdown timers * - Active download listing with detailed progress * - Console-based box UI with dynamic updates * - Color-coded status indicators and alerts * - Configurable update intervals and display options */ import { EventEmitter } from 'events'; class MonitoringDashboard extends EventEmitter { constructor(config = {}) { super(); this.config = { // Display settings updateInterval: config.updateInterval || 1000, // 1 second maxDisplayedDownloads: config.maxDisplayedDownloads || 10, progressBarWidth: config.progressBarWidth || 30, enableColors: config.enableColors !== false, enableUnicode: config.enableUnicode !== false, // Dashboard sections showServerStats: config.showServerStats !== false, showDownloadProgress: config.showDownloadProgress !== false, showActiveDownloads: config.showActiveDownloads !== false, showShutdownStatus: config.showShutdownStatus !== false, showPerformanceMetrics: config.showPerformanceMetrics !== false, // Auto-refresh settings autoRefresh: config.autoRefresh !== false, refreshOnEvents: config.refreshOnEvents !== false, clearConsoleOnUpdate: config.clearConsoleOnUpdate !== false, // Box drawing characters boxChars: config.enableUnicode ? { topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯', horizontal: '─', vertical: '│', cross: '┼', teeDown: '┬', teeUp: '┴', teeLeft: '┤', teeRight: '├' } : { topLeft: '+', topRight: '+', bottomLeft: '+', bottomRight: '+', horizontal: '-', vertical: '|', cross: '+', teeDown: '+', teeUp: '+', teeLeft: '+', teeRight: '+' }, // Progress bar characters progressChars: config.enableUnicode ? { filled: '▓', empty: '░', partial: ['▏', '▎', '▍', '▌', '▋', '▊', '▉'] } : { filled: '=', empty: ' ', partial: ['-'] }, ...config }; // Internal state this.isDisplaying = false; this.displayTimer = null; this.lastUpdate = null; this.displayBuffer = []; this.terminalWidth = process.stdout.columns || 80; this.terminalHeight = process.stdout.rows || 24; // Dashboard rendering state for in-place updates this.hasRenderedOnce = false; this.dashboardLineCount = 0; this.lastDisplayContent = ''; // Dashboard data this.serverStats = null; this.downloadProgress = null; this.shutdownStatus = null; this.activeDownloads = []; // External dependencies (injected) this.downloadMonitor = null; this.shutdownManager = null; this.s3Server = null; // Color codes (with fallbacks) this.colors = this.config.enableColors ? { reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', bgRed: '\x1b[41m', bgGreen: '\x1b[42m', bgYellow: '\x1b[43m' } : { reset: '', bright: '', dim: '', red: '', green: '', yellow: '', blue: '', magenta: '', cyan: '', white: '', bgRed: '', bgGreen: '', bgYellow: '' }; this.setupEventHandling(); this.setupTerminalHandling(); } // ============================================================================ // CONFIGURATION AND SETUP // ============================================================================ /** * Set external dependencies for dashboard data * @param {Object} dependencies - Object containing dependent services */ setDependencies(dependencies = {}) { this.downloadMonitor = dependencies.downloadMonitor; this.shutdownManager = dependencies.shutdownManager; this.s3Server = dependencies.s3Server; // Setup event listeners if auto-refresh on events is enabled if (this.config.refreshOnEvents) { this.setupDataEventListeners(); } } /** * Setup event listeners for automatic refresh */ setupDataEventListeners() { if (this.downloadMonitor) { this.downloadMonitor.on('downloadStarted', () => this.refreshDisplay()); this.downloadMonitor.on('downloadProgress', () => this.refreshDisplay()); this.downloadMonitor.on('downloadCompleted', () => this.refreshDisplay()); this.downloadMonitor.on('downloadFailed', () => this.refreshDisplay()); this.downloadMonitor.on('allDownloadsComplete', () => this.refreshDisplay()); } if (this.shutdownManager) { this.shutdownManager.on('shutdownScheduled', () => this.refreshDisplay()); this.shutdownManager.on('shutdownWarning', () => this.refreshDisplay()); this.shutdownManager.on('shutdownCancelled', () => this.refreshDisplay()); } } // ============================================================================ // DISPLAY CONTROL // ============================================================================ /** * Start real-time dashboard display * @param {Object} options - Display options * @returns {Object} Start result */ startRealTimeDisplay(options = {}) { if (this.isDisplaying) { return { success: false, error: 'Dashboard already displaying' }; } this.isDisplaying = true; this.lastUpdate = new Date(); // Initial display this.refreshDisplay(); // Setup auto-refresh timer if (this.config.autoRefresh && this.config.updateInterval > 0) { this.displayTimer = setInterval(() => { this.refreshDisplay(); }, this.config.updateInterval); } this.emit('displayStarted', { startTime: this.lastUpdate, config: this.config }); return { success: true, startTime: this.lastUpdate, autoRefresh: this.config.autoRefresh, updateInterval: this.config.updateInterval }; } /** * Stop real-time dashboard display * @returns {Object} Stop result */ stopRealTimeDisplay() { if (!this.isDisplaying) { return { success: false, error: 'Dashboard not displaying' }; } this.isDisplaying = false; // Clear timer if (this.displayTimer) { clearInterval(this.displayTimer); this.displayTimer = null; } const displayDuration = this.lastUpdate ? new Date() - this.lastUpdate : 0; this.emit('displayStopped', { stopTime: new Date(), duration: displayDuration }); return { success: true, duration: displayDuration }; } /** * Refresh the dashboard display * @param {boolean} force - Force refresh even if not displaying */ refreshDisplay(force = false) { if (!this.isDisplaying && !force) return; try { // Collect current data this.collectDashboardData(); // Build display buffer this.buildDisplayBuffer(); // Render to console this.renderToConsole(); this.lastUpdate = new Date(); } catch (error) { this.emit('error', { type: 'refreshDisplay', error: error.message }); } } // ============================================================================ // DATA COLLECTION // ============================================================================ /** * Collect current data from all sources */ collectDashboardData() { // Collect server statistics this.serverStats = this.collectServerStats(); // Collect download progress this.downloadProgress = this.collectDownloadProgress(); // Collect shutdown status this.shutdownStatus = this.collectShutdownStatus(); // Collect active downloads this.activeDownloads = this.collectActiveDownloads(); } /** * Collect server statistics * @returns {Object} Server stats */ collectServerStats() { const stats = { status: 'UNKNOWN', uptime: 0, port: 'N/A', publicUrl: null, requests: 0, connections: 0 }; if (this.s3Server) { try { stats.status = this.s3Server.isRunning ? 'RUNNING' : 'STOPPED'; stats.uptime = this.s3Server.startTime ? new Date() - this.s3Server.startTime : 0; stats.port = this.s3Server.config?.port || 'N/A'; stats.publicUrl = this.s3Server.tunnelUrl || (stats.port !== 'N/A' ? `http://localhost:${stats.port}` : null); stats.requests = this.s3Server.downloadStats?.totalDownloads || 0; stats.connections = this.s3Server.activeDownloads?.size || 0; } catch (error) { // Handle errors gracefully stats.status = 'ERROR'; } } return stats; } /** * Collect download progress summary * @returns {Object} Download progress */ collectDownloadProgress() { if (!this.downloadMonitor) { return { totalExpected: 0, totalCompleted: 0, totalFailed: 0, activeCount: 0, overallPercentage: 100, overallSpeed: 0, overallETA: null, totalBytes: 0, bytesTransferred: 0 }; } try { return this.downloadMonitor.getDownloadProgress(); } catch (error) { return { error: error.message }; } } /** * Collect shutdown manager status * @returns {Object} Shutdown status */ collectShutdownStatus() { if (!this.shutdownManager) { return { enabled: false, scheduled: false, reason: null, timeRemaining: null }; } try { const status = this.shutdownManager.getStatus(); return { enabled: status.isMonitoring, scheduled: status.shutdownScheduled, shuttingDown: status.isShuttingDown, reason: status.shutdownReason, timeRemaining: null, // TODO: Calculate from shutdown timer lastActivity: status.lastActivityTime, uptime: status.uptime }; } catch (error) { return { error: error.message }; } } /** * Collect active downloads * @returns {Array} Active downloads list */ collectActiveDownloads() { if (!this.downloadMonitor) { return []; } try { const progress = this.downloadMonitor.getDownloadProgress(); return progress.activeDownloads || []; } catch (error) { return []; } } // ============================================================================ // DISPLAY RENDERING // ============================================================================ /** * Build the display buffer with all dashboard sections */ buildDisplayBuffer() { this.displayBuffer = []; // Dashboard header this.addDashboardHeader(); // Server statistics section if (this.config.showServerStats) { this.addServerStatsSection(); } // Download progress section if (this.config.showDownloadProgress) { this.addDownloadProgressSection(); } // Active downloads section if (this.config.showActiveDownloads) { this.addActiveDownloadsSection(); } // Shutdown status section if (this.config.showShutdownStatus) { this.addShutdownStatusSection(); } // Dashboard footer this.addDashboardFooter(); } /** * Add dashboard header */ addDashboardHeader() { const title = 'S3 Object Storage Server'; const width = this.terminalWidth - 4; this.displayBuffer.push(this.buildBoxLine('top', width)); this.displayBuffer.push(this.buildTextLine(title, width, 'center')); this.displayBuffer.push(this.buildBoxLine('separator', width)); } /** * Add server statistics section */ addServerStatsSection() { const stats = this.serverStats; const width = this.terminalWidth - 4; // Status and uptime const statusColor = stats.status === 'RUNNING' ? this.colors.green : this.colors.red; const uptimeStr = this.formatDuration(stats.uptime); const leftText = `Status: ${statusColor}${stats.status}${this.colors.reset}`; const rightText = `Uptime: ${uptimeStr}`; this.displayBuffer.push(this.buildTextLine(leftText, width, 'left', rightText)); // Port and URL const portText = `Port: ${stats.port || 'N/A'}`; const urlText = stats.publicUrl ? `Public URL: ${stats.publicUrl}` : ''; this.displayBuffer.push(this.buildTextLine(portText, width, 'left', urlText)); this.displayBuffer.push(this.buildBoxLine('separator', width)); } /** * Add download progress section */ addDownloadProgressSection() { const progress = this.downloadProgress; const width = this.terminalWidth - 4; this.displayBuffer.push(this.buildTextLine('Downloads Progress', width, 'left')); if (progress.error) { this.displayBuffer.push(this.buildTextLine(`Error: ${progress.error}`, width, 'left')); } else { // Overall progress bar const progressBar = this.buildProgressBar(progress.overallPercentage, this.config.progressBarWidth); const progressText = `${progressBar} ${progress.totalCompleted}/${progress.totalExpected} (${Math.round(progress.overallPercentage)}%)`; this.displayBuffer.push(this.buildTextLine(progressText, width, 'left')); // Statistics line const activeText = `Active Downloads: ${progress.activeCount}`; const completedText = `Completed: ${progress.totalCompleted}`; const failedText = progress.totalFailed > 0 ? `${this.colors.red}Failed: ${progress.totalFailed}${this.colors.reset}` : `Failed: ${progress.totalFailed}`; this.displayBuffer.push(this.buildTextLine(activeText, width, 'left', `${completedText} ${failedText}`)); // Speed and ETA const speedText = `Speed: ${this.formatSpeed(progress.overallSpeed)}`; const totalText = `Total: ${this.formatBytes(progress.bytesTransferred)}`; const etaText = progress.overallETA ? `ETA: ${this.formatDuration(progress.overallETA * 1000)}` : ''; this.displayBuffer.push(this.buildTextLine(speedText, width, 'left', `${totalText} ${etaText}`)); } this.displayBuffer.push(this.buildBoxLine('separator', width)); } /** * Add active downloads section */ addActiveDownloadsSection() { const downloads = this.activeDownloads.slice(0, this.config.maxDisplayedDownloads); const width = this.terminalWidth - 4; this.displayBuffer.push(this.buildTextLine('Active Downloads', width, 'left')); if (downloads.length === 0) { this.displayBuffer.push(this.buildTextLine('No active downloads', width, 'center')); } else { for (const download of downloads) { const filename = this.truncateText(download.key || download.path || download.id, 20); const percentage = download.expectedSize > 0 ? (download.bytesTransferred / download.expectedSize) * 100 : 0; const progressBar = this.buildProgressBar(percentage, 16); const speedText = this.formatSpeed(download.speed || 0); const downloadText = `${filename} ${progressBar} ${Math.round(percentage)}% (${speedText})`; this.displayBuffer.push(this.buildTextLine(downloadText, width, 'left')); } } this.displayBuffer.push(this.buildBoxLine('separator', width)); } /** * Add shutdown status section */ addShutdownStatusSection() { const shutdown = this.shutdownStatus; const width = this.terminalWidth - 4; // Auto-shutdown status const statusText = shutdown.enabled ? `${this.colors.green}ON${this.colors.reset}` : `${this.colors.red}OFF${this.colors.reset}`; const triggerText = shutdown.reason ? `Trigger: ${shutdown.reason}` : 'Trigger: Completion + 30s'; this.displayBuffer.push(this.buildTextLine(`Auto-Shutdown: ${statusText}`, width, 'left', triggerText)); // Shutdown countdown or status if (shutdown.scheduled) { const statusColor = shutdown.shuttingDown ? this.colors.red : this.colors.yellow; const statusMessage = shutdown.shuttingDown ? 'SHUTTING DOWN' : 'SCHEDULED'; this.displayBuffer.push(this.buildTextLine(`Next Check: 00:00:05`, width, 'left', `Status: ${statusColor}${statusMessage}${this.colors.reset}`)); } else { this.displayBuffer.push(this.buildTextLine(`Next Check: 00:00:05`, width, 'left', `Status: Monitoring`)); } } /** * Add dashboard footer */ addDashboardFooter() { const width = this.terminalWidth - 4; this.displayBuffer.push(this.buildBoxLine('bottom', width)); } // ============================================================================ // DISPLAY UTILITIES // ============================================================================ /** * Build a box line (top, bottom, separator) * @param {string} type - Line type * @param {number} width - Line width * @returns {string} Box line */ buildBoxLine(type, width) { const chars = this.config.boxChars; switch (type) { case 'top': return chars.topLeft + chars.horizontal.repeat(width) + chars.topRight; case 'bottom': return chars.bottomLeft + chars.horizontal.repeat(width) + chars.bottomRight; case 'separator': return chars.teeRight + chars.horizontal.repeat(width) + chars.teeLeft; default: return chars.horizontal.repeat(width + 2); } } /** * Build a text line with optional left and right text * @param {string} leftText - Left-aligned text * @param {number} width - Line width * @param {string} align - Text alignment * @param {string} rightText - Right-aligned text * @returns {string} Text line */ buildTextLine(leftText, width, align = 'left', rightText = '') { const chars = this.config.boxChars; // Strip color codes for length calculation const stripColors = (text) => text.replace(/\x1b\[[0-9;]*m/g, ''); const leftLength = stripColors(leftText).length; const rightLength = stripColors(rightText).length; let content; const availableWidth = width - rightLength; if (align === 'center') { const padding = Math.max(0, Math.floor((availableWidth - leftLength) / 2)); content = ' '.repeat(padding) + leftText + ' '.repeat(availableWidth - padding - leftLength); } else { const padding = Math.max(0, availableWidth - leftLength); content = leftText + ' '.repeat(padding); } return chars.vertical + content + rightText + chars.vertical; } /** * Build a progress bar * @param {number} percentage - Progress percentage (0-100) * @param {number} width - Progress bar width * @returns {string} Progress bar */ buildProgressBar(percentage, width) { const chars = this.config.progressChars; const filledWidth = Math.floor((percentage / 100) * width); const emptyWidth = width - filledWidth; return chars.filled.repeat(filledWidth) + chars.empty.repeat(emptyWidth); } /** * Render the display buffer to console */ renderToConsole() { // For the first render, print a separator and track position if (!this.hasRenderedOnce) { this.hasRenderedOnce = true; console.log(''); // Add some space console.log('\x1b[90m📊 Live Monitoring Dashboard:\x1b[0m'); console.log('\x1b[90m' + '─'.repeat(80) + '\x1b[0m'); this.lastDisplayContent = ''; } // Create the current display content as a string const currentContent = this.displayBuffer.join('\n'); // Only update if content has actually changed significantly // This prevents minor updates from spamming the console if (this.lastDisplayContent && this.contentSimilarity(currentContent, this.lastDisplayContent) > 0.95) { return; // Skip very similar updates } // Try ANSI escape sequences for in-place update only if terminal supports it if (this.dashboardLineCount > 0 && process.stdout.isTTY && this.config.enableInPlaceUpdates !== false) { // Move cursor up and clear from that point process.stdout.write(`\x1b[${this.dashboardLineCount}A`); // Move cursor up process.stdout.write('\x1b[0J'); // Clear from cursor to end of screen } else if (this.dashboardLineCount > 0) { // If ANSI doesn't work well, just add a separator console.log('\x1b[90m' + '─'.repeat(40) + ' Updated ' + '─'.repeat(40) + '\x1b[0m'); } // Write each line of the dashboard for (const line of this.displayBuffer) { console.log(line); } // Keep track of content and line count for next update this.lastDisplayContent = currentContent; this.dashboardLineCount = this.displayBuffer.length; } /** * Calculate content similarity to avoid unnecessary updates */ contentSimilarity(str1, str2) { if (str1 === str2) return 1.0; if (!str1 || !str2) return 0.0; // Simple similarity check - count different characters const maxLen = Math.max(str1.length, str2.length); if (maxLen === 0) return 1.0; let same = 0; const minLen = Math.min(str1.length, str2.length); for (let i = 0; i < minLen; i++) { if (str1[i] === str2[i]) same++; } return same / maxLen; } // ============================================================================ // FORMATTING UTILITIES // ============================================================================ /** * Format bytes as human-readable string * @param {number} bytes - Bytes to format * @returns {string} Formatted string */ formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } /** * Format speed as human-readable string * @param {number} bytesPerSecond - Speed in bytes per second * @returns {string} Formatted speed */ formatSpeed(bytesPerSecond) { return this.formatBytes(bytesPerSecond) + '/s'; } /** * Format duration as human-readable string * @param {number} milliseconds - Duration in milliseconds * @returns {string} Formatted duration */ formatDuration(milliseconds) { if (milliseconds < 1000) return '< 1s'; const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${String(hours).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`; } else if (minutes > 0) { return `${String(minutes).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`; } else { return `${seconds}s`; } } /** * Truncate text to specified length * @param {string} text - Text to truncate * @param {number} maxLength - Maximum length * @returns {string} Truncated text */ truncateText(text, maxLength) { if (text.length <= maxLength) return text; return text.substring(0, maxLength - 3) + '...'; } // ============================================================================ // EVENT HANDLING // ============================================================================ /** * Setup event handling */ setupEventHandling() { // Handle errors gracefully this.on('error', (errorInfo) => { console.error('📊 MonitoringDashboard error:', errorInfo); }); } /** * Setup terminal handling for responsive display */ setupTerminalHandling() { // Update terminal dimensions on resize process.stdout.on('resize', () => { this.terminalWidth = process.stdout.columns || 80; this.terminalHeight = process.stdout.rows || 24; // Refresh display if active if (this.isDisplaying) { this.refreshDisplay(); } }); } // ============================================================================ // PUBLIC API // ============================================================================ /** * Get current dashboard status * @returns {Object} Dashboard status */ getStatus() { return { isDisplaying: this.isDisplaying, lastUpdate: this.lastUpdate, terminalWidth: this.terminalWidth, terminalHeight: this.terminalHeight, config: this.config }; } /** * Force a single dashboard render (for debugging) * @returns {string} Dashboard content */ renderOnce() { this.collectDashboardData(); this.buildDisplayBuffer(); return this.displayBuffer.join('\n'); } } export { MonitoringDashboard };