Initial WireGuard admin panel: FastAPI UI, peer key management, live monitoring, Docker deploy
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit
998dfe702c
@@ -0,0 +1,64 @@
|
||||
const REFRESH_MS = 5000;
|
||||
|
||||
function fmtBytes(num) {
|
||||
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
||||
let i = 0;
|
||||
while (Math.abs(num) >= 1024 && i < units.length - 1) {
|
||||
num /= 1024;
|
||||
i++;
|
||||
}
|
||||
return num.toFixed(1) + " " + units[i];
|
||||
}
|
||||
|
||||
function fmtHandshake(iso) {
|
||||
if (!iso) return "never";
|
||||
const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
|
||||
if (seconds < 60) return seconds + "s ago";
|
||||
if (seconds < 3600) return Math.floor(seconds / 60) + "m ago";
|
||||
return Math.floor(seconds / 3600) + "h ago";
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
let data;
|
||||
try {
|
||||
const res = await fetch("/api/status");
|
||||
if (!res.ok) return;
|
||||
data = await res.json();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const online = data.peers.filter((p) => p.online).length;
|
||||
const set = (id, value) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = value;
|
||||
};
|
||||
set("peer-count", data.peers.length);
|
||||
set("online-count", online);
|
||||
set("total-rx", fmtBytes(data.interface.total_rx));
|
||||
set("total-tx", fmtBytes(data.interface.total_tx));
|
||||
|
||||
const tbody = document.querySelector("#peer-table tbody");
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = "";
|
||||
for (const p of data.peers) {
|
||||
const tr = document.createElement("tr");
|
||||
const badge = p.online
|
||||
? '<span class="badge ok">online</span>'
|
||||
: p.enabled
|
||||
? '<span class="badge muted">offline</span>'
|
||||
: '<span class="badge err">disabled</span>';
|
||||
tr.innerHTML =
|
||||
`<td><a href="/peers/${p.id}"></a></td>` +
|
||||
`<td>${p.address}</td>` +
|
||||
`<td>${badge}</td>` +
|
||||
`<td>${fmtHandshake(p.latest_handshake)}</td>` +
|
||||
`<td>${fmtBytes(p.rx_bytes)}</td>` +
|
||||
`<td>${fmtBytes(p.tx_bytes)}</td>`;
|
||||
tr.querySelector("a").textContent = p.name;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, REFRESH_MS);
|
||||
@@ -0,0 +1,88 @@
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: #f4f5f7;
|
||||
color: #1f2430;
|
||||
}
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #1f2430;
|
||||
color: #fff;
|
||||
}
|
||||
nav .brand { font-weight: 700; margin-right: 1rem; }
|
||||
nav a { color: #cdd3e0; text-decoration: none; }
|
||||
nav a:hover { color: #fff; }
|
||||
nav form { margin-left: auto; }
|
||||
main { max-width: 960px; margin: 0 auto; padding: 1.5rem; }
|
||||
h1 { margin-top: 0; }
|
||||
|
||||
.cards { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||
flex: 1 1 220px;
|
||||
}
|
||||
.card h3 { margin: 0 0 0.5rem; font-size: 0.85rem; text-transform: uppercase; color: #6b7280; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; }
|
||||
th, td { text-align: left; padding: 0.6rem 0.8rem; border-bottom: 1px solid #e5e7eb; }
|
||||
thead th { background: #eef0f4; font-size: 0.8rem; text-transform: uppercase; color: #6b7280; }
|
||||
table.kv { background: transparent; }
|
||||
table.kv th { width: 200px; color: #6b7280; }
|
||||
|
||||
.badge { padding: 0.15rem 0.5rem; border-radius: 999px; font-size: 0.75rem; }
|
||||
.badge.ok { background: #d1fae5; color: #065f46; }
|
||||
.badge.err { background: #fee2e2; color: #991b1b; }
|
||||
.badge.muted { background: #e5e7eb; color: #4b5563; }
|
||||
.muted { color: #6b7280; }
|
||||
.error { color: #991b1b; }
|
||||
|
||||
button, .button {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.45rem 0.9rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
button:hover, .button:hover { background: #1d4ed8; }
|
||||
button.danger { background: #dc2626; }
|
||||
button.danger:hover { background: #b91c1c; }
|
||||
button.link { background: none; padding: 0; color: #cdd3e0; }
|
||||
button.link:hover { color: #fff; background: none; }
|
||||
form.inline { display: inline; }
|
||||
.actions form { margin-right: 0.25rem; }
|
||||
.actions-row { margin-top: 1rem; display: flex; gap: 0.5rem; }
|
||||
|
||||
input {
|
||||
padding: 0.45rem 0.6rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.add-peer { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem; }
|
||||
|
||||
.login-page { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
||||
.login-box {
|
||||
background: #fff;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||
width: 320px;
|
||||
}
|
||||
.login-box h1 { font-size: 1.3rem; text-align: center; }
|
||||
.login-box label { display: block; margin-bottom: 0.75rem; font-size: 0.9rem; }
|
||||
.login-box input { width: 100%; margin-top: 0.25rem; }
|
||||
.login-box button { width: 100%; margin-top: 0.5rem; }
|
||||
|
||||
.qr { max-width: 220px; display: block; margin-bottom: 0.5rem; }
|
||||
code { word-break: break-all; font-size: 0.85em; }
|
||||
Reference in New Issue
Block a user