Stop the admin panel hanging when slskd is down

/api/admin/system probed slskd with the shared client, whose 30s timeout is
sized for searches and transfers. So the panel sat on skeletons for the full
30s exactly when slskd was unreachable -- the case you open it to diagnose --
and the 5s UI poll stacked requests behind it. Probes now get 4s: above a LAN
round-trip, under the poll interval.

httpx.ConnectTimeout also stringifies to "", so the banner rendered
"slskd unreachable -- " with nothing after the dash. Fall back to the
exception class name.

Add dev.sh, because the documented dev commands could not work: MUSE_DB
defaults to /config/muse.db which a Mac cannot write, and the auth preflight
refuses to boot without a client ID.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Aryan SinghandClaude Opus 5 committed 2026-08-05 01:36:10 -05:00
1 parent f3c64d4fd0
commit 6ef580780d
4 files changed
+90 -12

No files matched your search

+24 -6
View File
@@ -434,14 +434,32 @@ def set_state(job_id: str, state: str, detail: str = "", **cols):
# ─────────────────────────── slskd ───────────────────────────
def slskd_client() -> httpx.AsyncClient:
def slskd_client(timeout: float = 30.0) -> httpx.AsyncClient:
"""30s suits searches and transfer polling, which are legitimately slow.
Status probes pass something much shorter: they run on a UI poll, and a
probe that outlasts its own poll interval stacks requests and leaves the
admin panel on skeletons for the whole timeout — precisely when slskd is
down and you opened the panel to find out why.
"""
return httpx.AsyncClient(
base_url=SLSKD_URL,
headers={"X-API-Key": SLSKD_API_KEY},
timeout=httpx.Timeout(30.0),
timeout=httpx.Timeout(timeout),
)
# slskd is on the LAN, so a reachable instance answers in milliseconds. Long
# enough to absorb a slow moment, short enough to stay under the 5s UI poll.
PROBE_TIMEOUT = 4.0
def why(e: Exception) -> str:
"""httpx's timeout and connection errors often str() to an empty string,
which would render as 'slskd unreachable — ' with nothing after it."""
return (str(e) or type(e).__name__)[:120]
def ext_of(remote_path: str) -> str:
# Soulseek paths are Windows-style; the API's `extension` field comes back
# empty in practice, so derive it ourselves.
@@ -1752,7 +1770,7 @@ async def admin_system(request: Request):
slskd: dict = {"ok": False}
try:
async with slskd_client() as sc:
async with slskd_client(PROBE_TIMEOUT) as sc:
r = await sc.get("/api/v0/application")
slskd["ok"] = r.status_code == 200
if r.status_code == 200:
@@ -1767,7 +1785,7 @@ async def admin_system(request: Request):
else:
slskd["error"] = f"HTTP {r.status_code}"
except Exception as e:
slskd["error"] = str(e)[:120]
slskd["error"] = why(e)
out["slskd"] = slskd
return out
@@ -1775,13 +1793,13 @@ async def admin_system(request: Request):
@app.get("/api/health")
async def health():
ok, detail = True, {}
async with slskd_client() as sc:
async with slskd_client(PROBE_TIMEOUT) as sc:
try:
r = await sc.get("/api/v0/application")
detail["slskd"] = r.status_code
ok = ok and r.status_code == 200
except Exception as e:
detail["slskd"] = str(e)
detail["slskd"] = why(e)
ok = False
detail["library_writable"] = os.access(LIBRARY_DIR, os.W_OK)
detail["downloads_readable"] = os.access(DOWNLOAD_DIR, os.R_OK)