fix: use accessible nodes API and fix date format for node usage

- Add get_user_accessible_nodes() to fetch user's available nodes
- Fix date format from ISO datetime to date-only (Y-m-d) for bandwidth stats
- Show all accessible nodes (with zero traffic if no stats)
- Add country_code to node usage response
This commit is contained in:
Fringg
2026-02-07 06:22:07 +03:00
parent 287a43ba65
commit c4da591731
3 changed files with 83 additions and 36 deletions

View File

@@ -686,48 +686,67 @@ async def get_user_node_usage(
start_date = end_date - timedelta(days=days)
async with service.get_api_client() as api:
# Get bandwidth stats for user
stats = await api.get_bandwidth_stats_user(
user.remnawave_uuid,
start_date.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
end_date.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
)
# Get user's accessible nodes
accessible_nodes = await api.get_user_accessible_nodes(user.remnawave_uuid)
if not accessible_nodes:
return UserNodeUsageResponse(items=[], period_days=days)
# Get all nodes for name resolution
nodes = await api.get_all_nodes()
node_map = {n.uuid: n.name for n in nodes}
node_name_map = {n.uuid: n.node_name for n in accessible_nodes}
node_cc_map = {n.uuid: n.country_code for n in accessible_nodes}
# Get bandwidth stats for user (use date-only format)
start_str = start_date.strftime('%Y-%m-%d')
end_str = end_date.strftime('%Y-%m-%d')
items = []
# Stats response contains per-node breakdown
if isinstance(stats, list):
for entry in stats:
node_uuid = entry.get('nodeUuid', '')
total = entry.get('totalBytes', 0) or entry.get('total', 0)
if node_uuid and total > 0:
items.append(
UserNodeUsageItem(
node_uuid=node_uuid,
node_name=node_map.get(node_uuid, node_uuid[:8]),
total_bytes=total,
try:
stats = await api.get_bandwidth_stats_user(user.remnawave_uuid, start_str, end_str)
if isinstance(stats, list):
for entry in stats:
node_uuid = entry.get('nodeUuid', '')
total = entry.get('totalBytes', 0) or entry.get('total', 0)
if node_uuid and total > 0:
items.append(
UserNodeUsageItem(
node_uuid=node_uuid,
node_name=node_name_map.get(node_uuid, node_uuid[:8]),
country_code=node_cc_map.get(node_uuid, ''),
total_bytes=total,
)
)
)
elif isinstance(stats, dict):
# Handle dict format with node entries
for node_uuid, data in stats.items():
if isinstance(data, dict):
total = data.get('totalBytes', 0) or data.get('total', 0)
elif isinstance(data, (int, float)):
total = int(data)
else:
continue
if total > 0:
items.append(
UserNodeUsageItem(
node_uuid=node_uuid,
node_name=node_map.get(node_uuid, node_uuid[:8]),
total_bytes=total,
elif isinstance(stats, dict):
for node_uuid, data in stats.items():
if isinstance(data, dict):
total = data.get('totalBytes', 0) or data.get('total', 0)
elif isinstance(data, (int, float)):
total = int(data)
else:
continue
if total > 0:
items.append(
UserNodeUsageItem(
node_uuid=node_uuid,
node_name=node_name_map.get(node_uuid, node_uuid[:8]),
country_code=node_cc_map.get(node_uuid, ''),
total_bytes=total,
)
)
except Exception:
logger.warning(f'Failed to get bandwidth stats for user {user_id}, returning nodes without traffic')
# Add accessible nodes with zero traffic if not in stats
seen_uuids = {item.node_uuid for item in items}
for node in accessible_nodes:
if node.uuid not in seen_uuids:
items.append(
UserNodeUsageItem(
node_uuid=node.uuid,
node_name=node.node_name,
country_code=node.country_code,
total_bytes=0,
)
)
# Sort by traffic descending
items.sort(key=lambda x: x.total_bytes, reverse=True)

View File

@@ -229,6 +229,7 @@ class UserNodeUsageItem(BaseModel):
node_uuid: str
node_name: str
country_code: str = ''
total_bytes: int

View File

@@ -564,6 +564,33 @@ class RemnaWaveAPI:
user = self._parse_user(response['response'])
return await self.enrich_user_with_happ_link(user)
async def get_user_accessible_nodes(self, uuid: str) -> list[RemnaWaveAccessibleNode]:
"""Получает список доступных нод для пользователя"""
try:
response = await self._make_request('GET', f'/api/users/{uuid}/accessible-nodes')
nodes_data = response.get('response', {}).get('activeNodes', [])
result = []
for node in nodes_data:
# Collect inbounds from activeSquads
inbounds: list[str] = []
for squad in node.get('activeSquads', []):
inbounds.extend(squad.get('activeInbounds', []))
result.append(
RemnaWaveAccessibleNode(
uuid=node['uuid'],
node_name=node['nodeName'],
country_code=node['countryCode'],
config_profile_uuid=node.get('configProfileUuid', ''),
config_profile_name=node.get('configProfileName', ''),
active_inbounds=inbounds,
)
)
return result
except RemnaWaveAPIError as e:
if e.status_code == 404:
return []
raise
async def get_all_users(self, start: int = 0, size: int = 100, enrich_happ_links: bool = False) -> dict[str, Any]:
params = {'start': start, 'size': size}
response = await self._make_request('GET', '/api/users', params=params)