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 guest 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.
Symptom + root cause + fix + lesson for the scrollbar grey strip at
very bottom of page. Patch already in bin/apply-26-incident-fixes.sh
and live in branding.xml on prod from earlier commit; this is the
documentation subsection that was missing dedicated coverage.
Two parallel fixes for MNS S1E4 black-video bug. Belt+braces.
INC5 fmp4-disable (this agent):
- Add localStorage.setItem('enableHlsFmp4', 'false') shim to
web-overrides/index.html (idempotent, marker INC5 fmp4=false 2026-05-09)
- Forces TS segments instead of fMP4 for all HLS transcodes,
works around upstream black-video bug with HEVC+fMP4
(jellyfin-webos#126, jellyfin#16612)
- Browser localStorage verified false via headless playwright;
server confirmed emitting -hls_segment_type fmp4 before fix
- Repo + deployed file md5 match: 5b212d7d60b8a2b910a2f47dd0470a09
INC5 AV1 force-transcode (parallel agent):
- Re-encoded MNS S1E1-5 AV1->H.264 in container; PlaybackInfo
now returns DirectStream/DirectPlay=true on S1E4
- Doc additions covering the AV1 work included here since
same file (already authored by parallel agent, not yet committed)
Two regressions slipped through INC1-3:
INC4a -- BLACK BAND behind every detail-page carousel
Pre-existing 2026-05-08 home-page rule painted .emby-scroller {bg:#000
!important} UNSCOPED. Hits every carousel inside .itemDetailPage incl
admin-only More from Season N, More Like This. INC1-3 transparent-scope
list missed .emby-scroller / .verticalSection / .padded-top-focusscale.
Fixed by extending scope.
INC4b -- VIDEO 'BLACK SCREEN' on play
Not actually black-screen. CPU-only nullstone cannot sustain real-time
4K HEVC HDR tonemap+x264 transcode -- 0.5x realtime, ffmpeg takes ~6s
per 3s segment. With user resume seeks adding restart overhead, total
wait ~18s before browser readyState rises. User saw black, gave up.
Fix: disable EnableTonemapping (R&M fake HDR per doc 21) + cap
RemoteClientBitrateLimit=20Mbps on every user (1080p target, no 4K
scale). Headless v2 test confirms HEVC + AV1 episodes now hit
readyState=3/4 within wait window; 4K HDR R&M still slow (heaviest).
INC4 testing methodology audit -- bin/headless-test-v2.py
v1 only logged in as guest and never clicked Play. v2 runs both admin
and guest, walks 3 codec-tagged items per role (HEVC/AV1/H.264),
clicks Play, captures <video> state, sweeps DOM for opaque bgs over
backdrop layer. False positives: off-viewport #reactRoot + collapsed
.mainDrawer (negative coords). Allowlist refinement TODO.
Open: 4K HDR sources still slow even post-fix. Real fix path = pre-
transcode masters to 1080p H.264 SDR via separate batch, OR migrate to
10.11.8 with vaapi/qsv driver fixed.
After INC1 fixed the Abspielen + first-fold backdrop, owner reported black
band hiding artwork in More from Season 1 / below-fold sections. Two more
patches required:
INC2 — pin .backdropContainer + .backgroundContainer position:fixed; height
100vh so backdrop persists during scroll. Added vertical fade ::after.
INC3 — extend transparent-scope to ALL detail-page sub-sections
(.detailVerticalSection, .scrollSlider, .padded-bottom-page,
.itemsContainer etc) so section wrappers don't paint over the pinned
backdrop section by section.
bin/headless-test.py now takes top + scrolled viewport screenshots.
full_page=True hides position:fixed regressions, dual-screenshot exposes
them. Use both to bisect.
bin/apply-26-incident-fixes.sh updated with INC2+INC3.
Open: AV1+Opus playback (Mike Nolan Show) still tracked for 10.11.8
migration. .detailLogo regression possible — test in actual browser.
Symptoms: Page Unresponsive on poster grid, posters missing then black
backdrops, 'Abspielen' German Play button surviving Traefik+force-english
chases, video black-screen on play.
Root causes (different from initial guesses):
- Browser hangs: deployed index.html drifted ahead of repo; uncommitted
forceEnglishUI() text-walker MutationObserver froze main thread on poster
lazy-load. Reverted to repo HEAD.
- 'Abspielen': Cineplex theme HARDCODES German via 'content:' ::after rule
-- not a Jellyfin locale issue. Doc 25 already proved per-user UICulture
is theatre. Override CSS with content: 'Play'.
- Backdrops black: BLACK-PASS CustomCss block paints #000 !important on
.layout-desktop / .pageContainer -- occludes backdrop layer (z-index:-1).
Existing transparent-scope rule used body.itemDetailPage selector that
doesn't match in 10.10.3 (body class is libraryDocument). Replaced with
:has(.itemDetailPage) ancestor scoping.
- HLS 499: encoding.xml had EnableThrottling+EnableSegmentDeletion=true,
segments reaped before browser re-request. Disabled both.
Verified via new bin/headless-test.py (playwright Chromium login + screenshot
+ computed-style probe). Fixes idempotent and re-runnable via new
bin/apply-26-incident-fixes.sh.
Open: AV1+Opus items still black-screen in Chrome due to DirectStream
codec-tag mislabel bug. Tracked for 10.11.8 migration.
Server-runtime focus; supplements doc 13. Headline: 4 concurrent ffmpeg
processes for ONE viewer all transcoding 1080p->2160p with PGS subtitle
burn-in, on uncapped jellyfin container sharing 12-core host with
uncapped Forgejo BlueBuild CI runner (88-99 % CPU). Load avg 15.4 on 12
cores. Throttling+SegmentDeletion still off (doc 13 finding 03 now
non-optional). Top quick-win: enable transcode throttling + segment
deletion + cap RemoteClientBitrateLimit.
Edge audit complementing doc 13 (server-side perf). Confirms cold-load
"feels slow" perception is dominated by:
- no HTTP compression at Traefik (2.74 MiB raw JS bundles per cold load)
- no Cache-Control on hashed-asset URLs (28 conditional GETs per warm load)
- first-fetch poster image transcode ~385 ms (server-side, doc 13 #02)
TLS, MTU, HTTP/2, cert chain, middleware chain, Pi-hole hairpin all
audited and clean. Pi-hole missing local DNS rewrite for arrflix.s8n.ru
(LAN clients hairpin via WAN unless /etc/hosts pin in place).
Top quick win: add `compress@file` middleware in
/opt/docker/traefik/config/dynamic.yml + reference from Jellyfin router
label. ~70 % cold-load wire-size reduction (2.74 MiB to ~0.82 MiB
gzip / ~0.69 MiB brotli). One file edit, no architectural change.
No fixes applied. No state mutated. No Traefik reload.
doc 20 covers the multi-layer pin (server / per-user / web SPA / Accept-
Language), the idempotent re-apply runner, drift-check curl one-liners,
known gaps, and a systemd-timer suggestion for weekly auto re-application.
bin/english-lockdown-runner.sh: idempotent runner that POSTs server-wide
UICulture / PreferredMetadataLanguage / MetadataCountryCode and per-user
UICulture / Audio+Subtitle prefs / PlayDefaultAudioTrack. Reads
JELLYFIN_API_TOKEN from env (set -u, refuses to run without it). One-line
summary per surface; exit 0 on full success, 1 on any failure.
doc 15 prefaced with a "Status as of 2026-05-08" section noting the
multi-agent lockdown sweep and cross-linking the audit baseline (doc 19,
sibling) and the new lockdown procedure (doc 20). Original body preserved
verbatim as historical context.
Replaced 25 occurrences across README, docs/00-overview, and
docs/04-theming-and-users. Removed the obsolete tv→arrflix rename
blockquote (rename complete) and deduped the Live-at bullet.
Token 76858153...f8b1 was committed across 9 docs + 1 snapshot RESTORE.md
and exposed via the brief public window of this repo. Replaced with
`<JELLYFIN_API_TOKEN>` placeholder.
WARNING: this commit only redacts HEAD — the token remains in git history.
Anyone who cloned during the public window has the full value. Treat the
old token as compromised and rotate at Jellyfin Dashboard > API Keys.
Original value backed up to private s8n/secrets/ARRFLIX/.
Owner saw "Abspielen" on the Play button — caused by every user having
Configuration.UICulture absent, so the web SPA falls back to browser
Accept-Language. No server-side flag exists to override this.
Adds docs/15-force-english.md with the per-user forcing mechanism,
limits (pre-auth splash bundle still uses navigator.language), and a
ready-to-execute bash script bin/force-english-all-users.sh that pins
UICulture=en-US on every user via POST /Users/{id}/Configuration.
Plan-only commit — no live config changed. Owner triggers when ready.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Live render audited at https://arrflix.s8n.ru. Owner believed NeutralFin
was applied; live /Branding/Configuration shows Cineplex v1.0.6 — a
sibling POST won the race per §3b operational rule. Audit covers visual
contract, drift table for every CustomCss + critical-path index.html
rule, NeutralFin variable conflicts, logo aspect ratio (235x85, fits),
ranked fix list. No state mutated; recommendation pending owner sign-off.
Migrated active CSS theme from Cineplex v1.0.6 to ElegantFin v25.12.31
with Netflix-red #E50914 accent overrides over ElegantFin's default
Jellyseerr-blue/violet palette. ARRFLIX wordmark logo preserved on both
.adminDrawerLogo img and .pageTitleWithLogo selectors (split-rule form).
Eight accent variables overridden at :root: --uiAccentColor, --activeColor
(+Alpha), --osdSeekBarPlayedColor, --checkboxCheckedBgColor,
--highlightOutlineColor, --btnSubmitColor, --btnSubmitBorderColor.
All prior custom blocks preserved verbatim: cast/crew hide, Quick Connect
hide, header-icon hide (§3c), white slider thumbs (§3d), pure-black bg
(§3d), Settings drawer hide, count badge hide, ARRFLIX logo override.
LoginDisclaimer + SplashscreenEnabled untouched.
POST → 204; GET /Branding/Configuration confirms no Cineplex import,
ElegantFin pinned to v25.12.31, all overrides intact. Smoke test on
https://arrflix.s8n.ru/ → HTTP 302 (baseline). No container restart.
Restructured §1: Cineplex content moved to §1 'Previous themes'
subsection (#### Why Cineplex won, #### Tradeoffs, #### What it looked
like, #### Theme history) with the new ElegantFin+recolor stack as
the canonical current theme.
Snapshot tag for rollback: snapshot-2026-05-08-pre-elegantfin
CSS selectors in CustomCss (a[href*=mypreferencesmenu], :has(...) wrappers)
weren't reliably hiding the entry — bundle renders it via MUI ListItemButton
+ React Router NavLink and the rendered DOM didn't match the wrapper rules.
Add nukeSettings() to the runtime shim: queries any
a[href*=mypreferencesmenu] / [to*=mypreferencesmenu], walks up to closest
li/.MuiListItem-root/[role=menuitem] and sets display:none. Wired into
start(), a new MutationObserver on document.body, and the existing 1s
setInterval. CustomCss rules left in place as belt-and-braces.
Doc: extend 10-spa-runtime-shim.md with the diagnosis, the bind-mount inode
gotcha (single-file binds + os.replace orphans the container's view), and
the nsenter-based recovery path.
Two cosmetic CSS tweaks appended to CustomCss via standard
GET-edit-POST flow (HTTP 204, all prior blocks preserved).
Thumbs: scope to .MuiSlider-thumb / .osdPositionSlider / .osdVolumeSlider
(plus legacy emby-slider .sliderThumb) -> #fff. Avoids touching
--mui-palette-primary-main so buttons/focus rings stay untinted.
Background: force --primary-background-color + --background-color and
html/body/.skinHeader/.mainAnimatedPages/#reactRoot/.dashboardDocument
to #000 so OLED + Netflix-style artwork fades read clean.
Last branding POST in this sequence.
Three top-right header icons hidden via CSS appended to existing CustomCss:
.headerSyncButton, .headerCastButton, .headerUserButton. Search button
.headerSearchButton intentionally untouched so its event handler still
fires. Selectors confirmed by grepping live JF 10.10.3 web bundle.
Branding POST returned 204; verified via GET that Cineplex @import,
cast/crew hide, ARRFLIX logo override, Quick Connect hide, Settings
drawer hide, LoginDisclaimer, and SplashscreenEnabled all preserved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Branding endpoint accepts full-object POSTs only — no field-level merge,
no ETag locking. Concurrent agents writing to /System/Configuration/branding
race; last writer wins. Lock-in: branding-CSS POST must be the final POST
in any sequence touching that endpoint.
The static <title>ARRFLIX</title> patch wasn't enough - Jellyfin's bundle
calls document.title=... on hydrate and per-route, so the tab kept showing
'Jellyfin'. Add a self-contained inline IIFE in <head> that:
- Replaces 'Jellyfin' with 'ARRFLIX' on the title (incl. ' - Jellyfin' suffix)
- Pins favicon hrefs to the existing data: URL already in the page
- Watches <head> via MutationObserver for SPA churn
- Has a 1s setInterval safety net for late-binding navigations
- One-shot unregisters the Jellyfin service worker so old clients reload fresh
bin/inject-shim.py is the source of truth - idempotent (replaces marker block).
docs/10-spa-runtime-shim.md covers root cause, deploy flow, SW eviction, and
how to extend the shim on Jellyfin upgrade.
- EnableUserPreferenceAccess=false on guest + 5 (hides Display, Home,
Playback, Subtitles pref pages — owner controls UX centrally).
- Wrapper bin/add-jellyfin-user.sh updated to bake this into all future
non-admin user creations.
- ROADMAP entries (added by sibling import agents):
- Imported: The Incredible Hulk (2008), TMDB 1724, 4 images
- Imported: Idiocracy (2006), TMDB 7512 (NOT 1542 = Office Space)
- Imported: American Dad! (2005) S01-S04, 58 eps, TMDB 1433
- WAN exposure docs added (doc 09, 256 lines): Gandi A record live,
no-guest middleware dropped, lockout=5 baked in. Owner still must
port-forward 80/443 on home router for actual public access.
Architecture A trimmed to: movies, tv, anime, musicvideos. Removed
mkdir/compose-mount/library-creation curl for home + music. Sections 6
(Music) and the Home-videos category remain in this doc as reference for
re-introduction later.
- 05-file-structure-rules: 1165-line authoritative ruleset covering 11 media
categories (movies, tv, anime, stand-up, concerts, docs, home video, extras,
subs, artwork, NFO). Architecture A flat layout chosen at
/home/user/media/{movies,tv,anime,musicvideos,music,home}. Top 3 gotchas
surfaced: no-per-item-folder breaks extras/NFO; year must be in parens;
anime absolute numbering past 99 needs Shoko or split-by-TVDB-season.
- 06-per-library-themes: 319-line research memo. Verdict: partially feasible.
No native per-library theming; CustomCss is global, Jellyfin web sets no
body class for libraryId/collectionType. Recommended approach: JS Injector
plugin (n00bcodr fork, MIT, last release 2025-12-08) + ~30-line shim that
mirrors topParentId/collectionType from URL hash to body class, plus three
scoped CSS blocks (body.lib-movies, body.lib-anime, body.lib-music).
Reaches 'tinted, branded, recognisable' — not pixel-perfect. Pixel-perfect
fidelity needs subdomain split (3 instances) at ~100x maintenance.