Convert 90+ LuCI view files from legacy cbi-button-* classes to KissTheme kiss-btn-* classes for consistent dark theme styling. Pattern conversions applied: - cbi-button-positive → kiss-btn-green - cbi-button-negative/remove → kiss-btn-red - cbi-button-apply → kiss-btn-cyan - cbi-button-action → kiss-btn-blue - cbi-button (plain) → kiss-btn Also replaced hardcoded colors (#080, #c00, #888, etc.) with CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.) for proper dark theme compatibility. Apps updated include: ai-gateway, auth-guardian, bandwidth-manager, cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure, glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager, mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer, metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status, secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats, service-registry, simplex, streamlit, system-hub, tor-shield, traffic-shaper, vhost-manager, vortex-dns, vortex-firewall, webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
346 lines
12 KiB
JavaScript
346 lines
12 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require poll';
|
|
'require ui';
|
|
'require secubox-admin/api as api';
|
|
'require secubox-admin/state-utils as stateUtils';
|
|
'require secubox-admin/components.StateIndicator as StateIndicator';
|
|
'require secubox-admin/components.StateTimeline as StateTimeline';
|
|
'require secubox/kiss-theme';
|
|
|
|
/**
|
|
* Admin Control Center - Main Dashboard
|
|
* Centralized management dashboard for SecuBox components and states
|
|
*/
|
|
|
|
return view.extend({
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null,
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
api.getAllComponentsWithStates({}),
|
|
api.getHealth().catch(function() { return {}; }),
|
|
api.getAlerts().catch(function() { return []; }),
|
|
api.getStateStatistics()
|
|
]).then(function(results) {
|
|
return {
|
|
components: results[0] || [],
|
|
health: results[1] || {},
|
|
alerts: results[2] || [],
|
|
statistics: results[3] || {}
|
|
};
|
|
});
|
|
},
|
|
|
|
renderStats: function(health, statistics) {
|
|
var c = KissTheme.colors;
|
|
var healthScore = health.health_score || 0;
|
|
var healthColor = healthScore >= 80 ? c.green : (healthScore >= 60 ? c.orange : c.red);
|
|
var runningCount = (statistics.by_state && statistics.by_state.running) || 0;
|
|
var errorCount = (statistics.by_state && statistics.by_state.error) || 0;
|
|
|
|
return [
|
|
KissTheme.stat(healthScore + '%', 'Health', healthColor),
|
|
KissTheme.stat(statistics.total || 0, 'Components', c.purple),
|
|
KissTheme.stat(runningCount, 'Running', c.green),
|
|
KissTheme.stat(errorCount, 'Errors', errorCount > 0 ? c.red : c.muted)
|
|
];
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
|
|
// Start polling for updates
|
|
poll.add(function() {
|
|
return self.load().then(function(newData) {
|
|
self.updateDashboard(newData);
|
|
});
|
|
}, 5);
|
|
|
|
var content = [
|
|
// Header
|
|
E('div', { 'style': 'margin-bottom: 24px;' }, [
|
|
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
|
|
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, '🎛️ Control Center'),
|
|
KissTheme.badge('Admin', 'purple')
|
|
]),
|
|
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' },
|
|
'Centralized management dashboard for components and system state')
|
|
]),
|
|
|
|
// Stats
|
|
E('div', { 'class': 'kiss-grid kiss-grid-4', 'id': 'control-stats', 'style': 'margin: 20px 0;' },
|
|
this.renderStats(data.health, data.statistics)),
|
|
|
|
// System Overview
|
|
this.renderSystemOverview(data.health, data.statistics),
|
|
|
|
// Two column layout
|
|
E('div', { 'class': 'kiss-grid kiss-grid-2', 'id': 'control-grid', 'style': 'margin-top: 20px;' }, [
|
|
this.renderAlertsPanel(data.alerts),
|
|
this.renderStateSummary(data.statistics, data.components)
|
|
]),
|
|
|
|
// Recent Transitions
|
|
this.renderRecentTransitions(data.components),
|
|
|
|
// Quick Actions
|
|
this.renderQuickActions(data.components)
|
|
];
|
|
|
|
return KissTheme.wrap(content, 'admin/secubox/admin/control-center');
|
|
},
|
|
|
|
/**
|
|
* Render system overview panel
|
|
*/
|
|
renderSystemOverview: function(health, statistics) {
|
|
var self = this;
|
|
var items = [];
|
|
|
|
// Uptime (if available)
|
|
if (health.uptime) {
|
|
items.push(E('div', {
|
|
'style': 'display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid var(--kiss-line);'
|
|
}, [
|
|
E('span', { 'style': 'color: var(--kiss-muted);' }, 'System Uptime'),
|
|
E('span', { 'style': 'font-weight: 600; color: var(--kiss-cyan);' }, api.formatUptime(health.uptime))
|
|
]));
|
|
}
|
|
|
|
// Memory Usage (if available)
|
|
if (health.memory) {
|
|
var memPercent = Math.round((health.memory.used / health.memory.total) * 100);
|
|
var memColor = memPercent >= 90 ? 'red' : (memPercent >= 75 ? 'orange' : 'green');
|
|
items.push(E('div', {
|
|
'style': 'display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid var(--kiss-line);'
|
|
}, [
|
|
E('span', { 'style': 'color: var(--kiss-muted);' }, 'Memory Usage'),
|
|
E('div', { 'style': 'display: flex; align-items: center; gap: 8px;' }, [
|
|
KissTheme.badge(memPercent + '%', memColor),
|
|
E('span', { 'style': 'font-size: 12px; color: var(--kiss-muted);' },
|
|
api.formatBytes(health.memory.used) + ' / ' + api.formatBytes(health.memory.total))
|
|
])
|
|
]));
|
|
}
|
|
|
|
// CPU Load (if available)
|
|
if (health.load) {
|
|
items.push(E('div', {
|
|
'style': 'display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid var(--kiss-line);'
|
|
}, [
|
|
E('span', { 'style': 'color: var(--kiss-muted);' }, 'Load Average'),
|
|
E('span', { 'style': 'font-family: monospace; color: var(--kiss-text);' },
|
|
Array.isArray(health.load) ? health.load.join(' / ') : String(health.load))
|
|
]));
|
|
}
|
|
|
|
// Last Check
|
|
items.push(E('div', {
|
|
'style': 'display: flex; align-items: center; justify-content: space-between; padding: 12px 0;'
|
|
}, [
|
|
E('span', { 'style': 'color: var(--kiss-muted);' }, 'Last Updated'),
|
|
E('span', { 'style': 'color: var(--kiss-text);' }, new Date().toLocaleTimeString())
|
|
]));
|
|
|
|
return KissTheme.card('System Overview', E('div', { 'id': 'system-overview-content' }, items));
|
|
},
|
|
|
|
/**
|
|
* Render state summary panel
|
|
*/
|
|
renderStateSummary: function(statistics, components) {
|
|
var categories = ['persistent', 'transient', 'runtime', 'error'];
|
|
var colorMap = { error: 'red', runtime: 'green', transient: 'blue', persistent: 'muted' };
|
|
|
|
var categoryItems = categories.map(function(category) {
|
|
var states = stateUtils.getStatesByCategory(category);
|
|
var count = 0;
|
|
states.forEach(function(state) {
|
|
count += (statistics.by_state && statistics.by_state[state]) || 0;
|
|
});
|
|
|
|
return E('div', {
|
|
'style': 'display: flex; align-items: center; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--kiss-line);'
|
|
}, [
|
|
E('span', { 'style': 'text-transform: capitalize; color: var(--kiss-muted);' }, category),
|
|
KissTheme.badge(String(count), colorMap[category])
|
|
]);
|
|
});
|
|
|
|
return KissTheme.card(
|
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
|
|
E('span', {}, 'State Summary'),
|
|
KissTheme.badge(statistics.total + ' total', 'purple')
|
|
]),
|
|
E('div', { 'id': 'state-summary-content' }, categoryItems)
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Render alerts panel
|
|
*/
|
|
renderAlertsPanel: function(alerts) {
|
|
var colorMap = { critical: 'red', warning: 'orange', info: 'blue' };
|
|
|
|
var content;
|
|
if (!alerts || alerts.length === 0) {
|
|
content = E('p', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 20px;' },
|
|
'No active alerts');
|
|
} else {
|
|
content = E('div', { 'style': 'display: flex; flex-direction: column; gap: 12px;' },
|
|
alerts.slice(0, 5).map(function(alert) {
|
|
var severity = alert.severity || 'info';
|
|
var color = colorMap[severity] || 'muted';
|
|
var borderColor = 'var(--kiss-' + color + ')';
|
|
|
|
return E('div', {
|
|
'style': 'padding: 12px; background: var(--kiss-bg); border-radius: 6px; border-left: 3px solid ' + borderColor + ';'
|
|
}, [
|
|
E('div', { 'style': 'display: flex; align-items: center; gap: 8px; margin-bottom: 6px;' }, [
|
|
KissTheme.badge(severity.toUpperCase(), color),
|
|
alert.timestamp ? E('span', {
|
|
'style': 'font-size: 11px; color: var(--kiss-muted); margin-left: auto;'
|
|
}, stateUtils.getTimeAgo(alert.timestamp)) : ''
|
|
]),
|
|
E('div', { 'style': 'color: var(--kiss-muted); font-size: 13px;' },
|
|
alert.message || 'No message')
|
|
]);
|
|
})
|
|
);
|
|
}
|
|
|
|
return KissTheme.card(
|
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
|
|
E('span', {}, 'System Alerts'),
|
|
alerts && alerts.length > 0 ? KissTheme.badge(alerts.length + ' alerts', 'orange') : KissTheme.badge('None', 'green')
|
|
]),
|
|
E('div', { 'id': 'alerts-content' }, content)
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Render recent transitions panel
|
|
*/
|
|
renderRecentTransitions: function(components) {
|
|
// Collect all state history from components
|
|
var allHistory = [];
|
|
components.forEach(function(comp) {
|
|
if (comp.state_info && comp.state_info.history) {
|
|
comp.state_info.history.forEach(function(entry) {
|
|
allHistory.push({
|
|
component_id: comp.id,
|
|
component_name: comp.name,
|
|
state: entry.state,
|
|
timestamp: entry.timestamp,
|
|
reason: entry.reason,
|
|
error_details: entry.error_details
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// Sort by timestamp (most recent first)
|
|
allHistory.sort(function(a, b) {
|
|
return new Date(b.timestamp) - new Date(a.timestamp);
|
|
});
|
|
|
|
var content;
|
|
if (allHistory.length > 0) {
|
|
var stateColors = {
|
|
running: 'green', stopped: 'muted', error: 'red',
|
|
starting: 'blue', stopping: 'orange', installed: 'cyan'
|
|
};
|
|
|
|
content = E('div', { 'style': 'display: flex; flex-direction: column; gap: 8px;' },
|
|
allHistory.slice(0, 10).map(function(entry) {
|
|
var color = stateColors[entry.state] || 'muted';
|
|
return E('div', {
|
|
'style': 'display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: var(--kiss-bg); border-radius: 6px;'
|
|
}, [
|
|
KissTheme.badge(entry.state, color),
|
|
E('span', { 'style': 'font-weight: 500;' }, entry.component_name || entry.component_id),
|
|
E('span', { 'style': 'font-size: 12px; color: var(--kiss-muted); margin-left: auto;' },
|
|
stateUtils.getTimeAgo(entry.timestamp))
|
|
]);
|
|
})
|
|
);
|
|
} else {
|
|
content = E('p', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 20px;' },
|
|
'No recent state transitions');
|
|
}
|
|
|
|
return KissTheme.card(
|
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
|
|
E('span', {}, 'Recent Transitions'),
|
|
KissTheme.badge(allHistory.length + ' events', 'blue')
|
|
]),
|
|
E('div', { 'id': 'transitions-content', 'style': 'margin-top: 12px;' }, content)
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Render quick actions panel
|
|
*/
|
|
renderQuickActions: function(components) {
|
|
var self = this;
|
|
|
|
var actions = [
|
|
{ label: 'Refresh', icon: '↻', color: 'blue', onClick: function() { location.reload(); } },
|
|
{ label: 'Components', icon: '◫', color: 'purple', onClick: function() { location.href = L.url('admin', 'secubox', 'components'); } },
|
|
{ label: 'State Manager', icon: '⚙', color: 'cyan', onClick: function() { location.href = L.url('admin', 'secubox', 'state-manager'); } },
|
|
{ label: 'Sync Registry', icon: '⇄', color: 'green', onClick: function() { self.syncRegistry(); } }
|
|
];
|
|
|
|
var actionsContent = E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' },
|
|
actions.map(function(action) {
|
|
return E('button', {
|
|
'class': 'kiss-btn kiss-btn-' + action.color,
|
|
'style': 'display: flex; align-items: center; gap: 8px; padding: 12px 20px;',
|
|
'click': action.onClick
|
|
}, [
|
|
E('span', { 'style': 'font-size: 18px;' }, action.icon),
|
|
action.label
|
|
]);
|
|
})
|
|
);
|
|
|
|
return KissTheme.card('Quick Actions', actionsContent);
|
|
},
|
|
|
|
/**
|
|
* Sync registry (call backend sync script)
|
|
*/
|
|
syncRegistry: function() {
|
|
ui.showModal(_('Syncing Component Registry'), [
|
|
E('p', { 'class': 'spinning' }, _('Synchronizing component registry from catalog...'))
|
|
]);
|
|
|
|
// TODO: Add RPC method for sync_registry
|
|
// For now, just reload after delay
|
|
setTimeout(function() {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', _('Registry sync completed')), 'info');
|
|
location.reload();
|
|
}, 2000);
|
|
},
|
|
|
|
/**
|
|
* Update dashboard with new data
|
|
*/
|
|
updateDashboard: function(data) {
|
|
var self = this;
|
|
|
|
// Update stats
|
|
var statsEl = document.getElementById('control-stats');
|
|
if (statsEl) {
|
|
statsEl.innerHTML = '';
|
|
this.renderStats(data.health, data.statistics).forEach(function(stat) {
|
|
statsEl.appendChild(stat);
|
|
});
|
|
}
|
|
}
|
|
});
|