@@ -513,6 +513,10 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
513513 scheduleCountdown();
514514 setTimeout(fetchStatus, REFRESH_MS); // initial schedule
515515 initializeLogs();
516+ initializeMetricsCharts();
517+
518+ // Start metrics updates
519+ setInterval(updateMetricsCharts, 5000); // Update every 5 seconds
516520
517521 // Event delegation for service control buttons
518522 document.addEventListener('click', function(event) {
@@ -763,6 +767,166 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
763767 }, 3000);
764768 }
765769 </script>
770+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
771+ <script>
772+ // Metrics visualization with Chart.js
773+ let metricsCharts = {};
774+
775+ function initializeMetricsCharts() {
776+ // CPU Chart
777+ const cpuCtx = document.getElementById('cpu-chart');
778+ if (cpuCtx) {
779+ metricsCharts.cpu = new Chart(cpuCtx, {
780+ type: 'line',
781+ data: {
782+ labels: [],
783+ datasets: [{
784+ label: 'CPU Usage %',
785+ data: [],
786+ borderColor: 'rgb(75, 192, 192)',
787+ backgroundColor: 'rgba(75, 192, 192, 0.1)',
788+ tension: 0.1
789+ }]
790+ },
791+ options: {
792+ responsive: true,
793+ scales: { y: { beginAtZero: true, max: 100 } }
794+ }
795+ });
796+ }
797+
798+ // Memory Chart
799+ const memoryCtx = document.getElementById('memory-chart');
800+ if (memoryCtx) {
801+ metricsCharts.memory = new Chart(memoryCtx, {
802+ type: 'line',
803+ data: {
804+ labels: [],
805+ datasets: [{
806+ label: 'Memory Usage %',
807+ data: [],
808+ borderColor: 'rgb(255, 99, 132)',
809+ backgroundColor: 'rgba(255, 99, 132, 0.1)',
810+ tension: 0.1
811+ }]
812+ },
813+ options: {
814+ responsive: true,
815+ scales: { y: { beginAtZero: true, max: 100 } }
816+ }
817+ });
818+ }
819+
820+ // Network Chart
821+ const networkCtx = document.getElementById('network-chart');
822+ if (networkCtx) {
823+ metricsCharts.network = new Chart(networkCtx, {
824+ type: 'line',
825+ data: {
826+ labels: [],
827+ datasets: [{
828+ label: 'Network RX (MB/s)',
829+ data: [],
830+ borderColor: 'rgb(54, 162, 235)',
831+ backgroundColor: 'rgba(54, 162, 235, 0.1)',
832+ tension: 0.1
833+ }, {
834+ label: 'Network TX (MB/s)',
835+ data: [],
836+ borderColor: 'rgb(255, 206, 86)',
837+ backgroundColor: 'rgba(255, 206, 86, 0.1)',
838+ tension: 0.1
839+ }]
840+ },
841+ options: {
842+ responsive: true,
843+ scales: { y: { beginAtZero: true } }
844+ }
845+ });
846+ }
847+
848+ // Disk Chart
849+ const diskCtx = document.getElementById('disk-chart');
850+ if (diskCtx) {
851+ metricsCharts.disk = new Chart(diskCtx, {
852+ type: 'doughnut',
853+ data: {
854+ labels: ['Used', 'Available'],
855+ datasets: [{
856+ data: [0, 100],
857+ backgroundColor: ['rgb(255, 99, 132)', 'rgb(75, 192, 192)']
858+ }]
859+ },
860+ options: {
861+ responsive: true,
862+ plugins: {
863+ legend: { position: 'bottom' }
864+ }
865+ }
866+ });
867+ }
868+ }
869+
870+ async function updateMetricsCharts() {
871+ try {
872+ const response = await fetch('/api/metrics');
873+ if (!response.ok) return;
874+
875+ const data = await response.json();
876+ const timestamp = new Date().toLocaleTimeString();
877+
878+ // Update CPU chart
879+ if (metricsCharts.cpu && data.metrics.cpu) {
880+ const chart = metricsCharts.cpu;
881+ chart.data.labels.push(timestamp);
882+ chart.data.datasets[0].data.push(data.metrics.cpu.percent || 0);
883+ if (chart.data.labels.length > 20) {
884+ chart.data.labels.shift();
885+ chart.data.datasets[0].data.shift();
886+ }
887+ chart.update('none');
888+ }
889+
890+ // Update Memory chart
891+ if (metricsCharts.memory && data.metrics.memory) {
892+ const chart = metricsCharts.memory;
893+ chart.data.labels.push(timestamp);
894+ chart.data.datasets[0].data.push(data.metrics.memory.percent || 0);
895+ if (chart.data.labels.length > 20) {
896+ chart.data.labels.shift();
897+ chart.data.datasets[0].data.shift();
898+ }
899+ chart.update('none');
900+ }
901+
902+ // Update Network chart
903+ if (metricsCharts.network && data.metrics.network) {
904+ const chart = metricsCharts.network;
905+ chart.data.labels.push(timestamp);
906+ chart.data.datasets[0].data.push((data.metrics.network.rx_sec || 0) / (1024 * 1024)); // MB/s
907+ chart.data.datasets[1].data.push((data.metrics.network.tx_sec || 0) / (1024 * 1024)); // MB/s
908+ if (chart.data.labels.length > 20) {
909+ chart.data.labels.shift();
910+ chart.data.datasets[0].data.shift();
911+ chart.data.datasets[1].data.shift();
912+ }
913+ chart.update('none');
914+ }
915+
916+ // Update Disk chart
917+ if (metricsCharts.disk && data.metrics.disk) {
918+ const chart = metricsCharts.disk;
919+ const used = data.metrics.disk.used || 0;
920+ const total = data.metrics.disk.total || 1;
921+ const available = total - used;
922+ chart.data.datasets[0].data = [used, available];
923+ chart.update('none');
924+ }
925+ } catch (e) {
926+ console.warn('Failed to update metrics charts:', e);
927+ }
928+ }
929+ </script>
766930</head>
767931<body>
768932 <header>
@@ -843,6 +1007,33 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
8431007 </tbody>
8441008 </table>` }
8451009
1010+ <!-- Metrics Section -->
1011+ <div class="metrics-section" style="margin-top:30px;">
1012+ <div style="margin-bottom:20px;">
1013+ <h2 style="font-size:1rem; font-weight:600; color:#334155; margin:0 0 16px;">Resource Monitoring</h2>
1014+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:20px; margin-bottom:20px;">
1015+ <div style="background:#fff; padding:16px; border-radius:8px; box-shadow:0 1px 3px rgba(0,0,0,0.1);">
1016+ <h3 style="font-size:0.9rem; margin:0 0 12px; color:#374151;">CPU Usage</h3>
1017+ <canvas id="cpu-chart" width="400" height="200"></canvas>
1018+ </div>
1019+ <div style="background:#fff; padding:16px; border-radius:8px; box-shadow:0 1px 3px rgba(0,0,0,0.1);">
1020+ <h3 style="font-size:0.9rem; margin:0 0 12px; color:#374151;">Memory Usage</h3>
1021+ <canvas id="memory-chart" width="400" height="200"></canvas>
1022+ </div>
1023+ </div>
1024+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:20px;">
1025+ <div style="background:#fff; padding:16px; border-radius:8px; box-shadow:0 1px 3px rgba(0,0,0,0.1);">
1026+ <h3 style="font-size:0.9rem; margin:0 0 12px; color:#374151;">Network I/O</h3>
1027+ <canvas id="network-chart" width="400" height="200"></canvas>
1028+ </div>
1029+ <div style="background:#fff; padding:16px; border-radius:8px; box-shadow:0 1px 3px rgba(0,0,0,0.1);">
1030+ <h3 style="font-size:0.9rem; margin:0 0 12px; color:#374151;">Disk I/O</h3>
1031+ <canvas id="disk-chart" width="400" height="200"></canvas>
1032+ </div>
1033+ </div>
1034+ </div>
1035+ </div>
1036+
8461037 <!-- Logs Section -->
8471038 <div class="logs-section">
8481039 <div class="logs-header">
@@ -907,6 +1098,79 @@ export async function startAdminDashboard(options = {}) {
9071098 console . log ( chalk . yellow ( ' Press Ctrl+C to stop\n' ) ) ;
9081099
9091100 // Initialize log file watcher
1101+ // Helper function to get system metrics
1102+ async function getSystemMetrics ( ) {
1103+ try {
1104+ // Import systeminformation dynamically to avoid dependency issues in tests
1105+ const si = await import ( 'systeminformation' ) ;
1106+
1107+ const [ cpuInfo , cpuLoad , memory , networkStats , disk ] = await Promise . all ( [
1108+ si . cpu ( ) . catch ( ( ) => ( { } ) ) ,
1109+ si . currentLoad ( ) . catch ( ( ) => ( { currentLoad : 0 } ) ) ,
1110+ si . mem ( ) . catch ( ( ) => ( { } ) ) ,
1111+ si . networkStats ( ) . catch ( ( ) => ( [ ] ) ) ,
1112+ si . fsSize ( ) . catch ( ( ) => ( [ ] ) )
1113+ ] ) ;
1114+
1115+ // Calculate network totals
1116+ const networkTotals = networkStats . reduce ( ( totals , iface ) => {
1117+ if ( iface . iface && ! iface . iface . startsWith ( 'lo' ) ) { // Skip loopback
1118+ totals . rx_bytes += iface . rx_bytes || 0 ;
1119+ totals . tx_bytes += iface . tx_bytes || 0 ;
1120+ totals . rx_sec += iface . rx_sec || 0 ;
1121+ totals . tx_sec += iface . tx_sec || 0 ;
1122+ }
1123+ return totals ;
1124+ } , { rx_bytes : 0 , tx_bytes : 0 , rx_sec : 0 , tx_sec : 0 } ) ;
1125+
1126+ // Calculate disk totals
1127+ const diskTotals = disk . reduce ( ( totals , volume ) => {
1128+ if ( volume . mount === '/' || volume . mount . startsWith ( '/' ) ) {
1129+ totals . total += volume . size || 0 ;
1130+ totals . used += volume . used || 0 ;
1131+ totals . available += volume . available || 0 ;
1132+ }
1133+ return totals ;
1134+ } , { total : 0 , used : 0 , available : 0 } ) ;
1135+
1136+ return {
1137+ cpu : {
1138+ cores : cpuInfo . cores || 0 ,
1139+ model : cpuInfo . model || 'Unknown' ,
1140+ speed : cpuInfo . speed || 0 ,
1141+ percent : Math . round ( cpuLoad . currentLoad || 0 )
1142+ } ,
1143+ memory : {
1144+ total : memory . total || 0 ,
1145+ used : memory . used || 0 ,
1146+ available : memory . available || memory . total - memory . used || 0 ,
1147+ percent : memory . total ? ( ( memory . used / memory . total ) * 100 ) : 0
1148+ } ,
1149+ network : {
1150+ interfaces : networkStats . map ( iface => iface . iface ) . filter ( name => name && ! name . startsWith ( 'lo' ) ) ,
1151+ total_rx : networkTotals . rx_bytes ,
1152+ total_tx : networkTotals . tx_bytes ,
1153+ rx_sec : networkTotals . rx_sec ,
1154+ tx_sec : networkTotals . tx_sec
1155+ } ,
1156+ disk : {
1157+ total : diskTotals . total ,
1158+ used : diskTotals . used ,
1159+ available : diskTotals . available ,
1160+ percent : diskTotals . total ? ( ( diskTotals . used / diskTotals . total ) * 100 ) : 0
1161+ }
1162+ } ;
1163+ } catch ( error ) {
1164+ console . warn ( 'Failed to get system metrics:' , error . message ) ;
1165+ return {
1166+ cpu : { cores : 0 , model : 'Unknown' , speed : 0 , percent : 0 } ,
1167+ memory : { total : 0 , used : 0 , available : 0 , percent : 0 } ,
1168+ network : { interfaces : [ ] , total_rx : 0 , total_tx : 0 , rx_sec : 0 , tx_sec : 0 } ,
1169+ disk : { total : 0 , used : 0 , available : 0 , percent : 0 }
1170+ } ;
1171+ }
1172+ }
1173+
9101174 globalLogWatcher = new LogFileWatcher ( cwd ) ;
9111175 try {
9121176 await globalLogWatcher . startWatching ( ) ;
@@ -972,6 +1236,40 @@ export async function startAdminDashboard(options = {}) {
9721236 return ;
9731237 }
9741238
1239+ if ( url . pathname === '/api/metrics' ) {
1240+ // API endpoint for system and service metrics
1241+ try {
1242+ const metrics = await getSystemMetrics ( ) ;
1243+ res . writeHead ( 200 , {
1244+ 'Content-Type' : 'application/json' ,
1245+ 'Access-Control-Allow-Origin' : '*'
1246+ } ) ;
1247+ res . end ( JSON . stringify ( {
1248+ metrics : {
1249+ timestamp : new Date ( ) . toISOString ( ) ,
1250+ cpu : metrics . cpu || { } ,
1251+ memory : metrics . memory || { } ,
1252+ network : metrics . network || { } ,
1253+ disk : metrics . disk || { }
1254+ } ,
1255+ systemInfo : {
1256+ cpu : { cores : metrics . cpu ?. cores || 0 , model : metrics . cpu ?. model || 'Unknown' } ,
1257+ memory : { total : metrics . memory ?. total || 0 } ,
1258+ disk : { total : metrics . disk ?. total || 0 , available : metrics . disk ?. available || 0 } ,
1259+ network : { interfaces : metrics . network ?. interfaces || [ ] }
1260+ }
1261+ } , null , 2 ) ) ;
1262+ } catch ( e ) {
1263+ console . error ( '❌ Metrics API error:' , e . message ) ;
1264+ res . writeHead ( 500 , {
1265+ 'Content-Type' : 'application/json' ,
1266+ 'Access-Control-Allow-Origin' : '*'
1267+ } ) ;
1268+ res . end ( JSON . stringify ( { error : e . message } ) ) ;
1269+ }
1270+ return ;
1271+ }
1272+
9751273 // Service management endpoints
9761274 if ( url . pathname === '/api/services/start' && req . method === 'POST' ) {
9771275 try {
0 commit comments