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

7.6 KiB
Raw Blame History

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.pyscp 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"

# 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 orderbranding.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.