diff --git a/.gitignore b/.gitignore index 21c16a8..f6c9e57 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ config/ *.db .venv/ .DS_Store +dev/ diff --git a/README.md b/README.md index ccfad65..35e48ba 100644 --- a/README.md +++ b/README.md @@ -95,16 +95,39 @@ dedicated endpoint is the only source that carries `track_position` and ### Developing ```bash -# terminal 1 — the API -uvicorn app:app --port 5041 +./dev.sh # http://127.0.0.1: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 ``` -`next dev` serves on :3000 and rewrites `/api/*` and `/auth/*` to the FastAPI -process, so the browser sees one origin and cookies behave exactly as they do -in production. `npm run build` writes `web/out/`, which is what ships. +`next dev` serves on :3000 with hot reload and rewrites `/api/*` and `/auth/*` +to the FastAPI process, so the browser sees one origin and cookies behave +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 diff --git a/app.py b/app.py index 0edcd3e..91da4cf 100644 --- a/app.py +++ b/app.py @@ -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) diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..318d279 --- /dev/null +++ b/dev.sh @@ -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