- Enhanced Settings page with 40+ configuration options across 7 sections - General, Dashboard, Module Management, Notifications - Alert Thresholds, Security, Advanced settings - Created dedicated Alerts page with filtering and sorting - Filter by severity (error/warning/info) and module - Sort by time/severity/module - Auto-refresh every 30 seconds - Color-coded severity with dismiss functionality - Created Monitoring page with real-time historical graphs - SVG-based charts for CPU, Memory, Disk, Network - Stores last 30 data points - Auto-refresh every 5 seconds - Animated chart drawing with area fills - Added monitoring page to menu configuration - Consistent modern design with gradients and animations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
353 lines
9.4 KiB
JavaScript
353 lines
9.4 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'require dom';
|
|
'require secubox/api as API';
|
|
'require poll';
|
|
|
|
// Load CSS
|
|
document.head.appendChild(E('link', {
|
|
'rel': 'stylesheet',
|
|
'type': 'text/css',
|
|
'href': L.resource('secubox/monitoring.css')
|
|
}));
|
|
|
|
return view.extend({
|
|
cpuHistory: [],
|
|
memoryHistory: [],
|
|
diskHistory: [],
|
|
networkHistory: [],
|
|
maxDataPoints: 30, // Keep last 30 data points
|
|
|
|
load: function() {
|
|
return this.refreshData();
|
|
},
|
|
|
|
refreshData: function() {
|
|
var self = this;
|
|
return API.getSystemHealth().then(function(data) {
|
|
self.addDataPoint(data);
|
|
return data;
|
|
});
|
|
},
|
|
|
|
addDataPoint: function(health) {
|
|
var timestamp = Date.now();
|
|
|
|
// Add CPU data
|
|
this.cpuHistory.push({
|
|
time: timestamp,
|
|
value: (health.cpu && health.cpu.percent) || 0
|
|
});
|
|
|
|
// Add Memory data
|
|
this.memoryHistory.push({
|
|
time: timestamp,
|
|
value: (health.memory && health.memory.percent) || 0
|
|
});
|
|
|
|
// Add Disk data
|
|
this.diskHistory.push({
|
|
time: timestamp,
|
|
value: (health.disk && health.disk.percent) || 0
|
|
});
|
|
|
|
// Add Network data (calculate rate)
|
|
var netRx = (health.network && health.network.rx_bytes) || 0;
|
|
var netTx = (health.network && health.network.tx_bytes) || 0;
|
|
this.networkHistory.push({
|
|
time: timestamp,
|
|
rx: netRx,
|
|
tx: netTx
|
|
});
|
|
|
|
// Keep only last N data points
|
|
if (this.cpuHistory.length > this.maxDataPoints) {
|
|
this.cpuHistory.shift();
|
|
}
|
|
if (this.memoryHistory.length > this.maxDataPoints) {
|
|
this.memoryHistory.shift();
|
|
}
|
|
if (this.diskHistory.length > this.maxDataPoints) {
|
|
this.diskHistory.shift();
|
|
}
|
|
if (this.networkHistory.length > this.maxDataPoints) {
|
|
this.networkHistory.shift();
|
|
}
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
var container = E('div', { 'class': 'secubox-monitoring-page' });
|
|
|
|
// Header
|
|
container.appendChild(this.renderHeader());
|
|
|
|
// Charts Grid
|
|
var chartsGrid = E('div', { 'class': 'secubox-charts-grid' }, [
|
|
this.renderChart('cpu', 'CPU Usage', '%'),
|
|
this.renderChart('memory', 'Memory Usage', '%'),
|
|
this.renderChart('disk', 'Disk Usage', '%'),
|
|
this.renderChart('network', 'Network Traffic', 'B/s')
|
|
]);
|
|
|
|
container.appendChild(chartsGrid);
|
|
|
|
// Current Stats Summary
|
|
container.appendChild(this.renderCurrentStats());
|
|
|
|
// Auto-refresh and update charts
|
|
poll.add(function() {
|
|
return self.refreshData().then(function() {
|
|
self.updateCharts();
|
|
self.updateCurrentStats();
|
|
});
|
|
}, 5); // Refresh every 5 seconds for monitoring
|
|
|
|
return container;
|
|
},
|
|
|
|
renderHeader: function() {
|
|
return E('div', { 'class': 'secubox-page-header secubox-monitoring-header' }, [
|
|
E('div', {}, [
|
|
E('h2', {}, '📊 System Monitoring'),
|
|
E('p', { 'class': 'secubox-page-subtitle' },
|
|
'Real-time system performance metrics and historical trends')
|
|
]),
|
|
E('div', { 'class': 'secubox-header-info' }, [
|
|
E('span', { 'class': 'secubox-badge' },
|
|
'⏱️ Refresh: Every 5 seconds'),
|
|
E('span', { 'class': 'secubox-badge' },
|
|
'📈 History: Last ' + this.maxDataPoints + ' points')
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderChart: function(type, title, unit) {
|
|
return E('div', { 'class': 'secubox-chart-card' }, [
|
|
E('h3', { 'class': 'secubox-chart-title' }, title),
|
|
E('div', { 'class': 'secubox-chart-container' }, [
|
|
E('svg', {
|
|
'id': 'chart-' + type,
|
|
'class': 'secubox-chart',
|
|
'viewBox': '0 0 600 200',
|
|
'preserveAspectRatio': 'none'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'secubox-chart-legend' }, [
|
|
E('span', { 'id': 'current-' + type, 'class': 'secubox-current-value' }, '0' + unit),
|
|
E('span', { 'class': 'secubox-chart-unit' }, 'Current')
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderCurrentStats: function() {
|
|
return E('div', { 'class': 'secubox-card' }, [
|
|
E('h3', { 'class': 'secubox-card-title' }, '📋 Current Statistics'),
|
|
E('div', { 'id': 'current-stats', 'class': 'secubox-stats-table' },
|
|
this.renderStatsTable())
|
|
]);
|
|
},
|
|
|
|
renderStatsTable: function() {
|
|
var latest = {
|
|
cpu: this.cpuHistory[this.cpuHistory.length - 1] || { value: 0 },
|
|
memory: this.memoryHistory[this.memoryHistory.length - 1] || { value: 0 },
|
|
disk: this.diskHistory[this.diskHistory.length - 1] || { value: 0 }
|
|
};
|
|
|
|
var stats = [
|
|
{ label: 'CPU Usage', value: latest.cpu.value + '%', icon: '⚡' },
|
|
{ label: 'Memory Usage', value: latest.memory.value + '%', icon: '💾' },
|
|
{ label: 'Disk Usage', value: latest.disk.value + '%', icon: '💿' },
|
|
{ label: 'Data Points', value: this.cpuHistory.length + ' / ' + this.maxDataPoints, icon: '📊' }
|
|
];
|
|
|
|
return E('div', { 'class': 'secubox-stats-grid' },
|
|
stats.map(function(stat) {
|
|
return E('div', { 'class': 'secubox-stat-item' }, [
|
|
E('span', { 'class': 'secubox-stat-icon' }, stat.icon),
|
|
E('div', { 'class': 'secubox-stat-details' }, [
|
|
E('div', { 'class': 'secubox-stat-label' }, stat.label),
|
|
E('div', { 'class': 'secubox-stat-value' }, stat.value)
|
|
])
|
|
]);
|
|
})
|
|
);
|
|
},
|
|
|
|
updateCharts: function() {
|
|
this.drawChart('cpu', this.cpuHistory, '#6366f1');
|
|
this.drawChart('memory', this.memoryHistory, '#22c55e');
|
|
this.drawChart('disk', this.diskHistory, '#f59e0b');
|
|
this.drawNetworkChart();
|
|
},
|
|
|
|
drawChart: function(type, data, color) {
|
|
var svg = document.getElementById('chart-' + type);
|
|
var currentEl = document.getElementById('current-' + type);
|
|
|
|
if (!svg || data.length === 0) return;
|
|
|
|
// Clear previous content
|
|
svg.innerHTML = '';
|
|
|
|
// Dimensions
|
|
var width = 600;
|
|
var height = 200;
|
|
var padding = 10;
|
|
|
|
// Find min/max for scaling
|
|
var maxValue = Math.max(...data.map(d => d.value), 100);
|
|
var minValue = 0;
|
|
|
|
// Create grid lines
|
|
for (var i = 0; i <= 4; i++) {
|
|
var y = height - (height - 2 * padding) * (i / 4) - padding;
|
|
svg.appendChild(E('line', {
|
|
'x1': padding,
|
|
'y1': y,
|
|
'x2': width - padding,
|
|
'y2': y,
|
|
'stroke': '#e5e7eb',
|
|
'stroke-width': '1',
|
|
'stroke-dasharray': '4'
|
|
}));
|
|
}
|
|
|
|
// Create path
|
|
var points = data.map(function(d, i) {
|
|
var x = padding + (width - 2 * padding) * (i / (this.maxDataPoints - 1 || 1));
|
|
var y = height - padding - ((d.value - minValue) / (maxValue - minValue)) * (height - 2 * padding);
|
|
return x + ',' + y;
|
|
}, this).join(' ');
|
|
|
|
// Draw area under the curve
|
|
if (points) {
|
|
var firstPoint = points.split(' ')[0];
|
|
var lastPoint = points.split(' ')[points.split(' ').length - 1];
|
|
var areaPoints = padding + ',' + (height - padding) + ' ' +
|
|
points + ' ' +
|
|
(lastPoint ? lastPoint.split(',')[0] : 0) + ',' + (height - padding);
|
|
|
|
svg.appendChild(E('polygon', {
|
|
'points': areaPoints,
|
|
'fill': color,
|
|
'fill-opacity': '0.1'
|
|
}));
|
|
}
|
|
|
|
// Draw line
|
|
svg.appendChild(E('polyline', {
|
|
'points': points,
|
|
'fill': 'none',
|
|
'stroke': color,
|
|
'stroke-width': '2',
|
|
'stroke-linejoin': 'round',
|
|
'stroke-linecap': 'round'
|
|
}));
|
|
|
|
// Draw dots for last few points
|
|
data.slice(-5).forEach(function(d, i) {
|
|
var idx = data.length - 5 + i;
|
|
if (idx < 0) return;
|
|
var x = padding + (width - 2 * padding) * (idx / (this.maxDataPoints - 1 || 1));
|
|
var y = height - padding - ((d.value - minValue) / (maxValue - minValue)) * (height - 2 * padding);
|
|
|
|
svg.appendChild(E('circle', {
|
|
'cx': x,
|
|
'cy': y,
|
|
'r': '3',
|
|
'fill': color
|
|
}));
|
|
}, this);
|
|
|
|
// Update current value
|
|
if (currentEl && data.length > 0) {
|
|
var unit = type === 'network' ? ' B/s' : '%';
|
|
currentEl.textContent = data[data.length - 1].value.toFixed(1) + unit;
|
|
}
|
|
},
|
|
|
|
drawNetworkChart: function() {
|
|
// For network, we'll draw both RX and TX
|
|
var svg = document.getElementById('chart-network');
|
|
var currentEl = document.getElementById('current-network');
|
|
|
|
if (!svg || this.networkHistory.length === 0) return;
|
|
|
|
svg.innerHTML = '';
|
|
|
|
var width = 600;
|
|
var height = 200;
|
|
var padding = 10;
|
|
|
|
// Calculate rates (bytes per second)
|
|
var rates = [];
|
|
for (var i = 1; i < this.networkHistory.length; i++) {
|
|
var prev = this.networkHistory[i - 1];
|
|
var curr = this.networkHistory[i];
|
|
var timeDiff = (curr.time - prev.time) / 1000; // seconds
|
|
|
|
if (timeDiff > 0) {
|
|
rates.push({
|
|
rx: (curr.rx - prev.rx) / timeDiff,
|
|
tx: (curr.tx - prev.tx) / timeDiff
|
|
});
|
|
}
|
|
}
|
|
|
|
if (rates.length === 0) return;
|
|
|
|
var maxRate = Math.max(
|
|
...rates.map(r => Math.max(r.rx, r.tx)),
|
|
1024 // At least 1KB/s scale
|
|
);
|
|
|
|
// Draw RX line (green)
|
|
var rxPoints = rates.map(function(r, i) {
|
|
var x = padding + (width - 2 * padding) * (i / (rates.length - 1 || 1));
|
|
var y = height - padding - (r.rx / maxRate) * (height - 2 * padding);
|
|
return x + ',' + y;
|
|
}).join(' ');
|
|
|
|
svg.appendChild(E('polyline', {
|
|
'points': rxPoints,
|
|
'fill': 'none',
|
|
'stroke': '#22c55e',
|
|
'stroke-width': '2'
|
|
}));
|
|
|
|
// Draw TX line (blue)
|
|
var txPoints = rates.map(function(r, i) {
|
|
var x = padding + (width - 2 * padding) * (i / (rates.length - 1 || 1));
|
|
var y = height - padding - (r.tx / maxRate) * (height - 2 * padding);
|
|
return x + ',' + y;
|
|
}).join(' ');
|
|
|
|
svg.appendChild(E('polyline', {
|
|
'points': txPoints,
|
|
'fill': 'none',
|
|
'stroke': '#3b82f6',
|
|
'stroke-width': '2'
|
|
}));
|
|
|
|
// Update current value
|
|
if (currentEl && rates.length > 0) {
|
|
var lastRate = rates[rates.length - 1];
|
|
currentEl.textContent = API.formatBytes(lastRate.rx + lastRate.tx) + '/s';
|
|
}
|
|
},
|
|
|
|
updateCurrentStats: function() {
|
|
var container = document.getElementById('current-stats');
|
|
if (container) {
|
|
dom.content(container, this.renderStatsTable());
|
|
}
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|