diff --git a/backend/bot/app/web/admin_api_impl/common.py b/backend/bot/app/web/admin_api_impl/common.py index bfc8653..7d1c36b 100644 --- a/backend/bot/app/web/admin_api_impl/common.py +++ b/backend/bot/app/web/admin_api_impl/common.py @@ -110,15 +110,24 @@ def _panel_nested_dicts(panel_user: Dict[str, Any], keys: Tuple[str, ...]) -> Li return out +def _panel_user_connection_containers(panel_user: Dict[str, Any]) -> List[Dict[str, Any]]: + traffic_containers = _panel_nested_dicts(panel_user, _PANEL_TRAFFIC_OBJECT_KEYS) + marker_containers = _panel_nested_dicts( + panel_user, + _PANEL_CONNECTION_MARKER_OBJECT_KEYS, + ) + for traffic_container in traffic_containers: + marker_containers.extend( + _panel_nested_dicts(traffic_container, _PANEL_CONNECTION_MARKER_OBJECT_KEYS) + ) + return [panel_user, *traffic_containers, *marker_containers] + + def _panel_user_last_connected_at(panel_user_data: Any) -> Optional[str]: panel_user = _panel_user_payload(panel_user_data) if not panel_user: return None - containers = [ - panel_user, - *_panel_nested_dicts(panel_user, _PANEL_CONNECTION_MARKER_OBJECT_KEYS), - ] - for container in containers: + for container in _panel_user_connection_containers(panel_user): for key in _PANEL_LAST_CONNECTED_KEYS: connected_at = _coerce_panel_datetime(container.get(key)) if connected_at: @@ -137,33 +146,34 @@ def _panel_user_positive_traffic_bytes(panel_user: Dict[str, Any]) -> bool: def _panel_user_has_connection_marker(panel_user: Dict[str, Any]) -> bool: - for key in _PANEL_CONNECTION_MARKER_KEYS: - if key in panel_user: - return True - for key in _PANEL_CONNECTION_MARKER_OBJECT_KEYS: - if key in panel_user: - return True + for container in _panel_user_connection_containers(panel_user): + for key in _PANEL_CONNECTION_MARKER_KEYS: + if key in container: + return True + for container in [panel_user, *_panel_nested_dicts(panel_user, _PANEL_TRAFFIC_OBJECT_KEYS)]: + for key in _PANEL_CONNECTION_MARKER_OBJECT_KEYS: + if key in container: + return True return False def _panel_user_has_connected_marker_value(panel_user: Dict[str, Any]) -> bool: - containers = [ - panel_user, - *_panel_nested_dicts(panel_user, _PANEL_CONNECTION_MARKER_OBJECT_KEYS), - ] - for container in containers: + for container in _panel_user_connection_containers(panel_user): for key in (*_PANEL_LAST_CONNECTED_KEYS, "firstConnectedAt", "first_connected_at"): if _coerce_panel_datetime(container.get(key)): return True for key in ("lastConnectedNodeUuid", "last_connected_node_uuid"): if str(container.get(key) or "").strip(): return True - for key in _PANEL_CONNECTION_MARKER_OBJECT_KEYS: - marker = panel_user.get(key) - if isinstance(marker, dict) and any(str(value or "").strip() for value in marker.values()): - return True - if marker and not isinstance(marker, dict): - return True + for container in [panel_user, *_panel_nested_dicts(panel_user, _PANEL_TRAFFIC_OBJECT_KEYS)]: + for key in _PANEL_CONNECTION_MARKER_OBJECT_KEYS: + marker = container.get(key) + if isinstance(marker, dict) and any( + str(value or "").strip() for value in marker.values() + ): + return True + if marker and not isinstance(marker, dict): + return True return False diff --git a/tests/test_admin_panel_activity.py b/tests/test_admin_panel_activity.py index ce32dfb..d1abd6e 100644 --- a/tests/test_admin_panel_activity.py +++ b/tests/test_admin_panel_activity.py @@ -72,7 +72,7 @@ class AdminPanelActivityTests(unittest.IsolatedAsyncioTestCase): def test_panel_activity_detects_connected_and_never_connected_users(self): self.assertEqual( common_module._panel_user_connection_activity( - {"onlineAt": "2026-06-05T12:00:00Z"} + {"userTraffic": {"onlineAt": "2026-06-05T12:00:00Z"}} ), { "status": "connected", @@ -82,10 +82,12 @@ class AdminPanelActivityTests(unittest.IsolatedAsyncioTestCase): self.assertEqual( common_module._panel_user_connection_activity( { - "onlineAt": None, - "firstConnectedAt": None, - "lastConnectedNode": None, - "userTraffic": {"lifetimeUsedTrafficBytes": 0}, + "userTraffic": { + "onlineAt": None, + "firstConnectedAt": None, + "lastConnectedNodeUuid": None, + "lifetimeUsedTrafficBytes": 0, + }, } ), {"status": "never", "last_connected_at": None}, @@ -115,15 +117,19 @@ class AdminPanelActivityTests(unittest.IsolatedAsyncioTestCase): async def get_user_by_uuid(panel_uuid): return { "never-panel": { - "onlineAt": None, - "firstConnectedAt": None, - "lastConnectedNode": None, + "userTraffic": { + "onlineAt": None, + "firstConnectedAt": None, + "lastConnectedNodeUuid": None, + }, }, - "connected-panel": {"onlineAt": "2026-06-05T12:00:00Z"}, + "connected-panel": {"userTraffic": {"onlineAt": "2026-06-05T12:00:00Z"}}, "also-never-panel": { - "onlineAt": None, - "firstConnectedAt": None, - "lastConnectedNode": None, + "userTraffic": { + "onlineAt": None, + "firstConnectedAt": None, + "lastConnectedNodeUuid": None, + }, }, "also-connected-panel": { "userTraffic": {"lifetimeUsedTrafficBytes": 1}, @@ -172,7 +178,7 @@ class AdminPanelActivityTests(unittest.IsolatedAsyncioTestCase): get_user_by_uuid=AsyncMock( return_value={ "subscriptionUrl": "https://panel.example/sub/short", - "onlineAt": "2026-06-05T12:00:00Z", + "userTraffic": {"onlineAt": "2026-06-05T12:00:00Z"}, } ) )