Files
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 1b227c2470 Multi-interface management, topology view, performance tuning, precompiled CSS
- Interface model with per-interface subnet/port/keys; import/adopt existing wg-quick configs (key-less imported peers, optional key rotation), cascade delete
- Split wireguard.py into a package (keys via cryptography X25519, status, addressing, conf parse/render, sync, host tuning)
- ECharts horizontal topology view (interface -> peers -> site subnets)
- Advanced options: MTU, MSS clamping, FwMark/Table, custom PostUp/PostDown, per-peer keepalive override
- Runtime settings (sample interval/retention, online threshold, UI refresh) with traffic sample pruning; host tuning (UDP buffers, backlog, GRO forwarding)
- Precompiled Tailwind CSS replacing Play CDN runtime (fixes FOUC); stable table layout and diffed polling renders
- Host network mode in compose; NAT/isolation iptables moved into app sync

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-07-05 14:01:44 +08:00

73 lines
2.4 KiB
JavaScript

let refreshMs = 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;
}
if (data.meta && data.meta.refresh_seconds) {
refreshMs = data.meta.refresh_seconds * 1000;
}
const tbody = document.querySelector("#peer-table tbody");
if (!tbody) return;
const badgeBase = "rounded-full px-2 py-0.5 text-xs font-medium";
const esc = (s) =>
String(s).replace(/[&<>"]/g, (ch) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]
);
let html = "";
for (const iface of data.interfaces) {
for (const p of iface.peers) {
const badge = p.online
? `<span class="${badgeBase} bg-emerald-100 text-emerald-700">online</span>`
: p.enabled
? `<span class="${badgeBase} bg-slate-100 text-slate-500">offline</span>`
: `<span class="${badgeBase} bg-red-100 text-red-700">disabled</span>`;
html +=
`<tr class="hover:bg-slate-50">` +
`<td class="truncate px-4 py-3"><a href="/peers/${p.id}" class="font-medium text-blue-600 hover:underline">${esc(p.name)}</a></td>` +
`<td class="truncate px-4 py-3 text-slate-500">${esc(iface.name)}</td>` +
`<td class="truncate px-4 py-3 font-mono text-xs">${esc(p.address)}</td>` +
`<td class="px-4 py-3">${badge}</td>` +
`<td class="whitespace-nowrap px-4 py-3 text-slate-500 tabular-nums">${fmtHandshake(p.latest_handshake)}</td>` +
`<td class="whitespace-nowrap px-4 py-3 text-slate-500 tabular-nums">${fmtBytes(p.rx_bytes)}</td>` +
`<td class="whitespace-nowrap px-4 py-3 text-slate-500 tabular-nums">${fmtBytes(p.tx_bytes)}</td>` +
`</tr>`;
}
}
if (tbody.dataset.html !== html) {
tbody.dataset.html = html;
tbody.innerHTML = html;
}
}
async function loop() {
await refresh();
setTimeout(loop, refreshMs);
}
loop();