4-variant ARRFLIX login picker served at localhost:666 (variants 01 The
Theater · 02 The Marquee · 03 The Cinema · 04 The Noir). Variant 01 was
selected. Includes theater-fullsize.html (1920x1080 mockup), 470x170
landscape arrflix-logo (PNG + b64), poster-bg backdrop, and tv_theater_port.py
CSS-block patcher reference for the later prod deploy.
Persisted from /tmp to survive reboot; iteration continues in this dir
under a 4-agent crew (Designer / Coder / Chromium / Inspector).
Episode resolved as Type=Episode under correct Series + Season but JF
MovieResolver did not parse SxxEyy from filename — ParentIndexNumber and
IndexNumber were null. Series-level + item-level full refresh did not fix.
Required manual API override (PUT IndexNumber=491, ParentIndexNumber=1,
LockData=true).
All 5 prior LFP episodes already have LockData=true — this appears to be
the established pattern for new Lex Fridman Podcast episodes. Generalise
into playbook §1c after one more confirmation.
Single-video imports per playbook §1d (collectionType=movies):
- Johnny Harris — Why the US is deporting so many people (20251031)
- The Guardian — NSA whistleblower Edward Snowden (20130709)
Snowden run exposed Jellyfin's single-file channel folder caveat:
MovieResolver parses folder name as item title when only one media file
exists. Worked around with PUT /Items/<id> Name + LockData=true.
Documented in the run log for future hardening into playbook §1d.
Procedural playbooks (READMEs, helpers) moved to git.s8n.ru/s8n/beta-flix
and rewritten for stock Jellyfin 10.11.8. Per-iteration runs/ and
CHANGELOG.md stay here as history. Replace the three top-level READMEs
with pointer stubs.
Brand-new container, brand-new volumes, ZERO ARRFLIX customisation.
Sister instance to prod (10.10.3, untouched) and dev (10.11.8 + scyfin).
P1+P2 from the rebuild plan: Movies + TV Shows libraries added via API,
library scan complete (4 movies / 12 series / 230 eps). Auto-scrape
matched 10/12 series + 4/4 movies to canonical TMDB IDs without manual
intervention; 3 unmatched are TMDB-absent indie content.
No theme, no shim, no CustomCss, no plugins, no user import, no home-
section seed — owner explicitly asked for a stock baseline.
Documents:
- Staged migration path (10.10.3 -> 10.10.7 -> 10.11.8). Direct skip is
unsupported per jellyfin#15027/#15244/#15293/#15504.
- Snapshots at /home/user/snapshots/.
- Schema diffs (library.db consolidated into jellyfin.db, ffmpeg 7.1.3).
- Theme swap: Cineplex v1.0.6 -> scyfin OLED (the only top-tier 10.11.x
theme with active 10.11 branch + zero open compat bugs).
- The 10.10.3 home-section bug (CustomPrefs write but no HomeSection row)
is fixed in 10.11.8 when POST body includes a HomeSections array.
Verified end-to-end with test user.
- Outstanding work before promoting dev to prod.
- Rollback procedure (EF Core migrations are forward-only).
The Jellyfin 10.10.3 REST endpoint won't INSERT HomeSection rows for the
'Jellyfin Web' client (only updates existing rows), so users whose web-
client DisplayPreferences was created without explicit home-section state
fall back to factory defaults that include Next Up. Applying CustomPrefs
via API doesn't help — the web client reads HomeSection rows.
Direct DB seed: for every DisplayPreferences row with zero HomeSection
rows, insert [Resume, LatestMedia, None*8]. Also replace Type=7 (NextUp)
with Type=0 (None) across all DPs. Idempotent.
Applied 2026-05-11 across prod (13 users) and dev (1 user). Verified
yummyhunny home now shows Continue Watching above Latest Media.
The Jellyfin 10.10.3 web client reads DisplayPreferences with the client
name 'Jellyfin Web'. The legacy 'emby' value is read by older SDKs only —
so writing only to 'emby' (the original script) updates the DB but has no
visible effect on the web UI. The empty 'Jellyfin Web' doc falls back to
factory defaults that include Next Up.
Now patches all four per-client docs ('Jellyfin Web', 'emby', 'emby-mobile',
'emby-web') and ensures both Continue Watching (resume) and Latest Media
are present so blank users don't fall back to defaults.
Applied 2026-05-11 across prod (13 users x 4 clients) and dev (1x4).
Applied 2026-05-11 across both prod (13 users) and dev (1 user).
Replaces 'nextup' homesection slot with 'none' on every user; seeds 'resume'
at slot 0 for users whose CustomPrefs was empty (would have fallen back to
the Jellyfin factory default that includes Next Up).
Idempotent — re-running is a no-op once converged.
Original-release bitmap subs (PGS, VobSub, dvd_subtitle) are first-class,
not stop-gaps. They're the canonical studio render — bitmap encoding is
just a format choice, not a quality compromise. OCR'd or AI-rebuilt
sidecars introduce transcription error that the source doesn't have.
STYLE.md changes:
- New "Source priority" section with 4 tiers: original text > original
bitmap > trusted text rips > WhisperX rebuild.
- "What lands on disk" loosened: at least one English stream (embedded
OR sidecar), keep embedded codec as-is, sidecar still .srt.
- New "OCR bitmap -> text" section documenting pgsrip recipe as an
optional UX-nicety augmentation, not a correctness fix.
- "Why these rules" now explains why original > pretty (esp. for older
shows like Futurama S1-3 / early Archer where the master is the only
authoritative source and upscale artifacts already dominate).
STOPGAP-SUBS.md: header note clarifying bitmap-from-disc is NOT a
stop-gap; lists Lilo & Stitch (2002) and Archer (2009) S02 as examples
of correct-as-shipped library entries.
Step 4 rewritten: /Library/Refresh is a silent no-op on this build,
must POST to /ScheduledTasks/Running/<scan-task-id> directly. Old
endpoint moved to known-broken table.
Step 5 rewritten: /Items/Counts is scope-cached and stays stale
even after items are indexed. Use /Shows/<id>/Episodes?Season=<NN>
as authoritative verify with provider + image-tag checks.
Both bugs surfaced in archer-s02-2009 run. LibraryMonitor inotify
auto-fire also confirmed broken (failed on lilo-stitch-2002 and
archer-s02-2009 runs).
Replaced user@192.168.0.100 (LAN IP, RFC1918 — flagged by
gitleaks lan-ip-rfc1918) with user@nullstone throughout. SSH config
already aliases nullstone -> 192.168.0.100. Aligns with CLAUDE.md
two-file doc rule: IPs belong in SYSTEM.md, not operational docs.
audit-coverage.py DEFAULT_OUT still pointed at processes/subtitles/
COVERAGE.md after the processes/->playbooks/ rename. Updated to
playbooks/subtitles/COVERAGE.md.
Refreshed COVERAGE.md to current state (230 episodes total, was 197):
+ Archer (10) — OK-EMBED
+ Maul Shadow Lord (10 new 2160p) — OK-EMBED
+ Maul [Before Upscale] (10 split) — OK-EMBED
+ Lilo & Stitch movie — sidecar OK
+ 9 American Dad gaps closed by owner since last audit
+ Big Lez / Donny & Clarence / Mike Nolan now 100%
Strict-sidecar shows: Sassy, Big Lez, Donny & Clarence, Mike Nolan,
American Dad (84% partial — 9 gaps remain). Other shows OK-EMBED
which is playable but not STYLE.md-compliant — fetch optional.
13 eps imported, all matched on TVDB/IMDb. Run notes flag two
new playbook bugs found: /Library/Refresh is a silent no-op (must
trigger /ScheduledTasks/Running/<id> directly) and /Items/Counts
is scope-cached (use /Shows/<id>/Episodes?Season=N for verify).
Pending follow-ups recorded for v1.1 playbook revision.
10 episodes, 1080p AI x265 10-bit Joy release, HE-AAC 5.1 ENG +
3x embedded DVDsub (ENG/SPA/FRE) per episode. Per README.md:41
quality bar — AI-upscale acceptable when source doesn't support 4K.
Items/Counts: Series 11->12, Episodes 207->217. Series item
9d22c409d531...
Run log notes 3rd consecutive LibraryMonitor failure — playbook
v1.1 should make manual /Library/Refresh MANDATORY. Same TMDb
auto-match miss as Maul; flagged for owner UI Identify step or
[tmdbid-NNNN] folder token.
Saved variant 1 "Netflix-cinema" detail-page redesign to
web-overrides/skins/detail-variant-01-netflix-cinema.html for
future reference. Not applied to dev/prod.
Imported Star Wars: Maul - Shadow Lord S01 2160p WEB-DL HEVC SDR
(10 episodes, ~21 GB) per playbooks/import-media/ v1.0. First
"replace-with-comparison" run:
- Existing 1080p upscale renamed in-place to
"Star Wars - Maul - Shadow Lord [Before Upscale] (2026)/"
with tvshow.nfo <lockdata>true</lockdata> to suppress Jellyfin
TMDb auto-merge with the new canonical.
- New 2160p staged on onyx, rsync'd to nullstone canonical path
/home/user/media/tv/Star Wars - Maul - Shadow Lord (2026)/Season 01/
- Both series live as separate items; Items/Counts bumped
Series 10->11, Episodes 197->207.
Run log at playbooks/import-media/runs/star-wars-maul-shadow-lord-
2026-2160p.md flags v1.1 follow-ups: document the
[Before Upscale] pattern, rsync resume idempotency, and
[tmdbid-NNNN] folder token for titles that fail auto-match.
ffprobe confirmed E01 = HEVC Main 8-bit 3840x2160 SDR @ 12 Mbps,
EAC3 5.1 ENG Atmos + ITA dub, 4x embedded subrip per episode.
Direct-play on capable clients.
TMDb match failed auto on both folders (recent Disney+ release);
operator to manually identify via UI.
Adds web-overrides/popup-designs/ with the 4-up preview (preview.html) and
three standalone candidate designs (a-strip.html / b-terminal.html /
c-minimal.html) so we can revisit the alternates later without re-running
the design generator. Owner picked A.
Design A is wired into Jellyfin's stock .upNextDialog by overriding its
CSS to a full-bleed bottom 26 % strip with white 'Start Now' CTA and a
custom SVG countdown ring that mirrors .upNextDialog-countdownText. The
DOM stays intact so Jellyfin's own countdown timer and click handlers on
.btnStartNow / .btnHide keep working untouched.
Shim is bracketed by NEXT-EP-POPUP-BEGIN / NEXT-EP-POPUP-END markers
inside the existing ARRFLIX-SHIM block in web-overrides/index-dev.html.
Only deployed to dev (dev.arrflix.s8n.ru) for spot-check; promote to
prod once verified by editing prod's index.html the same way and
redeploying via the nsenter trick.
Adds bin/revert-next-ep-popup.sh — sed-deletes between markers, defaults
to dev with --prod flag for prod target. Saves a timestamped backup and
prints the redeploy command.
Adds lib/audit-coverage.py: queries Jellyfin live for every series, every
episode, and every movie; classifies each by whether the English subtitle
comes from a sidecar, embedded stream, or doesn't exist; renders a
Markdown report with one-char-per-episode bars for visual scanning. Output
file is processes/subtitles/COVERAGE.md, regenerated on demand.
v2 sub-rest-fetch.py and v3 sub-a7d-fetch.py now invoke the audit at end
of a successful run, so the committed coverage file stays in sync with
library state without manual intervention. v3.5 yt-fetch path skips the
auto-call since it doesn't speak to Jellyfin directly; run audit manually
after copying YT sidecars to nullstone.
README.md surfaces the audit at the top so anyone landing in the recipe
folder sees current state before starting a run.
Owner accepted Sassy the Sasquatch S01 v3.5 YouTube-auto-CC subs as
'85 % acceptable, fine, not great' but flagged them for v4 WhisperX
rebuild. Adds a single worklist file (STOPGAP-SUBS.md) so every show
that ships via the v3.5 path gets logged for the eventual v4 sweep
instead of being silently forgotten.
Sassy run log gets a STOP-GAP banner at the top pointing to the new
worklist. README.md gets a stop-gap-exception note alongside the
STYLE.md hard-prereq paragraph. ROADMAP H5 now points at the worklist
file as the canonical source of which shows v4 needs to regenerate.
Adds lib/sub-yt-fetch.sh (yt-dlp wrapper) and lib/yt-clean.py (collapses
YouTube's rolling-window auto-caption VTT into a flat SRT). For shows
distributed YouTube-first that have no community subs anywhere -- verified
via three parallel research agents covering OpenSubtitles REST, OS legacy,
Addic7ed, SubDL, SubSource, and Podnapisi for the 5 niche shows in the
library, plus a price-vs-coverage analysis of OpenSubtitles VIP.
Findings: OS VIP would not have helped on the niche shows (it is
download-cap relief, not coverage unlock; same catalog as free). All 4
Jarrad Wright shows in the library (Sassy, Big Lez Saga, Donny &
Clarence, Mike Nolan) live on the same channel and have only YouTube
auto-CC available. v3.5 ships those, explicitly violating STYLE.md
'best quality' as a tracked stop-gap.
Sassy the Sasquatch S01 5/5 episodes subbed with cleaned auto-CC. Mike
Nolan special-case noted: a 'COMPLETE SEASON | SUBTITLES' YT upload from
Oct 2025 carries hand-typed CCs and should be preferred over per-episode
auto-CC when subbing that show.
ROADMAP H5 added: v4 WhisperX large-v3 on the friend RTX 4080 node will
regenerate the v3.5 stop-gap with proper-noun-prompted transcription
(~4-6%% WER vs ~12%% YT auto-CC) and restore the STYLE.md quality bar.
H1 OpenSubtitles credentials marked done (was completed 2026-05-09).
Stock Jellyfin paints .listItem.selected and .focused in cyan
(#00a4dc) on actionSheet/selectionList/dialogContainer dropdowns.
Clashes with Cineplex red on audio + subtitle pickers.
Pick: variant 04 "Hairline ring" — 1px #E50914 outline (offset -1px
inside row) + dark near-black bg + white text. Architectural,
quietly on-brand. Applied via body.arrflix-themed scope.
Saved alternative: web-overrides/skins/selector-variant-02-red-
underline.css — variant 02 "Red underline" (matches search-input
focus). Future skin/swap option per owner. Not currently active.
Reconciled prod drift: prod overlay had been externally modified
since last snapshot (md5 2da61583 -> c62898). Pulled prod's current
overlay as new repo baseline + snapshot. Then applied variant 04 on
top -> dev md5 0ab8b258 (= prod + v4). Prod still c62898 untouched.
Codifies the bar every fetch must hit:
- one plain English .srt per episode, named <base>.eng.srt
- drop SDH / HearingImpaired / MachineTranslated / AiTranslated / Forced
- prefer fps-matched, then highest download count
- UI label collapsed to just "English" via web-overrides shim
README.md now points at STYLE.md as a hard prereq before any fetch run.
The picker logic in v1/v2/v3 helpers already encodes these rules; STYLE.md
is the canonical source of truth that they should be checked against if
anyone (or future-me) is tempted to relax them.
Stock Jellyfin renders subtitle stream entries as
"<lang> - <CODEC> - (External|Internal|Embedded)[ - flag]". For ARRFLIX,
which has at most one subtitle format per language, the codec and source
suffix are noise. Add a small JS shim to web-overrides/index.html that
matches that exact shape and collapses the label to "<lang>" (with
"(Forced)" / "(SDH)" / "(Hearing Impaired)" if those flags are
present; "Default" is dropped since it's redundant when there's only one
stream of that language).
Audio labels like "5.1Ch Surround Sound - English - AAC - Stereo - Default"
have a different number of segments and don't match, so they pass through
untouched.
The shim runs after DOMContentLoaded and re-walks any nodes added later
via MutationObserver (covers actionsheet dropdowns that mount lazily).
Bracketed by /* SUB-LABEL-SHIM-BEGIN */ and /* SUB-LABEL-SHIM-END */
markers; bin/revert-sub-label-shim.sh deletes between the markers in one
sed pass and saves a timestamped backup. No container restart needed
(index.html is bind-mounted).
Adds lib/sub-a7d-fetch.py: free, no-daily-cap path via subliminal's
addic7ed provider (anonymous). Uses OpenSubtitles REST search-only (no
quota cost) to translate library S/E to the show's primary catalogue
numbering, then drives subliminal to download from Addic7ed and writes
sidecars direct to nullstone via SSH.
Picker quirks: subliminal series-name matcher is broken by '!' in the
title, so the script strips it before building the synthetic
Video.fromname() string. OS feature_details S/E happens to align with
Addic7ed's indexing for the test show (American Dad).
Recipe README now reflects three paths in cheapest-first order: v3
Addic7ed, v2 OS REST (20/day), v1 plugin. American Dad run log updated
to 49/58 (S01 7/7 v1, S02 16/16 mixed v2/v3, S03 16/19 v3, S04 10/16
v3). 9 misses identified, deferred to next OS REST quota window.
Adds lib/sub-rest-fetch.py: direct OpenSubtitles REST, looks up subs by
per-episode IMDB id (e.g. tt0511631) instead of the plugin's
(parent_imdb_id, season, episode) combo path. This sidesteps shows where
library numbering diverges from OpenSubtitles' catalogued numbering --
American Dad uses Hulu S1=7 eps; OS uses Fox S1=23 eps; the plugin path
returns 0 hits past S01E07 even though every per-episode IMDB id is
correct.
Recipe README updated to surface the two paths (v1 plugin / v2 REST) and
recommend v2 by default. American Dad run log now shows 19/58 episodes
subbed (S01 7/7 via v1, S02E01-E12 via v2). S02E13-S04 (39 eps) deferred
to next 20/day quota windows.
Quirk fixed in v2: OpenSubtitles /download endpoint consistently returns
HTTP 503 to Python urllib.request despite identical headers/body via curl.
_curl() shim routes all OS API calls through curl. Each 503 still
consumes a download slot, so urllib path was unsafe to retry on.
Adds processes/ umbrella for repeatable acquisition workflows. First child
is subtitles/, with recipe README (executable by Claude Code), CHANGELOG,
per-show run logs, and a tested helper at lib/sub-fetch.sh.
Run on American Dad: S01 (7 eps) passed, S02-S04 (51 eps) broke. Library
uses Hulu/DSP season ordering; OpenSubtitles indexes by Fox airing order;
plugin queries by (parent_imdb_id, season, episode) so library S02E01
returns 0 hits. v2 design = direct OpenSubtitles REST with per-episode
imdb_id lookup; pending API-key registration.
Image-12 incident: I'd set <video> z-index:9999, which covers the OSD
scrubber + buttons (Jellyfin's stock OSD controls live at z-index
1100-2000, above the 1000 of .videoPlayerContainer but BELOW our
9999). Drop the lift entirely. Stock z-index hierarchy already has
controls floating on top. The fix for black-screen was always
transparent ancestor backgrounds (L1+L2), never z-index.
Reorganized inject-middle-theme.py CSS string from one-line dense
concat into a triple-quoted multi-line block with header comments
explaining each section + the layer model + DO-NOT rules. Same
output bytes (verified md5 deterministic). Added long-form comment
header to JS too.
Doc 31 (new): "Theme layer model + edit guide" — comprehensive
checklist for any future CSS edit. Covers:
- Stacking order layer 0..7 with stock Jellyfin z-indexes
- The two body classes (.arrflix-themed, .arrflix-video-active)
- Specificity tiers + cascade order (L1 vs L2)
- CSS load order (inline < bundle < branding.xml)
- Recurring bug list (6 incidents now, all same anti-pattern)
- DO NOT DO foot-gun list
- 4-step smoke verify procedure
- CI gates still TODO
Snapshot bumped to md5 2da61583. Prod+dev byte-identical.
Two bugs in prior video-isolation fix (452ce68):
1. .htmlVideoPlayer wrong class — Jellyfin actually uses
.videoPlayerContainer + <video class="htmlvideoplayer"> (lowercase).
:has(.htmlVideoPlayer) never matched, so L1 :not(:has) was always
true — body never excluded from #000 paint, L2 (lower specificity
0,2,1) lost to L1 (0,3,1). Replaced :has() gate with :not(.arrflix
-video-active) — JS body class is reliable signal.
2. html background-color stayed transparent on details/video pages
despite 5 stylesheet rules saying #000 !important. Headless Chrome
reported computed bg as rgba(0,0,0,0) — root canvas propagation
bug or cascade quirk. Fix: JS shim sets inline style on
<html> with !important. Inline styles win against any stylesheet.
Result: html paints #000 (so letterbox bars are black not white),
body transparent during playback (L2 wins on equal specificity via
source order), videoPlayerContainer + <video> transparent → frames
visible. Off-video: body opaque #000 (L1 fires).
Verified DOM probe: html bg rgb(0,0,0), body rgba(0,0,0,0) when
arrflix-video-active. Both flip when class removed.
Two-layer defense for the recurring "black screen during playback"
bug class (5+ occurrences in 24h per doc 26/28/30):
L1 (prevention): scope every black-bg rule with
:not(:has(.htmlVideoPlayer)):not(:has(#videoOsdPage)) so the rules
self-disable while a player is in the DOM. Covers body,
#reactRoot, .skinBody, .backgroundContainer, .mainAnimatedPage,
.mainAnimatedPages, .pageContainer.
L2 (override): when JS-toggled body.arrflix-video-active is set,
high-specificity (0,3,2 + tag) transparent rule wins against any
ancestor opaque-bg rule (including future regressions someone adds
without scoping). Covers all known wrappers, the
videoPlayerContainer + videoPlayerContainer-onTop, #videoOsdPage,
.libraryPage, .htmlVideoPlayer.
L3 (z-index lift): force .htmlVideoPlayer + child <video> to
z-index:9999 + isolation:isolate during playback so it floats above
any opaque ancestor that still leaks through.
Verified in playwright: with arrflix-video-active + .htmlVideoPlayer
mounted, all 7 ancestors return rgba(0,0,0,0). Without — all 7
return rgb(0,0,0). Self-disabling works.
Lesson reinforced (doc 30 roadmap open): add darkPct assertion to
bin/headless-test-v2.py + xmllint CI gate. Five recurrences without
those gates says we keep relearning this. TODO next.
Grey #101010 stripe at bottom of pages: jellyfin-web theme.css:44-50
sets .backgroundContainer + html to #101010, neither Cineplex nor
prior overlay overrode it. Added rule forcing #000 across html, body,
.backgroundContainer, .skinBody, .mainAnimatedPages, .pageContainer,
#reactRoot under body.arrflix-themed.
Search input: stock cyan focus ring (#00a4dc, theme.css:262-272)
swapped for borderless slab + 2px red bottom on focus + soft red
box-shadow halo. Cineplex/Netflix-faithful.
Movies/Series nav: variant E "cinematic glow" picked from preview.
Active state = red text + text-shadow halo + font-weight 700. JS
hash matcher toggles .arrflix-nav.active when location.hash matches
#/movies.html or #/tv.html (and short forms). hashchange listener +
existing setInterval keep state in sync.
My Media row: .homePage .homeSectionsContainer .verticalSection.section0
hidden — matches prod which had it hidden via different mechanism.
Snapshot at snapshots/2026-05-09-v6-stable/index.html bumped to
md5 2c8f5d5f7c99611fa93d15c34fbe35d1; same as prod and dev.
Stock Jellyfin shows .headerBackButton on library pages
(Movies/Series). With our nav links already in headerLeft, the back
arrow is redundant clutter and confuses the home button intent.
Add .headerBackButton to the existing display:none rule under
body.arrflix-themed. Verified visually on dev (md5 c99aca0f), then
shipped to prod with overlay swap (md5 364cc890 -> c99aca0f). Both
sides byte-identical.
Snapshot at snapshots/2026-05-09-v6-stable/index.html updated.
Owner pronounced "near perfect". Save current state as the rollback
target. Replace older 2026-05-08-pre-elegantfin snapshot.
Snapshot md5 364cc890c58f02d07cf50b43b31a48f0 — matches both prod
and dev deployed overlay.
Doc 30 lists every file/path-of-record + rollback procedure +
remaining roadmap items.
Tag this commit v6-stable-2026-05-09 after push.
Favicon: prod's older lockFavicon() shim was clobbering our injected
A-logo <link> tags every head mutation. Tag our links with
data-arrflix-icon="A" + add a hijack IIFE that re-pins the A URL on
matching tags AND removes any other large data:image/png link tags.
Tonemap: encoding.xml flipped EnableTonemapping false to true on dev
+ prod (server-side, not in repo). Doc 21 documented this fix
2026-05-08; prod was still grey-washing HDR10 sources because
setparams was relabeling PQ pixels as bt709 without zscale + tonemap
conversion. API now reports EnableTonemapping=True. Next HDR10
transcode gets the proper zscale -> tonemap -> format ffmpeg chain.
Both verified on dev first then promoted. Prod overlay md5 c6c85076
to 364cc890. dev and prod overlay byte-identical.
assets/screenshots/01-search.png — search + suggestions list
assets/screenshots/02-detail-mandalorian.png — Mandalorian detail w/ backdrop
assets/screenshots/03-playback-sassy.png — Sassy the Sasquatch playback
Embedded in README.md above 'What you get' so the repo landing page on
git.s8n.ru reads as a finished product rather than text only.
Add ARRFLIX wordmark center, Movies/Series nav left, search right,
favicon=A-mark, auth gate so login stays stock, hide on video page.
Side-effect of branding.xml escape (<video> → <video>): prod's
CustomCss block now actually loads, so the INC7 transparent-video
rule reaches the browser. /Branding/Css.css 0 B → 36 256 B; doc-28
black-screen issue closed at the delivery layer.
Markers: ARRFLIX-MIDDLE-THEME-BEGIN/END (style + script) and
ARRFLIX-FAVICON-BEGIN/END (link). Idempotent.
See docs/29 for design + deploy procedure + recovery quirk.
Agent 6 applied SW-pin fix and marked verified via element state
(currentTime advancing, videoWidth=1920, readyState=4). Headless pixel
histogram still showed darkPct=100% — element decoded fine but CSS
overlay covered it.
Real cause: branding.xml BLACK-PASS paints .libraryPage with
#000 !important. Jellyfin OSD page renders <div id=videoOsdPage
class=libraryPage>; class match -> opaque black div above <video>.
Fix: extend transparent-scope using :has(.htmlVideoPlayer) +
#videoOsdPage selector. Post-fix darkPct=9.8% (was 100%), MNS S1E4
video frame visually paints.
Removed INC6 clear-cache-only middleware (no longer needed, was
burning HTTP cache every visit).
bin/apply-26-incident-fixes.sh extended with INC7 patch (idempotent
re-apply if branding.xml ever drifts back).
Lesson: video-element state alone is insufficient verification.
Always sample pixel histogram + canvas drawImage on the painted
viewport.
Five sibling agents converged on root cause:
jellyfin-asset-immutable Traefik router (priority 90) was matching
/web/serviceworker.js (Jellyfin PWA's actual SW filename), pinning it
with Cache-Control: public, max-age=31536000, immutable. The
priority-100 jellyfin-html-nocache router only excluded the literal
path /web/sw.js, missing serviceworker.js.
Stale SWs from earlier ARRFLIX iterations intercepted /Videos/* and
/web/* fetch events, returning cached/empty bytes. Result:
MediaSource appendBuffer got bad data -> black <video>. INC6's
Clear-Site-Data: "cache" couldn't fix it (per MDN spec, "cache"
excludes SW registrations; "storage" would have worked).
Fix: added jellyfin-sw-nocache router at priority 250 in
/opt/docker/traefik/config/dynamic.yml on nullstone, forcing
cache-no-store@file on /web/serviceworker.js + /web/sw.js. Hot-reload
via Traefik file provider, no docker restart.
Verified at the wire (curl -I /web/serviceworker.js now returns
no-cache, no-store, must-revalidate; main.jellyfin.bundle.js still
immutable as intended) and via headless Chromium probe of MNS S1E4
(33s of currentTime advance, readyState 4, videoWidth 1920x1080,
no errors, both s8n admin and USER-F user).
bin/prod-vs-dev-compare.py also lands as a one-shot diff helper used
during the investigation.
INC5 fmp4-disable shim required browser hard-reload to fire. Owner's
MNS S1E4 re-test still black-screened because cached index.html ran
old shim + fmp4-HLS bug recurred. Add Traefik response header
'Clear-Site-Data: "cache"' on /web/index.html. Cache-only is safe
(no cookie/storage wipe -> auth + localStorage preserved). One fresh
visit refetches index.html with new shim. Remove middleware after
owner confirms working, otherwise every revisit re-flushes cache.