Skip to content

Commit 9780d5b

Browse files
committed
fix: dashboard tolerates API cold starts (retry + friendly message instead of traceback)
1 parent 314431b commit 9780d5b

3 files changed

Lines changed: 71 additions & 17 deletions

File tree

dashboard/app.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from dashboard.client import AgentWatchClient
1+
from dashboard.client import AgentWatchClient, APIUnavailable
22

33
# Plain-language descriptions of each incident category, shown in the UI so a
44
# first-time visitor understands what the labels mean.
@@ -182,12 +182,21 @@ def render() -> None:
182182
page = _sidebar(st)
183183
st.title("AgentWatch — AI Incident Observatory")
184184

185-
if page == "Overview":
186-
_render_overview(st, api)
187-
elif page == "Incident Explorer":
188-
_render_explorer(st, api)
189-
elif page == "Review Queue":
190-
_render_review(st, api)
185+
try:
186+
if page == "Overview":
187+
_render_overview(st, api)
188+
elif page == "Incident Explorer":
189+
_render_explorer(st, api)
190+
elif page == "Review Queue":
191+
_render_review(st, api)
192+
except APIUnavailable:
193+
st.warning(
194+
"⏳ The API isn't responding yet. This demo runs on Render's free tier, "
195+
"where services sleep after ~15 minutes of inactivity and take 30–60s to "
196+
"wake up. Give it a moment and retry."
197+
)
198+
if st.button("Retry"):
199+
st.rerun()
191200

192201

193202
if __name__ == "__main__":

dashboard/client.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import os
2+
import time
3+
4+
5+
class APIUnavailable(Exception):
6+
"""Raised when the API cannot be reached or returns a non-JSON response."""
27

38

49
class AgentWatchClient:
@@ -10,28 +15,45 @@ def __init__(self, base_url: str | None = None, api_key: str | None = None, clie
1015
if client is None:
1116
import httpx
1217

13-
client = httpx.Client(base_url=self.base_url, timeout=30.0)
18+
client = httpx.Client(base_url=self.base_url, timeout=30.0, follow_redirects=True)
1419
self._client = client
1520

1621
def _headers(self) -> dict:
1722
return {"X-API-Key": self.api_key} if self.api_key else {}
1823

24+
def _get_json(self, path: str, params: dict | None = None, *, attempts: int = 3):
25+
last: Exception | None = None
26+
for i in range(attempts):
27+
try:
28+
resp = self._client.get(path, params=params or {})
29+
resp.raise_for_status()
30+
return resp.json()
31+
except Exception as exc: # network error, non-2xx, or non-JSON body
32+
last = exc
33+
if i < attempts - 1:
34+
time.sleep(1.5 * (i + 1))
35+
raise APIUnavailable(str(last)) from last
36+
1937
def incidents(self, **filters) -> dict:
2038
params = {k: v for k, v in filters.items() if v is not None}
21-
return self._client.get("/incidents", params=params).json()
39+
return self._get_json("/incidents", params)
2240

2341
def incident(self, incident_id: int) -> dict:
24-
return self._client.get(f"/incidents/{incident_id}").json()
42+
return self._get_json(f"/incidents/{incident_id}")
2543

2644
def stats(self) -> dict:
27-
return self._client.get("/stats").json()
45+
return self._get_json("/stats")
2846

2947
def review(
3048
self, incident_id: int, *, reviewer: str, decision: str, notes: str | None = None
3149
) -> dict:
32-
resp = self._client.post(
33-
f"/incidents/{incident_id}/review",
34-
json={"reviewer": reviewer, "decision": decision, "notes": notes},
35-
headers=self._headers(),
36-
)
37-
return resp.json()
50+
try:
51+
resp = self._client.post(
52+
f"/incidents/{incident_id}/review",
53+
json={"reviewer": reviewer, "decision": decision, "notes": notes},
54+
headers=self._headers(),
55+
)
56+
resp.raise_for_status()
57+
return resp.json()
58+
except Exception as exc:
59+
raise APIUnavailable(str(exc)) from exc

tests/test_dashboard_client.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,29 @@ def _seed(monkeypatch, tmp_path):
5454
)
5555

5656

57+
def test_client_raises_apiunavailable_on_error(monkeypatch):
58+
import pytest
59+
60+
import dashboard.client as c
61+
62+
monkeypatch.setattr(c.time, "sleep", lambda *a, **k: None) # no waiting in tests
63+
64+
class BadResp:
65+
def raise_for_status(self):
66+
raise RuntimeError("502 Bad Gateway (cold start)")
67+
68+
def json(self):
69+
return {}
70+
71+
class BadClient:
72+
def get(self, *a, **k):
73+
return BadResp()
74+
75+
api = c.AgentWatchClient(client=BadClient())
76+
with pytest.raises(c.APIUnavailable):
77+
api.stats()
78+
79+
5780
def test_client_reads_and_reviews(monkeypatch, tmp_path):
5881
_seed(monkeypatch, tmp_path)
5982
api = AgentWatchClient(client=TestClient(create_app()))

0 commit comments

Comments
 (0)