d339fbd917
Stats and node API calls were hitting /api/* at the site root instead of the hidden PANEL_PATH, causing stats to show unavailable behind nginx. Co-authored-by: Cursor <cursoragent@cursor.com>
150 lines
4.4 KiB
JavaScript
150 lines
4.4 KiB
JavaScript
function toast(msg) {
|
|
const el = document.getElementById("toast");
|
|
el.textContent = msg;
|
|
el.classList.remove("hidden");
|
|
setTimeout(() => el.classList.add("hidden"), 2200);
|
|
}
|
|
|
|
function apiUrl(path) {
|
|
const base = (document.body.dataset.base || "").replace(/\/$/, "");
|
|
return `${base}${path}`;
|
|
}
|
|
|
|
document.querySelectorAll("[data-copy]").forEach((btn) => {
|
|
btn.addEventListener("click", async () => {
|
|
const text = btn.dataset.copy;
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
toast("已复制到剪贴板");
|
|
} catch {
|
|
toast("复制失败,请手动选择文本");
|
|
}
|
|
});
|
|
});
|
|
|
|
const modal = document.getElementById("modal");
|
|
const addBtn = document.getElementById("addBtn");
|
|
const cancelBtn = document.getElementById("cancelBtn");
|
|
const confirmAddBtn = document.getElementById("confirmAddBtn");
|
|
const nodeName = document.getElementById("nodeName");
|
|
|
|
if (addBtn) {
|
|
addBtn.addEventListener("click", () => {
|
|
nodeName.value = "";
|
|
modal.classList.remove("hidden");
|
|
nodeName.focus();
|
|
});
|
|
}
|
|
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener("click", () => modal.classList.add("hidden"));
|
|
}
|
|
|
|
if (confirmAddBtn) {
|
|
confirmAddBtn.addEventListener("click", async () => {
|
|
const name = nodeName.value.trim() || "新节点";
|
|
confirmAddBtn.disabled = true;
|
|
try {
|
|
const res = await fetch(apiUrl("/api/nodes"), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "创建失败");
|
|
location.reload();
|
|
} catch (err) {
|
|
toast(err.message);
|
|
} finally {
|
|
confirmAddBtn.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll(".delete-btn").forEach((btn) => {
|
|
btn.addEventListener("click", async () => {
|
|
const id = btn.dataset.id;
|
|
if (!confirm("确定删除该节点?删除后对应链接将失效。")) return;
|
|
btn.disabled = true;
|
|
try {
|
|
const res = await fetch(apiUrl(`/api/nodes/${id}`), { method: "DELETE" });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || "删除失败");
|
|
location.reload();
|
|
} catch (err) {
|
|
toast(err.message);
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
});
|
|
|
|
function setStatusBadge(el, online) {
|
|
el.textContent = online ? "在线" : "离线";
|
|
el.classList.toggle("online", online);
|
|
el.classList.toggle("offline", !online);
|
|
}
|
|
|
|
function updateStats(data) {
|
|
const summary = data.summary || {};
|
|
const onlineEl = document.getElementById("summaryOnline");
|
|
const upEl = document.getElementById("summaryUp");
|
|
const downEl = document.getElementById("summaryDown");
|
|
const statusEl = document.getElementById("summaryStatus");
|
|
|
|
if (onlineEl) {
|
|
onlineEl.textContent = `${summary.online || 0} / ${summary.total_nodes || 0}`;
|
|
}
|
|
if (upEl) upEl.textContent = summary.upload_speed_human || "0 B/s";
|
|
if (downEl) downEl.textContent = summary.download_speed_human || "0 B/s";
|
|
if (statusEl) {
|
|
if (data.singbox) {
|
|
statusEl.textContent = "正常";
|
|
statusEl.className = "status-text ok";
|
|
} else {
|
|
statusEl.textContent = "不可用";
|
|
statusEl.className = "status-text err";
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll(".node-card[data-id]").forEach((card) => {
|
|
const node = (data.nodes || {})[card.dataset.id];
|
|
if (!node) return;
|
|
|
|
const status = card.querySelector('[data-role="status"]');
|
|
if (status) setStatusBadge(status, node.online);
|
|
|
|
const setText = (role, value) => {
|
|
const el = card.querySelector(`[data-role="${role}"]`);
|
|
if (el) el.textContent = value;
|
|
};
|
|
|
|
setText("connections", String(node.connections ?? 0));
|
|
setText("speed-up", node.upload_speed_human || "0 B/s");
|
|
setText("speed-down", node.download_speed_human || "0 B/s");
|
|
setText("total-up", node.upload_total_human || "0 B");
|
|
setText("total-down", node.download_total_human || "0 B");
|
|
});
|
|
}
|
|
|
|
async function refreshStats() {
|
|
try {
|
|
const res = await fetch(apiUrl("/api/stats"));
|
|
if (!res.ok) {
|
|
throw new Error(`HTTP ${res.status}`);
|
|
}
|
|
const data = await res.json();
|
|
updateStats(data);
|
|
} catch {
|
|
const statusEl = document.getElementById("summaryStatus");
|
|
if (statusEl) {
|
|
statusEl.textContent = "不可用";
|
|
statusEl.className = "status-text err";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (document.getElementById("summaryBar")) {
|
|
refreshStats();
|
|
setInterval(refreshStats, 5000);
|
|
}
|