From 9a86b8cb037277388fb9a7c147d63d37abcb789c Mon Sep 17 00:00:00 2001 From: savsis Date: Thu, 10 Sep 2026 22:14:16 +0500 Subject: [PATCH] admin: reset traffic counter per subscription (xray api statsquery -reset) Co-Authored-By: Claude Sonnet 5 --- admin.html | 20 +++++++++++++++++--- api.py | 14 ++++++++++++++ nodeprov.py | 16 ++++++++++++++++ xray_manager.py | 21 +++++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/admin.html b/admin.html index 068947d..7aad355 100644 --- a/admin.html +++ b/admin.html @@ -442,7 +442,7 @@

По подпискам

- +
ПользовательСерверВходящийИсходящийВсегоПользовательСерверВходящийИсходящийВсего
@@ -690,8 +690,22 @@ async function loadTraffic() { const body = document.getElementById("traffic-body"); body.innerHTML = t.per_subscription.length ? t.per_subscription.map((r) => ` - ${esc(r.username)}${esc(r.node_label)}${r.up_fmt}${r.down_fmt}${r.total_fmt} - `).join("") : '
Пока нет данных по трафику
'; + ${esc(r.username)}${esc(r.node_label)}${r.up_fmt}${r.down_fmt}${r.total_fmt} + + `).join("") : '
Пока нет данных по трафику
'; +} + +async function resetTraffic(uuid, btn) { + if (!confirm("Сбросить счётчик трафика для этой подписки?")) return; + btn.disabled = true; + btn.textContent = "…"; + try { + await api(`/admin/api/subscriptions/${uuid}/reset-traffic`, { method: "POST" }); + loadTraffic(); + } catch (e) { + btn.disabled = false; + btn.textContent = "Сбросить"; + } } function fmtDate(iso) { return iso ? iso.slice(0, 10) : "—"; } diff --git a/api.py b/api.py index b42c9d8..650e4cb 100644 --- a/api.py +++ b/api.py @@ -478,6 +478,20 @@ def admin_revoke_subscription(uuid: str, request: Request): return {"ok": True} +@app.post("/admin/api/subscriptions/{uuid}/reset-traffic") +def admin_reset_traffic(uuid: str, request: Request): + require_admin(request) + subs = db.list_all_subscriptions(limit=5000) + sub = next((s for s in subs if s["uuid"] == uuid), None) + if not sub: + raise HTTPException(404, "not found") + node = db.get_node(sub["node"]) + if not node: + raise HTTPException(404, "node not found") + ok = xray_manager.reset_stats_for_node(node, uuid) + return {"ok": ok} + + @app.get("/admin/api/users/{tg_id}/devices") def admin_list_devices(tg_id: int, request: Request): require_admin(request) diff --git a/nodeprov.py b/nodeprov.py index 947adf0..fdf8e50 100644 --- a/nodeprov.py +++ b/nodeprov.py @@ -330,6 +330,22 @@ def remote_query_stats(node: dict) -> dict: return result +def remote_reset_stats(node: dict, email: str) -> bool: + try: + client = _mgmt_connect(node["address"]) + try: + client.exec_command( + f"/usr/local/bin/xray api statsquery --server=127.0.0.1:10085 " + f"-pattern 'user>>>{email}>>>traffic' -reset", + timeout=10, + ) + finally: + client.close() + return True + except Exception: + return False + + STATUS_CMD = ( "echo LOAD:$(cut -d' ' -f1 /proc/loadavg); " "echo MEMTOTAL:$(grep MemTotal /proc/meminfo | awk '{print $2}'); " diff --git a/xray_manager.py b/xray_manager.py index 54e4474..0c75c20 100644 --- a/xray_manager.py +++ b/xray_manager.py @@ -159,6 +159,27 @@ def query_stats() -> dict: return result +def reset_stats(email: str) -> bool: + try: + subprocess.run( + ["/usr/local/bin/xray", "api", "statsquery", "--server=127.0.0.1:10085", + "-pattern", f"user>>>{email}>>>traffic", "-reset"], + capture_output=True, text=True, timeout=10, + ) + return True + except Exception: + return False + + +def reset_stats_for_node(node: dict, email: str) -> bool: + if node["kind"] == "local": + return reset_stats(email) + if node["kind"] == "managed": + import nodeprov + return nodeprov.remote_reset_stats(node, email) + return False + + def local_node_status() -> dict: try: with open("/proc/loadavg") as f: