feat(wolf): show Hermes GPU fallback state
This commit is contained in:
parent
89ea032142
commit
e9655ab61f
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"))
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 : [];
|
||||
|
||||
@ -35,6 +35,12 @@
|
||||
<span class="k mono">GPU</span>
|
||||
<strong class="mono">{{ wolf.gpuPriority }}</strong>
|
||||
</div>
|
||||
<div class="wolf-summary-item">
|
||||
<span class="k mono">Hermes</span>
|
||||
<strong class="mono" :class="wolf.localInferenceReady ? 'ok-text' : 'warn-text'">
|
||||
{{ wolf.inferencePath }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="wolf-summary-item">
|
||||
<span class="k mono">Firewall</span>
|
||||
<strong class="mono" :class="currentIpUnlocked ? 'ok-text' : 'warn-text'">
|
||||
@ -60,6 +66,14 @@
|
||||
<span class="k mono">Paired devices</span>
|
||||
<span class="v mono">{{ pairedDeviceNames }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">GPU owner</span>
|
||||
<span class="v mono">{{ wolf.gpuOwner }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Local model</span>
|
||||
<span class="v mono">{{ wolf.localModel }} · {{ wolf.localModelLoaded ? "loaded" : "unloaded" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
|
||||
@ -215,7 +215,18 @@ describe("account dashboard", () => {
|
||||
return jsonResponse({
|
||||
can_control_gpu: true,
|
||||
moonlight: { host: "moonlight.bstein.dev", source_ip: "181.1.87.186", unlock_ttl_seconds: 28800 },
|
||||
gpu: { priority: "ai", game_mode: { status: "idle", active: false } },
|
||||
gpu: {
|
||||
priority: "ai",
|
||||
game_mode: {
|
||||
status: "idle",
|
||||
active: false,
|
||||
gpu_owner: "hermes",
|
||||
model: "gpt-oss:20b",
|
||||
model_loaded: true,
|
||||
local_inference_ready: true,
|
||||
inference_path: "local",
|
||||
},
|
||||
},
|
||||
wolf: {
|
||||
api_enabled: true,
|
||||
clients: [{ name: "Desktop" }],
|
||||
@ -236,6 +247,11 @@ describe("account dashboard", () => {
|
||||
|
||||
expect(dashboard.wolf.status).toBe("ready");
|
||||
expect(dashboard.wolf.canControlGpu).toBe(true);
|
||||
expect(dashboard.wolf.gpuOwner).toBe("hermes");
|
||||
expect(dashboard.wolf.localModel).toBe("gpt-oss:20b");
|
||||
expect(dashboard.wolf.localModelLoaded).toBe(true);
|
||||
expect(dashboard.wolf.localInferenceReady).toBe(true);
|
||||
expect(dashboard.wolf.inferencePath).toBe("local");
|
||||
expect(dashboard.wolf.clients[0].name).toBe("Desktop");
|
||||
expect(dashboard.wolf.pendingPairRequests[0].pair_secret).toBe("secret-1");
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user