Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,11 @@ DEBUG=false
# Leave it unset only behind an HTTPS reverse proxy that passes the original
# Host. It must be a bare http(s) origin (no path, query, fragment or
# credentials), or the frontend refuses to start; case, a default port and a
# trailing / are normalized. Setting it makes `bun run start` (scripts/serve.ts)
# front the SvelteKit server with this origin. Set CSRF_ORIGIN to the same value.
# trailing / are normalized. Setting it makes the frontend server
# (`NODE_ENV=production bun scripts/serve.ts` from frontend/; not `bun run start`,
# under which Bun 1.4.2's default shell delivers one Ctrl+C twice and skips the
# SHUTDOWN_TIMEOUT drain) front the SvelteKit server with this origin. Set
# CSRF_ORIGIN to the same value.
# Every write through the frontend (POST, PUT, PATCH, DELETE) must send an
# Origin header equal to this origin: browsers do; scripts and API clients that
# write through the frontend port must too, or they get 403.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The scoped files list single-file and single-case commands.
- prek's pre-push stage runs basedpyright, svelte-check, backend pytest, dev_cli pytest and vitest. When `git push` fails locally, it is usually these hooks and not the remote. Commit messages must be Conventional Commits (a `commit-msg` hook checks them).
- Ruff is pinned (`ruff==0.16.8` in `backend/pyproject.toml`), and prek runs it through the backend project: `uv run --project backend --frozen ruff check --fix <paths>`. `dev_cli/` inherits the backend Ruff config through `extend`.
- Changing the backend's OpenAPI surface also means regenerating `frontend/src/lib/api/types.d.ts`. Nothing checks for drift automatically. See `frontend/CLAUDE.md`.
- Backend and frontend reach each other two ways. In production, the browser calls same-origin `/api/*`, which `frontend/src/routes/api/[...path]/+server.ts` proxies to `INTERNAL_API_URL`. Production starts `frontend/scripts/serve.ts` (`bun run start`): with `ORIGIN` set it fronts the adapter and supplies that origin (canonicalized and exported as `process.env.ORIGIN`); without it the adapter assumes `https` + `Host`, so plain-HTTP deployments must set `ORIGIN` (for example `ORIGIN=http://192.168.1.10:3000`) or signing in, setup and every other write fail with 403, and one startup warning says so. The backend's `CSRF_ORIGIN` must name the same origin (the Docker image defaults it to `ORIGIN`). The frontend's `handle` rejects any `POST`/`PUT`/`PATCH`/`DELETE` whose `Origin` is not that origin, so API clients writing through the frontend port must send `Origin`. Cookies follow `ORIGIN`'s scheme: the frontend relays backend cookies with `Secure` taken from `event.url`, so the backend's `SECURE_COOKIES` only matters for direct backend access. The proxy drops the front's `x-zondarr-*` headers and client `X-Forwarded-*`/`Forwarded` before calling the backend. In dev, `dev_cli` sets `PUBLIC_API_URL`, so the browser calls `:8000` directly and relies on `CORS_ORIGINS`. The proxy forwards `Origin`/`Referer` because `backend/src/zondarr/core/csrf.py` validates them. Don't strip those headers.
- Backend and frontend reach each other two ways. In production, the browser calls same-origin `/api/*`, which `frontend/src/routes/api/[...path]/+server.ts` proxies to `INTERNAL_API_URL`. Production starts `frontend/scripts/serve.ts` (from `frontend/`: `NODE_ENV=production bun scripts/serve.ts`, the `start` script's command; not `bun run start`, under which Bun 1.4.2's default shell delivers one Ctrl+C twice and the server exits 1 without the `SHUTDOWN_TIMEOUT` drain): with `ORIGIN` set it fronts the adapter and supplies that origin (canonicalized and exported as `process.env.ORIGIN`); without it the adapter assumes `https` + `Host`, so plain-HTTP deployments must set `ORIGIN` (for example `ORIGIN=http://192.168.1.10:3000`) or signing in, setup and every other write fail with 403, and one startup warning says so. The backend's `CSRF_ORIGIN` must name the same origin (the Docker image defaults it to `ORIGIN`). The frontend's `handle` rejects any `POST`/`PUT`/`PATCH`/`DELETE` whose `Origin` is not that origin, so API clients writing through the frontend port must send `Origin`. Cookies follow `ORIGIN`'s scheme: the frontend relays backend cookies with `Secure` taken from `event.url`, so the backend's `SECURE_COOKIES` only matters for direct backend access. The proxy drops the front's `x-zondarr-*` headers and client `X-Forwarded-*`/`Forwarded` before calling the backend. In dev, `dev_cli` sets `PUBLIC_API_URL`, so the browser calls `:8000` directly and relies on `CORS_ORIGINS`. The proxy forwards `Origin`/`Referer` because `backend/src/zondarr/core/csrf.py` validates them. Don't strip those headers.
- Tests use SQLite and fake media clients. Nothing talks to real Plex/Jellyfin or PostgreSQL.

## Reference files
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ bun install --frozen-lockfile
bun run dev
```

Build and start the production frontend from the same directory:
Build and start the production frontend, still in `frontend/`:

```sh
bun run build
bun run start
NODE_ENV=production bun scripts/serve.ts
```

`start` runs `scripts/serve.ts` with Bun in production mode. The Python
This is the command the `start` script runs. Run it directly: with Bun 1.4.2's
default shell, `bun run start` and `bun start` deliver one Ctrl+C twice, which
skips the `SHUTDOWN_TIMEOUT` drain and stops at once with exit status 1. The Python
backend runs separately; configure `INTERNAL_API_URL` for the frontend's server-side
API proxy. Leave `PUBLIC_API_URL` empty for same-origin browser requests. See
[.env.example](.env.example) for backend security, database and bootstrap settings.
Expand All @@ -55,7 +57,7 @@ fail, and one startup warning says so. Leave `ORIGIN` unset only behind an HTTPS
reverse proxy that passes the original `Host`. `ORIGIN` must be a bare `http(s)`
origin (no path, query, fragment or credentials), or the frontend refuses to start;
letter case, a default port and a trailing `/` are normalized. With `ORIGIN` set,
`start` fronts the SvelteKit server and supplies that origin.
`scripts/serve.ts` fronts the SvelteKit server and supplies that origin.

The backend's own CSRF check reads `CSRF_ORIGIN`: set it to the same value. The
Docker image defaults `CSRF_ORIGIN` to `ORIGIN`.
Expand Down
2 changes: 1 addition & 1 deletion frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Run these from `frontend/`, or prefix them with `bun run --cwd frontend` from th
- Component tests are named `*.svelte.test.ts` (plain `*.test.ts` is for non-component modules) and run in the `components` Vitest project, the only one with `svelteTesting()`; a plain `*.test.ts` that renders must be added to `componentTests` in `vite.config.ts`. Never render components in the `server` project: Kit 3's server `redirect()` breaks under the browser condition. Components that need `children` snippets or `bind:this` are rendered through a `*-test-wrapper.svelte`, as in `src/lib/components/error-boundary-test-wrapper.svelte`.
- `src/hooks.server.ts` redirects unauthenticated requests to `/login` unless the path is in `PUBLIC_PATHS`. A new public page needs an entry there. It also rejects any `POST`/`PUT`/`PATCH`/`DELETE` whose `Origin` is not `event.url.origin` (403, `FRONTEND_ORIGIN_MISMATCH`), and cookies the frontend sets or deletes take `secure` from `isSecureRequest(event.url)` (`$lib/server/request-origin.ts`).
- Everything that calls the backend (the `/api/[...path]` proxy, `/api/auth/setup`, the session refresh in `hooks.server.ts`) goes through `$lib/server/backend-relay.ts`: `backendRequestHeaders` drops hop-by-hop headers, the front's `x-zondarr-*` headers and client `X-Forwarded-*`/`Forwarded` (it keeps `Origin` and `Referer` for the backend's CSRF check), and backend `Set-Cookie` headers get `Secure` from `event.url` (`relayResponseHeaders`, or `event.cookies.parse` plus `relayedCookieOptions`), never from the backend.
- `@sveltejs/adapter-bun` is configured in `vite.config.ts` (`precompress: true`; no `svelte.config.js`), and `$lib` is an explicit alias there and in `tsconfig.json`. Production runs `bun run start`, which starts `scripts/serve.ts`: with `ORIGIN` set it fronts the adapter over a private Unix socket; without it the adapter listens directly and assumes `https` + `Host`. It parses `ORIGIN` by the origin contract every edbfi front shares (trimmed; canonical `url.origin` exported as `process.env.ORIGIN`; one fixed startup error and one fixed warning text, both pinned by tests) and forwards the raw request path (`forwardPath`), never `new URL(request.url)`. Its idle window (`CONNECTION_IDLE_TIMEOUT`/`IDLE_TIMEOUT`) applies to the client only: the timer is off from the end of the request body until the app answers, then re-armed (Bun 1.4.2 otherwise closes a request still waiting on the app). On shutdown it drains both sides within `SHUTDOWN_TIMEOUT` and exits at that deadline even if the app still has work in flight (a request waiting on the backend would otherwise keep the process alive). Its tests run in the `server` project against `scripts/fixtures/standin-adapter.js`.
- `@sveltejs/adapter-bun` is configured in `vite.config.ts` (`precompress: true`; no `svelte.config.js`), and `$lib` is an explicit alias there and in `tsconfig.json`. Production runs `NODE_ENV=production bun scripts/serve.ts` (the `start` script's command; run it directly: under `bun run start` Bun 1.4.2's default shell delivers one Ctrl+C or group `SIGTERM` twice, and the adapter treats the second as a second signal and exits 1 without the drain): with `ORIGIN` set it fronts the adapter over a private Unix socket; without it the adapter listens directly and assumes `https` + `Host`. It parses `ORIGIN` by the origin contract every edbfi front shares (trimmed; canonical `url.origin` exported as `process.env.ORIGIN`; one fixed startup error and one fixed warning text, both pinned by tests) and forwards the raw request path (`forwardPath`), never `new URL(request.url)`. Its idle window (`CONNECTION_IDLE_TIMEOUT`/`IDLE_TIMEOUT`) applies to the client only: the timer is off from the end of the request body until the app answers, then re-armed (Bun 1.4.2 otherwise closes a request still waiting on the app). On shutdown it drains both sides within `SHUTDOWN_TIMEOUT` and exits at that deadline even if the app still has work in flight (a request waiting on the backend would otherwise keep the process alive). Its tests run in the `server` project against `scripts/fixtures/standin-adapter.js`.

## Calling the backend

Expand Down
6 changes: 5 additions & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ repository root.
| Development server | `bun run dev` |
| Production build | `bun run build` |
| Preview the build | `bun run preview` |
| Start the built server | `bun run start` |
| Start the built server | `NODE_ENV=production bun scripts/serve.ts` (from `frontend/`) |
| Type check | `bun run check` |
| Lint and format check | `bun run check:biome` |
| Tests | `bun run test` |
Expand All @@ -31,6 +31,10 @@ repository root.
Use `bun run test`, not `bun test`: the latter is Bun's own runner and does not
compile Svelte.

Start the server directly, as above; the `start` script runs the same command. With
Bun 1.4.2's default shell, `bun run start` and `bun start` deliver one Ctrl+C twice,
which skips the `SHUTDOWN_TIMEOUT` drain and stops at once with exit status 1.

## Running with the backend

`uv run dev_cli` from the repository root starts the backend and this dev server
Expand Down