diff --git a/codex_retry_gateway_tui.py b/codex_retry_gateway_tui.py index 2689fcb..c4a055f 100644 --- a/codex_retry_gateway_tui.py +++ b/codex_retry_gateway_tui.py @@ -24,7 +24,7 @@ from typing import Any APP_NAME = "codex-retry-gateway-tui" -FALLBACK_VERSION = "0.1.9" +FALLBACK_VERSION = "0.1.10" DEFAULT_GATEWAY_ADMIN_PATH = "/__codex_retry_gateway" DEFAULT_GATEWAY_URL = "http://127.0.0.1:4610/__codex_retry_gateway" DEFAULT_API_URL = DEFAULT_GATEWAY_URL @@ -2023,6 +2023,17 @@ def open_url(url: str) -> None: subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) +def gateway_supports_image_profiles(status_payload: dict[str, Any]) -> bool: + config = status_payload.get("config") if isinstance(status_payload.get("config"), dict) else {} + state = status_payload.get("state") if isinstance(status_payload.get("state"), dict) else {} + paths = status_payload.get("paths") if isinstance(status_payload.get("paths"), dict) else {} + return ( + "image_profile_name" in config + or "image_profile_name" in state + or "image_profiles_dir" in paths + ) + + def fetch_dashboard_snapshot( gateway_url: str, status_url: str, @@ -2033,29 +2044,35 @@ def fetch_dashboard_snapshot( current_view: str = "overview", ) -> dict[str, Any]: gateway_root = gateway_admin_url(gateway_url) + status_endpoint = status_url or gateway_status_url(gateway_root) + + status_payload, status_error = fetch_optional_payload(status_endpoint, timeout, access_key) + payload = status_payload if isinstance(status_payload, dict) else {} endpoints = { - "status": status_url or gateway_status_url(gateway_root), "logs": build_api_url(gateway_root, "/api/logs", {"limit": 200}), "requests": build_api_url(gateway_root, "/api/requests", {"limit": 200}), "profiles": build_api_url(gateway_root, "/api/profiles"), - "image_profiles": build_api_url(gateway_root, "/api/image-profiles"), } + image_profiles_supported = gateway_supports_image_profiles(payload) + if image_profiles_supported: + endpoints["image_profiles"] = build_api_url(gateway_root, "/api/image-profiles") def fetch_optional(url: str, *, allow_not_found: bool = False) -> tuple[dict[str, Any], str]: return fetch_optional_payload(url, timeout, access_key, allow_not_found=allow_not_found) - with ThreadPoolExecutor(max_workers=5) as pool: + with ThreadPoolExecutor(max_workers=len(endpoints)) as pool: futures = { name: pool.submit(fetch_optional, url, allow_not_found=name == "image_profiles") for name, url in endpoints.items() } - status_payload, status_error = futures["status"].result() logs_payload, logs_error = futures["logs"].result() requests_payload, requests_error = futures["requests"].result() profiles_payload, profiles_error = futures["profiles"].result() - image_profiles_payload, image_profiles_error = futures["image_profiles"].result() + if image_profiles_supported: + image_profiles_payload, image_profiles_error = futures["image_profiles"].result() + else: + image_profiles_payload, image_profiles_error = {}, "" - payload = status_payload if isinstance(status_payload, dict) else {} requests_source = requests_payload if isinstance(requests_payload, dict) else {} logs_source = logs_payload if isinstance(logs_payload, dict) else {} profiles_source = profiles_payload if isinstance(profiles_payload, dict) else {} diff --git a/pyproject.toml b/pyproject.toml index b1219f1..f6a7326 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codex-retry-gateway-tui" -version = "0.1.9" +version = "0.1.10" description = "Terminal UI for codex-retry-gateway monitoring and control" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/test_payload.py b/tests/test_payload.py index 5c67770..6229aed 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -139,6 +139,35 @@ class CodexRetryGatewayTUITests(unittest.TestCase): self.assertEqual(payload, {}) self.assertEqual(error, "") + def test_dashboard_skips_image_profiles_endpoint_when_status_lacks_capability(self) -> None: + mod = load_module() + calls: list[str] = [] + + def fetch(url: str, timeout: int, access_key: str = "", *, allow_not_found: bool = False): + calls.append(url) + if url.endswith("/api/status"): + return {"config": {"profile_name": "legacy"}, "state": {}, "paths": {}}, "" + if "/api/requests" in url: + return {"entries": []}, "" + if "/api/logs" in url: + return {"entries": []}, "" + if "/api/profiles" in url: + return {"profiles": []}, "" + self.fail(f"unexpected endpoint {url}") + + with mock.patch.object(mod, "fetch_optional_payload", side_effect=fetch): + snapshot = mod.fetch_dashboard_snapshot("http://127.0.0.1:4610", "", 5) + + self.assertEqual(snapshot["image_profiles"], []) + self.assertEqual(snapshot["status_error"], "") + self.assertFalse(any("/api/image-profiles" in url for url in calls)) + + def test_gateway_image_profiles_capability_uses_status_field_presence(self) -> None: + mod = load_module() + self.assertFalse(mod.gateway_supports_image_profiles({"config": {"profile_name": "legacy"}})) + self.assertTrue(mod.gateway_supports_image_profiles({"config": {"image_profile_name": ""}})) + self.assertTrue(mod.gateway_supports_image_profiles({"paths": {"image_profiles_dir": "/tmp/profiles"}})) + def test_request_usage_summary_shows_cached_ratio(self) -> None: mod = load_module() row = { diff --git a/uv.lock b/uv.lock index 12ddf6e..e37153d 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "codex-retry-gateway-tui" -version = "0.1.9" +version = "0.1.10" source = { editable = "." } dependencies = [ { name = "textual" },