Commit graph

87 commits

Author SHA1 Message Date
s8n
c10a3987a7 subs: AD 49/58 -> 58/58 (closed 9 gaps via OS REST) 2026-05-10 06:14:14 +01:00
s8n
54997e54a1 subs/coverage refresh + fix DEFAULT_OUT path post-rename
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.
2026-05-10 06:03:26 +01:00
s8n
3079f5009b playbooks/import-media: log Archer S02 (2009) run
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.
2026-05-10 05:06:05 +01:00
s8n
cb9d5db1ce doc 32: nullstone storage upgrade plan
R&M S02-S08 import (~105GB) blocked on disk pressure (post-import
117GB->12GB free). Plan upgrade first.

Hardware probed: MSI X470 Gaming Plus Max, single Intel 512GB NVMe in
M2_1, M2_2 free, 6 SATA ports free.

Recommended path: 2TB NVMe in M2_2 (~£130) -> pvcreate -> vgextend
keystone-vg -> lvextend lv-home -> resize2fs. ~5 min downtime, no
reinstall. Alt: SATA SSD/HDD, USB external. Cheapest /GB = SATA HDD.

Doc covers procedure, alternatives, post-upgrade R&M bulk import
plan via playbooks/import-media/. Owner to pick + execute install.
2026-05-10 04:41:52 +01:00
s8n
a30edcfa2f import: archer s01 (2009)
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.
2026-05-10 03:51:04 +01:00
s8n
520f0fbee3 maul s01 2160p import + variant 1 detail-page skin
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.
2026-05-10 03:34:34 +01:00
s8n
508fc42a1e next-ep popup: design A (Cinematic Strip) shipped to dev + designs A/B/C archived
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.
2026-05-10 02:44:40 +01:00
s8n
24a9497e7d playbooks/ rename + import-media v1.0 + lilo&stitch run
processes/ -> playbooks/ (git mv preserves history; updated cross-refs
in ROADMAP, README, subtitles playbook + scripts).

playbooks/import-media/README.md v1.0 — 7-step import workflow:
  stage on onyx -> rsync to nullstone -> chmod -> verify scan ->
  Items/Counts bump -> optional subtitle pass -> run-log
Cross-references docs/05/07/08, ADMIN-GUIDE, README. Mirrors the
existing subtitles playbook structure (CHANGELOG + runs/_template).

CHANGELOG v1.0 lists known gaps (bin/cleanup-import.sh and
bin/normalize.py still doc-only, ROADMAP M6).

First run logged: playbooks/import-media/runs/lilo-stitch-2002.md.
Lilo & Stitch (2002) imported to /home/user/media/movies/, item
c2f4aff133c1b9631500fadf293b0b2f, TMDb 11544, MovieCount 3 -> 4.
LibraryMonitor didn't auto-fire — needed manual /Library/Refresh;
playbook updated to make this an unconditional step.

Source: 1080p BluRay HEVC 10-bit / EAC3 5.1 / 2x PGS embedded subs.
Per quality bar (README.md:41) — passes.
2026-05-10 02:29:57 +01:00
s8n
c6ec208520 processes/subtitles: COVERAGE.md live audit + auto-refresh on fetch
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.
2026-05-10 02:19:32 +01:00
s8n
fba9a5bfeb processes/subtitles: STOPGAP-SUBS.md tracker for v3.5 → v4 cross-ref
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.
2026-05-10 01:18:27 +01:00
s8n
eb71cf6beb processes/subtitles: v3.5 YouTube auto-CC stop-gap + Sassy 5/5
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).
2026-05-10 01:05:07 +01:00
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
s8n
755088e7fc selector v4 hairline ring + reconcile prod drift + skin alt v2
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.
2026-05-10 00:28:55 +01:00
s8n
7ce1539ea7 processes/subtitles: STYLE.md — USER-G style for picks + display
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.
2026-05-10 00:13:51 +01:00
s8n
b3ead71b7e shim: shorten subtitle stream labels in detail dropdowns
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).
2026-05-10 00:05:16 +01:00
s8n
43f55643be processes/subtitles: v3 Addic7ed fetcher + AD 49/58 subbed
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.
2026-05-09 23:31:10 +01:00
s8n
23520df2df processes/subtitles: v2 REST fetcher + AD S02E01-E12 subbed
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.
2026-05-09 23:09:09 +01:00
s8n
fedf3388b8 processes: subtitle acquisition v1 + AD S01 run
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.
2026-05-09 22:56:31 +01:00
s8n
1ed55152b7 fix: drop video z-index hack + heavy comments + doc 31 layer model
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.
2026-05-09 22:41:51 +01:00
s8n
4f13db63f9 fix v7: html-bg pin + correct video class names
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.
2026-05-09 22:33:45 +01:00
s8n
452ce68d7a isolate video player against opaque-bg regressions (recurring INC class)
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.
2026-05-09 22:18:31 +01:00
s8n
e9d209da73 polish: pure-black bg + search bar fix + variant E nav glow + My Media hide
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.
2026-05-09 21:47:00 +01:00
s8n
92e2426734 hide back arrow on themed pages
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.
2026-05-09 19:43:27 +01:00
s8n
a943933363 doc 30 v6-stable success + snapshot save state
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.
2026-05-09 12:52:44 +01:00
s8n
9003b55c81 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
83fbfbf35e readme: add 3 ARRFLIX screenshots (detail, playback, search)
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.
2026-05-09 10:10:38 +01:00
s8n
0c7d0aef14 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
3d388e8de7 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
a4ababcfbf doc 28: record commit hash + nullstone backup path for INC7 fix 2026-05-09 02:50:21 +01:00
s8n
3a7d96aacb 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 USER-F 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
af2d625e4f doc 27: status snapshot 2026-05-09 visual
Visual status board: symptoms killed (8/8), roadmap (done /
pending / high / medium / deferred / strategic), library codec
matrix, repo file tree, next-click. Point-in-time after doc-26
incident closed. For ongoing roadmap see ROADMAP.md.
2026-05-09 02:23:53 +01:00
s8n
648e5d5238 doc 26 INC6: Clear-Site-Data cache-only for shim deploy
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.
2026-05-09 02:10:52 +01:00
s8n
b5e0d95826 doc 26: add INC5 scrollbar grey-strip subsection
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.
2026-05-09 02:05:16 +01:00
s8n
557317d104 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+USER-F+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
3c7cf64d1e doc 26 INC5: disable fMP4-HLS client-side + AV1 force-transcode
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)
2026-05-09 01:58:45 +01:00
s8n
439b600740 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 USER-F and never clicked Play. v2 runs both admin
  and USER-F, 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
9fe7644701 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
0dd3539838 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
1bfd122f6a 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
f01cdd4007 doc 25: english leak deep-dive (Abspielen post-lockdown) 2026-05-08 22:09:59 +01:00
s8n
c972605af9 import-log: youtube-sassy-the-sasquatch 2026-05-08 — eps 1-5 (ep6 age-restricted) 2026-05-08 22:03:42 +01:00
s8n
3dd5d3c6f2 doc 22: jellyfin runtime perf audit (read-only)
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.
2026-05-08 17:51:06 +01:00
s8n
ea8dc71617 doc 23: arrflix edge / network / browser-load-path perf audit (read-only)
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.
2026-05-08 17:50:52 +01:00
s8n
7e63e5e236 audit: storage I/O for arrflix media path 2026-05-08 17:47:19 +01:00
s8n
16b3f831d9 audit: rick-and-morty color/HDR diagnosis 2026-05-08 17:45:34 +01:00
s8n
494ee4e814 ROADMAP: action owner = s8n 2026-05-08 17:25:56 +01:00
s8n
d7aa38909d ROADMAP: rewrite with snapshot table, status emojis, Open above Done
- Snapshot table at top (live stats: prod/dev URLs, theme, eps, disk, users)
- Open items first (high/med/low + effort + blocker)
- Blocked + Deferred middle
- Done section moved to bottom per owner request
- Visual: emoji-tagged severity, tables, scannable
- Updated counts: 6 series, 175 eps, 156G free, 9 users, 17 docs
2026-05-08 17:20:37 +01:00
s8n
21a69672f1 doc 19: english-only lockdown audit (read-only baseline)
Cross-layer audit supplementing docs 15 and 16. Confirms doc-15 root
cause still live (8/8 users have UICulture absent; force-english script
unrun), enumerates 93 served locale chunks (de-json contains 'Abspielen'),
and proposes 4-pronged remediation: per-user POST + wrapper patch +
Traefik Accept-Language rewrite + navigator.language shim.

Read-only. No Jellyfin mutations performed.
2026-05-08 17:05:11 +01:00
s8n
27b737ff2f 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
395c6d2833 web: english-lockdown shim — pin locale + hide switchers 2026-05-08 17:04:03 +01:00