# 30 — v6-stable success — 2026-05-09

> Save state. Owner pronounced "near perfect". Both servers (prod + dev) byte-identical overlay (`md5 364cc890c58f02d07cf50b43b31a48f0`), both branding.xml parses cleanly, both `EnableTonemapping=true`, both serve `/Branding/Css.css` 36 256 B (Cineplex theme delivered to browser), playback verified visually green on dev and prod.

Tag: `v6-stable-2026-05-09`. Snapshot at `snapshots/2026-05-09-v6-stable/index.html` (md5 matches deployed overlay). Older snapshot `snapshots/2026-05-08-pre-elegantfin/` removed — replaced by this one.

---

## What works

| Surface | State |
|---|---|
| Logo center | ARRFLIX wordmark dead-center in header (235x85 PNG inlined as data-URL from branding.xml). |
| Nav left | `MOVIES` + `SERIES` uppercase links inside `.headerLeft`. Bare `#/movies.html` and `#/tv.html` hrefs (no `topParentId` query). |
| Search right | Stock `.headerSearchButton` pushed to flex-end via `.headerRight{justify-content:flex-end}`. |
| Login page | Stock-with-Cineplex (auth-gated `body.arrflix-themed`). ARRFLIX top-left red, Manual Login form, Welcome footer. No user picker, no Quick Connect. |
| Video player | `.skinHeader` hidden via `body.arrflix-video-active` — no theme bar leaking on top of `<video>`. Specificity (0,4,2) beats Cineplex's `display:flex !important` rule (0,3,2). |
| Favicon | A-mark (red Netflix-style "A") in browser tab. Hijack JS removes stock wordmark icon links + pins `data-arrflix-icon="A"` href against the older `lockFavicon()` shim's `setInterval` clobber. |
| Streaming | HDR10 sources tonemap correctly (`EnableTonemapping=true` flipped — see doc 21). Doc-28 INC7 transparent-`<video>` rule reaches browsers because branding.xml `<video>` literal is escaped to `&lt;video&gt;` so XML parser stops choking on the CustomCss block. |

## Files of record

| File | md5 | Purpose |
|------|-----|---------|
| `web-overrides/index.html` | `364cc890c58f02d07cf50b43b31a48f0` | The compiled overlay. Deployed to both prod and dev (under different host filenames). |
| `snapshots/2026-05-09-v6-stable/index.html` | same as above | Frozen save state for rollback. |
| `bin/inject-middle-theme.py` | (current) | Builder. Idempotent. Reads `web-overrides/assets/{arrflix-A.b64,arrflix-wordmark.b64-url}`. Writes a backup `*.bak.pre-middle-v6.<unix-ts>` before overwriting. |
| `web-overrides/assets/arrflix-A.png` | (138x180 trimmed, transparent bg) | Source asset — favicon "A" mark. |
| `web-overrides/assets/arrflix-A.b64` | 29 192 chars | Inline-ready base64 of the A mark. |
| `web-overrides/assets/arrflix-wordmark.b64-url` | 11 350 chars | Inline-ready data-URL of the ARRFLIX wordmark (235x85). Extracted from branding.xml. |

## Server-side state (not in repo, document for rollback)

| Path | State |
|------|-------|
| `/opt/docker/jellyfin/web-overrides/index.html` (prod) | md5 `364cc890`. owned `root:root`. Bind-mounted `/jellyfin/jellyfin-web/index.html:ro`. |
| `/opt/docker/jellyfin/web-overrides/index.html.bak.pre-favfix.1778318089` | rollback target — pre-v6+favfix prod overlay. |
| `/opt/docker/jellyfin-dev/web-overrides/index-dev.html` (dev) | md5 `364cc890`. owned `user:user`. Same content as prod. |
| `/opt/docker/jellyfin-dev/web-overrides/index-dev.html.bak.pre-middle-v6` | rollback target — pre-v6 dev overlay. |
| `/home/docker/jellyfin/config/config/branding.xml` | XML-valid (`<video>` escaped). 36 607 B. CustomCss reaches browsers via `/Branding/Css.css`. |
| `/home/docker/jellyfin/config/config/branding.xml.bak.pre-middle-v6.1778295444` | rollback target — pre-escape. |
| `/home/docker/jellyfin/config/config/encoding.xml` | `EnableTonemapping=true`, `TonemappingAlgorithm=bt2390`, `HardwareAccelerationType=none`. |
| `/home/docker/jellyfin/config/config/encoding.xml.bak.pre-tonemap.1778318089` | rollback target — pre-flip. |
| `/home/docker/jellyfin-dev/config/config/branding.xml` | Same content as prod. |
| `/home/docker/jellyfin-dev/config/config/branding.xml.bak.dev-pre-resync` | rollback target — pre-resync (dev's older minimal branding). |
| `/home/docker/jellyfin-dev/config/config/encoding.xml` | `EnableTonemapping=true`. |
| Container: `jellyfin` | `jellyfin/jellyfin:10.10.3`, healthy, restart unless-stopped. |
| Container: `jellyfin-dev` | same image. |

## Accounts

### Prod
- `s8n` — admin. Hidden. Password is private.
- `USER-A`, `USER-G`, `USER-F`, `USER-B`, `USER-E`, `5`, `USER-D`, `USER-C`, `Jayden`, `IX`, `ferghal` — non-admin, hidden.
- `Loseious` — non-admin, hidden, `EnablePlaybackRemuxing=true`. Created 2026-05-09. Password is private.

### Dev
- `test` / `123` — admin, hidden. Single-account theme test sandbox.

## Roadmap closed in this iteration

| Item | Status |
|------|--------|
| Streaming on prod (doc 28) | ✅ closed — branding.xml XML escape was the missing delivery layer. INC7 transparent-`<video>` rule now reaches browsers. |
| Theme parity dev↔prod | ✅ overlay md5 identical. |
| Favicon = A-mark | ✅ hijack JS pins our `data-arrflix-icon="A"` links against `lockFavicon` clobber. |
| Tonemap HDR10 (doc 21) | ✅ `EnableTonemapping=false → true` on both servers. ffmpeg gains `zscale → tonemap → format` stage on next transcode of HDR10 source. |
| Quick Connect off + manual login | ✅ both prod and dev (`QuickConnectAvailable=false` in system.xml). All non-admin users `IsHidden=true` so no picker. |
| Video page header leak | ✅ `body.arrflix-video-active` toggle hides `.skinHeader` during playback; specificity (0,4,2) beats Cineplex (0,3,2). |
| Duplicate "Movies" h3 on library pages | ✅ `body.arrflix-themed .skinHeader .headerLeft > h3.pageTitle:not(.pageTitleWithLogo){display:none!important}`. |

## Roadmap open (deferred — non-blocking)

| Item | Note |
|------|------|
| `compose-dev/docker-compose.yml` in repo lacks the overlay bind-mount | The host has it; repo is drift. |
| Locale-en-only chunk JS files (94 of them) bind-mounted on prod, absent on dev | Dev users get stock locale strings. Cosmetic only. |
| `xmllint --noout branding.xml` in CI | Silent XML parse failure cost a multi-hour debug cycle. |
| `bin/headless-test-v2.py` darkPct assertion | INC7 lesson — element state alone (currentTime, readyState) doesn't catch CSS-overlay-over-video. |
| Movies/TV stuck-spinner from cached `?topParentId=movies` URL | Stock Jellyfin `viewContainer.tryRestoreView` quirk. Not a v6 regression. |
| Splashscreen blurred-poster login bg | Owner reference image #2 had it. Currently neither prod nor dev renders it. |

## Lesson

The whole BLACK-PASS / INC7 / Traefik-SW chain in doc 26 + 28 was correctly diagnosed but the **delivery** layer was broken since 2026-05-08 by a single unescaped `<video>` literal in a CSS comment. `xmllint --noout` would have caught it instantly. **Add it to CI.** Silent XML parse failures with zero UI feedback are the worst class of bug — Jellyfin returned HTTP 200 with empty body for `/Branding/Css.css`, no banner, no admin alert.

## Rollback

If anything regresses, restore in this order:

```bash
# Overlay rollback (prod)
docker run --rm --userns=host -v /opt/docker/jellyfin/web-overrides:/d:rw alpine \
  sh -c 'cp /d/index.html.bak.pre-favfix.1778318089 /d/index.html && chown root:root /d/index.html'

# Branding rollback (prod) — only if XML escape causes new issues
docker run --rm --userns=host -v /home/docker/jellyfin/config/config:/d:rw alpine \
  cp /d/branding.xml.bak.pre-middle-v6.1778295444 /d/branding.xml

# Tonemap rollback (prod)
docker run --rm --userns=host -v /home/docker/jellyfin/config/config:/d:rw alpine \
  cp /d/encoding.xml.bak.pre-tonemap.1778318089 /d/encoding.xml

docker restart jellyfin
```

Or git-side rollback to the previous commit `52a7df6` (pre-favfix v6) and re-deploy.
