diff --git a/backend/atlas_portal/ariadne_client.py b/backend/atlas_portal/ariadne_client.py index eb5f865..757c6ab 100644 --- a/backend/atlas_portal/ariadne_client.py +++ b/backend/atlas_portal/ariadne_client.py @@ -43,6 +43,7 @@ def request_raw( *, payload: Any | None = None, params: dict[str, Any] | None = None, + timeout_sec: float | None = None, ) -> httpx.Response: """Send one authenticated request to Ariadne and return the raw response. @@ -55,9 +56,10 @@ def request_raw( url = _url(path) attempts = max(1, settings.ARIADNE_RETRY_COUNT) + request_timeout = timeout_sec if timeout_sec is not None else settings.ARIADNE_TIMEOUT_SEC for attempt in range(1, attempts + 1): try: - with httpx.Client(timeout=settings.ARIADNE_TIMEOUT_SEC) as client: + with httpx.Client(timeout=request_timeout) as client: resp = client.request( method, url, @@ -78,7 +80,7 @@ def request_raw( "method": method, "path": path, "attempt": attempt, - "timeout_sec": settings.ARIADNE_TIMEOUT_SEC, + "timeout_sec": request_timeout, "error": str(exc), }, ) @@ -93,6 +95,7 @@ def proxy( *, payload: Any | None = None, params: dict[str, Any] | None = None, + timeout_sec: float | None = None, ) -> tuple[Any, int]: """Proxy an Ariadne response through Flask as JSON plus status code. @@ -101,7 +104,7 @@ def proxy( """ try: - resp = request_raw(method, path, payload=payload, params=params) + resp = request_raw(method, path, payload=payload, params=params, timeout_sec=timeout_sec) except AriadneError as exc: return jsonify({"error": str(exc)}), exc.status_code diff --git a/backend/atlas_portal/routes/account_wolf.py b/backend/atlas_portal/routes/account_wolf.py index 988eb44..f380b8a 100644 --- a/backend/atlas_portal/routes/account_wolf.py +++ b/backend/atlas_portal/routes/account_wolf.py @@ -5,7 +5,7 @@ from typing import Any from flask import jsonify, request -from .. import ariadne_client +from .. import ariadne_client, settings from ..keycloak import require_auth, require_account_access @@ -142,7 +142,12 @@ def register_account_wolf(app) -> None: ok, resp = _require_account() if not ok: return resp - return ariadne_client.proxy("POST", "/api/admin/game-mode/start", payload=_json_payload()) + return ariadne_client.proxy( + "POST", + "/api/admin/game-mode/start", + payload=_json_payload(), + timeout_sec=settings.ARIADNE_GAME_MODE_TIMEOUT_SEC, + ) @app.route("/api/account/wolf/game-mode/stop", methods=["POST"]) @require_auth @@ -150,7 +155,12 @@ def register_account_wolf(app) -> None: ok, resp = _require_account() if not ok: return resp - return ariadne_client.proxy("POST", "/api/admin/game-mode/stop", payload=_json_payload()) + return ariadne_client.proxy( + "POST", + "/api/admin/game-mode/stop", + payload=_json_payload(), + timeout_sec=settings.ARIADNE_GAME_MODE_TIMEOUT_SEC, + ) @app.route("/api/account/wolf/admin/firewall/unlock", methods=["POST"]) @require_auth diff --git a/backend/atlas_portal/settings.py b/backend/atlas_portal/settings.py index ce5a6e4..0c3da27 100644 --- a/backend/atlas_portal/settings.py +++ b/backend/atlas_portal/settings.py @@ -80,6 +80,7 @@ KEYCLOAK_ADMIN_REALM = os.getenv("KEYCLOAK_ADMIN_REALM", KEYCLOAK_REALM) ARIADNE_URL = os.getenv("ARIADNE_URL", "").strip() ARIADNE_TIMEOUT_SEC = float(os.getenv("ARIADNE_TIMEOUT_SEC", "10")) +ARIADNE_GAME_MODE_TIMEOUT_SEC = float(os.getenv("ARIADNE_GAME_MODE_TIMEOUT_SEC", "900")) ARIADNE_RETRY_COUNT = int(os.getenv("ARIADNE_RETRY_COUNT", "2")) ARIADNE_RETRY_BACKOFF_SEC = float(os.getenv("ARIADNE_RETRY_BACKOFF_SEC", "0.2")) diff --git a/backend/tests/test_account_wolf.py b/backend/tests/test_account_wolf.py index c024609..e17c804 100644 --- a/backend/tests/test_account_wolf.py +++ b/backend/tests/test_account_wolf.py @@ -8,13 +8,20 @@ from atlas_portal.routes import account_wolf class DummyAriadne: def __init__(self, enabled: bool = True) -> None: self._enabled = enabled - self.calls: list[tuple[str, str, object | None, dict | None]] = [] + self.calls: list[tuple[str, str, object | None, dict | None, float | None]] = [] def enabled(self) -> bool: return self._enabled - def proxy(self, method: str, path: str, payload: object | None = None, params: dict | None = None): - self.calls.append((method, path, payload, params)) + def proxy( + self, + method: str, + path: str, + payload: object | None = None, + params: dict | None = None, + timeout_sec: float | None = None, + ): + self.calls.append((method, path, payload, params, timeout_sec)) return jsonify({"path": path, "payload": payload, "params": params}) @@ -42,7 +49,7 @@ def test_wolf_status_proxies_source_ip(monkeypatch) -> None: ) assert resp.status_code == 200 - assert ariadne.calls == [("GET", "/api/game-stream/status", None, {"source_ip": "1.2.3.4"})] + assert ariadne.calls == [("GET", "/api/game-stream/status", None, {"source_ip": "1.2.3.4"}, None)] def test_wolf_status_prefers_public_query_ip(monkeypatch) -> None: @@ -54,7 +61,7 @@ def test_wolf_status_prefers_public_query_ip(monkeypatch) -> None: ) assert resp.status_code == 200 - assert ariadne.calls == [("GET", "/api/game-stream/status", None, {"source_ip": "181.1.87.186"})] + assert ariadne.calls == [("GET", "/api/game-stream/status", None, {"source_ip": "181.1.87.186"}, None)] def test_wolf_unlock_uses_current_source_ip(monkeypatch) -> None: @@ -68,7 +75,7 @@ def test_wolf_unlock_uses_current_source_ip(monkeypatch) -> None: ) assert resp.status_code == 200 - assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ttl_seconds": 120, "ip": "5.6.7.8"}, None)] + assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ttl_seconds": 120, "ip": "5.6.7.8"}, None, None)] def test_wolf_unlock_prefers_public_payload_ip(monkeypatch) -> None: @@ -81,7 +88,7 @@ def test_wolf_unlock_prefers_public_payload_ip(monkeypatch) -> None: ) assert resp.status_code == 200 - assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ip": "181.1.87.186"}, None)] + assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ip": "181.1.87.186"}, None, None)] def test_wolf_unlock_ignores_private_payload_ip(monkeypatch) -> None: @@ -94,7 +101,7 @@ def test_wolf_unlock_ignores_private_payload_ip(monkeypatch) -> None: ) assert resp.status_code == 200 - assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ip": "5.6.7.8"}, None)] + assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ip": "5.6.7.8"}, None, None)] def test_wolf_source_ip_prefers_nearest_public_proxy_value(monkeypatch) -> None: @@ -108,7 +115,7 @@ def test_wolf_source_ip_prefers_nearest_public_proxy_value(monkeypatch) -> None: ) assert resp.status_code == 200 - assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ip": "181.1.87.186"}, None)] + assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ip": "181.1.87.186"}, None, None)] def test_wolf_pairing_and_admin_actions_proxy(monkeypatch) -> None: @@ -125,11 +132,11 @@ def test_wolf_pairing_and_admin_actions_proxy(monkeypatch) -> None: client.post("/api/account/wolf/admin/firewall/unlock", json={"ip": "8.8.8.8", "target_user": "olya"}) assert ariadne.calls == [ - ("GET", "/api/game-stream/pairing/status", None, {"source_ip": "181.1.87.186"}), - ("POST", "/api/game-stream/pairing/submit-pin", {"pair_secret": "secret", "pin": "1234", "source_ip": "181.1.87.186"}, None), - ("POST", "/api/admin/game-mode/start", {"game": "steam"}, None), - ("POST", "/api/admin/game-mode/stop", {"game": "steam"}, None), - ("POST", "/api/admin/game-stream/firewall/unlock", {"ip": "8.8.8.8", "target_user": "olya"}, None), + ("GET", "/api/game-stream/pairing/status", None, {"source_ip": "181.1.87.186"}, None), + ("POST", "/api/game-stream/pairing/submit-pin", {"pair_secret": "secret", "pin": "1234", "source_ip": "181.1.87.186"}, None, None), + ("POST", "/api/admin/game-mode/start", {"game": "steam"}, None, account_wolf.settings.ARIADNE_GAME_MODE_TIMEOUT_SEC), + ("POST", "/api/admin/game-mode/stop", {"game": "steam"}, None, account_wolf.settings.ARIADNE_GAME_MODE_TIMEOUT_SEC), + ("POST", "/api/admin/game-stream/firewall/unlock", {"ip": "8.8.8.8", "target_user": "olya"}, None, None), ] @@ -143,7 +150,7 @@ def test_wolf_user_revoke_accepts_public_payload_ip(monkeypatch) -> None: ) assert resp.status_code == 200 - assert ariadne.calls == [("POST", "/api/game-stream/firewall/revoke", {"ip": "9.9.9.9"}, None)] + assert ariadne.calls == [("POST", "/api/game-stream/firewall/revoke", {"ip": "9.9.9.9"}, None, None)] def test_wolf_routes_require_account_and_ariadne(monkeypatch) -> None: diff --git a/frontend/src/account/useWolfDashboard.js b/frontend/src/account/useWolfDashboard.js index d52e4b9..b3ed985 100644 --- a/frontend/src/account/useWolfDashboard.js +++ b/frontend/src/account/useWolfDashboard.js @@ -47,8 +47,13 @@ export function useWolfDashboard() { sourceIp: "", unlockTtlSeconds: 28800, gpuPriority: "unknown", + gpuOwner: "unknown", gameModeStatus: "unknown", gameModeActive: false, + localModel: "unknown", + localModelLoaded: false, + localInferenceReady: false, + inferencePath: "fallback", selectedGame: "steam", note: "", manualIp: "", @@ -82,8 +87,14 @@ export function useWolfDashboard() { wolf.sourceIp = data.moonlight?.source_ip || ""; wolf.unlockTtlSeconds = Number(data.moonlight?.unlock_ttl_seconds || 28800); wolf.gpuPriority = data.gpu?.priority || "unknown"; - wolf.gameModeStatus = data.gpu?.game_mode?.status || "unknown"; - wolf.gameModeActive = Boolean(data.gpu?.game_mode?.active); + const gameMode = data.gpu?.game_mode || {}; + wolf.gpuOwner = gameMode.gpu_owner || "unknown"; + wolf.gameModeStatus = gameMode.status || "unknown"; + wolf.gameModeActive = Boolean(gameMode.active); + wolf.localModel = gameMode.model || "unknown"; + wolf.localModelLoaded = Boolean(gameMode.model_loaded); + wolf.localInferenceReady = Boolean(gameMode.local_inference_ready); + wolf.inferencePath = gameMode.inference_path || "fallback"; wolf.clients = Array.isArray(data.wolf?.clients) ? data.wolf.clients : []; wolf.pendingPairRequests = Array.isArray(data.wolf?.pending_pair_requests) ? data.wolf.pending_pair_requests : []; wolf.sessions = Array.isArray(data.wolf?.sessions) ? data.wolf.sessions : []; diff --git a/frontend/src/components/WolfAccountCard.vue b/frontend/src/components/WolfAccountCard.vue index 09c5666..47f4b64 100644 --- a/frontend/src/components/WolfAccountCard.vue +++ b/frontend/src/components/WolfAccountCard.vue @@ -35,6 +35,12 @@ GPU {{ wolf.gpuPriority }} +