mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 12:34:38 +00:00
- Add ARP-based IP collision detection for multi-device environments - MAC-based pseudo-random IP offset to spread devices across range - Gratuitous ARP announcement to prevent IP conflicts - Fix EspressoBin DSA network: target wan interface, not eth0 CPU port - Static IP fallback 192.168.255.250 when DHCP unavailable - Sync all build scripts to version 1.7.0 - Add screenshot script with 90+ module URLs - Add mock HTML screenshots for documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
809 lines
31 KiB
HTML
809 lines
31 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SecuBox Control Center</title>
|
|
<link rel="stylesheet" href="/shared/design-tokens.css">
|
|
<link rel="stylesheet" href="/shared/crt-light.css">
|
|
<link rel="stylesheet" href="/shared/sidebar-light.css">
|
|
<style>
|
|
/**
|
|
* SecuBox Control Center — 6-Module Design System v2.0
|
|
* Overrides and dashboard-specific styles
|
|
*/
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
font-family: var(--font-body);
|
|
background: var(--bg);
|
|
background-image: radial-gradient(ellipse at 50% 40%, rgba(10,88,64,0.02) 0%, transparent 70%);
|
|
color: var(--text);
|
|
display: flex;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* Main content */
|
|
.main { flex: 1; margin-left: 220px; }
|
|
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--surface);
|
|
}
|
|
|
|
.header-title { display: flex; align-items: center; gap: 1rem; }
|
|
|
|
.header-title h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: var(--root-main);
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.badge {
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--size-xs);
|
|
font-weight: 600;
|
|
font-family: var(--font-mono);
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
border: 1px solid;
|
|
}
|
|
|
|
.badge.version {
|
|
background: var(--bg);
|
|
border-color: var(--border);
|
|
color: var(--muted);
|
|
}
|
|
|
|
.badge.active {
|
|
background: rgba(10,88,64,0.15);
|
|
border-color: var(--root-main);
|
|
color: var(--root-main);
|
|
}
|
|
|
|
.header-actions { display: flex; gap: 0.75rem; }
|
|
|
|
.btn {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: var(--radius-md);
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: var(--size-sm);
|
|
font-family: var(--font-body);
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.btn:hover {
|
|
border-color: var(--root-main);
|
|
color: var(--root-main);
|
|
}
|
|
|
|
.btn.danger {
|
|
background: rgba(128,48,24,0.1);
|
|
border-color: var(--boot-main);
|
|
color: var(--boot-main);
|
|
}
|
|
|
|
.btn.danger:hover {
|
|
background: rgba(128,48,24,0.2);
|
|
}
|
|
|
|
.btn.primary {
|
|
background: rgba(10,88,64,0.1);
|
|
border-color: var(--root-main);
|
|
color: var(--root-main);
|
|
}
|
|
|
|
.btn.primary:hover {
|
|
background: rgba(10,88,64,0.2);
|
|
}
|
|
|
|
.content { padding: 1.5rem; }
|
|
|
|
.subtitle {
|
|
color: var(--muted);
|
|
font-size: var(--size-sm);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
/* Stats row - 6-module color coding */
|
|
.stats-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, 1fr);
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: 1.25rem;
|
|
text-align: center;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.stat-card:hover {
|
|
border-color: var(--border2);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.stat-card .value {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
font-family: var(--font-mono);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.stat-card .label {
|
|
font-size: var(--size-xs);
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* 6-Module stat colors */
|
|
.stat-card.mesh .value { color: var(--mesh-main); }
|
|
.stat-card.root .value { color: var(--root-main); }
|
|
.stat-card.wall .value { color: var(--wall-main); }
|
|
.stat-card.boot .value { color: var(--boot-main); }
|
|
.stat-card.mind .value { color: var(--mind-main); }
|
|
.stat-card.auth .value { color: var(--auth-main); }
|
|
/* Legacy compat */
|
|
.stat-card.cyan .value { color: var(--mesh-main); }
|
|
.stat-card.green .value { color: var(--root-main); }
|
|
.stat-card.yellow .value { color: var(--wall-main); }
|
|
.stat-card.red .value { color: var(--boot-main); }
|
|
|
|
/* Grid layout */
|
|
.grid-2 { display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; }
|
|
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: 1.25rem;
|
|
margin-bottom: 1.5rem;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.card:hover {
|
|
border-color: var(--border2);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.card-header h2 {
|
|
font-size: var(--size-sm);
|
|
font-weight: 700;
|
|
color: var(--wall-main);
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.card-header a {
|
|
color: var(--root-main);
|
|
font-size: var(--size-sm);
|
|
text-decoration: none;
|
|
transition: color var(--transition-fast);
|
|
}
|
|
|
|
.card-header a:hover { color: var(--root-light); }
|
|
|
|
/* Module table */
|
|
.module-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.module-table th {
|
|
text-align: left;
|
|
padding: 0.75rem;
|
|
color: var(--wall-main);
|
|
font-size: var(--size-xs);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.module-table td {
|
|
padding: 0.75rem;
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: var(--size-sm);
|
|
color: var(--text);
|
|
}
|
|
|
|
.module-table tr:hover {
|
|
background: rgba(10,88,64,0.03);
|
|
}
|
|
|
|
.module-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: var(--radius-sm);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 0.5rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 0.2rem 0.5rem;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--size-xs);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.status-badge.active {
|
|
background: rgba(10,88,64,0.15);
|
|
color: var(--root-main);
|
|
}
|
|
|
|
.status-badge.inactive {
|
|
background: rgba(128,48,24,0.15);
|
|
color: var(--boot-main);
|
|
}
|
|
|
|
/* Network addresses */
|
|
.network-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
|
|
|
.network-item {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
background: var(--bg);
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.network-item .label {
|
|
font-size: var(--size-xs);
|
|
color: var(--muted);
|
|
margin-bottom: 0.25rem;
|
|
font-family: var(--font-mono);
|
|
letter-spacing: 0.1em;
|
|
}
|
|
|
|
.network-item .value {
|
|
color: var(--mesh-main);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* Quick actions */
|
|
.quick-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
|
|
|
|
.action-btn {
|
|
padding: 0.75rem;
|
|
background: rgba(16,74,136,0.1);
|
|
border: 1px solid var(--mesh-main);
|
|
border-radius: var(--radius-md);
|
|
color: var(--mesh-main);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
font-size: var(--size-sm);
|
|
font-family: var(--font-body);
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.action-btn:hover {
|
|
background: rgba(16,74,136,0.2);
|
|
}
|
|
|
|
.action-btn.green {
|
|
background: rgba(10,88,64,0.1);
|
|
border-color: var(--root-main);
|
|
color: var(--root-main);
|
|
}
|
|
|
|
.action-btn.green:hover {
|
|
background: rgba(10,88,64,0.2);
|
|
}
|
|
|
|
/* Alert timeline */
|
|
.alert-item {
|
|
padding: 0.75rem;
|
|
border-left: 3px solid var(--wall-main);
|
|
background: rgba(154,96,16,0.1);
|
|
margin-bottom: 0.5rem;
|
|
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
|
}
|
|
|
|
.alert-item .time {
|
|
font-size: var(--size-xs);
|
|
color: var(--muted);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.alert-item .message {
|
|
font-size: var(--size-sm);
|
|
}
|
|
|
|
/* System health */
|
|
.health-item { margin-bottom: 1rem; }
|
|
|
|
.health-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 0.25rem;
|
|
font-size: var(--size-sm);
|
|
}
|
|
|
|
.health-bar {
|
|
height: 8px;
|
|
background: var(--bg);
|
|
border-radius: var(--radius-sm);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.health-bar .fill {
|
|
height: 100%;
|
|
border-radius: var(--radius-sm);
|
|
transition: width 0.3s;
|
|
}
|
|
|
|
.health-bar .fill.red { background: var(--boot-main); }
|
|
.health-bar .fill.yellow { background: var(--wall-main); }
|
|
.health-bar .fill.green { background: var(--root-main); }
|
|
.health-bar .fill.cyan { background: var(--mesh-main); }
|
|
|
|
.health-percent {
|
|
font-size: var(--size-xs);
|
|
color: var(--muted);
|
|
text-align: right;
|
|
margin-top: 0.25rem;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* Form elements */
|
|
input, select, textarea {
|
|
font-family: var(--font-mono);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
padding: 0.5rem;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
input:focus, select:focus, textarea:focus {
|
|
outline: none;
|
|
border-color: var(--root-main);
|
|
box-shadow: 0 0 0 2px rgba(10,88,64,0.1);
|
|
}
|
|
|
|
/* Scrollbar */
|
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
::-webkit-scrollbar-track { background: var(--bg); }
|
|
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
|
|
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
|
|
|
|
@media (max-width: 1200px) { .stats-row { grid-template-columns: repeat(3, 1fr); } }
|
|
@media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } }
|
|
@media (max-width: 768px) { .sidebar { display: none; } .main { margin-left: 0; } }
|
|
</style>
|
|
</head>
|
|
<body class="crt-light">
|
|
<nav class="sidebar" id="sidebar"></nav>
|
|
|
|
<main class="main">
|
|
<header class="header">
|
|
<div class="header-title">
|
|
<h1>SecuBox Control Center</h1>
|
|
<span class="badge version" id="version">v1.5.10</span>
|
|
<span class="badge version" id="build-info" title="Build timestamp">Build: --</span>
|
|
<span class="badge active" id="status-badge">-/- Active</span>
|
|
</div>
|
|
<div class="header-actions">
|
|
<button class="btn" onclick="location.href='/system/'">Settings</button>
|
|
<button class="btn danger" onclick="logout()">Logout</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content">
|
|
<p class="subtitle">Security | Network | System automation</p>
|
|
|
|
<div class="stats-row">
|
|
<div class="stat-card mesh">
|
|
<div class="value" id="total-modules">-</div>
|
|
<div class="label">Modules</div>
|
|
</div>
|
|
<div class="stat-card mesh">
|
|
<div class="value" id="installed-modules">-</div>
|
|
<div class="label">Installed</div>
|
|
</div>
|
|
<div class="stat-card root">
|
|
<div class="value" id="active-modules">-</div>
|
|
<div class="label">Active</div>
|
|
</div>
|
|
<div class="stat-card wall">
|
|
<div class="value" id="health-score">-%</div>
|
|
<div class="label">Health</div>
|
|
</div>
|
|
<div class="stat-card boot">
|
|
<div class="value" id="alerts-count">0</div>
|
|
<div class="label">Alerts</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-2">
|
|
<div>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2>Modules Overview</h2>
|
|
<a href="/system/">View All</a>
|
|
</div>
|
|
<table class="module-table">
|
|
<thead>
|
|
<tr><th>Module</th><th>Category</th><th>Status</th><th>Version</th></tr>
|
|
</thead>
|
|
<tbody id="modules-table">
|
|
<tr><td colspan="4">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2>System Health</h2>
|
|
</div>
|
|
<div class="health-item">
|
|
<div class="health-header"><span>CPU</span><span id="cpu-info">Load: -</span></div>
|
|
<div class="health-bar"><div class="fill red" id="cpu-bar" style="width: 0%"></div></div>
|
|
<div class="health-percent" id="cpu-percent">0%</div>
|
|
</div>
|
|
<div class="health-item">
|
|
<div class="health-header"><span>Memory</span><span id="mem-info">- / -</span></div>
|
|
<div class="health-bar"><div class="fill yellow" id="mem-bar" style="width: 0%"></div></div>
|
|
<div class="health-percent" id="mem-percent">0%</div>
|
|
</div>
|
|
<div class="health-item">
|
|
<div class="health-header"><span>Storage</span><span id="disk-info">- / -</span></div>
|
|
<div class="health-bar"><div class="fill red" id="disk-bar" style="width: 0%"></div></div>
|
|
<div class="health-percent" id="disk-percent">0%</div>
|
|
</div>
|
|
<div class="health-item">
|
|
<div class="health-header"><span>Network</span><span id="net-info">-</span></div>
|
|
<div class="health-bar"><div class="fill cyan" id="net-bar" style="width: 0%"></div></div>
|
|
<div class="health-percent" id="net-percent">0%</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<!-- Network Mode Selection Card -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2>Network Mode</h2>
|
|
<a href="/netmodes/">Configure</a>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
|
|
<span style="font-size: 2rem;" id="mode-icon">🔀</span>
|
|
<div>
|
|
<div style="font-weight: bold; color: var(--root-main);" id="current-mode-name">Router</div>
|
|
<div style="font-size: 0.8rem; color: var(--muted);" id="current-mode-desc">Loading...</div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-bottom: 0.75rem;">
|
|
<label style="font-size: 0.75rem; color: var(--muted); margin-bottom: 0.25rem; display: block;">Change Mode:</label>
|
|
<select id="mode-select" style="width: 100%; padding: 0.5rem; border-radius: 4px;">
|
|
<option value="router">Router (NAT + DHCP)</option>
|
|
<option value="sniffer-inline">Inline Sniffer</option>
|
|
<option value="sniffer-passive">Passive Sniffer</option>
|
|
<option value="access-point">Access Point</option>
|
|
<option value="relay">VPN Relay</option>
|
|
</select>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<button class="btn" onclick="previewMode()" style="flex: 1;">Preview</button>
|
|
<button class="btn primary" onclick="applyMode()" style="flex: 1;">Apply</button>
|
|
</div>
|
|
<div id="mode-interfaces" style="margin-top: 0.75rem; font-size: 0.8rem; color: var(--muted);"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h2>Network Addresses</h2></div>
|
|
<div class="network-grid">
|
|
<div class="network-item">
|
|
<div class="label">LAN</div>
|
|
<div class="value" id="lan-ip">-</div>
|
|
</div>
|
|
<div class="network-item">
|
|
<div class="label">BR-WAN</div>
|
|
<div class="value" id="wan-ip">-</div>
|
|
</div>
|
|
<div class="network-item">
|
|
<div class="label">PUBLIC IPV4</div>
|
|
<div class="value" id="pub-ipv4">N/A</div>
|
|
</div>
|
|
<div class="network-item">
|
|
<div class="label">PUBLIC IPV6</div>
|
|
<div class="value" id="pub-ipv6">N/A</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h2>Quick Actions</h2></div>
|
|
<div class="quick-actions">
|
|
<button class="action-btn" onclick="restartServices()">Restart Services</button>
|
|
<button class="action-btn green" onclick="checkUpdates()">Update Packages</button>
|
|
<button class="action-btn" onclick="viewLogs()">View Logs</button>
|
|
<button class="action-btn green" onclick="exportConfig()">Export Config</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h2>Alert Timeline</h2></div>
|
|
<div id="alerts-list">
|
|
<div class="alert-item">
|
|
<div class="time">-</div>
|
|
<div class="message">No alerts</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const API = '/api/v1/hub';
|
|
|
|
// ── Authentication ────────────────────────────────────────────
|
|
function getToken() {
|
|
return localStorage.getItem('sbx_token') || localStorage.getItem('secubox_token');
|
|
}
|
|
|
|
function checkAuth() {
|
|
if (!getToken()) {
|
|
window.location.href = '/login.html?redirect=' + encodeURIComponent(window.location.pathname);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!checkAuth()) {
|
|
document.body.innerHTML = '<div style="padding:2rem;color:var(--root-main);">Redirecting to login...</div>';
|
|
}
|
|
|
|
let menuData = null;
|
|
|
|
async function api(endpoint, method = 'GET', data = null) {
|
|
const token = getToken();
|
|
if (!token) { checkAuth(); return {}; }
|
|
const opts = {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }
|
|
};
|
|
if (data) opts.body = JSON.stringify(data);
|
|
try {
|
|
const res = await fetch(API + endpoint, opts);
|
|
if (res.status === 401) {
|
|
localStorage.removeItem('sbx_token');
|
|
localStorage.removeItem('secubox_token');
|
|
checkAuth();
|
|
return {};
|
|
}
|
|
return res.json();
|
|
} catch (e) { return {}; }
|
|
}
|
|
|
|
function formatBytes(b) {
|
|
if (!b) return '0 B';
|
|
const k = 1024, s = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(b) / Math.log(k));
|
|
return (b / Math.pow(k, i)).toFixed(1) + ' ' + s[i];
|
|
}
|
|
|
|
async function loadMenu() {
|
|
try {
|
|
const data = await api('/menu');
|
|
if (!data || !data.categories) return;
|
|
menuData = data;
|
|
document.getElementById('status-badge').textContent = `${data.total_active}/${data.total_installed} Active`;
|
|
} catch (e) { console.error('Failed to load menu:', e); }
|
|
}
|
|
|
|
async function loadDashboard() {
|
|
const data = await api('/dashboard');
|
|
if (!data || data.detail) return;
|
|
|
|
const modules = data.modules || {};
|
|
const total = Object.keys(modules).length;
|
|
const active = Object.values(modules).filter(m => m.active).length;
|
|
|
|
if (data.build_info) {
|
|
const bi = data.build_info;
|
|
const buildEl = document.getElementById('build-info');
|
|
if (bi.build_timestamp) {
|
|
const d = new Date(bi.build_timestamp);
|
|
buildEl.textContent = 'Build: ' + d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
buildEl.title = `Built: ${bi.build_timestamp}\nCommit: ${bi.git_commit || 'N/A'}\nBoard: ${bi.board || 'generic'}`;
|
|
}
|
|
if (bi.version) document.getElementById('version').textContent = 'v' + bi.version;
|
|
}
|
|
|
|
document.getElementById('total-modules').textContent = total;
|
|
document.getElementById('installed-modules').textContent = menuData?.total_installed || total;
|
|
document.getElementById('active-modules').textContent = menuData?.total_active || active;
|
|
|
|
const cpu = data.cpu_percent || 0;
|
|
const mem = data.memory_percent || 0;
|
|
const disk = data.disk_percent || 0;
|
|
const load = data.load_avg || [0, 0, 0];
|
|
|
|
document.getElementById('cpu-info').textContent = 'Load: ' + load[0]?.toFixed(2);
|
|
document.getElementById('cpu-bar').style.width = cpu + '%';
|
|
document.getElementById('cpu-percent').textContent = cpu.toFixed(0) + '%';
|
|
document.getElementById('mem-bar').style.width = mem + '%';
|
|
document.getElementById('mem-percent').textContent = mem.toFixed(0) + '%';
|
|
document.getElementById('disk-bar').style.width = disk + '%';
|
|
document.getElementById('disk-percent').textContent = disk.toFixed(0) + '%';
|
|
|
|
const tbody = document.getElementById('modules-table');
|
|
if (menuData && menuData.categories) {
|
|
const allItems = menuData.categories.flatMap(c => c.items);
|
|
tbody.innerHTML = allItems.slice(0, 10).map(item => `
|
|
<tr onclick="location.href='${item.path}'" style="cursor:pointer">
|
|
<td><span class="module-icon">${item.icon}</span>${item.name}</td>
|
|
<td>${item.category}</td>
|
|
<td><span class="status-badge ${item.active ? 'active' : 'inactive'}">${item.active ? 'Active' : 'Inactive'}</span></td>
|
|
<td>-</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
tbody.innerHTML = Object.entries(modules).map(([name, m]) => `
|
|
<tr>
|
|
<td><span class="module-icon">📦</span>${name.charAt(0).toUpperCase() + name.slice(1)}</td>
|
|
<td>-</td>
|
|
<td><span class="status-badge ${m.active ? 'active' : 'inactive'}">${m.active ? 'Active' : 'Inactive'}</span></td>
|
|
<td>-</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
async function loadHealth() {
|
|
const data = await api('/system_health');
|
|
if (data && data.overall !== undefined) {
|
|
document.getElementById('health-score').textContent = data.overall + '%';
|
|
}
|
|
}
|
|
|
|
async function loadAlerts() {
|
|
const data = await api('/alerts');
|
|
const alerts = data || [];
|
|
document.getElementById('alerts-count').textContent = Array.isArray(alerts) ? alerts.length : 0;
|
|
|
|
const list = document.getElementById('alerts-list');
|
|
if (Array.isArray(alerts) && alerts.length > 0) {
|
|
list.innerHTML = alerts.slice(0, 5).map(a => `
|
|
<div class="alert-item">
|
|
<div class="time">${new Date().toLocaleTimeString()}</div>
|
|
<div class="message">${a.type?.replace(/_/g, ' ') || 'Alert'}: ${a.module || ''}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
async function loadNetwork() {
|
|
const data = await api('/network_summary');
|
|
document.getElementById('net-info').textContent = data?.wan_status || 'Unknown';
|
|
document.getElementById('lan-ip').textContent = '192.168.10.1';
|
|
document.getElementById('wan-ip').textContent = '192.168.1.1';
|
|
}
|
|
|
|
function restartServices() { api('/execute_action', 'POST', { action: 'restart_services' }).then(() => alert('Services restarted')); }
|
|
function checkUpdates() { location.href = '/system/'; }
|
|
function viewLogs() { location.href = '/system/'; }
|
|
function exportConfig() { alert('Export started'); }
|
|
|
|
async function logout() {
|
|
await fetch('/api/v1/portal/logout', { method: 'POST', credentials: 'include' });
|
|
localStorage.removeItem('secubox_token');
|
|
localStorage.removeItem('sbx_token');
|
|
window.location.href = '/login.html';
|
|
}
|
|
|
|
async function loadNetworkMode() {
|
|
const data = await api('/network_mode');
|
|
if (!data || data.detail) return;
|
|
|
|
document.getElementById('mode-icon').textContent = data.mode_icon || '🔀';
|
|
document.getElementById('current-mode-name').textContent = data.mode_name || data.current_mode || 'Router';
|
|
document.getElementById('current-mode-desc').textContent = data.mode_desc || '';
|
|
|
|
const select = document.getElementById('mode-select');
|
|
if (select && data.current_mode) select.value = data.current_mode;
|
|
|
|
if (data.available_modes && data.available_modes.length > 0) {
|
|
select.innerHTML = data.available_modes.map(m =>
|
|
`<option value="${m.id}" ${m.id === data.current_mode ? 'selected' : ''}>${m.icon || '📦'} ${m.name}</option>`
|
|
).join('');
|
|
}
|
|
|
|
const ifacesDiv = document.getElementById('mode-interfaces');
|
|
if (data.interfaces && ifacesDiv) {
|
|
const wan = (data.interfaces.wan || []).join(', ') || 'N/A';
|
|
const lan = (data.interfaces.lan || []).join(', ') || 'N/A';
|
|
ifacesDiv.innerHTML = `<span style="color: var(--wall-main);">WAN:</span> ${wan} | <span style="color: var(--root-main);">LAN:</span> ${lan}`;
|
|
}
|
|
|
|
if (data.pending_mode) {
|
|
document.getElementById('current-mode-desc').innerHTML =
|
|
`<span style="color: var(--wall-main);">Pending: ${data.pending_mode}</span>`;
|
|
}
|
|
}
|
|
|
|
async function previewMode() {
|
|
const mode = document.getElementById('mode-select').value;
|
|
const data = await api('/network_mode/preview?mode=' + mode);
|
|
if (data && data.yaml_preview) {
|
|
alert('Preview for ' + mode + ':\n\n' + data.yaml_preview.substring(0, 500) + '...\n\nGo to Network Modes page for full config.');
|
|
}
|
|
}
|
|
|
|
async function applyMode() {
|
|
const mode = document.getElementById('mode-select').value;
|
|
if (!confirm(`Change network mode to "${mode}"?\n\nThis will reconfigure network interfaces and may disconnect you temporarily.`)) return;
|
|
|
|
const token = getToken();
|
|
if (!token) { checkAuth(); return; }
|
|
|
|
try {
|
|
const res = await fetch(API + '/network_mode', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
|
body: JSON.stringify({ mode: mode, dry_run: false })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
alert('Network mode changed to: ' + mode + '\n\nPage will reload in 5 seconds...');
|
|
setTimeout(() => location.reload(), 5000);
|
|
} else {
|
|
alert('Failed to change mode: ' + (data.error || 'Unknown error'));
|
|
}
|
|
} catch (e) { alert('Error: ' + e.message); }
|
|
|
|
loadNetworkMode();
|
|
}
|
|
|
|
async function refresh() {
|
|
await loadMenu();
|
|
loadDashboard();
|
|
loadHealth();
|
|
loadAlerts();
|
|
loadNetwork();
|
|
loadNetworkMode();
|
|
}
|
|
|
|
if (checkAuth()) {
|
|
refresh();
|
|
setInterval(refresh, 30000);
|
|
}
|
|
</script>
|
|
<script src="/shared/sidebar.js"></script>
|
|
<script src="/shared/crt-engine.js"></script>
|
|
</body>
|
|
</html>
|