legacy-arrflix/testing/THEMING.md
s8n d9d6bdba64 testing/ folder: theme-edit guides + error catalog + headless recipes
7 docs in /testing/ — institutional memory after 6+ regressions in
24-48h on the v6 theme. Read before any edit.

  README.md           — index + quickstart
  THEMING.md          — safe-edit checklist + layer/specificity tables
  ERROR-PATTERNS.md   — 12 cataloged patterns (Symptom/Cause/Diag/Fix/Prev)
  HEADLESS-PROBE.md   — 11 playwright recipes (md5 chain, darkPct,
                        ancestor bg sample, dropdown listItem probe)
  ROLLBACK.md         — 8 emergency revert recipes (overlay, branding,
                        encoding, full-from-repo, dev-clone-prod,
                        git-revert, pw-reset, bind-mount inode-swap)
  SMOKE-TEST.md       — manual + headless verify checklist
  DEPLOY.md           — dev → prod promotion workflow with backup +
                        chown root + restart inode-swap

Empty subdirs: snipUSER-Es/, recipes/, incidents/ (post-mortems land here).

Goal: stop reinventing the same fixes. Catalog every error class,
codify the recovery, build a skills folder for future ARRFLIX work.
2026-05-10 00:47:20 +01:00

123 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# THEMING — how to edit the ARRFLIX theme without breaking it
> Short, actionable companion to `docs/31-theme-layer-model-and-edit-guide.md`.
> Read 31 once for the why; come back here for the checklist every edit.
## TL;DR — checklist before EVERY theme edit
1. Read `docs/31-theme-layer-model-and-edit-guide.md` (canonical layer model).
2. Decide: **am I painting an ancestor of `<video>`?** (see layer table below).
3. If yes → scope with `body.arrflix-themed:not(.arrflix-video-active)` AND add the matching transparent rule under `body.arrflix-themed.arrflix-video-active`.
4. Use the specificity table to predict what wins. `!important` does NOT promote specificity.
5. Edit `bin/inject-middle-theme.py` — NEVER hot-patch the deployed overlay.
6. `python3 bin/inject-middle-theme.py``scp web-overrides/index.html dev:/opt/docker/jellyfin-dev/web-overrides/``docker restart jellyfin-dev`.
7. Hard-refresh browser (`Ctrl+Shift+R`) — bind-mount serves stale otherwise.
8. Run `testing/SMOKE-TEST.md` (login, home, video, OSD).
9. Green? Promote per `testing/DEPLOY.md`. Red? `testing/ROLLBACK.md`.
## The layer model (condensed)
| Layer | Element | bg ownership |
|------:|---------|--------------|
| 0 | `<html>` | `#000` (JS inline-style pinned via `setProperty(...,'important')`) |
| 1 | `<body>` | L1 `#000` off-video / L2 `transparent` on-video |
| 2 | `.backgroundContainer` / `.skinBody` / `#reactRoot` | follows L1/L2 |
| 3 | `.mainAnimatedPages` / `.pageContainer` | follows L1/L2 |
| 4 | `.skinHeader` | `#000` off-video, `display:none` on-video |
| 5 | `.videoPlayerContainer` (z:1000) → `<video.htmlvideoplayer>` | transparent on-video |
| 6 | `.osdControls` / `.videoOsdBottom` (z:~11001500) | DO NOT touch — Jellyfin owns |
| 7 | `.dialogContainer` / `.actionSheet` (z:~2000+) | DO NOT touch — Jellyfin owns |
## Specificity quick reference
| Selector | (a,b,c) | When to use |
|----------|---------|-------------|
| `body` | (0,0,1) | almost never |
| `body.arrflix-themed` | (0,1,1) | base theme rule, off/on video both |
| `body.arrflix-themed:not(.arrflix-video-active)` | (0,2,1) | **L1**: off-video bg paint |
| `body.arrflix-themed.arrflix-video-active` | (0,2,1) | **L2**: on-video transparent |
| `body.arrflix-themed.arrflix-video-active #videoOsdPage` | (1,2,1) | beats Cineplex `#videoOsdPage .pageContainer` (1,1,0) |
| `body.arrflix-video-active:not(:has(#loginPage:not(.hide))) .skinHeader` | (0,4,2) | beats Cineplex `display:flex` on header |
L1 and L2 tie on (0,2,1). **Source order decides** — L2 must come AFTER L1 in `inject-middle-theme.py`. Reordering reopens the black-screen bug.
## DO NOT DO
- Set `z-index` on `<video>` or `.videoPlayerContainer` above 1000 → covers OSD scrubber/buttons (image-12 incident).
- Add `background-color` rules without `:not(.arrflix-video-active)` gate → black-screen-over-video.
- Hot-patch `/opt/docker/jellyfin/web-overrides/index.html` in place → repo↔prod drift, INC1 root cause.
- `cp` overlay then skip `docker restart jellyfin` → bind-mount inode swap, container serves stale.
- Use `:has(.htmlVideoPlayer)` (camelCase) — class is `.htmlvideoplayer` lowercase. The selector silently never matches.
- Drop a `<video>` literal into `branding.xml` `<CustomCss>` (even in a comment) without escaping → XML parse fails silently, theme disappears site-wide. Use `&lt;video&gt;`.
- Add `!important` hoping it beats a higher-specificity rule. Among `!important` rules, specificity still wins.
## When to add to L1/L2 paired rules
If your rule paints `background`, `background-color`, or `background-image` on **any** of:
```
body, html, .backgroundContainer, .skinBody, .mainAnimatedPage, .mainAnimatedPages,
.pageContainer, #reactRoot, .videoPlayerContainer, #videoOsdPage, .libraryPage,
video.htmlvideoplayer, .emby-scroller, .backdropContainer
```
→ Add the selector to **BOTH** lists in `bin/inject-middle-theme.py`:
- **L1 list** — under `/* --- L1: PURE-BLACK BG (off-video only) ------ */`, prefixed with `body.arrflix-themed:not(.arrflix-video-active)`.
- **L2 list** — under the L2 transparent block, prefixed with `body.arrflix-themed.arrflix-video-active`, value `background:transparent !important`.
Always paired. Off-video must stay opaque black; on-video must be transparent so `<video>` pixels show through.
## Safe-edit recipe — "make the search input focus ring red"
```bash
# 1. Edit the injector (NOT the deployed overlay)
$EDITOR /tmp/arrflix-recon/bin/inject-middle-theme.py
# Add inside the CSS string, search-input section:
# body.arrflix-themed .searchFields input:focus {
# border-color: #E50914 !important;
# box-shadow: 0 0 0 2px rgba(229,9,20,.35) !important;
# }
# Specificity (0,2,1) — does NOT touch a <video> ancestor → no L1/L2 pairing needed.
# 2. Regenerate the overlay
cd /tmp/arrflix-recon && python3 bin/inject-middle-theme.py
# 3. Sanity: exactly one marker block
grep -c ARRFLIX-MIDDLE-THEME-BEGIN web-overrides/index.html # = 1
# 4. Push to dev only
scp web-overrides/index.html nullstone:/opt/docker/jellyfin-dev/web-overrides/index.html
ssh nullstone 'docker restart jellyfin-dev'
# 5. Verify served
curl -s https://dev.arrflix.s8n.ru/web/index.html | grep -c ARRFLIX-MIDDLE-THEME-BEGIN # = 1
# 6. Hard-refresh browser, run testing/SMOKE-TEST.md, then promote per testing/DEPLOY.md.
```
## How to add a new skin variant
Skin variants are alternative CSS blocks that swap a single visual concern (selector highlight, header logo treatment, etc.) without forking the whole theme.
- Location: `web-overrides/skins/`.
- Naming: `<concern>-variant-<NN>-<short-slug>.css` (e.g. `selector-variant-02-red-underline.css`).
- Format: file-level comment header explaining what concern it replaces, which variant is currently active in `inject-middle-theme.py`, and a "drop into the CSS string" instruction. Body is plain CSS scoped under `body.arrflix-themed …`.
- Activation: copy the rules into the matching section of `bin/inject-middle-theme.py`, regen overlay, deploy. Skins are NOT auto-loaded — the file is a parking spot.
## Common pitfalls
- **camelCase vs lowercase classes** — `.htmlVideoPlayer` does NOT exist; the real class is `.htmlvideoplayer`. Same trap on `.videoOsdBottom` (correct) vs `.videoosdbottom` (wrong).
- **Cineplex CSS load order** — `branding.xml``@import url('/web/cineplex.css')` is injected as a `<style>` AFTER our inline block. On equal specificity Cineplex wins. Bump specificity, do NOT reorder.
- **`branding.xml` XML parse** — `<CustomCss>` content must be XML-safe. Escape `<` `>` in any CSS comment that mentions HTML tags. Silent failure = whole branding skipped.
- **iframes / shadow DOM** — Jellyfin web does not currently use either. N/A; skip.
- **`backdrop-filter: blur()`** — only renders if there's content scrolling/painted behind the fixed element. On a pure-black bg the blur is invisible (no pixel diff). Test on a page with a poster backdrop.
- **`getComputedStyle(html).backgroundColor` returns `rgba(0,0,0,0)`** despite stylesheet rules — Chromium root-canvas quirk. We pin `<html>` via JS `style.setProperty('background-color','#000','important')`. Don't fight it from CSS.
## See also
- `docs/31-theme-layer-model-and-edit-guide.md` — canonical layer model and history of past incidents.
- `testing/ERROR-PATTERNS.md` — catalog of past mistakes (INC1INC7, v6-stable, image-12).
- `testing/SMOKE-TEST.md` — 4-step manual verify after any theme change.
- `testing/HEADLESS-PROBE.md` — Playwright recipes for DOM / `darkPct` / OSD-visible assertions.
- `testing/DEPLOY.md` / `testing/ROLLBACK.md` — promote-to-prod and revert procedures.