All 'require module.submodule' directives changed to 'require module/submodule' to match LuCI's module loading convention. Affected packages: - luci-app-auth-guardian - luci-app-glances - luci-app-localai - luci-app-magicmirror2 - luci-app-mitmproxy - luci-app-mmpm - luci-app-mqtt-bridge - luci-app-ndpid - luci-app-network-modes - luci-app-secubox-admin - luci-app-secubox-portal - luci-app-wireguard-dashboard Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
526 lines
15 KiB
JavaScript
526 lines
15 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require poll';
|
|
'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';
|
|
|
|
/**
|
|
* Admin Control Center - Main Dashboard
|
|
* Centralized management dashboard for SecuBox components and states
|
|
*/
|
|
|
|
return view.extend({
|
|
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] || {}
|
|
};
|
|
});
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
|
|
var container = E('div', { 'class': 'cyberpunk-mode control-center' }, [
|
|
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
|
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
|
E('link', { 'rel': 'stylesheet',
|
|
'href': L.resource('secubox-admin/common.css') }),
|
|
E('link', { 'rel': 'stylesheet',
|
|
'href': L.resource('secubox-admin/admin.css') }),
|
|
|
|
E('div', { 'class': 'cyber-header' }, [
|
|
E('div', { 'class': 'cyber-header-title' }, '🎛️ CONTROL CENTER'),
|
|
E('div', { 'class': 'cyber-header-subtitle' }, 'Centralized management dashboard for components and system state')
|
|
])
|
|
]);
|
|
|
|
// System Overview Panel
|
|
var overviewPanel = this.renderSystemOverview(data.health, data.statistics);
|
|
container.appendChild(overviewPanel);
|
|
|
|
// Component State Summary
|
|
var stateSummary = this.renderStateSummary(data.statistics, data.components);
|
|
container.appendChild(stateSummary);
|
|
|
|
// Alerts Panel
|
|
if (data.alerts && data.alerts.length > 0) {
|
|
var alertsPanel = this.renderAlertsPanel(data.alerts);
|
|
container.appendChild(alertsPanel);
|
|
}
|
|
|
|
// Recent State Transitions
|
|
var transitionsPanel = this.renderRecentTransitions(data.components);
|
|
container.appendChild(transitionsPanel);
|
|
|
|
// Quick Actions
|
|
var actionsPanel = this.renderQuickActions(data.components);
|
|
container.appendChild(actionsPanel);
|
|
|
|
// Start polling for updates
|
|
poll.add(function() {
|
|
return self.load().then(function(newData) {
|
|
self.updateDashboard(newData);
|
|
});
|
|
}, 5); // Poll every 5 seconds
|
|
|
|
return container;
|
|
},
|
|
|
|
/**
|
|
* Render system overview panel
|
|
*/
|
|
renderSystemOverview: function(health, statistics) {
|
|
var panel = E('div', {
|
|
'id': 'system-overview-panel',
|
|
'class': 'cbi-section',
|
|
'style': 'margin-bottom: 2rem;'
|
|
});
|
|
|
|
var panelTitle = E('h3', { 'class': 'section-title' }, 'System Overview');
|
|
panel.appendChild(panelTitle);
|
|
|
|
var grid = E('div', {
|
|
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem;'
|
|
});
|
|
|
|
// Health Score Card
|
|
var healthScore = health.health_score || 0;
|
|
var healthColor = healthScore >= 80 ? '#10b981' : (healthScore >= 60 ? '#f59e0b' : '#ef4444');
|
|
var healthCard = this.createMetricCard(
|
|
'Health Score',
|
|
healthScore + '%',
|
|
'Overall system health',
|
|
healthColor
|
|
);
|
|
grid.appendChild(healthCard);
|
|
|
|
// Total Components Card
|
|
var totalComponents = statistics.total || 0;
|
|
var componentsCard = this.createMetricCard(
|
|
'Total Components',
|
|
String(totalComponents),
|
|
'Registered in system',
|
|
'#8b5cf6'
|
|
);
|
|
grid.appendChild(componentsCard);
|
|
|
|
// Running Components Card
|
|
var runningCount = (statistics.by_state && statistics.by_state.running) || 0;
|
|
var runningCard = this.createMetricCard(
|
|
'Running',
|
|
String(runningCount),
|
|
'Active components',
|
|
'#10b981'
|
|
);
|
|
grid.appendChild(runningCard);
|
|
|
|
// Error Components Card
|
|
var errorCount = (statistics.by_state && statistics.by_state.error) || 0;
|
|
var errorCard = this.createMetricCard(
|
|
'Errors',
|
|
String(errorCount),
|
|
'Require attention',
|
|
'#ef4444'
|
|
);
|
|
grid.appendChild(errorCard);
|
|
|
|
// Uptime (if available)
|
|
if (health.uptime) {
|
|
var uptimeCard = this.createMetricCard(
|
|
'System Uptime',
|
|
api.formatUptime(health.uptime),
|
|
'Since last boot',
|
|
'#06b6d4'
|
|
);
|
|
grid.appendChild(uptimeCard);
|
|
}
|
|
|
|
// Memory Usage (if available)
|
|
if (health.memory) {
|
|
var memPercent = Math.round((health.memory.used / health.memory.total) * 100);
|
|
var memColor = memPercent >= 90 ? '#ef4444' : (memPercent >= 75 ? '#f59e0b' : '#10b981');
|
|
var memCard = this.createMetricCard(
|
|
'Memory Usage',
|
|
memPercent + '%',
|
|
api.formatBytes(health.memory.used) + ' / ' + api.formatBytes(health.memory.total),
|
|
memColor
|
|
);
|
|
grid.appendChild(memCard);
|
|
}
|
|
|
|
panel.appendChild(grid);
|
|
|
|
return panel;
|
|
},
|
|
|
|
/**
|
|
* Create a metric card
|
|
*/
|
|
createMetricCard: function(title, value, subtitle, color) {
|
|
var card = E('div', {
|
|
'class': 'metric-card',
|
|
'style': 'padding: 1.5rem; border-radius: 0.5rem; border-left: 4px solid ' + color + '; background-color: ' + color + '10; transition: all 0.2s;'
|
|
});
|
|
|
|
card.addEventListener('mouseenter', function() {
|
|
this.style.transform = 'translateY(-2px)';
|
|
this.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
|
|
});
|
|
|
|
card.addEventListener('mouseleave', function() {
|
|
this.style.transform = 'translateY(0)';
|
|
this.style.boxShadow = 'none';
|
|
});
|
|
|
|
var titleEl = E('div', {
|
|
'style': 'font-size: 0.875rem; color: #6b7280; font-weight: 500; margin-bottom: 0.5rem;'
|
|
}, title);
|
|
card.appendChild(titleEl);
|
|
|
|
var valueEl = E('div', {
|
|
'style': 'font-size: 1.875rem; font-weight: 700; color: ' + color + '; margin-bottom: 0.25rem;'
|
|
}, value);
|
|
card.appendChild(valueEl);
|
|
|
|
var subtitleEl = E('div', {
|
|
'style': 'font-size: 0.75rem; color: #9ca3af;'
|
|
}, subtitle);
|
|
card.appendChild(subtitleEl);
|
|
|
|
return card;
|
|
},
|
|
|
|
/**
|
|
* Render state summary panel
|
|
*/
|
|
renderStateSummary: function(statistics, components) {
|
|
var panel = E('div', {
|
|
'id': 'state-summary-panel',
|
|
'class': 'cbi-section',
|
|
'style': 'margin-bottom: 2rem;'
|
|
});
|
|
|
|
var panelTitle = E('h3', { 'class': 'section-title' }, 'Component State Summary');
|
|
panel.appendChild(panelTitle);
|
|
|
|
// State statistics
|
|
var stateStats = StateIndicator.renderStatistics(statistics);
|
|
panel.appendChild(stateStats);
|
|
|
|
// State distribution by category
|
|
var categorySection = E('div', { 'style': 'margin-top: 2rem;' });
|
|
var categoryTitle = E('h4', { 'style': 'font-size: 1rem; font-weight: 600; margin-bottom: 1rem;' },
|
|
'Distribution by Category');
|
|
categorySection.appendChild(categoryTitle);
|
|
|
|
var categories = ['persistent', 'transient', 'runtime', 'error'];
|
|
var categoryGrid = E('div', {
|
|
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;'
|
|
});
|
|
|
|
categories.forEach(function(category) {
|
|
var states = stateUtils.getStatesByCategory(category);
|
|
var count = 0;
|
|
states.forEach(function(state) {
|
|
count += (statistics.by_state && statistics.by_state[state]) || 0;
|
|
});
|
|
|
|
var color = category === 'error' ? '#ef4444' :
|
|
category === 'runtime' ? '#10b981' :
|
|
category === 'transient' ? '#3b82f6' : '#6b7280';
|
|
|
|
var card = E('div', {
|
|
'style': 'padding: 1rem; border-radius: 0.5rem; background-color: ' + color + '10; border: 1px solid ' + color + '40;'
|
|
});
|
|
|
|
var countEl = E('div', {
|
|
'style': 'font-size: 1.5rem; font-weight: 700; color: ' + color + ';'
|
|
}, String(count));
|
|
card.appendChild(countEl);
|
|
|
|
var labelEl = E('div', {
|
|
'style': 'font-size: 0.875rem; color: #6b7280; margin-top: 0.25rem; text-transform: capitalize;'
|
|
}, category);
|
|
card.appendChild(labelEl);
|
|
|
|
categoryGrid.appendChild(card);
|
|
});
|
|
|
|
categorySection.appendChild(categoryGrid);
|
|
panel.appendChild(categorySection);
|
|
|
|
return panel;
|
|
},
|
|
|
|
/**
|
|
* Render alerts panel
|
|
*/
|
|
renderAlertsPanel: function(alerts) {
|
|
var panel = E('div', {
|
|
'id': 'alerts-panel',
|
|
'class': 'cbi-section',
|
|
'style': 'margin-bottom: 2rem;'
|
|
});
|
|
|
|
var panelTitle = E('h3', { 'class': 'section-title' }, 'System Alerts');
|
|
panel.appendChild(panelTitle);
|
|
|
|
var alertsList = E('div', { 'style': 'margin-top: 1rem;' });
|
|
|
|
alerts.slice(0, 5).forEach(function(alert) {
|
|
var severity = alert.severity || 'info';
|
|
var color = severity === 'critical' ? '#ef4444' :
|
|
severity === 'warning' ? '#f59e0b' :
|
|
severity === 'info' ? '#3b82f6' : '#6b7280';
|
|
|
|
var alertCard = E('div', {
|
|
'style': 'padding: 1rem; border-radius: 0.5rem; border-left: 4px solid ' + color + '; background-color: ' + color + '10; margin-bottom: 0.75rem;'
|
|
});
|
|
|
|
var header = E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;' });
|
|
|
|
var severityBadge = E('span', {
|
|
'style': 'display: inline-block; padding: 0.125rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 600; background-color: ' + color + '; color: white; text-transform: uppercase;'
|
|
}, severity);
|
|
header.appendChild(severityBadge);
|
|
|
|
if (alert.timestamp) {
|
|
var time = E('span', {
|
|
'style': 'font-size: 0.75rem; color: #6b7280;'
|
|
}, stateUtils.getTimeAgo(alert.timestamp));
|
|
header.appendChild(time);
|
|
}
|
|
|
|
alertCard.appendChild(header);
|
|
|
|
var message = E('div', {
|
|
'style': 'font-size: 0.875rem; color: #4b5563;'
|
|
}, alert.message || 'No message');
|
|
alertCard.appendChild(message);
|
|
|
|
alertsList.appendChild(alertCard);
|
|
});
|
|
|
|
panel.appendChild(alertsList);
|
|
|
|
return panel;
|
|
},
|
|
|
|
/**
|
|
* Render recent transitions panel
|
|
*/
|
|
renderRecentTransitions: function(components) {
|
|
var panel = E('div', {
|
|
'id': 'recent-transitions-panel',
|
|
'class': 'cbi-section',
|
|
'style': 'margin-bottom: 2rem;'
|
|
});
|
|
|
|
var panelTitle = E('h3', { 'class': 'section-title' }, 'Recent State Transitions');
|
|
panel.appendChild(panelTitle);
|
|
|
|
// 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);
|
|
});
|
|
|
|
if (allHistory.length > 0) {
|
|
var timeline = StateTimeline.render(allHistory.slice(0, 20), {
|
|
limit: 10,
|
|
showRelativeTime: true,
|
|
showCategory: true,
|
|
onShowMore: function() {
|
|
// TODO: Show full history modal
|
|
console.log('Show more history');
|
|
}
|
|
});
|
|
panel.appendChild(timeline);
|
|
} else {
|
|
var emptyMsg = E('div', {
|
|
'style': 'padding: 2rem; text-align: center; color: #6b7280;'
|
|
}, 'No recent state transitions');
|
|
panel.appendChild(emptyMsg);
|
|
}
|
|
|
|
return panel;
|
|
},
|
|
|
|
/**
|
|
* Render quick actions panel
|
|
*/
|
|
renderQuickActions: function(components) {
|
|
var self = this;
|
|
var panel = E('div', {
|
|
'id': 'quick-actions-panel',
|
|
'class': 'cbi-section',
|
|
'style': 'margin-bottom: 2rem;'
|
|
});
|
|
|
|
var panelTitle = E('h3', { 'class': 'section-title' }, 'Quick Actions');
|
|
panel.appendChild(panelTitle);
|
|
|
|
var actionsGrid = E('div', {
|
|
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem;'
|
|
});
|
|
|
|
// Refresh All Button
|
|
var refreshBtn = this.createActionButton(
|
|
'Refresh Dashboard',
|
|
'↻',
|
|
'#3b82f6',
|
|
function() {
|
|
location.reload();
|
|
}
|
|
);
|
|
actionsGrid.appendChild(refreshBtn);
|
|
|
|
// View All Components Button
|
|
var viewComponentsBtn = this.createActionButton(
|
|
'View All Components',
|
|
'◫',
|
|
'#8b5cf6',
|
|
function() {
|
|
location.href = L.url('admin', 'secubox', 'components');
|
|
}
|
|
);
|
|
actionsGrid.appendChild(viewComponentsBtn);
|
|
|
|
// State Manager Button
|
|
var stateManagerBtn = this.createActionButton(
|
|
'State Manager',
|
|
'⚙',
|
|
'#06b6d4',
|
|
function() {
|
|
location.href = L.url('admin', 'secubox', 'state-manager');
|
|
}
|
|
);
|
|
actionsGrid.appendChild(stateManagerBtn);
|
|
|
|
// Sync Registry Button
|
|
var syncBtn = this.createActionButton(
|
|
'Sync Registry',
|
|
'⇄',
|
|
'#10b981',
|
|
function() {
|
|
self.syncRegistry();
|
|
}
|
|
);
|
|
actionsGrid.appendChild(syncBtn);
|
|
|
|
panel.appendChild(actionsGrid);
|
|
|
|
return panel;
|
|
},
|
|
|
|
/**
|
|
* Create action button
|
|
*/
|
|
createActionButton: function(label, icon, color, onClick) {
|
|
var button = E('button', {
|
|
'class': 'btn cbi-button cbi-button-action',
|
|
'style': 'display: flex; flex-direction: column; align-items: center; padding: 1.5rem; border-radius: 0.5rem; border: 2px solid ' + color + '; background-color: white; cursor: pointer; transition: all 0.2s; width: 100%;'
|
|
});
|
|
|
|
button.addEventListener('mouseenter', function() {
|
|
this.style.backgroundColor = color + '10';
|
|
this.style.transform = 'translateY(-2px)';
|
|
});
|
|
|
|
button.addEventListener('mouseleave', function() {
|
|
this.style.backgroundColor = 'white';
|
|
this.style.transform = 'translateY(0)';
|
|
});
|
|
|
|
button.addEventListener('click', onClick);
|
|
|
|
var iconEl = E('div', {
|
|
'style': 'font-size: 2rem; color: ' + color + '; margin-bottom: 0.5rem;'
|
|
}, icon);
|
|
button.appendChild(iconEl);
|
|
|
|
var labelEl = E('div', {
|
|
'style': 'font-size: 0.875rem; font-weight: 600; color: ' + color + ';'
|
|
}, label);
|
|
button.appendChild(labelEl);
|
|
|
|
return button;
|
|
},
|
|
|
|
/**
|
|
* Sync registry (call backend sync script)
|
|
*/
|
|
syncRegistry: function() {
|
|
var self = this;
|
|
|
|
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) {
|
|
// Update system overview
|
|
var overviewPanel = document.getElementById('system-overview-panel');
|
|
if (overviewPanel) {
|
|
var newOverview = this.renderSystemOverview(data.health, data.statistics);
|
|
overviewPanel.replaceWith(newOverview);
|
|
}
|
|
|
|
// Update state summary
|
|
var summaryPanel = document.getElementById('state-summary-panel');
|
|
if (summaryPanel) {
|
|
var newSummary = this.renderStateSummary(data.statistics, data.components);
|
|
summaryPanel.replaceWith(newSummary);
|
|
}
|
|
|
|
// Update recent transitions
|
|
var transitionsPanel = document.getElementById('recent-transitions-panel');
|
|
if (transitionsPanel) {
|
|
var newTransitions = this.renderRecentTransitions(data.components);
|
|
transitionsPanel.replaceWith(newTransitions);
|
|
}
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|