Implements comprehensive system control and monitoring dashboard with health metrics, service management, system logs, and backup/restore functionality. Features: - Real-time system monitoring with visual gauges (CPU, RAM, Disk) - Comprehensive system information (hostname, model, uptime, kernel) - Health metrics with temperature monitoring and storage breakdown - Service management with start/stop/restart/enable/disable actions - System log viewer with filtering and configurable line count - Configuration backup creation and download (base64 encoded) - Configuration restore from backup file - System reboot functionality with confirmation Components: - RPCD backend (luci.system-hub): 10 ubus methods * status, get_system_info, get_health * list_services, service_action * get_logs, backup_config, restore_config * reboot, get_storage - 4 JavaScript views: overview, services, logs, backup - ACL with read/write permissions segregation - Comprehensive README with API documentation Technical implementation: - System info from /proc filesystem and sysinfo - Health metrics: CPU load, memory breakdown, disk usage, temperature - Service control via /etc/init.d scripts - Log retrieval via logread with filtering - Backup/restore using sysupgrade with base64 encoding - Visual gauges with SVG circular progress indicators - Color-coded health status (green/orange/red) Dashboard Features: - Circular gauges for CPU, Memory, Disk (120px with 10px stroke) - System information cards with detailed metrics - Temperature monitoring with thermal zone detection - Storage table for all mount points with progress bars - Service table with inline action buttons - Terminal-style log display (black bg, green text) - File upload for backup restore - Modal confirmations for destructive actions Architecture follows SecuBox standards: - RPCD naming convention (luci. prefix) - Menu paths match view file structure - All JavaScript in strict mode - Form-based configuration management - Comprehensive error handling Dependencies: coreutils, coreutils-base64 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
246 lines
8.0 KiB
JavaScript
246 lines
8.0 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require poll';
|
|
'require system-hub/api as API';
|
|
|
|
return L.view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
API.getSystemInfo(),
|
|
API.getHealth(),
|
|
API.getStatus()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var sysInfo = data[0] || {};
|
|
var health = data[1] || {};
|
|
var status = data[2] || {};
|
|
|
|
var v = E('div', { 'class': 'cbi-map' }, [
|
|
E('h2', {}, _('System Hub - Overview')),
|
|
E('div', { 'class': 'cbi-map-descr' }, _('Central system control and monitoring'))
|
|
]);
|
|
|
|
// System Information Card
|
|
var infoSection = E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('System Information')),
|
|
E('div', { 'class': 'table' }, [
|
|
E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
|
E('strong', {}, _('Hostname: ')),
|
|
E('span', {}, sysInfo.hostname || 'unknown')
|
|
]),
|
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
|
E('strong', {}, _('Model: ')),
|
|
E('span', {}, sysInfo.model || 'Unknown')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
|
E('strong', {}, _('OpenWrt: ')),
|
|
E('span', {}, sysInfo.openwrt_version || 'Unknown')
|
|
]),
|
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
|
E('strong', {}, _('Kernel: ')),
|
|
E('span', {}, sysInfo.kernel || 'unknown')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
|
E('strong', {}, _('Uptime: ')),
|
|
E('span', {}, sysInfo.uptime_formatted || '0d 0h 0m')
|
|
]),
|
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
|
E('strong', {}, _('Local Time: ')),
|
|
E('span', {}, sysInfo.local_time || 'unknown')
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
v.appendChild(infoSection);
|
|
|
|
// Health Metrics with Gauges
|
|
var healthSection = E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('System Health'))
|
|
]);
|
|
|
|
var gaugesContainer = E('div', { 'style': 'display: flex; justify-content: space-around; flex-wrap: wrap; margin: 20px 0;' });
|
|
|
|
// CPU Load Gauge
|
|
var cpuLoad = parseFloat(health.load ? health.load['1min'] : status.health ? status.health.cpu_load : '0');
|
|
var cpuPercent = Math.min((cpuLoad * 100 / (health.cpu ? health.cpu.cores : 1)), 100);
|
|
gaugesContainer.appendChild(this.createGauge('CPU Load', cpuPercent, cpuLoad.toFixed(2)));
|
|
|
|
// Memory Gauge
|
|
var memPercent = health.memory ? health.memory.percent : (status.health ? status.health.mem_percent : 0);
|
|
var memUsed = health.memory ? (health.memory.used_kb / 1024).toFixed(0) : 0;
|
|
var memTotal = health.memory ? (health.memory.total_kb / 1024).toFixed(0) : 0;
|
|
gaugesContainer.appendChild(this.createGauge('Memory', memPercent, memUsed + ' / ' + memTotal + ' MB'));
|
|
|
|
// Disk Gauge
|
|
var diskPercent = status.disk_percent || 0;
|
|
var diskInfo = '';
|
|
if (health.storage && health.storage.length > 0) {
|
|
var root = health.storage.find(function(s) { return s.mountpoint === '/'; });
|
|
if (root) {
|
|
diskPercent = root.percent;
|
|
diskInfo = root.used + ' / ' + root.size;
|
|
}
|
|
}
|
|
gaugesContainer.appendChild(this.createGauge('Disk Usage', diskPercent, diskInfo || diskPercent + '%'));
|
|
|
|
healthSection.appendChild(gaugesContainer);
|
|
v.appendChild(healthSection);
|
|
|
|
// CPU Info
|
|
if (health.cpu) {
|
|
var cpuSection = E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('CPU Information')),
|
|
E('div', { 'class': 'table' }, [
|
|
E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
|
E('strong', {}, _('Model: ')),
|
|
E('span', {}, health.cpu.model)
|
|
]),
|
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
|
E('strong', {}, _('Cores: ')),
|
|
E('span', {}, String(health.cpu.cores))
|
|
])
|
|
]),
|
|
E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td left' }, [
|
|
E('strong', {}, _('Load Average: ')),
|
|
E('span', {}, (health.load ? health.load['1min'] + ' / ' + health.load['5min'] + ' / ' + health.load['15min'] : 'N/A'))
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
v.appendChild(cpuSection);
|
|
}
|
|
|
|
// Temperature
|
|
if (health.temperatures && health.temperatures.length > 0) {
|
|
var tempSection = E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Temperature'))
|
|
]);
|
|
|
|
var tempTable = E('table', { 'class': 'table' }, [
|
|
E('tr', { 'class': 'tr table-titles' }, [
|
|
E('th', { 'class': 'th' }, _('Zone')),
|
|
E('th', { 'class': 'th' }, _('Temperature'))
|
|
])
|
|
]);
|
|
|
|
health.temperatures.forEach(function(temp) {
|
|
var color = temp.celsius > 80 ? 'red' : (temp.celsius > 60 ? 'orange' : 'green');
|
|
tempTable.appendChild(E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, temp.zone),
|
|
E('td', { 'class': 'td' }, [
|
|
E('span', { 'style': 'color: ' + color + '; font-weight: bold;' }, temp.celsius + '°C')
|
|
])
|
|
]));
|
|
});
|
|
|
|
tempSection.appendChild(tempTable);
|
|
v.appendChild(tempSection);
|
|
}
|
|
|
|
// Storage
|
|
if (health.storage && health.storage.length > 0) {
|
|
var storageSection = E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Storage'))
|
|
]);
|
|
|
|
var storageTable = E('table', { 'class': 'table' }, [
|
|
E('tr', { 'class': 'tr table-titles' }, [
|
|
E('th', { 'class': 'th' }, _('Mountpoint')),
|
|
E('th', { 'class': 'th' }, _('Filesystem')),
|
|
E('th', { 'class': 'th' }, _('Size')),
|
|
E('th', { 'class': 'th' }, _('Used')),
|
|
E('th', { 'class': 'th' }, _('Available')),
|
|
E('th', { 'class': 'th' }, _('Use %'))
|
|
])
|
|
]);
|
|
|
|
health.storage.forEach(function(storage) {
|
|
var color = storage.percent > 90 ? 'red' : (storage.percent > 75 ? 'orange' : 'green');
|
|
storageTable.appendChild(E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, E('strong', {}, storage.mountpoint)),
|
|
E('td', { 'class': 'td' }, E('code', {}, storage.filesystem)),
|
|
E('td', { 'class': 'td' }, storage.size),
|
|
E('td', { 'class': 'td' }, storage.used),
|
|
E('td', { 'class': 'td' }, storage.available),
|
|
E('td', { 'class': 'td' }, [
|
|
E('div', { 'style': 'display: flex; align-items: center;' }, [
|
|
E('div', { 'style': 'flex: 1; background: #eee; height: 10px; border-radius: 5px; margin-right: 10px;' }, [
|
|
E('div', {
|
|
'style': 'background: ' + color + '; width: ' + storage.percent + '%; height: 100%; border-radius: 5px;'
|
|
})
|
|
]),
|
|
E('span', {}, storage.percent + '%')
|
|
])
|
|
])
|
|
]));
|
|
});
|
|
|
|
storageSection.appendChild(storageTable);
|
|
v.appendChild(storageSection);
|
|
}
|
|
|
|
// Auto-refresh every 5 seconds
|
|
poll.add(L.bind(function() {
|
|
return Promise.all([
|
|
API.getHealth(),
|
|
API.getStatus()
|
|
]).then(L.bind(function(refreshData) {
|
|
// Update would go here in a production implementation
|
|
}, this));
|
|
}, this), 5);
|
|
|
|
return v;
|
|
},
|
|
|
|
createGauge: function(label, percent, detail) {
|
|
var color = percent > 90 ? '#dc3545' : (percent > 75 ? '#fd7e14' : '#28a745');
|
|
var size = 120;
|
|
var strokeWidth = 10;
|
|
var radius = (size - strokeWidth) / 2;
|
|
var circumference = 2 * Math.PI * radius;
|
|
var offset = circumference - (percent / 100 * circumference);
|
|
|
|
return E('div', { 'style': 'text-align: center; margin: 10px;' }, [
|
|
E('div', {}, [
|
|
E('svg', { 'width': size, 'height': size, 'style': 'transform: rotate(-90deg);' }, [
|
|
E('circle', {
|
|
'cx': size/2,
|
|
'cy': size/2,
|
|
'r': radius,
|
|
'fill': 'none',
|
|
'stroke': '#eee',
|
|
'stroke-width': strokeWidth
|
|
}),
|
|
E('circle', {
|
|
'cx': size/2,
|
|
'cy': size/2,
|
|
'r': radius,
|
|
'fill': 'none',
|
|
'stroke': color,
|
|
'stroke-width': strokeWidth,
|
|
'stroke-dasharray': circumference,
|
|
'stroke-dashoffset': offset,
|
|
'stroke-linecap': 'round'
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'style': 'margin-top: -' + (size/2 + 10) + 'px; font-size: 20px; font-weight: bold; color: ' + color + ';' }, Math.round(percent) + '%'),
|
|
E('div', { 'style': 'margin-top: ' + (size/2 - 10) + 'px; font-weight: bold;' }, label),
|
|
E('div', { 'style': 'font-size: 12px; color: #666;' }, detail)
|
|
]);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|