diff --git a/panel/app.py b/panel/app.py index 42fb9c4..e074b7a 100644 --- a/panel/app.py +++ b/panel/app.py @@ -97,7 +97,7 @@ def login_required(view): return wrapped -def apply_singbox() -> tuple[bool, str]: +def render_singbox_config() -> tuple[bool, str]: env = os.environ.copy() env["JIEDIAN_ROOT"] = str(ROOT) proc = subprocess.run( @@ -108,9 +108,23 @@ def apply_singbox() -> tuple[bool, str]: ) if proc.returncode != 0: return False, proc.stderr or proc.stdout or "配置生成失败" - restart = subprocess.run(["systemctl", "restart", "sing-box"], capture_output=True, text=True) - if restart.returncode != 0: - return False, restart.stderr or restart.stdout or "sing-box 重启失败" + return True, "ok" + + +def restart_singbox_async() -> None: + """后台重启 sing-box,避免添加/删除节点 API 长时间阻塞。""" + subprocess.Popen( + ["systemctl", "restart", "sing-box"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + +def apply_singbox() -> tuple[bool, str]: + ok, msg = render_singbox_config() + if not ok: + return False, msg + restart_singbox_async() return True, "ok" diff --git a/panel/static/app.js b/panel/static/app.js index 5bdbc47..6d44b3d 100644 --- a/panel/static/app.js +++ b/panel/static/app.js @@ -90,23 +90,38 @@ if (cancelBtn) { cancelBtn.addEventListener("click", () => modal.classList.add("hidden")); } +function setButtonBusy(btn, busy, busyText) { + if (!btn) return; + if (busy) { + if (!btn.dataset.label) btn.dataset.label = btn.textContent; + btn.textContent = busyText; + btn.disabled = true; + } else { + btn.textContent = btn.dataset.label || btn.textContent; + btn.disabled = false; + } +} + if (confirmAddBtn) { confirmAddBtn.addEventListener("click", async () => { const name = nodeName.value.trim() || "新节点"; - confirmAddBtn.disabled = true; + setButtonBusy(confirmAddBtn, true, "创建中…"); + if (cancelBtn) cancelBtn.disabled = true; try { const res = await fetch(apiUrl("/api/nodes"), { method: "POST", + credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "创建失败"); - location.reload(); + toast("节点已创建,配置生效中…"); + setTimeout(() => location.reload(), 600); } catch (err) { - toast(err.message); - } finally { - confirmAddBtn.disabled = false; + toast(err.message || "创建失败"); + setButtonBusy(confirmAddBtn, false); + if (cancelBtn) cancelBtn.disabled = false; } }); } @@ -115,15 +130,19 @@ document.querySelectorAll(".delete-btn").forEach((btn) => { btn.addEventListener("click", async () => { const id = btn.dataset.id; if (!confirm("确定删除该节点?删除后对应链接将失效。")) return; - btn.disabled = true; + setButtonBusy(btn, true, "删除中…"); try { - const res = await fetch(apiUrl(`/api/nodes/${id}`), { method: "DELETE" }); + const res = await fetch(apiUrl(`/api/nodes/${id}`), { + method: "DELETE", + credentials: "same-origin", + }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "删除失败"); - location.reload(); + toast("已删除,配置生效中…"); + setTimeout(() => location.reload(), 600); } catch (err) { - toast(err.message); - btn.disabled = false; + toast(err.message || "删除失败"); + setButtonBusy(btn, false); } }); }); diff --git a/panel/stats.py b/panel/stats.py index dce2e04..eb0283d 100644 --- a/panel/stats.py +++ b/panel/stats.py @@ -279,14 +279,17 @@ def collect_node_stats() -> dict: matched = [c for c in connections if _match_connection(c, node)] if not matched and single_node and has_connections: matched = connections + if not matched and (session_up + session_down) > 0: + matched = [None] # 有活跃会话但 Clash 未返回连接详情 if not matched and single_node and global_active: up_speed = global_up_speed down_speed = global_down_speed online = ( len(matched) > 0 + or (session_up + session_down) > 0 or (up_speed + down_speed) > 512 - or (single_node and global_active) + or (single_node and (global_active or has_connections)) ) if online: