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:
1 parent
f3c64d4fd0
commit
6ef580780d
4 files changed
+90
-12
No files matched your search
@@ -4,3 +4,4 @@ config/
|
|||||||
*.db
|
*.db
|
||||||
.venv/
|
.venv/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
dev/
|
||||||
@@ -95,16 +95,39 @@ dedicated endpoint is the only source that carries `track_position` and
|
|||||||
### Developing
|
### Developing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# terminal 1 — the API
|
./dev.sh # http://127.0.0.1:5041
|
||||||
uvicorn app:app --port 5041
|
```
|
||||||
|
|
||||||
# terminal 2 — the UI, proxying /api and /auth to the above
|
Creates the venv on first run, keeps its state in `dev/` (gitignored), and
|
||||||
|
serves the prebuilt `web/out` exactly as the container does — same CSP, same
|
||||||
|
routes, same middleware. Add `--build` after editing the UI.
|
||||||
|
|
||||||
|
It sets **`MUSE_AUTH=off`**, so there is no OAuth app to register against a
|
||||||
|
localhost callback. With auth off every role check reports `admin`, which is
|
||||||
|
what keeps the admin panel reachable; the startup banner says so in capitals.
|
||||||
|
|
||||||
|
What works with nothing else running: search, autocomplete, 30-second previews,
|
||||||
|
album and artist pages, artwork-derived accent, the admin panel, and queueing.
|
||||||
|
Requests reach `searching` and then fail, because acquisition needs slskd —
|
||||||
|
point `SLSKD_URL`/`SLSKD_API_KEY` at a real instance to test that half.
|
||||||
|
|
||||||
|
Two things read differently on a Mac. **CPU and memory show `—`**: those come
|
||||||
|
from `/proc`, which is Linux-only, so they populate in the container and not
|
||||||
|
here (load average and disk use `getloadavg`/`statvfs` and work everywhere).
|
||||||
|
And the **`Users` count includes accounts you seed by hand** — with auth off
|
||||||
|
nobody can sign in, so add them through the panel's Invite button or the API.
|
||||||
|
|
||||||
|
For UI work, leave that running and start Next's dev server beside it:
|
||||||
|
|
||||||
|
```bash
|
||||||
cd web && npm install && npm run dev
|
cd web && npm install && npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
`next dev` serves on :3000 and rewrites `/api/*` and `/auth/*` to the FastAPI
|
`next dev` serves on :3000 with hot reload and rewrites `/api/*` and `/auth/*`
|
||||||
process, so the browser sees one origin and cookies behave exactly as they do
|
to the FastAPI process, so the browser sees one origin and cookies behave
|
||||||
in production. `npm run build` writes `web/out/`, which is what ships.
|
exactly as they do in production. `npm run build` writes `web/out/`, which is
|
||||||
|
what ships. Note that :3000 serves the UI without the CSP the Python process
|
||||||
|
attaches — check anything CSP-sensitive on :5041 after a `--build`.
|
||||||
|
|
||||||
## Bulk import
|
## Bulk import
|
||||||
|
|
||||||
|
|||||||
@@ -434,14 +434,32 @@ def set_state(job_id: str, state: str, detail: str = "", **cols):
|
|||||||
|
|
||||||
# ─────────────────────────── slskd ───────────────────────────
|
# ─────────────────────────── 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(
|
return httpx.AsyncClient(
|
||||||
base_url=SLSKD_URL,
|
base_url=SLSKD_URL,
|
||||||
headers={"X-API-Key": SLSKD_API_KEY},
|
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:
|
def ext_of(remote_path: str) -> str:
|
||||||
# Soulseek paths are Windows-style; the API's `extension` field comes back
|
# Soulseek paths are Windows-style; the API's `extension` field comes back
|
||||||
# empty in practice, so derive it ourselves.
|
# empty in practice, so derive it ourselves.
|
||||||
@@ -1752,7 +1770,7 @@ async def admin_system(request: Request):
|
|||||||
|
|
||||||
slskd: dict = {"ok": False}
|
slskd: dict = {"ok": False}
|
||||||
try:
|
try:
|
||||||
async with slskd_client() as sc:
|
async with slskd_client(PROBE_TIMEOUT) as sc:
|
||||||
r = await sc.get("/api/v0/application")
|
r = await sc.get("/api/v0/application")
|
||||||
slskd["ok"] = r.status_code == 200
|
slskd["ok"] = r.status_code == 200
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
@@ -1767,7 +1785,7 @@ async def admin_system(request: Request):
|
|||||||
else:
|
else:
|
||||||
slskd["error"] = f"HTTP {r.status_code}"
|
slskd["error"] = f"HTTP {r.status_code}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
slskd["error"] = str(e)[:120]
|
slskd["error"] = why(e)
|
||||||
out["slskd"] = slskd
|
out["slskd"] = slskd
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -1775,13 +1793,13 @@ async def admin_system(request: Request):
|
|||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
async def health():
|
async def health():
|
||||||
ok, detail = True, {}
|
ok, detail = True, {}
|
||||||
async with slskd_client() as sc:
|
async with slskd_client(PROBE_TIMEOUT) as sc:
|
||||||
try:
|
try:
|
||||||
r = await sc.get("/api/v0/application")
|
r = await sc.get("/api/v0/application")
|
||||||
detail["slskd"] = r.status_code
|
detail["slskd"] = r.status_code
|
||||||
ok = ok and r.status_code == 200
|
ok = ok and r.status_code == 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
detail["slskd"] = str(e)
|
detail["slskd"] = why(e)
|
||||||
ok = False
|
ok = False
|
||||||
detail["library_writable"] = os.access(LIBRARY_DIR, os.W_OK)
|
detail["library_writable"] = os.access(LIBRARY_DIR, os.W_OK)
|
||||||
detail["downloads_readable"] = os.access(DOWNLOAD_DIR, os.R_OK)
|
detail["downloads_readable"] = os.access(DOWNLOAD_DIR, os.R_OK)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run muse on this machine, against live Deezer, with no OAuth app and no slskd.
|
||||||
|
#
|
||||||
|
# ./dev.sh serve the built UI from web/out on :5041
|
||||||
|
# ./dev.sh --build rebuild web/out first (needed after any UI edit)
|
||||||
|
#
|
||||||
|
# For UI work with hot reload, leave this running and start `next dev` beside
|
||||||
|
# it -- see "Developing" in the README.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
[[ "${1:-}" == "--build" ]] && (cd web && npm install && npm run build)
|
||||||
|
|
||||||
|
if [[ ! -d .venv ]]; then
|
||||||
|
python3 -m venv .venv
|
||||||
|
.venv/bin/pip install -q -r requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p dev/library dev/downloads
|
||||||
|
|
||||||
|
# Auth off, because the alternative is registering an OAuth app whose callback
|
||||||
|
# points at localhost. Every role check then reports admin, which is what makes
|
||||||
|
# the admin panel reachable here. The startup banner says this loudly; heed it.
|
||||||
|
export MUSE_AUTH="${MUSE_AUTH:-off}"
|
||||||
|
export MUSE_DB="${MUSE_DB:-dev/muse.db}"
|
||||||
|
export MUSE_LIBRARY_DIR="${MUSE_LIBRARY_DIR:-dev/library}"
|
||||||
|
export MUSE_DOWNLOAD_DIR="${MUSE_DOWNLOAD_DIR:-dev/downloads}"
|
||||||
|
export MUSE_COOKIE_SECURE=false
|
||||||
|
|
||||||
|
# Unset by default: with no slskd, requests queue and then fail at the search
|
||||||
|
# step, which is the correct behaviour and still exercises the whole UI path.
|
||||||
|
# Point these at a real instance to test acquisition end to end.
|
||||||
|
export SLSKD_URL="${SLSKD_URL:-http://127.0.0.1:5030}"
|
||||||
|
export SLSKD_API_KEY="${SLSKD_API_KEY:-}"
|
||||||
|
|
||||||
|
exec .venv/bin/uvicorn app:app --host 127.0.0.1 --port 5041 --reload
|
||||||
Reference in new issue
Block a user