From cfdd3435bccb4290d3428e27fa74bba5abfc1851 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 2 Nov 2026 19:50:44 -0800 Subject: [PATCH] fix(audiobookshelf): mount the app at the base path its SPA was built for ROUTER_BASE_PATH=/ served a healthy API and a web UI that could never boot. The index.html in the pinned image is built with routerBasePath:/audiobookshelf, so every script request fell through to the SPA fallback and came back as text/html, which Chrome refuses to execute. healthcheck, the API and the phone app all stayed fine, which is exactly how this hid behind an API-only deploy check. The deployer now reads the mounted base path out of the unit and hands it to the bootstrapper as ABS_URL, so the API door, the login route and the OIDC redirect subfolder cannot drift from the unit. healthcheck answers at the root under either base path, so the health gate is unchanged. --- .../audiobookshelf/quadlet/audiobookshelf.container | 13 +++++++++++-- scripts/deploy-remote.sh | 10 ++++++++-- tests/dankserver_quadlet.bats | 7 ++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/dankserver/audiobookshelf/quadlet/audiobookshelf.container b/dankserver/audiobookshelf/quadlet/audiobookshelf.container index e694cbd..872443a 100644 --- a/dankserver/audiobookshelf/quadlet/audiobookshelf.container +++ b/dankserver/audiobookshelf/quadlet/audiobookshelf.container @@ -33,9 +33,16 @@ Network=pasta # README is the npm package's naming and is INERT here: with ABS_PORT=8000 the # server listened on :80 under base path /audiobookshelf, so the unit sat # `active (running)` while every probe to :8000 was refused (measured 2026-11-02, -# first deploy). ROUTER_BASE_PATH=/ is what makes the URL the plain host:port. +# first deploy). +# ROUTER_BASE_PATH MUST be /audiobookshelf: the SPA in /app/client/dist/index.html is +# built with `routerBasePath:"/audiobookshelf"` baked in, so it asks for its assets under +# that prefix no matter what the server is mounted at. With `/` the server answers the +# page but every script request falls through to the SPA fallback and comes back as +# text/html — Chrome refuses to execute it and the UI never leaves "Loading…" +# (measured 2026-11-03; the API and /healthcheck were fine, which is why an API-only +# verification missed it). Environment=PORT=8000 -Environment=ROUTER_BASE_PATH=/ +Environment=ROUTER_BASE_PATH=/audiobookshelf Environment=CONFIG_PATH=/config Environment=METADATA_PATH=/metadata Environment=TZ=America/Vancouver @@ -45,6 +52,8 @@ Volume=/home/dank/Containers/audiobookshelf/metadata:/metadata Volume=/mnt/entropy/Audiobooks:/audiobooks:ro # The image is Alpine: it ships wget and no curl, so the health check must name # the binary that exists or the container is unhealthy while serving perfectly. +# /healthcheck answers `OK` at the root AND under the base path (measured on both +# base paths), so the probe below is correct whatever ROUTER_BASE_PATH says. HealthCmd=wget -q -O /dev/null http://127.0.0.1:8000/healthcheck HealthInterval=30s HealthTimeout=10s diff --git a/scripts/deploy-remote.sh b/scripts/deploy-remote.sh index 5160157..7e90e4b 100755 --- a/scripts/deploy-remote.sh +++ b/scripts/deploy-remote.sh @@ -1315,7 +1315,8 @@ deploy_audiobookshelf() { mkdir -p /home/dank/Containers/audiobookshelf/config \ /home/dank/Containers/audiobookshelf/metadata systemctl --user restart audiobookshelf.service - # /healthcheck answers once the HTTP server is bound; the library scan that + # /healthcheck answers once the HTTP server is bound, at the root whatever + # ROUTER_BASE_PATH says (measured on both base paths); the library scan that # follows runs in the background and must not gate the deploy. wait_for_health audiobookshelf http://127.0.0.1:8000/healthcheck 90 # A server with no root user and no library is "healthy" and useless, so the @@ -1323,7 +1324,12 @@ deploy_audiobookshelf() { # it creates the account (credential in the mode-600 host secret file), the # Audiobooks library over the /audiobooks bind, and fires the scan. Every # step is idempotent, so a re-deploy costs one incremental scan. - scripts/audiobookshelf-init.sh + # The bootstrapper has to speak the same base path the unit mounts (the API, the + # login route and the OIDC redirect subfolder all hang off it), so the URL is read + # out of the unit file here instead of being restated in the script's default. + abs_base=$(awk -F= '/^Environment=ROUTER_BASE_PATH=/{print $3}' \ + dankserver/audiobookshelf/quadlet/audiobookshelf.container) + ABS_URL="http://127.0.0.1:8000${abs_base}" scripts/audiobookshelf-init.sh } # The content server's single admin account and the desktop's basic-auth diff --git a/tests/dankserver_quadlet.bats b/tests/dankserver_quadlet.bats index ae4b34e..9b66f9f 100755 --- a/tests/dankserver_quadlet.bats +++ b/tests/dankserver_quadlet.bats @@ -182,8 +182,13 @@ load helpers # under base path /audiobookshelf and the deploy gate timed out beside a unit # that was active (running). PORT and ROUTER_BASE_PATH are what the image's own # ENV block and the server source actually use. + # The base path is not a free choice either: /app/client/dist/index.html inside the + # pinned image carries routerBasePath:"/audiobookshelf", so a server mounted at / hands + # out a page whose every script request falls through to the SPA fallback and comes back + # text/html — the UI never leaves "Loading…" while the API stays healthy (measured + # 2026-11-02, which is exactly how it hid behind an API-only verification). unit="$REPO_ROOT/dankserver/audiobookshelf/quadlet/audiobookshelf.container" grep -q '^Environment=PORT=8000$' "$unit" || return 1 - grep -q '^Environment=ROUTER_BASE_PATH=/$' "$unit" || return 1 + grep -q '^Environment=ROUTER_BASE_PATH=/audiobookshelf$' "$unit" || return 1 ! grep -qE '^Environment=ABS_' "$unit" || return 1 }