411 lines
14 KiB
JavaScript
411 lines
14 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'require dom';
|
|
'require poll';
|
|
'require secubox/api as API';
|
|
'require secubox/theme as Theme';
|
|
'require secubox/nav as SecuNav';
|
|
|
|
// Respect LuCI language/theme preferences
|
|
var secuLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
|
Theme.init({ language: secuLang });
|
|
|
|
return view.extend({
|
|
cpuHistory: [],
|
|
memoryHistory: [],
|
|
diskHistory: [],
|
|
networkHistory: [],
|
|
maxDataPoints: 60,
|
|
latestHealth: {},
|
|
|
|
load: function() {
|
|
return this.refreshData();
|
|
},
|
|
|
|
refreshData: function() {
|
|
var self = this;
|
|
return API.getSystemHealth().then(function(data) {
|
|
var health = data || {};
|
|
self.latestHealth = health;
|
|
self.addDataPoint(health);
|
|
return health;
|
|
});
|
|
},
|
|
|
|
addDataPoint: function(health) {
|
|
var timestamp = Date.now();
|
|
|
|
this.cpuHistory.push({ time: timestamp, value: (health.cpu && health.cpu.percent) || 0 });
|
|
this.memoryHistory.push({ time: timestamp, value: (health.memory && health.memory.percent) || 0 });
|
|
this.diskHistory.push({ time: timestamp, value: (health.disk && health.disk.percent) || 0 });
|
|
|
|
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 });
|
|
|
|
['cpuHistory', 'memoryHistory', 'diskHistory', 'networkHistory'].forEach(function(key) {
|
|
if (this[key].length > this.maxDataPoints)
|
|
this[key].shift();
|
|
}, this);
|
|
},
|
|
|
|
render: function() {
|
|
var container = E('div', { 'class': 'secubox-monitoring-page' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/common.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/secubox.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/monitoring.css') }),
|
|
SecuNav.renderTabs('monitoring'),
|
|
this.renderHeader(),
|
|
this.renderHero(),
|
|
this.renderChartsGrid(),
|
|
this.renderCurrentStatsCard()
|
|
]);
|
|
|
|
this.updateCharts();
|
|
this.updateCurrentStats();
|
|
|
|
var self = this;
|
|
poll.add(function() {
|
|
return self.refreshData().then(function() {
|
|
self.updateCharts();
|
|
self.updateCurrentStats();
|
|
});
|
|
}, 5);
|
|
|
|
this.hideLegacyTabMenu();
|
|
|
|
return container;
|
|
},
|
|
|
|
renderHeader: function() {
|
|
var snapshot = this.getLatestSnapshot();
|
|
var rates = this.getNetworkRateSummary();
|
|
|
|
return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [
|
|
E('div', {}, [
|
|
E('h2', { 'class': 'sh-page-title' }, [
|
|
E('span', { 'class': 'sh-page-title-icon' }, '📡'),
|
|
_('SecuBox Monitoring')
|
|
]),
|
|
E('p', { 'class': 'sh-page-subtitle' },
|
|
_('Live telemetry for CPU, memory, storage, and network throughput'))
|
|
]),
|
|
E('div', { 'class': 'sh-header-meta' }, [
|
|
this.renderHeaderChip('cpu', '🔥', _('CPU'), snapshot.cpu.value.toFixed(1) + '%', snapshot.cpu.value),
|
|
this.renderHeaderChip('memory', '💾', _('Memory'), snapshot.memory.value.toFixed(1) + '%', snapshot.memory.value),
|
|
this.renderHeaderChip('disk', '💿', _('Disk'), snapshot.disk.value.toFixed(1) + '%', snapshot.disk.value),
|
|
this.renderHeaderChip('uptime', '⏱', _('Uptime'), API.formatUptime(snapshot.uptime || 0)),
|
|
this.renderHeaderChip('net', '🌐', _('Network'), rates.summary)
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderHeaderChip: function(id, icon, label, value, percent) {
|
|
return E('div', { 'class': 'sh-header-chip' }, [
|
|
E('span', { 'class': 'sh-chip-icon' }, icon),
|
|
E('div', { 'class': 'sh-chip-text' }, [
|
|
E('span', { 'class': 'sh-chip-label' }, label),
|
|
E('strong', { 'id': 'secubox-monitoring-chip-' + id, 'style': (typeof percent === 'number') ? ('color:' + this.getColorForValue(percent)) : '' }, value)
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderHero: function() {
|
|
var snapshot = this.getLatestSnapshot();
|
|
|
|
var badges = [
|
|
this.renderHeroBadge('cpu', '🔥', _('CPU Usage'), snapshot.cpu.value.toFixed(1) + '%', snapshot.cpu.value),
|
|
this.renderHeroBadge('memory', '💾', _('Memory Usage'), snapshot.memory.value.toFixed(1) + '%', snapshot.memory.value),
|
|
this.renderHeroBadge('disk', '💿', _('Disk Usage'), snapshot.disk.value.toFixed(1) + '%', snapshot.disk.value),
|
|
this.renderHeroBadge('uptime', '⏱', _('Uptime'), API.formatUptime(snapshot.uptime || 0))
|
|
];
|
|
|
|
return E('section', { 'class': 'sb-card secubox-monitoring-hero' }, [
|
|
E('div', { 'class': 'sb-card-header' }, [
|
|
E('div', {}, [
|
|
E('h2', {}, _('Advanced System Monitoring')),
|
|
E('p', { 'class': 'sb-card-subtitle' }, _('Live telemetry for CPU, memory, storage, and network throughput'))
|
|
]),
|
|
E('span', { 'class': 'sb-badge sb-badge-ghost' }, _('Last update: ') +
|
|
(snapshot.timestamp ? new Date(snapshot.timestamp).toLocaleTimeString() : _('Initializing')))
|
|
]),
|
|
E('div', { 'class': 'secubox-monitoring-badges', 'id': 'secubox-monitoring-badges' }, badges)
|
|
]);
|
|
},
|
|
|
|
renderHeroBadge: function(id, icon, label, value, percent) {
|
|
var color = typeof percent === 'number' ? this.getColorForValue(percent) : null;
|
|
return E('div', { 'class': 'secubox-hero-badge', 'data-metric': id }, [
|
|
E('div', { 'class': 'secubox-hero-icon' }, icon),
|
|
E('div', { 'class': 'secubox-hero-meta' }, [
|
|
E('span', { 'class': 'secubox-hero-label' }, label),
|
|
E('span', {
|
|
'class': 'secubox-hero-value',
|
|
'id': 'secubox-hero-' + id,
|
|
'style': color ? 'color:' + color : ''
|
|
}, value)
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderChartsGrid: function() {
|
|
return E('section', { 'class': 'secubox-charts-grid' }, [
|
|
this.renderChartCard('cpu', _('CPU Usage'), '%', '#6366f1'),
|
|
this.renderChartCard('memory', _('Memory Usage'), '%', '#22c55e'),
|
|
this.renderChartCard('disk', _('Disk Usage'), '%', '#f59e0b'),
|
|
this.renderChartCard('network', _('Network Throughput'), 'B/s', '#3b82f6')
|
|
]);
|
|
},
|
|
|
|
renderChartCard: function(type, title, unit, accent) {
|
|
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',
|
|
'data-accent': accent
|
|
})
|
|
),
|
|
E('div', { 'class': 'secubox-chart-legend' }, [
|
|
E('span', { 'id': 'current-' + type, 'class': 'secubox-current-value' }, '0' + unit),
|
|
E('span', { 'class': 'secubox-chart-unit' }, _('Live'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderCurrentStatsCard: function() {
|
|
return E('section', { 'class': 'sb-card' }, [
|
|
E('div', { 'class': 'sb-card-header' }, [
|
|
E('h2', {}, _('Current Statistics')),
|
|
E('p', { 'class': 'sb-card-subtitle' }, _('Real-time snapshot of key resources'))
|
|
]),
|
|
E('div', { 'id': 'current-stats', 'class': 'secubox-stats-table' }, this.renderStatsTable())
|
|
]);
|
|
},
|
|
|
|
renderStatsTable: function() {
|
|
var snapshot = this.getLatestSnapshot();
|
|
var rates = this.getNetworkRateSummary();
|
|
var load = (this.latestHealth.load && this.latestHealth.load[0]) || '0.00';
|
|
|
|
var stats = [
|
|
{ label: _('CPU Usage'), value: snapshot.cpu.value.toFixed(1) + '%', icon: '⚡' },
|
|
{ label: _('Memory Usage'), value: snapshot.memory.value.toFixed(1) + '%', icon: '💾' },
|
|
{ label: _('Disk Usage'), value: snapshot.disk.value.toFixed(1) + '%', icon: '💿' },
|
|
{ label: _('System Load (1m)'), value: load, icon: '📈' },
|
|
{ label: _('Network RX/TX'), value: rates.summary, icon: '🌐' },
|
|
{ label: _('Data Window'), value: this.cpuHistory.length + ' / ' + this.maxDataPoints, icon: '🕒' }
|
|
];
|
|
|
|
return 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;
|
|
|
|
var width = 600;
|
|
var height = 200;
|
|
var padding = 12;
|
|
|
|
var values = data.map(function(d) { return d.value; });
|
|
var maxValue = Math.max(100, Math.max.apply(Math, values));
|
|
var minValue = 0;
|
|
var pathPoints = data.map(function(point, idx) {
|
|
var x = padding + (width - 2 * padding) * (idx / Math.max(1, this.maxDataPoints - 1));
|
|
var y = height - padding - ((point.value - minValue) / (maxValue - minValue)) * (height - 2 * padding);
|
|
return x + ',' + y;
|
|
}, this).join(' ');
|
|
|
|
svg.innerHTML = '';
|
|
|
|
svg.appendChild(E('polyline', {
|
|
'points': pathPoints,
|
|
'fill': 'none',
|
|
'stroke': color,
|
|
'stroke-width': '2',
|
|
'stroke-linejoin': 'round',
|
|
'stroke-linecap': 'round'
|
|
}));
|
|
|
|
if (currentEl) {
|
|
currentEl.textContent = (data[data.length - 1].value || 0).toFixed(1) + '%';
|
|
}
|
|
},
|
|
|
|
drawNetworkChart: function() {
|
|
var svg = document.getElementById('chart-network');
|
|
var currentEl = document.getElementById('current-network');
|
|
if (!svg || this.networkHistory.length < 2)
|
|
return;
|
|
|
|
var width = 600;
|
|
var height = 200;
|
|
var padding = 12;
|
|
var rates = this.networkHistory.slice(1).map(function(point, idx) {
|
|
var prev = this.networkHistory[idx];
|
|
var seconds = Math.max(1, (point.time - prev.time) / 1000);
|
|
return {
|
|
time: point.time,
|
|
rx: Math.max(0, (point.rx - prev.rx) / seconds),
|
|
tx: Math.max(0, (point.tx - prev.tx) / seconds)
|
|
};
|
|
}, this);
|
|
|
|
var maxRate = Math.max(1024, Math.max.apply(Math, rates.map(function(r) {
|
|
return Math.max(r.rx, r.tx);
|
|
})));
|
|
|
|
function buildPoints(fn) {
|
|
return rates.map(function(point, idx) {
|
|
var x = padding + (width - 2 * padding) * (idx / Math.max(1, rates.length - 1));
|
|
var y = height - padding - (fn(point) / maxRate) * (height - 2 * padding);
|
|
return x + ',' + y;
|
|
}).join(' ');
|
|
}
|
|
|
|
svg.innerHTML = '';
|
|
svg.appendChild(E('polyline', {
|
|
'points': buildPoints(function(p) { return p.rx; }),
|
|
'fill': 'none',
|
|
'stroke': '#22c55e',
|
|
'stroke-width': '2'
|
|
}));
|
|
svg.appendChild(E('polyline', {
|
|
'points': buildPoints(function(p) { return p.tx; }),
|
|
'fill': 'none',
|
|
'stroke': '#3b82f6',
|
|
'stroke-width': '2'
|
|
}));
|
|
|
|
if (currentEl) {
|
|
var last = rates[rates.length - 1];
|
|
currentEl.textContent = API.formatBytes(last.rx + last.tx) + '/s';
|
|
}
|
|
},
|
|
|
|
updateCurrentStats: function() {
|
|
var statsContainer = document.getElementById('current-stats');
|
|
if (statsContainer)
|
|
dom.content(statsContainer, this.renderStatsTable());
|
|
|
|
var snapshot = this.getLatestSnapshot();
|
|
this.updateHeroBadges(snapshot);
|
|
this.updateHeaderChips(snapshot);
|
|
},
|
|
|
|
updateHeroBadges: function(snapshot) {
|
|
var badges = document.querySelectorAll('.secubox-hero-badge');
|
|
if (!badges.length)
|
|
return;
|
|
|
|
var values = {
|
|
cpu: snapshot.cpu.value.toFixed(1) + '%',
|
|
memory: snapshot.memory.value.toFixed(1) + '%',
|
|
disk: snapshot.disk.value.toFixed(1) + '%',
|
|
uptime: API.formatUptime(snapshot.uptime || 0)
|
|
};
|
|
|
|
Object.keys(values).forEach(function(key) {
|
|
var target = document.getElementById('secubox-hero-' + key);
|
|
if (target) {
|
|
target.textContent = values[key];
|
|
if (key !== 'uptime') {
|
|
var numeric = snapshot[key] && snapshot[key].value || 0;
|
|
target.style.color = this.getColorForValue(numeric);
|
|
}
|
|
}
|
|
}, this);
|
|
},
|
|
|
|
updateHeaderChips: function(snapshot) {
|
|
this.setHeaderChipValue('cpu', snapshot.cpu.value.toFixed(1) + '%', snapshot.cpu.value);
|
|
this.setHeaderChipValue('memory', snapshot.memory.value.toFixed(1) + '%', snapshot.memory.value);
|
|
this.setHeaderChipValue('disk', snapshot.disk.value.toFixed(1) + '%', snapshot.disk.value);
|
|
this.setHeaderChipValue('uptime', API.formatUptime(snapshot.uptime || 0));
|
|
var rates = this.getNetworkRateSummary();
|
|
this.setHeaderChipValue('net', rates.summary);
|
|
},
|
|
|
|
setHeaderChipValue: function(id, text, percent) {
|
|
var target = document.getElementById('secubox-monitoring-chip-' + id);
|
|
if (!target)
|
|
return;
|
|
|
|
target.textContent = text;
|
|
if (typeof percent === 'number')
|
|
target.style.color = this.getColorForValue(percent);
|
|
else
|
|
target.style.color = '';
|
|
},
|
|
|
|
hideLegacyTabMenu: function() {
|
|
window.requestAnimationFrame(function() {
|
|
var menus = document.querySelectorAll('.main > .tabmenu, .main > .cbi-tabmenu');
|
|
menus.forEach(function(menu) {
|
|
menu.style.display = 'none';
|
|
});
|
|
});
|
|
},
|
|
|
|
getLatestSnapshot: function() {
|
|
return {
|
|
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 },
|
|
uptime: (this.latestHealth && this.latestHealth.uptime) || 0,
|
|
timestamp: (this.latestHealth && this.latestHealth.timestamp) || Date.now()
|
|
};
|
|
},
|
|
|
|
getNetworkRateSummary: function() {
|
|
if (this.networkHistory.length < 2)
|
|
return { summary: '0 B/s' };
|
|
|
|
var last = this.networkHistory[this.networkHistory.length - 1];
|
|
var prev = this.networkHistory[this.networkHistory.length - 2];
|
|
var seconds = Math.max(1, (last.time - prev.time) / 1000);
|
|
var rx = Math.max(0, (last.rx - prev.rx) / seconds);
|
|
var tx = Math.max(0, (last.tx - prev.tx) / seconds);
|
|
|
|
return {
|
|
summary: API.formatBytes(rx) + '/s ↓ · ' + API.formatBytes(tx) + '/s ↑'
|
|
};
|
|
},
|
|
|
|
getColorForValue: function(value) {
|
|
if (value >= 90) return '#ef4444';
|
|
if (value >= 75) return '#f59e0b';
|
|
if (value >= 50) return '#3b82f6';
|
|
return '#22c55e';
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|