admin: reset traffic counter per subscription (xray api statsquery -reset)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
fb11aafda6
commit
9a86b8cb03
4 changed files with 68 additions and 3 deletions
20
admin.html
20
admin.html
|
|
@ -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
14
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)
|
||||
|
|
|
|||
16
nodeprov.py
16
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}'); "
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue