secubox-deb/packages/secubox-reality/webui/reality.html
CyberMind-FR f9ac5289e6 feat(reality): fleet crt-light skin + shared sidebar + navbar menu entry
Swap the standalone MESH-blue charter in reality.html for the fleet
crt-light/P31-phosphor skin (Courier Prime, --p31-* palette,
.main{margin-left:220px}) matching secubox-tor and other webuis, and
replace the bespoke breadcrumb sidebar with the shared sidebar.js/
sidebar-light.css injector so /reality/ appears in the common navbar.
All panels (server list+create, validate-target, apply, clients+link
QR, volume-guard config+stats) and their JS/fetch wiring are unchanged.

Add menu.d/562-reality.json (mesh category) and install it via
debian/rules, mirroring secubox-tor. python3-segno was already
declared in debian/control.
2026-07-09 15:07:58 +02:00

551 lines
24 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 - Reality</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/shared/crt-light.css">
<link rel="stylesheet" href="/shared/sidebar-light.css">
<style>
:root {
/* P31 Phosphor spectrum */
--p31-peak: #00dd44;
--p31-hot: #00ff55;
--p31-mid: #009933;
--p31-dim: #006622;
--p31-ghost: #003311;
--p31-decay: #ffb347;
--p31-decay-dim: #cc7722;
/* Tube glass */
--tube-light: #e8f5e9;
--tube-pale: #c8e6c9;
--tube-soft: #a5d6a7;
/* Legacy mappings */
--bg-dark: var(--tube-light);
--bg-card: var(--tube-pale);
--bg-sidebar: var(--tube-black);
--border: var(--tube-soft);
--text: var(--tube-dark);
--text-dim: var(--p31-dim);
--primary: var(--p31-peak);
--cyan: var(--p31-peak);
--green: var(--p31-peak);
--red: #ff4466;
--yellow: var(--p31-decay);
--orange: var(--p31-decay-dim);
--purple: #a371f7;
/* Bloom effects */
--bloom-text: 0 0 2px var(--p31-peak), 0 0 6px var(--p31-peak), 0 0 14px rgba(51,255,102,0.5);
--bloom-soft: 0 0 6px var(--p31-peak), 0 0 14px rgba(51,255,102,0.5);
--bloom-amber: 0 0 3px var(--p31-decay), 0 0 10px rgba(255,179,71,0.4);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Courier Prime', 'Courier New', monospace;
background: var(--tube-light);
background-image: radial-gradient(ellipse at 50% 40%, rgba(51,255,102,0.025) 0%, transparent 70%);
color: var(--tube-dark);
display: flex;
min-height: 100vh;
font-size: 14px;
line-height: 1.6;
}
code, .mono, input, textarea, select, button, table { font-family: 'Courier Prime', 'Courier New', monospace; }
.main { flex: 1; margin-left: 220px; padding: 1.5rem; min-width: 0; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.25rem; flex-wrap: wrap; gap: 0.75rem; }
.header h1 {
font-size: 1.4rem; font-weight: 700; letter-spacing: 0.05em;
color: var(--p31-hot); text-shadow: var(--bloom-text);
display: flex; align-items: center; gap: 0.5rem;
}
.header .desc { font-size: 0.8rem; color: var(--text-dim); margin-top: 0.2rem; }
#errbar { display: none; background: rgba(255,68,102,0.1); border: 1px solid var(--red); color: var(--red); padding: 0.6rem 1rem; border-radius: 8px; margin-bottom: 1rem; font-size: 0.8rem; }
#errbar.show { display: block; }
.card {
background: var(--tube-pale); border: 1px solid var(--tube-soft); border-radius: 8px;
padding: 1.25rem 1.4rem; margin-bottom: 1.25rem;
box-shadow: inset 0 0 20px rgba(0,0,0,0.3);
}
.card:hover { border-color: var(--p31-dim); box-shadow: 0 0 8px rgba(51,255,102,0.05), inset 0 0 20px rgba(0,0,0,0.3); }
.card h2 {
font-size: 0.85rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
color: var(--p31-decay); text-shadow: var(--bloom-amber);
margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;
}
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
@media (max-width: 900px) { .grid2 { grid-template-columns: 1fr; } }
@media (max-width: 768px) { .main { margin-left: 0; } }
label { display: block; font-size: 0.7rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.08em; margin: 0.6rem 0 0.25rem; }
input, select {
width: 100%; padding: 0.45rem 0.6rem; border-radius: 6px; font-size: 0.85rem;
font-family: 'Courier Prime', monospace;
background: var(--tube-light); border: 1px solid var(--tube-soft); color: var(--p31-mid);
}
input:focus, select:focus { outline: none; border-color: var(--p31-dim); box-shadow: 0 0 8px rgba(51,255,102,0.1); }
.btn {
padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.8rem;
font-family: 'Courier Prime', monospace; letter-spacing: 0.06em; text-transform: uppercase;
border: 1px solid var(--tube-soft); background: var(--tube-pale); color: var(--p31-mid);
transition: all 0.2s;
}
.btn:hover { border-color: var(--p31-dim); color: var(--p31-peak); text-shadow: var(--bloom-soft); box-shadow: 0 0 12px rgba(51,255,102,0.1); }
.btn.primary { border-color: var(--p31-peak); color: var(--p31-peak); background: rgba(51,255,102,0.1); }
.btn.primary:hover { background: rgba(51,255,102,0.2); box-shadow: 0 0 16px rgba(51,255,102,0.2); color: var(--p31-peak); }
.btn.danger { border-color: var(--red); color: var(--red); background: transparent; }
.btn.danger:hover { background: rgba(255,68,102,0.15); }
.btn.small { padding: 0.3rem 0.6rem; font-size: 0.7rem; }
.actions { display: flex; gap: 0.5rem; margin-top: 0.75rem; flex-wrap: wrap; align-items: center; }
table { width: 100%; border-collapse: collapse; font-size: 12px; font-family: 'Courier Prime', monospace; }
th, td { padding: 0.5rem 0.6rem; text-align: left; border-bottom: 1px solid var(--p31-ghost); vertical-align: top; }
th { color: var(--p31-decay); text-shadow: var(--bloom-amber); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; }
td { color: var(--p31-mid); }
tr:hover { background: rgba(51,255,102,0.03); }
.mono-cell { font-size: 11px; word-break: break-all; }
.empty { color: var(--p31-dim); font-style: italic; padding: 0.6rem 0; font-size: 0.8rem; }
.pill { display: inline-block; padding: 0.15rem 0.55rem; border-radius: 999px; font-size: 10px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; border: 1px solid; }
.pill.green { background: rgba(51,255,102,0.12); border-color: var(--p31-peak); color: var(--p31-peak); text-shadow: var(--bloom-soft); }
.pill.yellow { background: rgba(255,179,71,0.12); border-color: var(--p31-decay); color: var(--p31-decay-dim); }
.pill.red { background: rgba(255,68,102,0.12); border-color: var(--red); color: var(--red); }
.pill.blue { background: rgba(51,255,102,0.08); border-color: var(--p31-mid); color: var(--p31-mid); }
.pill.gray { background: rgba(0,102,34,0.08); border-color: var(--p31-ghost); color: var(--p31-dim); }
.qr-box { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-top: 0.75rem; }
.qr-box svg { width: 160px; height: 160px; border: 1px solid var(--tube-soft); border-radius: 8px; background: #fff; }
.uri-box { flex: 1; min-width: 240px; word-break: break-all; font-size: 11px; background: var(--tube-light); border: 1px solid var(--tube-soft); border-radius: 6px; padding: 0.6rem; color: var(--p31-mid); }
.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; margin-top: 0.75rem; }
.stat { background: var(--tube-light); border: 1px solid var(--tube-soft); border-radius: 8px; padding: 0.75rem 0.9rem; }
.stat .l { font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--p31-dim); }
.stat .v { font-size: 18px; font-weight: 700; color: var(--p31-peak); text-shadow: 0 0 10px currentColor; margin-top: 0.2rem; }
@media (max-width: 700px) { .stat-row { grid-template-columns: repeat(2, 1fr); } }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--tube-light); }
::-webkit-scrollbar-thumb { background: var(--p31-dim); }
::-webkit-scrollbar-thumb:hover { background: var(--p31-mid); }
</style>
</head>
<body class="crt-light">
<nav class="sidebar" id="sidebar"></nav>
<script src="/shared/sidebar.js"></script>
<main class="main">
<div class="header">
<div>
<h1>🕶️ Reality egress manager</h1>
<div class="desc">Provision, validate and monitor VLESS+Reality+Vision (Xray-core) egress points.</div>
</div>
<button class="btn" id="refreshAll">↻ Refresh</button>
</div>
<div id="errbar"></div>
<!-- server list + create -->
<section class="card" id="servers">
<h2>Egress points</h2>
<table id="serversTable">
<thead><tr><th>Remark</th><th>dest / SNI</th><th>Port</th><th>Status</th><th>Public key</th><th></th></tr></thead>
<tbody></tbody>
</table>
<div class="empty" id="serversEmpty" style="display:none;">No egress point provisioned yet.</div>
<h2 style="margin-top:1.5rem;">Provision new server</h2>
<form id="createServerForm">
<div class="grid2">
<div>
<label>Remark</label>
<input name="remark" required maxlength="128" placeholder="edge-eu-1">
<label>Reality dest (camouflage target)</label>
<input name="dest" required placeholder="www.microsoft.com">
<label>SNI</label>
<input name="sni" required placeholder="www.microsoft.com">
</div>
<div>
<label>Listen port</label>
<input name="listen_port" type="number" value="443" min="1" max="65535">
<label>Dest port</label>
<input name="dest_port" type="number" value="443" min="1" max="65535">
<label>shortId count</label>
<input name="short_id_count" type="number" value="1" min="1" max="8">
</div>
</div>
<div class="actions"><button class="btn primary" type="submit">Provision (keys + UUID)</button></div>
</form>
</section>
<!-- validate target -->
<section class="card" id="validate">
<h2>Validate target (TLS1.3 + X25519 sanity gate)</h2>
<form id="validateForm">
<div class="grid2">
<div><label>Host</label><input name="host" required placeholder="www.microsoft.com"></div>
<div><label>Port</label><input name="port" type="number" value="443" min="1" max="65535"></div>
</div>
<div class="actions"><button class="btn" type="submit">Validate</button></div>
</form>
<div id="validateResult" style="margin-top:0.75rem;"></div>
</section>
<!-- apply + clients + link -->
<section class="card" id="apply">
<h2>Apply (write config + start instance)</h2>
<div class="grid2">
<div>
<label>Server</label>
<select id="applyServerSelect"></select>
<label><input type="checkbox" id="applyForce" style="width:auto;display:inline-block;"> force (skip target sanity gate)</label>
</div>
<div style="align-self:end;">
<div class="actions"><button class="btn primary" id="applyBtn" type="button">Apply</button></div>
</div>
</div>
<div id="applyResult" style="margin-top:0.75rem;"></div>
</section>
<section class="card" id="clients">
<h2>Clients + link</h2>
<div class="grid2">
<div>
<label>Server</label>
<select id="clientServerSelect"></select>
<label>Client email / remark</label>
<input id="clientEmail" placeholder="alice@example.com">
<div class="actions"><button class="btn" id="createClientBtn" type="button">Add client</button></div>
</div>
<div>
<table id="clientsTable">
<thead><tr><th>Email</th><th>shortId</th><th></th></tr></thead>
<tbody></tbody>
</table>
<div class="empty" id="clientsEmpty" style="display:none;">No clients for this server.</div>
</div>
</div>
<div class="qr-box" id="linkBox" style="display:none;">
<div id="qrHolder"></div>
<div class="uri-box" id="uriHolder"></div>
</div>
</section>
<!-- volume guard -->
<section class="card" id="guard">
<h2>Volume guard (sliding window)</h2>
<form id="guardForm">
<div class="grid2">
<div>
<label>Window (hours)</label>
<input name="window_hours" type="number" min="1" value="48">
<label>Soft limit (GB)</label>
<input name="soft_gb" type="number" min="0" step="0.1" value="80">
</div>
<div>
<label>Hard limit (GB, action = disable)</label>
<input name="hard_gb" type="number" min="0" step="0.1" value="100">
</div>
</div>
<div class="actions"><button class="btn primary" type="submit">Save guard config</button></div>
</form>
</section>
<section class="card" id="stats">
<h2>Stats (per-window delta)</h2>
<div id="statsBox" class="empty">No stats yet — click Refresh.</div>
</section>
</main>
<script>
(function () {
'use strict';
var API_BASE = '/api/mesh/reality';
function esc(s) {
if (s === null || s === undefined) return '';
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function showError(msg) {
var bar = document.getElementById('errbar');
bar.textContent = msg;
bar.classList.add('show');
setTimeout(function () { bar.classList.remove('show'); }, 6000);
}
// Fail-safe fetch: never throws to the caller's caller; always resolves
// to {ok, status, data} so UI code can render a graceful error instead
// of a blank/broken panel.
function api(path, opts) {
opts = opts || {};
var token = window.localStorage.getItem('sbx_token') || '';
var headers = Object.assign({ 'Content-Type': 'application/json' }, opts.headers || {});
if (token) headers['Authorization'] = 'Bearer ' + token;
return fetch(API_BASE + path, {
method: opts.method || 'GET',
headers: headers,
body: opts.body ? JSON.stringify(opts.body) : undefined,
credentials: 'same-origin',
}).then(function (resp) {
if (resp.status === 401) {
window.location.href = '/login.html?redirect=' + encodeURIComponent(window.location.pathname);
return { ok: false, status: 401, data: null };
}
return resp.text().then(function (text) {
var data = null;
try { data = text ? JSON.parse(text) : null; } catch (e) { /* non-JSON body */ }
return { ok: resp.ok, status: resp.status, data: data };
});
}).catch(function (err) {
showError('Network error: ' + err.message);
return { ok: false, status: 0, data: null };
});
}
function statusPill(status) {
var cls = 'gray';
if (status === 'running') cls = 'green';
else if (status === 'applied') cls = 'blue';
else if (status === 'disabled_volume_guard') cls = 'red';
else if (status === 'new') cls = 'yellow';
return '<span class="pill ' + cls + '">' + esc(status) + '</span>';
}
var STATE = { servers: [], selectedClientServer: null };
function loadServers() {
return api('/servers').then(function (r) {
if (!r.ok || !r.data) { showError('Failed to load servers'); return; }
STATE.servers = r.data;
renderServers();
renderServerSelects();
});
}
function renderServers() {
var tbody = document.querySelector('#serversTable tbody');
tbody.innerHTML = '';
document.getElementById('serversEmpty').style.display = STATE.servers.length ? 'none' : 'block';
STATE.servers.forEach(function (s) {
var tr = document.createElement('tr');
tr.innerHTML =
'<td>' + esc(s.remark) + '</td>' +
'<td class="mono-cell">' + esc(s.dest) + ' / ' + esc(s.sni) + '</td>' +
'<td>' + esc(s.listen_port) + '</td>' +
'<td>' + statusPill(s.status) + '</td>' +
'<td class="mono-cell">' + esc((s.public_key || '').slice(0, 16)) + '…</td>' +
'<td><button class="btn small danger" data-del="' + esc(s.id) + '">Delete</button></td>';
tbody.appendChild(tr);
});
tbody.querySelectorAll('[data-del]').forEach(function (btn) {
btn.addEventListener('click', function () {
if (!confirm('Delete this egress point? Keys and config are destroyed.')) return;
api('/servers/' + encodeURIComponent(btn.getAttribute('data-del')), { method: 'DELETE' })
.then(function (r) { if (r.ok) { loadServers(); } else { showError('Delete failed'); } });
});
});
}
function renderServerSelects() {
['applyServerSelect', 'clientServerSelect'].forEach(function (id) {
var sel = document.getElementById(id);
var prev = sel.value;
sel.innerHTML = '';
STATE.servers.forEach(function (s) {
var opt = document.createElement('option');
opt.value = s.id;
opt.textContent = s.remark + ' (' + s.status + ')';
sel.appendChild(opt);
});
if (prev) sel.value = prev;
});
if (document.getElementById('clientServerSelect').value) {
loadClients(document.getElementById('clientServerSelect').value);
}
}
document.getElementById('createServerForm').addEventListener('submit', function (ev) {
ev.preventDefault();
var fd = new FormData(ev.target);
api('/servers', {
method: 'POST',
body: {
remark: fd.get('remark'),
dest: fd.get('dest'),
sni: fd.get('sni'),
listen_port: parseInt(fd.get('listen_port'), 10),
dest_port: parseInt(fd.get('dest_port'), 10),
short_id_count: parseInt(fd.get('short_id_count'), 10),
},
}).then(function (r) {
if (r.ok) { ev.target.reset(); loadServers(); }
else { showError('Provision failed: ' + esc(r.data && r.data.detail || r.status)); }
});
});
document.getElementById('validateForm').addEventListener('submit', function (ev) {
ev.preventDefault();
var fd = new FormData(ev.target);
var box = document.getElementById('validateResult');
box.innerHTML = 'Validating…';
api('/validate-target', { method: 'POST', body: { host: fd.get('host'), port: parseInt(fd.get('port'), 10) } })
.then(function (r) {
if (!r.ok || !r.data) { box.innerHTML = '<span class="pill red">error</span>'; return; }
var d = r.data;
box.innerHTML =
(d.ok ? '<span class="pill green">SANE</span>' : '<span class="pill red">INSANE</span>') + ' ' +
'<span class="pill ' + (d.reachable ? 'green' : 'red') + '">reachable=' + d.reachable + '</span> ' +
'<span class="pill ' + (d.tls13 ? 'green' : 'red') + '">TLS1.3=' + d.tls13 + '</span> ' +
'<span class="pill ' + (d.x25519 ? 'green' : 'red') + '">X25519=' + d.x25519 + '</span> ' +
'<span class="pill blue">ALPN=' + esc((d.alpn || []).join(',') || '-') + '</span>' +
(d.reason ? '<div class="empty">' + esc(d.reason) + '</div>' : '');
});
});
document.getElementById('applyBtn').addEventListener('click', function () {
var id = document.getElementById('applyServerSelect').value;
if (!id) { showError('No server selected'); return; }
var force = document.getElementById('applyForce').checked;
var box = document.getElementById('applyResult');
box.innerHTML = 'Applying…';
api('/apply?force=' + (force ? 'true' : 'false'), { method: 'POST', body: { server_id: id } })
.then(function (r) {
if (!r.ok || !r.data) {
box.innerHTML = '<span class="pill red">' + esc(r.data && r.data.detail || 'apply failed') + '</span>';
return;
}
var inst = r.data.instance || {};
box.innerHTML = '<span class="pill ' + (inst.running ? 'green' : 'yellow') + '">' +
esc(inst.unit) + ' — ' + esc(inst.active) + '</span>';
loadServers();
});
});
document.getElementById('createClientBtn').addEventListener('click', function () {
var id = document.getElementById('clientServerSelect').value;
var email = document.getElementById('clientEmail').value.trim();
if (!id || !email) { showError('Select a server and enter a client email/remark'); return; }
api('/clients', { method: 'POST', body: { server_id: id, email: email } })
.then(function (r) {
if (r.ok) { document.getElementById('clientEmail').value = ''; loadClients(id); }
else { showError('Create client failed'); }
});
});
function loadClients(serverId) {
STATE.selectedClientServer = serverId;
return api('/clients?server_id=' + encodeURIComponent(serverId)).then(function (r) {
var tbody = document.querySelector('#clientsTable tbody');
tbody.innerHTML = '';
var clients = (r.ok && r.data) ? r.data : [];
document.getElementById('clientsEmpty').style.display = clients.length ? 'none' : 'block';
clients.forEach(function (c) {
var tr = document.createElement('tr');
tr.innerHTML =
'<td>' + esc(c.email) + '</td>' +
'<td class="mono-cell">' + esc(c.short_id) + '</td>' +
'<td>' +
'<button class="btn small" data-link="' + esc(c.id) + '">Link</button> ' +
'<button class="btn small danger" data-delc="' + esc(c.id) + '">Del</button>' +
'</td>';
tbody.appendChild(tr);
});
tbody.querySelectorAll('[data-link]').forEach(function (btn) {
btn.addEventListener('click', function () { showLink(btn.getAttribute('data-link')); });
});
tbody.querySelectorAll('[data-delc]').forEach(function (btn) {
btn.addEventListener('click', function () {
api('/clients/' + encodeURIComponent(btn.getAttribute('data-delc')), { method: 'DELETE' })
.then(function (r2) { if (r2.ok) loadClients(serverId); });
});
});
});
}
document.getElementById('clientServerSelect').addEventListener('change', function (ev) {
loadClients(ev.target.value);
});
function showLink(clientId) {
api('/link?client_id=' + encodeURIComponent(clientId)).then(function (r) {
if (!r.ok || !r.data) { showError('Failed to build link'); return; }
var box = document.getElementById('linkBox');
box.style.display = 'flex';
document.getElementById('uriHolder').textContent = r.data.uri || '';
var qrHolder = document.getElementById('qrHolder');
// qr_svg is generated server-side (segno) from a URI we built
// ourselves — not raw user input — so it's safe to insert as markup.
qrHolder.innerHTML = r.data.qr_svg || '<div class="empty">QR unavailable (segno not installed)</div>';
});
}
document.getElementById('guardForm').addEventListener('submit', function (ev) {
ev.preventDefault();
var fd = new FormData(ev.target);
api('/guard', {
method: 'PUT',
body: {
window_hours: parseInt(fd.get('window_hours'), 10),
soft_gb: parseFloat(fd.get('soft_gb')),
hard_gb: parseFloat(fd.get('hard_gb')),
action: 'disable',
},
}).then(function (r) {
if (r.ok) { loadStats(); } else { showError('Save guard failed: ' + esc(r.data && r.data.detail || r.status)); }
});
});
function loadGuard() {
api('/guard').then(function (r) {
if (!r.ok || !r.data) return;
var f = document.getElementById('guardForm');
f.window_hours.value = r.data.window_hours;
f.soft_gb.value = r.data.soft_gb;
f.hard_gb.value = r.data.hard_gb;
});
}
function loadStats() {
var box = document.getElementById('statsBox');
api('/stats').then(function (r) {
if (!r.ok || !r.data || !r.data.servers || !r.data.servers.length) {
box.className = 'empty';
box.textContent = 'No stats yet.';
return;
}
box.className = '';
box.innerHTML = r.data.servers.map(function (s) {
return '<div class="stat-row">' +
'<div class="stat"><div class="l">' + esc(s.server_id.slice(0, 8)) + '</div><div class="v">' + esc(s.total_gb) + ' GB</div></div>' +
'<div class="stat"><div class="l">window</div><div class="v">' + esc(s.window_hours) + 'h</div></div>' +
'<div class="stat"><div class="l">soft/hard</div><div class="v">' + esc(s.soft_gb) + '/' + esc(s.hard_gb) + '</div></div>' +
'<div class="stat"><div class="l">action</div><div class="v">' + (s.action_taken ? '<span class="pill red">' + esc(s.action_taken) + '</span>' : '<span class="pill green">ok</span>') + '</div></div>' +
'</div>';
}).join('');
});
}
document.getElementById('refreshAll').addEventListener('click', function () {
loadServers(); loadGuard(); loadStats();
});
// initial load
loadServers();
loadGuard();
loadStats();
})();
</script>
<script src="/shared/crt-engine.js"></script>
</body>
</html>