Commit graph

19 commits

Author SHA1 Message Date
s8n
1168ba6479 favicon hijack + tonemap fix shipped to prod
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.
2026-05-09 10:17:40 +01:00
s8n
52a7df6695 middle-theme v6 + branding.xml video escape
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> → &lt;video&gt;): 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.
2026-05-09 04:01:49 +01:00
s8n
d0e7af3099 doc 28 INC7-final: CSS overlay covering <video> was actual cause
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.
2026-05-09 03:04:41 +01:00
s8n
917d21b3be doc 28 + INC7: fix prod black-screen via SW cache pin
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.
2026-05-09 02:50:00 +01:00
s8n
1ee698b592 doc 26: case CLOSED — final state + 8 forbidden patterns
All 8 owner-reported symptoms resolved across 5 iterations (INC1-5):

INC1 — index.html drift revert + :has() transparent-scope + Cineplex
       Abspielen override + encoding.xml HLS 499 fix
INC2 — pin .backdropContainer position:fixed (persistent backdrop)
INC3 — extend transparent-scope through detail-page sub-sections
INC4 — .emby-scroller transparent (kill black band behind carousels) +
       EnableTonemapping=false + 20Mbps RemoteClientBitrateLimit cap +
       headless-test-v2.py (admin+guest+click-play+bg-sweep)
INC5 — AV1 source re-encode (MNS S1E2/E4/E5 to H.264/AAC) +
       enableHlsFmp4=false localStorage shim +
       ::-webkit-scrollbar styled to ARRFLIX palette

Verification: headless playwright on Chrome + Firefox UA confirms MNS
S1E4 plays 1920x1080 readyState=4 currentTime advancing. Owner
double-confirmed solved.

Doc 26 final state section + 18-item forbidden-pattern checklist added
for future operators.
2026-05-09 02:03:39 +01:00
s8n
6288c57781 doc 26 INC4: black band + 4K HDR slow transcode + v2 test + methodology audit
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.
2026-05-09 01:46:47 +01:00
s8n
9b06bb48c6 doc 26 INC2+INC3: pin backdrop, transparent sub-sections
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.
2026-05-09 01:21:01 +01:00
s8n
549c86efdf doc 26 + bin: incident 2026-05-09 + headless smoke-test
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.
2026-05-09 01:11:38 +01:00
s8n
826d76733c force English everywhere on all 9 users + wrapper
AudioLanguagePreference=eng, SubtitleLanguagePreference=eng,
SubtitleMode=Default, PlayDefaultAudioTrack=true, UICulture=en-US.
Per-user Configuration POST applied to all 9 existing users + wrapper
updated for future creations.
2026-05-08 23:46:13 +01:00
s8n
d5d68563d2 docs+bin: English-only lockdown — re-apply runner + doc 20
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.
2026-05-08 17:04:12 +01:00
s8n
d2120c636f web: english-lockdown shim — pin locale + hide switchers 2026-05-08 17:04:03 +01:00
s8n
14f63e8027 doc 15: force English UI for all users (plan + script)
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>
2026-05-08 04:22:04 +01:00
s8n
d41aaa04fd shim: nukeSettings() drops drawer Settings link for non-admins
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.
2026-05-08 03:51:48 +01:00
s8n
514dcb6ffc Branding shim: lock <title> + favicon at runtime against SPA overwrites
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.
2026-05-08 03:25:16 +01:00
s8n
6bf78d66e1 Restrict non-admin users + 3 imports landed
- 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.
2026-05-08 03:18:58 +01:00
s8n
1f5ba31483 Rename: nasflix → ARRFLIX + apply Cineplex theme
Domain + repo rename: nasflix.s8n.ru → arrflix.s8n.ru, NASFLIX → ARRFLIX
(Forgejo repo, Pi-hole DNS, Traefik file+label routes, compose env+labels,
onyx /etc/hosts, branding LoginDisclaimer, all repo refs, logo asset).

Theme: ElegantFin → Cineplex v1.0.6 (MRunkehl, pinned). Picked by research
agent over JellyFlix (halted), DarkFlix (10.8.x only), Theme Park (no
Netflix preset). Real #E50914 + Netflix Sans webfont + transform:scale
hover + gradient login backdrop. Doc 04 updated with full candidate
matrix, theme-history subsection, rollback-to-ElegantFin snippet.

Logo asset saved at assets/logo.png (235x85 RGBA).

Live: https://arrflix.s8n.ru → 302. tv.s8n.ru + nasflix.s8n.ru retired (404).
2026-05-08 02:57:34 +01:00
s8n
cb95dce8bc Rename: tv.s8n.ru → nasflix.s8n.ru, jellyfin-stack → NASFLIX
- Domain: tv.s8n.ru retired (404). nasflix.s8n.ru live (302 → /web).
  Pi-hole local DNS updated. Traefik file-provider router rule + docker-label
  router rule both flipped. Jellyfin PublishedServerUrl env updated. Cert
  re-issued via Gandi DNS-01. Onyx /etc/hosts pin moved.
- Repo: forgejo PATCH /api/v1/repos rename. Local clone remote URL updated.
  All in-tree refs to tv.s8n.ru and jellyfin-stack swept (sed).
- Scope: TV Shows + Movies only. anime/, musicvideos/, home/, music/,
  docs-*/ libraries removed from canonical layout. Sections kept as
  reference for re-introduction.
- Branding LoginDisclaimer text updated to nasflix.s8n.ru.
2026-05-08 02:53:46 +01:00
s8n
ab16861314 wrapper: default new users to English audio + subs (was pol/eng)
Owner flipped libraries to PreferredMetadataLanguage=en, MetadataCountryCode=US.
New-user defaults must match. Wrapper now sets:
  AudioLanguagePreference=eng (was pol)
  SubtitleLanguagePreference=eng
  SubtitleMode=Default (was Always — let player decide based on audio)
2026-05-08 02:37:23 +01:00
s8n
b935d6d42e Add wrapper script: add-jellyfin-user.sh
Jellyfin has no native global default for new-user DisplayPreferences;
home-layout defaults are baked into the web bundle. This wrapper layers
the s8n canonical prefs on top after user creation:

  - Home sections: resume, resumeaudio, nextup, latestmedia, none x6
    (drops the 'My Media' tile row — sidebar already exposes libraries)
  - SubtitleMode=Always, SubtitleLanguagePreference=eng
  - AudioLanguagePreference=pol

Use for every new user from now on; achieves the 'global default' the
admin wanted without patching the web bundle.

Already retroactively applied to s8n + guest.
2026-05-08 02:09:41 +01:00