admin: reset traffic counter per subscription (xray api statsquery -reset)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Savsis? 2026-09-10 22:14:16 +05:00
parent fb11aafda6
commit 9a86b8cb03
4 changed files with 68 additions and 3 deletions

View file

@ -442,7 +442,7 @@
<div class="section">
<div class="section-head"><h2>По подпискам</h2></div>
<div class="table-wrap"><table><thead><tr>
<th>Пользователь</th><th>Сервер</th><th>Входящий</th><th>Исходящий</th><th>Всего</th>
<th>Пользователь</th><th>Сервер</th><th>Входящий</th><th>Исходящий</th><th>Всего</th><th></th>
</tr></thead><tbody id="traffic-body"></tbody></table></div>
</div>
</div>
@ -690,8 +690,22 @@ async function loadTraffic() {
const body = document.getElementById("traffic-body");
body.innerHTML = t.per_subscription.length ? t.per_subscription.map((r) => `
<tr><td>${esc(r.username)}</td><td>${esc(r.node_label)}</td><td>${r.up_fmt}</td><td>${r.down_fmt}</td><td>${r.total_fmt}</td></tr>
`).join("") : '<tr><td colspan="5"><div class="empty">Пока нет данных по трафику</div></td></tr>';
<tr><td>${esc(r.username)}</td><td>${esc(r.node_label)}</td><td>${r.up_fmt}</td><td>${r.down_fmt}</td><td>${r.total_fmt}</td>
<td><button class="muted-btn" onclick="resetTraffic('${r.uuid}', this)">Сбросить</button></td></tr>
`).join("") : '<tr><td colspan="6"><div class="empty">Пока нет данных по трафику</div></td></tr>';
}
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) : "—"; }

14
api.py
View file

@ -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)

View file

@ -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}'); "

View file

@ -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: