The web UI could never boot. A fresh Audiobookshelf answers /healthcheck 200 and serves its API perfectly, but index.html inside the pinned image is built with routerBasePath:"/audiobookshelf" — so the SPA asks for its assets under that prefix whatever ROUTER_BASE_PATH says. Mounted at /, every script request fell through to the SPA fallback, came back text/html, and Chrome refused to execute it: the page sat at "Loading…". The health gate, the API and the phone app were all healthy, which is exactly how an API-only deploy check misses it.
The SSO round trip died at the IdP with authentik's Redirect URI Error. ABS builds redirect_uri as <origin><authOpenIDSubfolderForRedirectURLs>/auth/openid/callback, and that key is unset on a database created before OIDC was enabled — the 2.17.4 migration only fills a DB that already had OIDC on. The URL carried the literal string undefined.
ROUTER_BASE_PATH=/audiobookshelf; header records why the value is not a free choice; /healthcheck measured to answer at the root under either base path, so the health gate is unchanged
scripts/deploy-remote.sh
the handler reads the mounted base path out of the unit and passes it to the bootstrapper as ABS_URL — the number is never restated
scripts/audiobookshelf-init.sh
base-path-aware default URL; sends authOpenIDSubfolderForRedirectURLs, derived from ABS_URL's path instead of becoming a second knob, and verifies it came back
scripts/authentik-audiobookshelf-sso.sh (new)
the IdP half in git: provider, groups, the abs_groups scope mapping, the redirect URIs. That state lives in authentik's database and a blueprint re-import reverts it silently, so it needs a re-runnable door. HS256: no provider on the fleet carries a signing key, so the client secret is the signature key
tests/dankserver_quadlet.bats
the base-path assertion moves from / to /audiobookshelf — the pinned value was the bug
tests/audiobookshelf_init.bats
stub gained PATCH /api/auth-settings and base-path routing; two new seams: the subfolder that reaches the wire follows ABS_URL (prefixed and root arms), and the two scripts build the same redirect path
vault
the base-path law, the SSO contract (which script owns which half), the phone app's new server address
Verification
./tests/run.sh — 824 tests, all pass; the two new seams confirmed running by name.
Throwaway container (base /audiobookshelf, its own empty config, no shared state): asset …/_nuxt/c7f3e29.js → 200 application/javascript, /audiobookshelf/healthcheck → OK.
Provider side already applied to the live IdP (the script is idempotent): uris ['http://192.168.0.235:8000/audiobookshelf/auth/openid/callback', '…/audiobookshelf/auth/openid/mobile-redirect'], claim for akadmin → {'abs_groups': ['admin', 'user']}.
Pending on merge: deploy.sh dankserver --service audiobookshelf (base path + subfolder), then the browser round trip into ABS as admin, then the phone app against http://192.168.0.235:8000/audiobookshelf.
Rollback
Revert cfdd343..3138e78 and redeploy the service. The local frank account stays enabled in authActiveAuthMethods throughout, so the box is reachable either way.
## What was broken
Two faults, both invisible to every gate we had.
**The web UI could never boot.** A fresh Audiobookshelf answers `/healthcheck` 200 and serves its API perfectly, but `index.html` inside the pinned image is built with `routerBasePath:"/audiobookshelf"` — so the SPA asks for its assets under that prefix whatever `ROUTER_BASE_PATH` says. Mounted at `/`, every script request fell through to the SPA fallback, came back `text/html`, and Chrome refused to execute it: the page sat at "Loading…". The health gate, the API and the phone app were all healthy, which is exactly how an API-only deploy check misses it.
**The SSO round trip died at the IdP** with authentik's *Redirect URI Error*. ABS builds `redirect_uri` as `<origin><authOpenIDSubfolderForRedirectURLs>/auth/openid/callback`, and that key is **unset** on a database created before OIDC was enabled — the 2.17.4 migration only fills a DB that *already* had OIDC on. The URL carried the literal string `undefined`.
## What changes
| File | Change |
|---|---|
| `dankserver/audiobookshelf/quadlet/audiobookshelf.container` | `ROUTER_BASE_PATH=/audiobookshelf`; header records why the value is not a free choice; `/healthcheck` measured to answer at the root under **either** base path, so the health gate is unchanged |
| `scripts/deploy-remote.sh` | the handler reads the mounted base path out of the unit and passes it to the bootstrapper as `ABS_URL` — the number is never restated |
| `scripts/audiobookshelf-init.sh` | base-path-aware default URL; sends `authOpenIDSubfolderForRedirectURLs`, derived from `ABS_URL`'s path instead of becoming a second knob, and verifies it came back |
| `scripts/authentik-audiobookshelf-sso.sh` (new) | the IdP half in git: provider, groups, the `abs_groups` scope mapping, the redirect URIs. That state lives in authentik's database and a blueprint re-import reverts it silently, so it needs a re-runnable door. HS256: no provider on the fleet carries a signing key, so the client secret *is* the signature key |
| `tests/dankserver_quadlet.bats` | the base-path assertion moves from `/` to `/audiobookshelf` — the pinned value **was** the bug |
| `tests/audiobookshelf_init.bats` | stub gained `PATCH /api/auth-settings` and base-path routing; two new seams: the subfolder that reaches the wire follows `ABS_URL` (prefixed and root arms), and the two scripts build the same redirect path |
| vault | the base-path law, the SSO contract (which script owns which half), the phone app's new server address |
## Verification
- `./tests/run.sh` — 824 tests, all pass; the two new seams confirmed running by name.
- Throwaway container (base `/audiobookshelf`, its own empty config, no shared state): asset `…/_nuxt/c7f3e29.js` → `200 application/javascript`, `/audiobookshelf/healthcheck` → `OK`.
- Provider side already applied to the live IdP (the script is idempotent): `uris ['http://192.168.0.235:8000/audiobookshelf/auth/openid/callback', '…/audiobookshelf/auth/openid/mobile-redirect']`, claim for `akadmin` → `{'abs_groups': ['admin', 'user']}`.
- Pending on merge: `deploy.sh dankserver --service audiobookshelf` (base path + subfolder), then the browser round trip into ABS as admin, then the phone app against `http://192.168.0.235:8000/audiobookshelf`.
## Rollback
Revert `cfdd343..3138e78` and redeploy the service. The local `frank` account stays enabled in `authActiveAuthMethods` throughout, so the box is reachable either way.
---
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
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.
The live round trip died on authentik with Redirect URI Error. ABS builds redirect_uri as <origin><authOpenIDSubfolderForRedirectURLs>/auth/openid/callback, and that key is unset on a database created before SSO (the 2.17.4 migration only fills a DB that already had OIDC enabled), so the URL carried the literal string undefined. Arming through the API therefore has to send the value; it is the mounted base path, so it is derived from ABS_URL rather than becoming a second knob an operator has to keep in sync.
authentik-audiobookshelf-sso.sh is the IdP half kept in git: provider, groups, the abs_groups scope mapping and the redirect URIs. That state lives in authentik's database, and a blueprint re-import reverts hand-made rows silently, so it needs a re-runnable door. Tokens are HS256 because no provider on the fleet carries a signing key, which makes the client secret the signature key.
The stub server now records the auth-settings PATCH body, so the seam asserts the subfolder that reaches the wire follows ABS_URL for both the prefixed and root shapes, and a second seam pins that the provider script and the bootstrapper build the same redirect path.
The page owned the URL as a plain host:port; every client now carries the /audiobookshelf prefix. Added the two laws a next operator needs (why the base path is not a choice, why the redirect subfolder must be sent) and the Single sign-on section naming which script owns which half.
Measured on both shapes: ping returns success:true and healthcheck returns OK at the root whether or not the app is mounted under a prefix, which is why the phone app's connectivity check works before it ever resolves the base path. The paragraph now says which routes were probed instead of asserting 200s in general, and the frontmatter description fits the OKF 120-char budget the linter enforces.
dank
merged commit 7e9bfbc2be into main2026-11-02 20:02:58 -08:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What was broken
Two faults, both invisible to every gate we had.
The web UI could never boot. A fresh Audiobookshelf answers
/healthcheck200 and serves its API perfectly, butindex.htmlinside the pinned image is built withrouterBasePath:"/audiobookshelf"— so the SPA asks for its assets under that prefix whateverROUTER_BASE_PATHsays. Mounted at/, every script request fell through to the SPA fallback, came backtext/html, and Chrome refused to execute it: the page sat at "Loading…". The health gate, the API and the phone app were all healthy, which is exactly how an API-only deploy check misses it.The SSO round trip died at the IdP with authentik's Redirect URI Error. ABS builds
redirect_urias<origin><authOpenIDSubfolderForRedirectURLs>/auth/openid/callback, and that key is unset on a database created before OIDC was enabled — the 2.17.4 migration only fills a DB that already had OIDC on. The URL carried the literal stringundefined.What changes
dankserver/audiobookshelf/quadlet/audiobookshelf.containerROUTER_BASE_PATH=/audiobookshelf; header records why the value is not a free choice;/healthcheckmeasured to answer at the root under either base path, so the health gate is unchangedscripts/deploy-remote.shABS_URL— the number is never restatedscripts/audiobookshelf-init.shauthOpenIDSubfolderForRedirectURLs, derived fromABS_URL's path instead of becoming a second knob, and verifies it came backscripts/authentik-audiobookshelf-sso.sh(new)abs_groupsscope mapping, the redirect URIs. That state lives in authentik's database and a blueprint re-import reverts it silently, so it needs a re-runnable door. HS256: no provider on the fleet carries a signing key, so the client secret is the signature keytests/dankserver_quadlet.bats/to/audiobookshelf— the pinned value was the bugtests/audiobookshelf_init.batsPATCH /api/auth-settingsand base-path routing; two new seams: the subfolder that reaches the wire followsABS_URL(prefixed and root arms), and the two scripts build the same redirect pathVerification
./tests/run.sh— 824 tests, all pass; the two new seams confirmed running by name./audiobookshelf, its own empty config, no shared state): asset…/_nuxt/c7f3e29.js→200 application/javascript,/audiobookshelf/healthcheck→OK.uris ['http://192.168.0.235:8000/audiobookshelf/auth/openid/callback', '…/audiobookshelf/auth/openid/mobile-redirect'], claim forakadmin→{'abs_groups': ['admin', 'user']}.deploy.sh dankserver --service audiobookshelf(base path + subfolder), then the browser round trip into ABS as admin, then the phone app againsthttp://192.168.0.235:8000/audiobookshelf.Rollback
Revert
cfdd343..3138e78and redeploy the service. The localfrankaccount stays enabled inauthActiveAuthMethodsthroughout, so the box is reachable either way.cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb