UNPKG

tcp-serial-relay

Version:

Modular TCP to Serial relay service with comprehensive logging, monitoring and web dashboard

1,009 lines (869 loc) • 37.9 kB
#!/usr/bin/env node // scripts/build-dashboard.js // Build script for the TCP-Serial Relay Dashboard const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); console.log('Building TCP-Serial Relay Dashboard...'); // Path to the dashboard directory const dashboardDir = path.join(__dirname, '../dashboard'); // Check if dashboard directory exists if (!fs.existsSync(dashboardDir)) { console.error('Dashboard directory not found. Make sure the dashboard directory exists at:', dashboardDir); process.exit(1); } try { // Navigate to the dashboard directory and build the Next.js app console.log('Installing dashboard dependencies...'); execSync('npm install', { cwd: dashboardDir, stdio: 'inherit' }); console.log('Building Next.js dashboard...'); execSync('npm run build', { cwd: dashboardDir, stdio: 'inherit' }); console.log('Dashboard built successfully!'); // Create a .npmignore file to ensure the built dashboard is included in the package const npmignorePath = path.join(__dirname, '../.npmignore'); let npmignoreContent = ''; // Read existing .npmignore if it exists if (fs.existsSync(npmignorePath)) { npmignoreContent = fs.readFileSync(npmignorePath, 'utf8'); } // Make sure we're not ignoring the dashboard build output const linesToEnsure = [ '# Include dashboard build output', '!dashboard/.next/standalone/**', '!dashboard/.next/static/**', '!dashboard/public/**' ]; let modified = false; for (const line of linesToEnsure) { if (!npmignoreContent.includes(line)) { npmignoreContent += '\n' + line; modified = true; } } if (modified) { fs.writeFileSync(npmignorePath, npmignoreContent); console.log('Updated .npmignore to include dashboard build output'); } // Create a README for the dashboard const readmePath = path.join(dashboardDir, 'README.md'); const readmeContent = `# TCP-Serial Relay Dashboard This is the web dashboard for the TCP-Serial Relay package. It provides a user-friendly interface for monitoring and configuring the relay service. ## Usage The dashboard can be started along with the relay service using: \`\`\`bash tcp-serial-relay dashboard \`\`\` For more options, run: \`\`\`bash tcp-serial-relay dashboard --help \`\`\` `; fs.writeFileSync(readmePath, readmeContent); console.log('Created dashboard README'); console.log('Dashboard build process completed successfully!'); } catch (error) { console.error('Error building dashboard:', error); process.exit(1); } max-width: 1400px; margin: 0 auto; padding: 20px; } .header { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 15px; padding: 20px; margin-bottom: 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); } .header h1 { color: #2c3e50; margin-bottom: 10px; font-size: 2em; } .status-indicator { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 20px; font-weight: 600; font-size: 0.9em; } .status-healthy { background: #d4edda; color: #155724; } .status-warning { background: #fff3cd; color: #856404; } .status-error { background: #f8d7da; color: #721c24; } .status-unknown { background: #e2e3e5; color: #383d41; } .status-dot { width: 8px; height: 8px; border-radius: 50%; animation: pulse 2s infinite; } .dot-green { background: #28a745; } .dot-yellow { background: #ffc107; } .dot-red { background: #dc3545; } .dot-gray { background: #6c757d; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .main-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; } .card { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 15px; padding: 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); transition: transform 0.2s ease; } .card:hover { transform: translateY(-2px); } .card h2 { color: #2c3e50; margin-bottom: 15px; font-size: 1.3em; border-bottom: 2px solid #e9ecef; padding-bottom: 8px; } .metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 15px; } .metric { text-align: center; padding: 15px; background: #f8f9fa; border-radius: 10px; border-left: 4px solid #007bff; } .metric-value { font-size: 1.8em; font-weight: bold; color: #007bff; margin-bottom: 5px; } .metric-label { font-size: 0.85em; color: #6c757d; text-transform: uppercase; letter-spacing: 0.5px; } .connection-status { display: flex; justify-content: space-between; margin-bottom: 15px; } .connection { flex: 1; margin: 0 10px; padding: 15px; border-radius: 10px; text-align: center; } .connection-tcp { background: linear-gradient(135deg, #667eea, #764ba2); color: white; } .connection-serial { background: linear-gradient(135deg, #f093fb, #f5576c); color: white; } .connection h3 { margin-bottom: 8px; font-size: 1.1em; } .connection-details { font-size: 0.9em; opacity: 0.9; } .full-width { grid-column: 1 / -1; } .tabs { display: flex; background: #f8f9fa; border-radius: 10px; padding: 5px; margin-bottom: 15px; } .tab { flex: 1; padding: 10px; text-align: center; border-radius: 7px; cursor: pointer; transition: all 0.3s ease; font-weight: 600; } .tab.active { background: #007bff; color: white; } .tab:hover:not(.active) { background: #e9ecef; } .tab-content { display: none; } .tab-content.active { display: block; } .log-viewer { background: #1a1a1a; border-radius: 10px; padding: 15px; max-height: 400px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 0.85em; line-height: 1.4; } .log-entry { margin-bottom: 5px; padding: 2px 0; } .log-timestamp { color: #6c757d; } .log-level-info { color: #17a2b8; } .log-level-warn { color: #ffc107; } .log-level-error { color: #dc3545; } .log-level-debug { color: #6f42c1; } .log-message { color: #e9ecef; } .config-form { display: grid; gap: 15px; } .form-group { display: grid; grid-template-columns: 150px 1fr; gap: 10px; align-items: center; } .form-group label { font-weight: 600; color: #495057; } .form-group input, .form-group select { padding: 10px; border: 2px solid #e9ecef; border-radius: 8px; font-size: 1em; transition: border-color 0.3s ease; } .form-group input:focus, .form-group select:focus { outline: none; border-color: #007bff; } .btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 1em; font-weight: 600; cursor: pointer; transition: all 0.3s ease; text-transform: uppercase; letter-spacing: 0.5px; } .btn-primary { background: linear-gradient(135deg, #007bff, #0056b3); color: white; } .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3); } .btn-danger { background: linear-gradient(135deg, #dc3545, #c82333); color: white; } .btn-success { background: linear-gradient(135deg, #28a745, #1e7e34); color: white; } .btn-secondary { background: linear-gradient(135deg, #6c757d, #5a6268); color: white; } .action-buttons { display: flex; gap: 10px; justify-content: center; margin-top: 20px; } .alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 15px; font-weight: 500; } .alert-success { background: #d4edda; color: #155724; border-left: 4px solid #28a745; } .alert-error { background: #f8d7da; color: #721c24; border-left: 4px solid #dc3545; } .alert-info { background: #d1ecf1; color: #0c5460; border-left: 4px solid #17a2b8; } .alert-warning { background: #fff3cd; color: #856404; border-left: 4px solid #ffc107; } .connection-indicator { position: fixed; top: 20px; right: 20px; padding: 8px 16px; border-radius: 20px; font-size: 0.8em; font-weight: 600; z-index: 1000; } .connection-online { background: #d4edda; color: #155724; } .connection-offline { background: #f8d7da; color: #721c24; } @media (max-width: 768px) { .main-grid { grid-template-columns: 1fr; } .form-group { grid-template-columns: 1fr; } .connection-status { flex-direction: column; } .connection { margin: 5px 0; } } </style> </head> <body> <div class="connection-indicator" id="connection-indicator"> <span>Connecting...</span> </div> <div class="container"> <div class="header"> <h1>TCP-Serial Relay Dashboard</h1> <div id="overall-status" class="status-indicator status-unknown"> <div class="status-dot dot-gray"></div> <span>Connecting...</span> </div> </div> <div class="main-grid"> <!-- Status Card --> <div class="card"> <h2>System Status</h2> <div class="connection-status"> <div class="connection connection-tcp"> <h3>TCP Connection</h3> <div class="connection-details" id="tcp-status"> <div>Status: <span id="tcp-connected">Unknown</span></div> <div>Endpoint: <span id="tcp-endpoint">-</span></div> <div>Bytes: <span id="tcp-bytes">-</span></div> </div> </div> <div class="connection connection-serial"> <h3>Serial Connection</h3> <div class="connection-details" id="serial-status"> <div>Status: <span id="serial-connected">Unknown</span></div> <div>Port: <span id="serial-port">-</span></div> <div>Bytes: <span id="serial-bytes">-</span></div> </div> </div> </div> <div id="status-message" class="alert alert-info"> Initializing connection... </div> </div> <!-- Metrics Card --> <div class="card"> <h2>Performance Metrics</h2> <div class="metrics-grid"> <div class="metric"> <div class="metric-value" id="metric-uptime">0s</div> <div class="metric-label">Uptime</div> </div> <div class="metric"> <div class="metric-value" id="metric-transfers">0</div> <div class="metric-label">Data Transfers</div> </div> <div class="metric"> <div class="metric-value" id="metric-errors">0</div> <div class="metric-label">Errors</div> </div> <div class="metric"> <div class="metric-value" id="metric-bytes">0 B</div> <div class="metric-label">Total Bytes</div> </div> </div> </div> <!-- Management Panel --> <div class="card full-width"> <h2>Management Panel</h2> <div class="tabs"> <div class="tab active" onclick="switchTab('logs')">Live Logs</div> <div class="tab" onclick="switchTab('config')">Configuration</div> <div class="tab" onclick="switchTab('control')">Control</div> </div> <!-- Logs Tab --> <div id="tab-logs" class="tab-content active"> <div class="log-viewer" id="log-viewer"> <div class="log-entry"> <span class="log-timestamp">[${new Date().toLocaleTimeString()}]</span> <span class="log-level-info">[INFO]</span> <span class="log-message">Dashboard initialized</span> </div> </div> </div> <!-- Configuration Tab --> <div id="tab-config" class="tab-content"> <form class="config-form" id="config-form"> <div class="form-group"> <label for="tcp-ip">TCP IP:</label> <input type="text" id="tcp-ip" name="tcpIp" placeholder="192.168.1.90"> </div> <div class="form-group"> <label for="tcp-port">TCP Port:</label> <input type="number" id="tcp-port" name="tcpPort" placeholder="10002"> </div> <div class="form-group"> <label for="serial-path">Serial Path:</label> <input type="text" id="serial-path" name="serialPath" placeholder="/dev/ttyUSB0"> </div> <div class="form-group"> <label for="serial-baud">Baud Rate:</label> <select id="serial-baud" name="serialBaud"> <option value="9600">9600</option> <option value="19200">19200</option> <option value="38400">38400</option> <option value="57600">57600</option> <option value="115200">115200</option> </select> </div> <div class="form-group"> <label for="serial-parity">Parity:</label> <select id="serial-parity" name="serialParity"> <option value="none">None</option> <option value="even">Even</option> <option value="odd">Odd</option> <option value="mark">Mark</option> <option value="space">Space</option> </select> </div> <div class="form-group"> <label for="log-level">Log Level:</label> <select id="log-level" name="logLevel"> <option value="error">Error</option> <option value="warn">Warning</option> <option value="info">Info</option> <option value="debug">Debug</option> </select> </div> </form> <div class="action-buttons"> <button class="btn btn-primary" onclick="saveConfig()">Save Configuration</button> <button class="btn btn-secondary" onclick="loadConfig()">Reload</button> <button class="btn btn-secondary" onclick="loadPorts()">Refresh Ports</button> </div> </div> <!-- Control Tab --> <div id="tab-control" class="tab-content"> <div class="action-buttons"> <button class="btn btn-success" onclick="startService()">Start Service</button> <button class="btn btn-danger" onclick="stopService()">Stop Service</button> <button class="btn btn-primary" onclick="restartService()">Restart Service</button> <button class="btn btn-secondary" onclick="checkHealth()">Health Check</button> </div> <div id="control-output" class="log-viewer" style="margin-top: 20px; max-height: 200px;"> <div class="log-entry"> <span class="log-message">Ready for commands...</span> </div> </div> </div> </div> </div> </div> <script> // Dashboard JavaScript with real API integration let currentStatus = { overall: 'unknown', connections: { tcp: { connected: false, endpoint: '-', bytes: 0 }, serial: { connected: false, port: '-', bytes: 0 } }, metrics: { uptime: 0, dataTransfers: 0, errors: 0, totalBytes: 0 }, message: 'Initializing...' }; let logs = []; let config = {}; let ws = null; let reconnectTimer = null; // WebSocket connection management function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = \`\${protocol}//\${window.location.host}/ws\`; try { ws = new WebSocket(wsUrl); ws.onopen = function() { console.log('WebSocket connected'); updateConnectionIndicator(true); if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } }; ws.onmessage = function(event) { try { const data = JSON.parse(event.data); handleWebSocketMessage(data); } catch (error) { console.error('Failed to parse WebSocket message:', error); } }; ws.onclose = function() { console.log('WebSocket disconnected'); updateConnectionIndicator(false); // Attempt to reconnect after 5 seconds reconnectTimer = setTimeout(() => { connectWebSocket(); }, 5000); }; ws.onerror = function(error) { console.error('WebSocket error:', error); updateConnectionIndicator(false); }; } catch (error) { console.error('Failed to create WebSocket connection:', error); updateConnectionIndicator(false); // Retry connection after 5 seconds reconnectTimer = setTimeout(() => { connectWebSocket(); }, 5000); } } function updateConnectionIndicator(connected) { const indicator = document.getElementById('connection-indicator'); if (connected) { indicator.className = 'connection-indicator connection-online'; indicator.querySelector('span').textContent = 'Connected'; } else { indicator.className = 'connection-indicator connection-offline'; indicator.querySelector('span').textContent = 'Disconnected'; } } function handleWebSocketMessage(data) { switch (data.type) { case 'status': currentStatus = data.payload; updateStatus(); break; case 'log': addLog(data.payload.level, data.payload.message, data.payload.timestamp); break; case 'config': config = data.payload.config; loadConfigToForm(); break; default: console.log('Unknown message type:', data.type); } } // Tab switching function switchTab(tabName) { document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); event.target.classList.add('active'); document.getElementById(\`tab-\${tabName}\`).classList.add('active'); } // Status updates function updateStatus() { // Update overall status const statusEl = document.getElementById('overall-status'); statusEl.className = 'status-indicator'; statusEl.querySelector('.status-dot').className = 'status-dot'; let statusText = 'Unknown'; let statusClass = 'status-unknown'; let dotClass = 'dot-gray'; if (currentStatus.success === true) { statusText = 'Healthy'; statusClass = 'status-healthy'; dotClass = 'dot-green'; } else if (currentStatus.success === false) { statusText = 'Error'; statusClass = 'status-error'; dotClass = 'dot-red'; } else if (currentStatus.connections && (currentStatus.connections.tcp?.connected || currentStatus.connections.serial?.connected)) { statusText = 'Warning'; statusClass = 'status-warning'; dotClass = 'dot-yellow'; } statusEl.classList.add(statusClass); statusEl.querySelector('.status-dot').classList.add(dotClass); statusEl.querySelector('span').textContent = statusText; // Update connections if (currentStatus.connections) { const tcp = currentStatus.connections.tcp || {}; const serial = currentStatus.connections.serial || {}; document.getElementById('tcp-connected').textContent = tcp.connected ? 'Connected' : 'Disconnected'; document.getElementById('tcp-endpoint').textContent = tcp.endpoint || \`\${config.tcpIp || '-'}:\${config.tcpPort || '-'}\`; document.getElementById('tcp-bytes').textContent = formatBytes(tcp.totalBytesReceived + tcp.totalBytesSent || 0); document.getElementById('serial-connected').textContent = serial.connected ? 'Connected' : 'Disconnected'; document.getElementById('serial-port').textContent = serial.path || config.serialPath || '-'; document.getElementById('serial-bytes').textContent = formatBytes(serial.totalBytesReceived + serial.totalBytesSent || 0); } // Update metrics if (currentStatus.metrics) { document.getElementById('metric-uptime').textContent = formatDuration(currentStatus.duration || 0); document.getElementById('metric-transfers').textContent = currentStatus.metrics.dataTransfers || 0; document.getElementById('metric-errors').textContent = currentStatus.metrics.errors || 0; const totalBytes = (currentStatus.metrics.bytesTransferredTcpToSerial || 0) + (currentStatus.metrics.bytesTransferredSerialToTcp || 0); document.getElementById('metric-bytes').textContent = formatBytes(totalBytes); } // Update status message const messageEl = document.getElementById('status-message'); messageEl.textContent = currentStatus.message || 'No status available'; const messageClass = currentStatus.success === false ? 'alert-error' : currentStatus.success === true ? 'alert-success' : 'alert-info'; messageEl.className = \`alert \${messageClass}\`; } // Utility functions function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } function formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) return \`\${hours}h \${minutes % 60}m\`; if (minutes > 0) return \`\${minutes}m \${seconds % 60}s\`; return \`\${seconds}s\`; } // Logging function addLog(level, message, timestamp) { const logTimestamp = timestamp ? new Date(timestamp).toLocaleTimeString() : new Date().toLocaleTimeString(); const logEntry = { timestamp: logTimestamp, level, message }; logs.push(logEntry); // Keep only last 100 logs if (logs.length > 100) { logs = logs.slice(-100); } updateLogViewer(); } function updateLogViewer() { const logViewer = document.getElementById('log-viewer'); logViewer.innerHTML = logs.map(log => \` <div class="log-entry"> <span class="log-timestamp">[\${log.timestamp}]</span> <span class="log-level-\${log.level}">[\${log.level.toUpperCase()}]</span> <span class="log-message">\${log.message}</span> </div> \`).join(''); logViewer.scrollTop = logViewer.scrollHeight; } // API functions async function apiCall(endpoint, method = 'GET', data = null) { const options = { method, headers: { 'Content-Type': 'application/json' } }; if (data) { options.body = JSON.stringify(data); } try { const response = await fetch(\`/api\${endpoint}\`, options); if (!response.ok) { throw new Error(\`HTTP \${response.status}: \${response.statusText}\`); } return await response.json(); } catch (error) { console.error('API call failed:', error); addLog('error', \`API call failed: \${error.message}\`); throw error; } } // Configuration management async function loadConfig() { try { const response = await apiCall('/config'); config = response.config || {}; loadConfigToForm(); addLog('info', 'Configuration loaded'); } catch (error) { addLog('error', 'Failed to load configuration'); } } function loadConfigToForm() { if (config.tcpIp) document.getElementById('tcp-ip').value = config.tcpIp; if (config.tcpPort) document.getElementById('tcp-port').value = config.tcpPort; if (config.serialPath) document.getElementById('serial-path').value = config.serialPath; if (config.serialBaud) document.getElementById('serial-baud').value = config.serialBaud; if (config.serialParity) document.getElementById('serial-parity').value = config.serialParity; if (config.logLevel) document.getElementById('log-level').value = config.logLevel; } async function saveConfig() { try { const newConfig = { tcpIp: document.getElementById('tcp-ip').value, tcpPort: parseInt(document.getElementById('tcp-port').value), serialPath: document.getElementById('serial-path').value, serialBaud: parseInt(document.getElementById('serial-baud').value), serialParity: document.getElementById('serial-parity').value, logLevel: document.getElementById('log-level').value }; await apiCall('/config', 'POST', newConfig); config = newConfig; addLog('info', 'Configuration saved successfully'); updateStatus(); // Update display with new config } catch (error) { addLog('error', 'Failed to save configuration'); } } async function loadPorts() { try { const response = await apiCall('/ports'); const serialPathInput = document.getElementById('serial-path'); if (response.ports && response.ports.length > 0) { // Create or update datalist for serial path suggestions let datalist = document.getElementById('serial-ports-list'); if (!datalist) { datalist = document.createElement('datalist'); datalist.id = 'serial-ports-list'; serialPathInput.parentNode.appendChild(datalist); serialPathInput.setAttribute('list', 'serial-ports-list'); } datalist.innerHTML = response.ports.map(port => \`<option value="\${port.path}">\${port.path} - \${port.manufacturer || 'Unknown'}</option>\` ).join(''); addLog('info', \`Found \${response.ports.length} serial port(s)\`); } else { addLog('warn', 'No serial ports found'); } } catch (error) { addLog('error', 'Failed to load serial ports'); } } // Service control async function startService() { try { await apiCall('/control/start', 'POST'); addLog('info', 'Start command sent'); } catch (error) { addLog('error', 'Failed to start service'); } } async function stopService() { try { await apiCall('/control/stop', 'POST'); addLog('info', 'Stop command sent'); } catch (error) { addLog('error', 'Failed to stop service'); } } async function restartService() { try { await apiCall('/control/restart', 'POST'); addLog('info', 'Restart command sent'); } catch (error) { addLog('error', 'Failed to restart service'); } } async function checkHealth() { try { const health = await apiCall('/health'); const controlOutput = document.getElementById('control-output'); controlOutput.innerHTML = \` <div class="log-entry"><span class="log-message">Health Check Results:</span></div> <div class="log-entry"><span class="log-level-info">[INFO]</span> <span class="log-message">Overall Status: \${health.status}</span></div> <div class="log-entry"><span class="log-level-info">[INFO]</span> <span class="log-message">Uptime: \${formatDuration(health.uptime || 0)}</span></div> \${health.connections ? Object.entries(health.connections).map(([key, conn]) => \`<div class="log-entry"><span class="log-level-info">[INFO]</span> <span class="log-message">\${key.toUpperCase()} Connection: \${conn.connected ? 'OK' : 'FAILED'}</span></div>\` ).join('') : ''} \${health.metrics ? \` <div class="log-entry"><span class="log-level-info">[INFO]</span> <span class="log-message">Data Transfers: \${health.metrics.dataTransfers || 0}</span></div> <div class="log-entry"><span class="log-level-info">[INFO]</span> <span class="log-message">Error Count: \${health.metrics.errors || 0}</span></div> \` : ''} <div class="log-entry"><span class="log-level-info">[INFO]</span> <span class="log-message">Health check completed</span></div> \`; addLog('info', \`Health check completed - Status: \${health.status}\`); } catch (error) { addLog('error', 'Health check failed'); } } // Initialize dashboard async function init() { addLog('info', 'Dashboard initializing...'); // Connect WebSocket connectWebSocket(); // Load initial data try { await loadConfig(); const statusResponse = await apiCall('/status'); if (statusResponse.status) { currentStatus = statusResponse.status; updateStatus(); } } catch (error) { addLog('warn', 'Failed to load initial data'); } addLog('info', 'Dashboard initialized'); } // Start the dashboard when page loads document.addEventListener('DOMContentLoaded', init); // Handle page visibility changes to reconnect WebSocket document.addEventListener('visibilitychange', function() { if (!document.hidden && (!ws || ws.readyState !== WebSocket.OPEN)) { addLog('info', 'Page became visible, reconnecting...'); connectWebSocket(); } }); </script> </body> </html>`; // Write the dashboard file const indexPath = path.join(dashboardDir, 'index.html'); fs.writeFileSync(indexPath, dashboardHtml, 'utf8'); console.log('āœ… Dashboard HTML created at:', indexPath); // Create a simple CSS file for any additional styles const cssContent = `/* Additional dashboard styles can be added here */ .custom-styles { /* Future enhancements */ }`; const cssPath = path.join(dashboardDir, 'dashboard.css'); fs.writeFileSync(cssPath, cssContent, 'utf8'); console.log('āœ… Dashboard CSS created at:', cssPath); // Create a configuration file for the dashboard const dashboardConfig = { name: "TCP-Serial Relay Dashboard", version: "1.1.0", description: "Web-based monitoring and control interface", defaultPort: 3000, features: [ "Real-time status monitoring", "Live log streaming", "Configuration management", "Service control", "Health monitoring" ], apiEndpoints: { status: "/api/status", config: "/api/config", logs: "/api/logs", health: "/api/health", control: "/api/control/*", ports: "/api/ports" } }; const configPath = path.join(dashboardDir, 'dashboard-config.json'); fs.writeFileSync(configPath, JSON.stringify(dashboardConfig, null, 2), 'utf8'); console.log('āœ… Dashboard configuration created at:', configPath); console.log('\nšŸŽ‰ Dashboard build completed successfully!'); console.log('\nTo start the dashboard:'); console.log(' npm run start:dashboard'); console.log('\nTo start with full relay service:'); console.log(' npm run start:with-dashboard'); console.log('\nDashboard will be available at: http://localhost:3000');