secubox-deb/docs/screenshots/home.html
CyberMind-FR 4165d6d66c feat(network): Smart auto-IP with ARP collision detection (v1.7.0.2)
- 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>
2026-04-14 16:28:02 +02:00

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>