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: