secubox-openwrt/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js
CyberMind-FR 5902ac500a release: v0.1.1 - Unified Theme System with Dark/Light Mode Support
Major Features:
• Centralized theme system across SecuBox and System Hub
• Three theme modes: dark (default), light, and system (auto-detect)
• Single theme setting in SecuBox controls both plugins
• Real-time theme switching with OS preference detection

SecuBox Changes:
• Added theme.js manager for centralized theme control
• Implemented CSS variables for dark/light mode (secubox.css)
• Fixed hardcoded colors in dashboard.css, alerts.css, monitoring.css
• Integrated theme.js in all 7 views (dashboard, modules, alerts, monitoring, settings, etc.)
• Added get_theme RPC method to luci.secubox backend
• Updated ACL permissions to include get_theme (read access)
• Version updated to 0.1.1

System Hub Changes:
• Added theme.js manager using SecuBox theme API
• Implemented CSS variables for dark/light mode (dashboard.css)
• Integrated theme.js in all 9 views (overview, health, services, logs, backup, components, remote, settings, diagnostics)
• Version updated to 0.1.1
• README updated with maintainer info

Theme System Architecture:
• Configuration: /etc/config/secubox (option theme: dark|light|system)
• RPCD Backend: luci.secubox/get_theme method
• Frontend: theme.js modules (secubox/theme.js, system-hub/theme.js)
• CSS Variables: --sb-bg, --sb-bg-card, --sb-border, --sb-text, --sb-text-muted, --sb-shadow
• Auto-detection: prefers-color-scheme media query for system mode

Documentation:
• Added LUCI_DEVELOPMENT_REFERENCE.md with comprehensive LuCI development patterns
• Documented ubus/RPC types, baseclass.extend() patterns, ACL structure
• Common errors and solutions from implementation experience

Bug Fixes:
• Fixed SecuBox theme not applying visually (CSS variables now used)
• Fixed missing secubox.css in view imports
• Fixed ACL access denied for get_theme method
• Fixed hardcoded colors preventing theme switching

Testing:
• Verified theme switching works in all SecuBox tabs
• Verified theme switching works in all System Hub tabs
• Verified dark/light/system modes function correctly
• Verified single setting controls both plugins

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 16:09:15 +01:00

235 lines
7.5 KiB
JavaScript

'use strict';
'require view';
'require poll';
'require system-hub/api as API';
'require system-hub/theme as Theme';
// Load CSS
document.head.appendChild(E('link', {
'rel': 'stylesheet',
'type': 'text/css',
'href': L.resource('system-hub/dashboard.css')
}));
// Initialize theme
Theme.init();
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.cpu ? health.cpu.load_1m : '0');
var cpuPercent = health.cpu ? health.cpu.usage : 0;
gaugesContainer.appendChild(this.createGauge('CPU Load', cpuPercent, cpuLoad.toFixed(2)));
// Memory Gauge
var memPercent = health.memory ? health.memory.usage : 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 = health.disk ? health.disk.usage : 0;
var diskUsed = health.disk ? (health.disk.used_kb / 1024).toFixed(0) : 0;
var diskTotal = health.disk ? (health.disk.total_kb / 1024).toFixed(0) : 0;
var diskInfo = diskUsed + ' / ' + diskTotal + ' MB';
gaugesContainer.appendChild(this.createGauge('Disk Usage', diskPercent, diskInfo));
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', {}, _('Cores: ')),
E('span', {}, String(health.cpu.cores))
]),
E('div', { 'class': 'td left', 'width': '50%' }, [
E('strong', {}, _('Usage: ')),
E('span', {}, health.cpu.usage + '%')
])
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left' }, [
E('strong', {}, _('Load Average: ')),
E('span', {}, (health.cpu.load_1m + ' / ' + health.cpu.load_5m + ' / ' + health.cpu.load_15m))
])
])
])
]);
v.appendChild(cpuSection);
}
// Temperature
if (health.temperature && health.temperature.value > 0) {
var tempValue = health.temperature.value;
var tempColor = tempValue > 80 ? 'red' : (tempValue > 60 ? 'orange' : 'green');
var tempSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Temperature')),
E('div', { 'class': 'table' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left' }, [
E('strong', {}, _('System Temperature: ')),
E('span', { 'style': 'color: ' + tempColor + '; font-weight: bold;' }, tempValue + '°C')
])
])
])
]);
v.appendChild(tempSection);
}
// Storage (Root Filesystem)
if (health.disk) {
var diskColor = health.disk.usage > 90 ? 'red' : (health.disk.usage > 75 ? 'orange' : 'green');
var storageSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Storage (Root Filesystem)')),
E('div', { 'class': 'table' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left', 'width': '50%' }, [
E('strong', {}, _('Total: ')),
E('span', {}, (health.disk.total_kb / 1024).toFixed(0) + ' MB')
]),
E('div', { 'class': 'td left', 'width': '50%' }, [
E('strong', {}, _('Used: ')),
E('span', {}, (health.disk.used_kb / 1024).toFixed(0) + ' MB')
])
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left' }, [
E('strong', {}, _('Usage: ')),
E('div', { 'style': 'display: inline-flex; align-items: center; width: 200px;' }, [
E('div', { 'style': 'flex: 1; background: #eee; height: 10px; border-radius: 5px; margin-right: 10px;' }, [
E('div', {
'style': 'background: ' + diskColor + '; width: ' + health.disk.usage + '%; height: 100%; border-radius: 5px;'
})
]),
E('span', { 'style': 'font-weight: bold; color: ' + diskColor }, health.disk.usage + '%')
])
])
])
])
]);
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
});