feat: tune visual of web app admin panel

This commit is contained in:
3252a8
2026-05-12 09:59:15 +03:00
parent fe976022ec
commit c8d4b3565c
8 changed files with 766 additions and 91 deletions
+81
View File
@@ -556,6 +556,32 @@ class PanelApiService:
return response_data.get("response")
return None
async def get_nodes_bandwidth_usage(
self,
*,
start: str,
end: str,
top_nodes_limit: int = 64,
) -> Optional[Dict[str, Any]]:
"""Per-node usage for a date range (Remnawave GET /bandwidth-stats/nodes).
Query dates are calendar dates (YYYY-MM-DD), same as the panel UI analytics.
Response includes topNodes[{ uuid, name, countryCode, total }, ...] where total is bytes.
"""
response_data = await self._request(
"GET",
"/bandwidth-stats/nodes",
params={
"start": start,
"end": end,
"topNodesLimit": top_nodes_limit,
},
log_full_response=False,
)
if response_data and not response_data.get("error") and "response" in response_data:
return response_data.get("response")
return None
async def get_user_bandwidth_stats(self, user_uuid: str) -> Optional[Dict[str, Any]]:
endpoint = f"/bandwidth-stats/users/{user_uuid}"
response_data = await self._request("GET", endpoint, log_full_response=False)
@@ -700,6 +726,61 @@ class PanelApiService:
logging.error("Failed to remove users from squad %s. Response: %s", squad_uuid, response_data)
return False
async def get_nodes_online_lookups(self) -> Dict[str, Dict[str, int]]:
"""Live ``usersOnline`` per node from ``GET /nodes`` (node directory).
Newer panels expose Prometheus-style metrics under ``/system/stats/nodes``
(``nodes: [{ usersOnline, ... }]``). Older/alternate builds only return
historical rows (e.g. ``lastSevenDays``) without live counts. The node
directory response always includes ``usersOnline`` and ``uuid``.
Returns:
``{"byUuid": {uuid_lower: int}, "byName": {name_lower: int}}``
"""
by_uuid: Dict[str, int] = {}
by_name: Dict[str, int] = {}
page_size = 100
start = 0
while True:
response_data = await self._request(
"GET",
"/nodes",
params={"size": page_size, "start": start},
log_full_response=False,
)
if not response_data or response_data.get("error"):
break
resp = response_data.get("response")
batch: List[Dict[str, Any]] = []
if isinstance(resp, list):
batch = [x for x in resp if isinstance(x, dict)]
elif isinstance(resp, dict):
inner = resp.get("nodes") or resp.get("items") or []
batch = [x for x in inner if isinstance(x, dict)]
if not batch:
break
for n in batch:
uid = n.get("uuid") or n.get("nodeUuid") or n.get("node_uuid")
uo = n.get("usersOnline")
if uo is None:
uo = n.get("users_online")
if uo is None:
continue
try:
val = int(uo)
except (TypeError, ValueError):
continue
if uid:
by_uuid[str(uid).strip().lower()] = val
name = n.get("name")
if name and isinstance(name, str) and name.strip():
by_name[name.strip().lower()] = val
if len(batch) < page_size:
break
start += page_size
await asyncio.sleep(0.05)
return {"byUuid": by_uuid, "byName": by_name}
async def get_nodes_statistics(self) -> Optional[Dict[str, Any]]:
"""Get nodes statistics"""
response_data = await self._request("GET", "/system/stats/nodes", log_full_response=False)