diff --git a/compose-dev/docker-compose.yml b/compose-dev/docker-compose.yml new file mode 100644 index 0000000..6dacfd8 --- /dev/null +++ b/compose-dev/docker-compose.yml @@ -0,0 +1,45 @@ +# Jellyfin DEV — second instance for theme/branding experimentation +# Deploy path on nullstone: /opt/docker/jellyfin-dev/ +# Domain: dev.arrflix.s8n.ru (LAN-only via Pi-hole local DNS + no-guest middleware) +# +# Purpose: +# - Isolated playground for trying themes (Cineplex, ElegantFin, NeutralFin, ...) +# without touching the live arrflix.s8n.ru that real users (marco, house, guest, 5) +# are watching. +# - Same media library mounted READ-ONLY so dev sees the same titles but cannot +# mutate the on-disk library. +# - Separate config/cache so first-run wizard, accounts and branding live here only. +# - LAN-only: no-guest middleware on router; do NOT publish to WAN. +# +# Image pinned to 10.10.3 to match prod for theme parity. Bump prod first, then +# match here, never the other way around. + +services: + jellyfin-dev: + image: jellyfin/jellyfin:10.10.3 + container_name: jellyfin-dev + restart: unless-stopped + user: "1000:1000" + userns_mode: "host" + environment: + - TZ=Europe/London + - JELLYFIN_PublishedServerUrl=https://dev.arrflix.s8n.ru + volumes: + - /home/docker/jellyfin-dev/config:/config + - /home/docker/jellyfin-dev/cache:/cache + - /home/user/media:/media:ro + networks: + - proxy + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + - "traefik.http.routers.jellyfin-dev.rule=Host(`dev.arrflix.s8n.ru`)" + - "traefik.http.routers.jellyfin-dev.entrypoints=websecure" + - "traefik.http.routers.jellyfin-dev.tls=true" + - "traefik.http.routers.jellyfin-dev.tls.certresolver=letsencrypt" + - "traefik.http.routers.jellyfin-dev.middlewares=security-headers@file,no-guest@file" + - "traefik.http.services.jellyfin-dev.loadbalancer.server.port=8096" + +networks: + proxy: + external: true diff --git a/docs/12-dev-instance.md b/docs/12-dev-instance.md new file mode 100644 index 0000000..ea323b0 --- /dev/null +++ b/docs/12-dev-instance.md @@ -0,0 +1,174 @@ +# 12 — Jellyfin DEV instance for theme experimentation + +A second Jellyfin container, `jellyfin-dev`, runs alongside prod on +nullstone. Same media library (read-only), separate config/cache/users, +separate domain. LAN-only by design — you can break it freely without +real users (marco, house, guest, 5) noticing. + +--- + +## Architecture diff + +| Aspect | Prod | Dev | +|-------------------|-------------------------------------|-------------------------------------------| +| Container | `jellyfin` | `jellyfin-dev` | +| Image | `jellyfin/jellyfin:10.10.3` | `jellyfin/jellyfin:10.10.3` (must match) | +| Compose path | `/opt/docker/jellyfin/` | `/opt/docker/jellyfin-dev/` | +| Config dir | `/home/docker/jellyfin/{config,cache}` | `/home/docker/jellyfin-dev/{config,cache}` | +| Media mount | `/home/user/media:/media:ro` | `/home/user/media:/media:ro` (SAME, RO) | +| Domain | `arrflix.s8n.ru` | `dev.arrflix.s8n.ru` | +| Pi-hole DNS | `dns.hosts` in pihole.toml | `dns.hosts` in pihole.toml (added 2026-05-08) | +| Traefik router | `Host(arrflix.s8n.ru)` | `Host(dev.arrflix.s8n.ru)` | +| Cert | LE DNS-01 (Gandi) | LE DNS-01 (auto-issued on first request) | +| Middleware | `security-headers@file` only | `security-headers@file,no-guest@file` | +| WAN exposure | Yes during WAN window (doc 09) | NEVER — LAN-only forever | +| Internal port | `8096` | `8096` | +| User | `1000:1000` | `1000:1000` | +| `userns_mode` | `host` | `host` | +| index.html shim | Bind-mounted (doc 10) | None (vanilla shell — clean theme canvas) | +| Branding/auth | Configured | Empty — first-run wizard required | + +The compose file lives in this repo at `compose-dev/docker-compose.yml` +and is deployed to nullstone at `/opt/docker/jellyfin-dev/docker-compose.yml`. + +--- + +## How to use + +1. Open `https://dev.arrflix.s8n.ru` from any LAN/tailnet box. First visit hits the + first-run wizard — create an admin user (use any throwaway name; nothing + shared with prod). +2. Add libraries pointing at the same paths prod uses: + - `/media/movies` + - `/media/tv` + The library ROOTS are shared (read-only); dev will rescrape independently + into its own `library.db`. That's intentional — dev is a clean slate. +3. Apply a theme via Branding API or via the SPA shim (doc 10) by dropping + files into `/opt/docker/jellyfin-dev/web-overrides/` and adding the same + bind-mount pattern as prod (currently absent for a clean canvas). +4. Test, watch, break. Prod remains untouched on `arrflix.s8n.ru`. + +--- + +## Theme workflow (dev → prod) + +When a dev theme is "shipped": + +1. **Export branding** from dev: + ```bash + curl -k -H "X-Emby-Token: $DEV_TOKEN" \ + https://dev.arrflix.s8n.ru/Branding/Configuration > /tmp/branding.json + ``` +2. **POST to prod**: + ```bash + curl -k -X POST \ + -H "X-Emby-Token: *redacted*" \ + -H "Content-Type: application/json" \ + --data @/tmp/branding.json \ + https://arrflix.s8n.ru/System/Configuration/branding + ``` +3. If the theme involves SPA-shim files (custom JS/CSS), `rsync` them from + `dev:/opt/docker/jellyfin-dev/web-overrides/` to + `prod:/opt/docker/jellyfin/web-overrides/` and hot-reload prod via the + bind-mount (no container restart needed for read-only mounts on file + change — Jellyfin will serve the new file on next request). + +Auth tokens for dev are local to the dev instance — they'll be issued by +the dev wizard. They DO NOT cross over. + +--- + +## Reset / wipe dev + +When experiments make a mess: + +```bash +ssh user@192.168.0.100 +cd /opt/docker/jellyfin-dev +docker compose down +sudo rm -rf /home/docker/jellyfin-dev/config/* /home/docker/jellyfin-dev/cache/* +# (use the privileged-userns-host bypass if no sudo: +# docker run --rm --privileged --userns=host -v /home/docker:/h alpine \ +# sh -c 'rm -rf /h/jellyfin-dev/config/* /h/jellyfin-dev/cache/*') +docker compose up -d +``` + +First-run wizard reappears. The media library is intact (read-only mount, +unaffected). + +--- + +## LAN-only enforcement + +`no-guest@file` middleware (defined in `/opt/docker/traefik/config/dynamic.yml`) +restricts source IPs to: +- `127.0.0.0/8` +- `192.168.0.0/24` (LAN) +- `100.64.0.1/32` onyx, `100.64.0.2/32` nullstone, `100.64.0.4/32` office (tailnet) +- `82.22.5.233/32` YOU500 home IP +- `172.20.0.0/24` docker proxy gateway + +Anyone outside that list trying `https://dev.arrflix.s8n.ru` from the WAN +gets a Traefik 403. Even if a guest tailnet node (100.64.0.3 friend GPU) +hits dev, no-guest blocks them — only `tag:admin` and `tag:infra` are +allowed. + +There is **no plan** to expose dev publicly. If you need to test something +WAN-shaped, do it on prod inside the WAN window (doc 09) — never widen +dev's allowlist. + +--- + +## Risks and non-risks + +- **Read-only media mount.** Dev cannot write to `/home/user/media`. + Theme experiments cannot accidentally rename, delete or scramble files. +- **Separate library.db.** Dev rescrapes from scratch. If a metadata + experiment in dev produces bad results, it never touches prod metadata. +- **Same Traefik instance.** Both routers share the proxy network and the + one Traefik. A misconfigured label on dev could *theoretically* shadow + prod's router, but the rules are `Host(dev.arrflix.s8n.ru)` vs + `Host(arrflix.s8n.ru)` — disjoint. Sanity-check after any compose edit + with `curl -kI https://arrflix.s8n.ru/`. +- **Same image tag.** Bumping prod to a new Jellyfin version means + bumping dev too; do prod first, then sync dev. Never test a version + bump on dev and forget to mirror prod — the API surface might drift. +- **No shared sessions.** Tokens, users, watch progress, playlists are + 100% isolated. A test admin in dev cannot act on prod, and vice versa. + +--- + +## Quick reference + +``` +# Status +ssh user@192.168.0.100 'docker ps --filter name=jellyfin' + +# Logs +ssh user@192.168.0.100 'docker logs jellyfin-dev --tail 100 -f' + +# Restart +ssh user@192.168.0.100 'cd /opt/docker/jellyfin-dev && docker compose restart' + +# Stop / start +ssh user@192.168.0.100 'cd /opt/docker/jellyfin-dev && docker compose down' +ssh user@192.168.0.100 'cd /opt/docker/jellyfin-dev && docker compose up -d' + +# Health check from onyx +curl -kI https://dev.arrflix.s8n.ru +# expect HTTP/2 302, location: web/ +``` + +--- + +## DNS pin path used + +The dev hostname was added to Pi-hole's `dns.hosts` array in +`/opt/docker/pihole/etc-pihole/pihole.toml` (alongside the existing +LAN-only entries) and Pi-hole was restarted to pick up the change. +The legacy `custom.list` file is still present but is no longer the +authoritative source — `dns.hosts` in `pihole.toml` is what +`pihole-FTL` actually consults. + +If `dev.arrflix.s8n.ru` ever fails to resolve, restart Pi-hole and +re-check the `dns.hosts` array. diff --git a/docs/14-theme-audit.md b/docs/14-theme-audit.md new file mode 100644 index 0000000..6f16575 --- /dev/null +++ b/docs/14-theme-audit.md @@ -0,0 +1,617 @@ +# 14 — Theme Audit + Detail-Page Backdrop Diagnosis + +Status: **read-only audit**, executed 2026-05-08 against +`https://arrflix.s8n.ru` (Jellyfin 10.10.3 on nullstone). The owner has +just rolled back to **Cineplex v1.0.6** (the Netflix-faithful theme) +after a brief ElegantFin → NeutralFin experiment that was documented in +docs 04 §3e and 11 respectively. Reported issue: on detail pages the +**backdrop image leaves a visible vertical black band on the left** where +the title/info column sits. Owner asked for a forward plan, not a fix. + +> **No state mutated.** No POST to `/System/Configuration/branding`, +> no edit to `/jellyfin/jellyfin-web/index.html`, no docker action. +> Read-only over SSH and against the public `/Branding/Configuration` +> + authenticated `/System/Configuration/branding` endpoints. + +--- + +## 1. Current state inventory + +### 1a. Active theme + +`/System/Configuration/branding` returns: + +| Field | Value | +|---|---| +| `LoginDisclaimer` | `"Welcome to ARRFLIX - Private invite only service"` | +| `SplashscreenEnabled` | `true` | +| `CustomCss` (size) | **25 225 chars** (most of which is the embedded ARRFLIX wordmark data-URL — twice) | + +Sole `@import` line: + +```css +@import url("https://cdn.jsdelivr.net/gh/MRunkehl/cineplex@v1.0.6/cineplex.css"); +``` + +Cineplex itself transitively imports +`cineplex@v1.0.5/finity-theme/finity-complete.css` (its parent theme, +**Finity** by prism2001). This matters for the backdrop diagnosis below. + +### 1b. CustomCss block inventory (every rule, in order) + +`!important` declarations: **17**. `#E50914` occurrences: **0** in +CustomCss; **1** in `web-overrides/index.html` critical-path `