secubox-openwrt/package/secubox/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js
CyberMind-FR ce9c42bc37 feat(dashboard): Add LAN, BR-WAN, and public IPs to network panel
- Update RPCD get_public_ips to return 4 IP fields:
  - lan_ipv4: br-lan interface IP
  - wan_ipv4: br-wan interface IP
  - public_ipv4: Real public IP (cached from ipify.org)
  - public_ipv6: Global IPv6 from br-wan
- Update dashboard to display 4-column IP grid with icons
- Add responsive CSS for 2x2 layout on small screens

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-12 06:17:27 +01:00

593 lines
22 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require poll';
'require ui';
'require fs';
'require secubox/kiss-theme';
/**
* SecuBox Dashboard - KISS Edition
* Self-contained with inline CSS, no external dependencies
*/
var callSystemBoard = rpc.declare({ object: 'system', method: 'board', expect: {} });
var callSystemInfo = rpc.declare({ object: 'system', method: 'info', expect: {} });
var callDashboardData = rpc.declare({
object: 'luci.secubox',
method: 'get_dashboard_data',
expect: {}
});
var callSystemHealth = rpc.declare({
object: 'luci.secubox',
method: 'get_system_health',
expect: {}
});
var callGetModules = rpc.declare({
object: 'luci.secubox',
method: 'getModules',
expect: {}
});
var callGetAlerts = rpc.declare({
object: 'luci.secubox',
method: 'get_alerts',
expect: {}
});
var callPublicIPs = rpc.declare({
object: 'luci.secubox',
method: 'get_public_ips',
expect: {}
});
// Utilities
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function formatUptime(seconds) {
if (!seconds) return '0m';
var d = Math.floor(seconds / 86400);
var h = Math.floor((seconds % 86400) / 3600);
var m = Math.floor((seconds % 3600) / 60);
if (d > 0) return d + 'd ' + h + 'h';
if (h > 0) return h + 'h ' + m + 'm';
return m + 'm';
}
function getBarColor(percent) {
if (percent >= 90) return '#ef4444';
if (percent >= 70) return '#f59e0b';
return '#22c55e';
}
function getScoreColor(score) {
if (score >= 80) return '#22c55e';
if (score >= 60) return '#3b82f6';
if (score >= 40) return '#f59e0b';
return '#ef4444';
}
return view.extend({
data: {
dashboard: {},
health: {},
modules: [],
alerts: [],
publicIPs: {},
board: {},
info: {}
},
load: function() {
var self = this;
return Promise.all([
callSystemBoard().catch(function() { return {}; }),
callSystemInfo().catch(function() { return {}; }),
callDashboardData().catch(function() { return {}; }),
callSystemHealth().catch(function() { return {}; }),
callGetModules().catch(function() { return { modules: [] }; }),
callGetAlerts().catch(function() { return { alerts: [] }; }),
callPublicIPs().catch(function() { return {}; })
]).then(function(results) {
self.data = {
board: results[0] || {},
info: results[1] || {},
dashboard: results[2] || {},
health: results[3] || {},
modules: (results[4] && results[4].modules) || [],
alerts: (results[5] && results[5].alerts) || [],
publicIPs: results[6] || {}
};
return self.data;
});
},
renderHeader: function() {
var d = this.data.dashboard || {};
var status = d.status || {};
var counts = d.counts || {};
var health = this.data.health || {};
var score = (health.overall && health.overall.score) || health.score || 0;
var modules = this.data.modules || [];
var running = modules.filter(function(m) { return m.status === 'active' || m.running; }).length;
var alertCount = (this.data.alerts || []).length;
var chips = [
{ icon: '\uD83C\uDFF7', label: 'Version', value: status.version || '0.0.0' },
{ icon: '\uD83D\uDCE6', label: 'Modules', value: modules.length },
{ icon: '\uD83D\uDFE2', label: 'Running', value: running, color: running > 0 ? '#22c55e' : '' },
{ icon: '\u26A0\uFE0F', label: 'Alerts', value: alertCount, color: alertCount > 0 ? '#f59e0b' : '' },
{ icon: '\u2764\uFE0F', label: 'Health', value: score + '/100', color: getScoreColor(score) }
];
return E('div', { 'class': 'sb-header' }, [
E('div', {}, [
E('h2', { 'class': 'sb-title' }, '\uD83D\uDE80 SecuBox Control Center'),
E('p', { 'class': 'sb-subtitle' }, 'Security \u00B7 Network \u00B7 System automation')
]),
E('div', { 'class': 'sb-chips' }, chips.map(function(chip) {
return E('div', { 'class': 'sb-chip', 'style': chip.color ? 'border-color:' + chip.color : '' }, [
E('span', { 'class': 'sb-chip-icon' }, chip.icon),
E('div', {}, [
E('span', { 'class': 'sb-chip-label' }, chip.label),
E('strong', { 'style': chip.color ? 'color:' + chip.color : '' }, String(chip.value))
])
]);
}))
]);
},
renderStatsCards: function() {
var d = this.data.dashboard || {};
var counts = d.counts || {};
var modules = this.data.modules || [];
var health = this.data.health || {};
var score = (health.overall && health.overall.score) || health.score || 0;
var alertCount = (this.data.alerts || []).length;
var running = modules.filter(function(m) { return m.status === 'active' || m.running; }).length;
var installed = modules.filter(function(m) { return m.installed || m.enabled; }).length;
var stats = [
{ id: 'total', icon: '\uD83D\uDCE6', label: 'Total Modules', value: counts.total || modules.length },
{ id: 'installed', icon: '\uD83D\uDCBE', label: 'Installed', value: counts.installed || installed },
{ id: 'running', icon: '\uD83D\uDFE2', label: 'Active', value: counts.running || running },
{ id: 'health', icon: '\u2764\uFE0F', label: 'Health Score', value: score + '/100' },
{ id: 'alerts', icon: '\u26A0\uFE0F', label: 'Alerts', value: alertCount }
];
return E('div', { 'class': 'sb-stats' }, stats.map(function(stat) {
return E('div', { 'class': 'sb-stat-card' }, [
E('div', { 'class': 'sb-stat-icon' }, stat.icon),
E('div', {}, [
E('div', { 'class': 'sb-stat-value', 'data-stat': stat.id }, String(stat.value)),
E('div', { 'class': 'sb-stat-label' }, stat.label)
])
]);
}));
},
renderHealthPanel: function() {
var health = this.data.health || {};
var cpu = health.cpu || {};
var memory = health.memory || {};
var disk = health.disk || {};
var network = health.network || {};
// Extract CPU load (handle both "1.83 1.95 1.69" string and load_1m field)
var cpuLoad = cpu.load_1min || cpu.load_1m || '0.00';
if (cpu.load && typeof cpu.load === 'string') {
cpuLoad = cpu.load.split(' ')[0] || '0.00';
}
var metrics = [
{
id: 'cpu',
icon: '\uD83D\uDD25',
label: 'CPU',
percent: cpu.percent || cpu.usage_percent || cpu.usage || 0,
detail: 'Load: ' + cpuLoad
},
{
id: 'memory',
icon: '\uD83D\uDCBE',
label: 'Memory',
percent: memory.percent || memory.usage_percent || memory.usage || 0,
detail: formatBytes((memory.used_kb || 0) * 1024) + ' / ' + formatBytes((memory.total_kb || 0) * 1024)
},
{
id: 'disk',
icon: '\uD83D\uDCBF',
label: 'Storage',
percent: disk.percent || disk.usage_percent || disk.usage || 0,
detail: formatBytes((disk.used_kb || 0) * 1024) + ' / ' + formatBytes((disk.total_kb || 0) * 1024)
},
{
id: 'network',
icon: '\uD83C\uDF10',
label: 'Network',
percent: network.wan_up ? 100 : 0,
detail: network.wan_up ? 'WAN Online' : 'WAN Offline'
}
];
return E('div', { 'class': 'sb-panel' }, [
E('h3', {}, '\uD83D\uDCCA System Health'),
E('div', { 'class': 'sb-health-metrics' }, metrics.map(function(m) {
var pct = Math.min(100, Math.max(0, m.percent));
return E('div', { 'class': 'sb-metric' }, [
E('div', { 'class': 'sb-metric-header' }, [
E('span', {}, m.icon + ' ' + m.label),
E('span', { 'data-stat': m.id + '-detail' }, m.detail)
]),
E('div', { 'class': 'sb-bar' }, [
E('div', {
'class': 'sb-bar-fill',
'data-stat': m.id + '-bar',
'style': 'width:' + pct + '%;background:' + getBarColor(pct)
})
]),
E('span', { 'class': 'sb-metric-pct', 'data-stat': m.id + '-pct' }, pct + '%')
]);
}))
]);
},
renderPublicIPsPanel: function() {
var ips = this.data.publicIPs || {};
var lanIpv4 = ips.lan_ipv4 || 'N/A';
var wanIpv4 = ips.wan_ipv4 || 'N/A';
var publicIpv4 = ips.public_ipv4 || 'N/A';
var publicIpv6 = ips.public_ipv6 || 'N/A';
var ipv6Display = publicIpv6.length > 20 ? publicIpv6.substring(0, 17) + '...' : publicIpv6;
return E('div', { 'class': 'sb-panel' }, [
E('h3', {}, '\uD83C\uDF10 Network Addresses'),
E('div', { 'class': 'sb-ip-grid sb-ip-grid-4' }, [
E('div', { 'class': 'sb-ip-box' }, [
E('div', { 'class': 'sb-ip-icon' }, '\uD83C\uDFE0'),
E('div', { 'class': 'sb-ip-label' }, 'LAN'),
E('div', {
'class': 'sb-ip-value',
'data-stat': 'lan-ipv4',
'title': lanIpv4,
'style': 'color:' + (ips.lan_available ? '#22c55e' : '#888')
}, lanIpv4)
]),
E('div', { 'class': 'sb-ip-box' }, [
E('div', { 'class': 'sb-ip-icon' }, '\uD83D\uDD17'),
E('div', { 'class': 'sb-ip-label' }, 'BR-WAN'),
E('div', {
'class': 'sb-ip-value',
'data-stat': 'wan-ipv4',
'title': wanIpv4,
'style': 'color:' + (ips.wan_available ? '#3b82f6' : '#888')
}, wanIpv4)
]),
E('div', { 'class': 'sb-ip-box' }, [
E('div', { 'class': 'sb-ip-icon' }, '\uD83C\uDF10'),
E('div', { 'class': 'sb-ip-label' }, 'Public IPv4'),
E('div', {
'class': 'sb-ip-value',
'data-stat': 'public-ipv4',
'title': publicIpv4,
'style': 'color:' + (ips.ipv4_available ? '#22c55e' : '#888')
}, publicIpv4)
]),
E('div', { 'class': 'sb-ip-box' }, [
E('div', { 'class': 'sb-ip-icon' }, '\uD83D\uDD37'),
E('div', { 'class': 'sb-ip-label' }, 'Public IPv6'),
E('div', {
'class': 'sb-ip-value',
'data-stat': 'public-ipv6',
'title': publicIpv6,
'style': 'color:' + (ips.ipv6_available ? '#22c55e' : '#888')
}, ipv6Display)
])
])
]);
},
renderModulesPanel: function() {
var modules = this.data.modules || [];
var topModules = modules.slice(0, 8);
var rows = topModules.map(function(mod) {
var status = mod.status || 'unknown';
var isActive = status === 'active' || mod.running;
return E('tr', {}, [
E('td', {}, [
E('span', { 'class': 'sb-mod-icon' }, mod.icon || '\uD83D\uDCE6'),
mod.name || mod.id
]),
E('td', {}, mod.category || '-'),
E('td', {}, E('span', {
'class': 'sb-badge',
'style': 'background:' + (isActive ? '#22c55e' : '#6b7280')
}, isActive ? 'Active' : 'Inactive')),
E('td', {}, mod.version || '-')
]);
});
if (rows.length === 0) {
rows.push(E('tr', {}, [
E('td', { 'colspan': '4', 'style': 'text-align:center;color:#888' }, 'No modules found')
]));
}
return E('div', { 'class': 'sb-panel sb-panel-wide' }, [
E('div', { 'class': 'sb-panel-header' }, [
E('h3', {}, '\uD83D\uDCE6 Modules Overview'),
E('button', {
'class': 'sb-link-btn',
'click': function() { window.location.href = L.url('admin/secubox/modules'); }
}, 'View All \u2192')
]),
E('table', { 'class': 'sb-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, 'Module'),
E('th', {}, 'Category'),
E('th', {}, 'Status'),
E('th', {}, 'Version')
])),
E('tbody', {}, rows)
])
]);
},
renderQuickActions: function() {
var self = this;
var actions = [
{ id: 'restart_services', icon: '\uD83D\uDD04', label: 'Restart Services', color: '#f59e0b' },
{ id: 'update_packages', icon: '\u2B06\uFE0F', label: 'Update Packages', color: '#3b82f6' },
{ id: 'view_logs', icon: '\uD83D\uDCDC', label: 'View Logs', color: '#8b5cf6' },
{ id: 'export_config', icon: '\uD83D\uDCE6', label: 'Export Config', color: '#22c55e' }
];
return E('div', { 'class': 'sb-panel' }, [
E('h3', {}, '\u26A1 Quick Actions'),
E('div', { 'class': 'sb-actions' }, actions.map(function(action) {
return E('button', {
'class': 'sb-action-btn',
'style': 'border-color:' + action.color + ';color:' + action.color,
'click': function() { self.runAction(action.id); }
}, [
E('span', {}, action.icon),
E('span', {}, action.label)
]);
}))
]);
},
renderAlertsPanel: function() {
var alerts = (this.data.alerts || []).slice(0, 5);
var items = alerts.map(function(alert) {
var sev = (alert.severity || 'info').toLowerCase();
var color = sev === 'critical' ? '#ef4444' : sev === 'warning' ? '#f59e0b' : '#3b82f6';
return E('div', { 'class': 'sb-alert-item', 'style': 'border-left-color:' + color }, [
E('div', { 'class': 'sb-alert-time' }, alert.timestamp || alert.time || ''),
E('div', {}, [
E('strong', {}, alert.title || alert.message || 'Alert'),
alert.source ? E('span', { 'class': 'sb-alert-source' }, ' \u2022 ' + alert.source) : ''
])
]);
});
if (items.length === 0) {
items.push(E('div', { 'class': 'sb-empty' }, [
E('span', {}, '\uD83C\uDF89'),
E('p', {}, 'No alerts in the last 24 hours')
]));
}
return E('div', { 'class': 'sb-panel' }, [
E('h3', {}, '\u26A0\uFE0F Alert Timeline'),
E('div', { 'class': 'sb-alerts-list' }, items)
]);
},
runAction: function(actionId) {
ui.showModal('Executing...', [
E('p', { 'class': 'spinning' }, 'Running ' + actionId + '...')
]);
return rpc.declare({
object: 'luci.secubox',
method: 'quick_action',
params: ['action'],
expect: {}
})(actionId).then(function(result) {
ui.hideModal();
ui.addNotification(null, E('p', {}, 'Action completed'), 'info');
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, 'Action failed: ' + (err.message || err)), 'error');
});
},
render: function() {
var self = this;
// Start polling
poll.add(function() {
return Promise.all([
callSystemHealth().catch(function() { return {}; }),
callGetAlerts().catch(function() { return { alerts: [] }; }),
callPublicIPs().catch(function() { return {}; })
]).then(function(results) {
self.data.health = results[0] || {};
self.data.alerts = (results[1] && results[1].alerts) || [];
self.data.publicIPs = results[2] || {};
self.updateLiveData();
});
}, 15);
var content = E('div', { 'class': 'sb-dashboard' }, [
E('style', {}, this.getStyles()),
this.renderHeader(),
this.renderStatsCards(),
E('div', { 'class': 'sb-grid' }, [
E('div', { 'class': 'sb-col' }, [
this.renderModulesPanel(),
this.renderHealthPanel()
]),
E('div', { 'class': 'sb-col' }, [
this.renderPublicIPsPanel(),
this.renderQuickActions(),
this.renderAlertsPanel()
])
])
]);
return KissTheme.wrap(content, 'admin/secubox/dashboard');
},
updateLiveData: function() {
var health = this.data.health || {};
var cpu = health.cpu || {};
var memory = health.memory || {};
var disk = health.disk || {};
var network = health.network || {};
// Extract CPU load (handle both "1.83 1.95 1.69" string and load_1m field)
var cpuLoad = cpu.load_1min || cpu.load_1m || '0.00';
if (cpu.load && typeof cpu.load === 'string') {
cpuLoad = cpu.load.split(' ')[0] || '0.00';
}
// Update health metrics
var metrics = {
cpu: { pct: cpu.percent || cpu.usage_percent || cpu.usage || 0, detail: 'Load: ' + cpuLoad },
memory: { pct: memory.percent || memory.usage_percent || memory.usage || 0, detail: formatBytes((memory.used_kb || 0) * 1024) + ' / ' + formatBytes((memory.total_kb || 0) * 1024) },
disk: { pct: disk.percent || disk.usage_percent || disk.usage || 0, detail: formatBytes((disk.used_kb || 0) * 1024) + ' / ' + formatBytes((disk.total_kb || 0) * 1024) },
network: { pct: network.wan_up ? 100 : 0, detail: network.wan_up ? 'WAN Online' : 'WAN Offline' }
};
Object.keys(metrics).forEach(function(key) {
var m = metrics[key];
var bar = document.querySelector('[data-stat="' + key + '-bar"]');
var pct = document.querySelector('[data-stat="' + key + '-pct"]');
var detail = document.querySelector('[data-stat="' + key + '-detail"]');
if (bar) {
bar.style.width = m.pct + '%';
bar.style.background = getBarColor(m.pct);
}
if (pct) pct.textContent = m.pct + '%';
if (detail) detail.textContent = m.detail;
});
// Update network IPs
var ips = this.data.publicIPs || {};
var lanEl = document.querySelector('[data-stat="lan-ipv4"]');
var wanEl = document.querySelector('[data-stat="wan-ipv4"]');
var pubIpv4El = document.querySelector('[data-stat="public-ipv4"]');
var pubIpv6El = document.querySelector('[data-stat="public-ipv6"]');
if (lanEl) {
lanEl.textContent = ips.lan_ipv4 || 'N/A';
lanEl.style.color = ips.lan_available ? '#22c55e' : '#888';
}
if (wanEl) {
wanEl.textContent = ips.wan_ipv4 || 'N/A';
wanEl.style.color = ips.wan_available ? '#3b82f6' : '#888';
}
if (pubIpv4El) {
pubIpv4El.textContent = ips.public_ipv4 || 'N/A';
pubIpv4El.style.color = ips.ipv4_available ? '#22c55e' : '#888';
}
if (pubIpv6El) {
var ipv6 = ips.public_ipv6 || 'N/A';
pubIpv6El.textContent = ipv6.length > 20 ? ipv6.substring(0, 17) + '...' : ipv6;
pubIpv6El.style.color = ips.ipv6_available ? '#22c55e' : '#888';
}
// Update alerts count
var alertsEl = document.querySelector('[data-stat="alerts"]');
if (alertsEl) alertsEl.textContent = (this.data.alerts || []).length;
// Update health score
var score = (health.overall && health.overall.score) || health.score || 0;
var healthEl = document.querySelector('[data-stat="health"]');
if (healthEl) healthEl.textContent = score + '/100';
},
getStyles: function() {
return [
'.sb-dashboard { max-width: 1400px; margin: 0 auto; padding: 20px; }',
'.sb-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; margin-bottom: 24px; padding: 20px; background: #fff; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }',
'.sb-title { margin: 0; font-size: 24px; font-weight: 700; }',
'.sb-subtitle { margin: 4px 0 0; color: #666; font-size: 14px; }',
'.sb-chips { display: flex; gap: 12px; flex-wrap: wrap; }',
'.sb-chip { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #f8f9fa; border: 1px solid #e5e7eb; border-radius: 8px; }',
'.sb-chip-icon { font-size: 18px; }',
'.sb-chip-label { font-size: 11px; color: #666; display: block; }',
'.sb-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; margin-bottom: 24px; }',
'.sb-stat-card { display: flex; align-items: center; gap: 12px; padding: 16px; background: #fff; border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }',
'.sb-stat-icon { font-size: 28px; }',
'.sb-stat-value { font-size: 20px; font-weight: 700; }',
'.sb-stat-label { font-size: 12px; color: #666; }',
'.sb-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }',
'@media (max-width: 900px) { .sb-grid { grid-template-columns: 1fr; } }',
'.sb-col { display: flex; flex-direction: column; gap: 20px; }',
'.sb-panel { background: #fff; border-radius: 10px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }',
'.sb-panel-wide { grid-column: span 1; }',
'.sb-panel h3 { margin: 0 0 16px; font-size: 16px; font-weight: 600; }',
'.sb-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }',
'.sb-panel-header h3 { margin: 0; }',
'.sb-link-btn { background: none; border: none; color: #3b82f6; cursor: pointer; font-size: 13px; }',
'.sb-health-metrics { display: flex; flex-direction: column; gap: 14px; }',
'.sb-metric { }',
'.sb-metric-header { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; }',
'.sb-bar { height: 10px; background: #e5e7eb; border-radius: 5px; overflow: hidden; }',
'.sb-bar-fill { height: 100%; border-radius: 5px; transition: width 0.3s, background 0.3s; }',
'.sb-metric-pct { font-size: 12px; color: #666; display: block; text-align: right; margin-top: 4px; }',
'.sb-ip-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }',
'.sb-ip-grid-4 { grid-template-columns: 1fr 1fr; }',
'@media (min-width: 600px) { .sb-ip-grid-4 { grid-template-columns: repeat(4, 1fr); } }',
'.sb-ip-box { background: #f8f9fa; padding: 12px; border-radius: 8px; text-align: center; }',
'.sb-ip-icon { font-size: 20px; margin-bottom: 6px; }',
'.sb-ip-label { font-size: 11px; color: #888; margin-bottom: 2px; text-transform: uppercase; font-weight: 600; }',
'.sb-ip-value { font-family: monospace; font-size: 12px; word-break: break-all; }',
'.sb-table { width: 100%; border-collapse: collapse; }',
'.sb-table th, .sb-table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #e5e7eb; font-size: 13px; }',
'.sb-table th { background: #f8f9fa; font-weight: 600; font-size: 11px; text-transform: uppercase; color: #666; }',
'.sb-table tbody tr:hover { background: #f8f9fa; }',
'.sb-mod-icon { margin-right: 8px; }',
'.sb-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; color: #fff; font-size: 11px; font-weight: 600; }',
'.sb-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }',
'.sb-action-btn { display: flex; align-items: center; gap: 8px; padding: 12px; background: transparent; border: 1px solid; border-radius: 8px; cursor: pointer; font-size: 13px; transition: background 0.2s; }',
'.sb-action-btn:hover { background: rgba(0,0,0,0.03); }',
'.sb-alerts-list { display: flex; flex-direction: column; gap: 10px; }',
'.sb-alert-item { padding: 12px; background: #f8f9fa; border-radius: 6px; border-left: 3px solid; }',
'.sb-alert-time { font-size: 11px; color: #888; margin-bottom: 4px; }',
'.sb-alert-source { font-size: 12px; color: #888; }',
'.sb-empty { text-align: center; padding: 24px; color: #888; }',
'.sb-empty span { font-size: 32px; display: block; margin-bottom: 8px; }',
'@media (prefers-color-scheme: dark) {',
' .sb-dashboard { color: #e5e7eb; }',
' .sb-header, .sb-stat-card, .sb-panel { background: #1f2937; }',
' .sb-chip { background: #374151; border-color: #4b5563; }',
' .sb-chip-label, .sb-stat-label, .sb-subtitle { color: #9ca3af; }',
' .sb-table th { background: #374151; color: #9ca3af; }',
' .sb-table td { border-color: #374151; }',
' .sb-table tbody tr:hover { background: #374151; }',
' .sb-ip-box, .sb-alert-item { background: #374151; }',
' .sb-bar { background: #374151; }',
' .sb-action-btn:hover { background: rgba(255,255,255,0.05); }',
'}'
].join('\n');
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});