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.
This commit is contained in:
s8n 2026-05-09 01:21:01 +01:00
parent 549c86efdf
commit 9b06bb48c6
3 changed files with 127 additions and 13 deletions

View file

@ -40,15 +40,11 @@ s = open(p).read()
patch = """ patch = """
/* ARRFLIX 2026-05-09 — incident fixes (see docs/26-incident-2026-05-09-...). /* ARRFLIX 2026-05-09 — incident fixes (see docs/26-incident-2026-05-09-...).
1. Cineplex theme hardcodes German "Abspielen" via ::after on .play_arrow. INC1: Cineplex theme hardcodes German "Abspielen" via content: ::after.
Override with English. The German text was in CSS, not Jellyfin locale — INC1: BLACK-PASS occludes backdrop; transparent-scope via :has().
so all Traefik Accept-Language rewrites + force-english-all-users.sh INC2: pin backdrop position:fixed so it persists across scroll.
chases never fixed it. INC3: extend transparent-scope through detail-page sub-sections so
2. The BLACK-PASS block paints #000 !important on .layout-desktop, section wrappers don't paint over the pinned backdrop. */
.pageContainer, .padded-bottom-page etc — these wrap the backdrop layer
(z-index:-1) and occlude it. Use :has() to scope only when an
.itemDetailPage descendant exists, so backdrop renders only on detail
pages. */
.mainDetailButtons .material-icons.play_arrow::after { .mainDetailButtons .material-icons.play_arrow::after {
content: "Play" !important; content: "Play" !important;
} }
@ -64,6 +60,57 @@ patch = """
background-color: transparent !important; background-color: transparent !important;
background: transparent !important; background: transparent !important;
} }
.layout-desktop .backdropContainer,
.layout-mobile .backdropContainer,
.layout-tv .backdropContainer,
.layout-desktop .backgroundContainer,
.layout-mobile .backgroundContainer,
.layout-tv .backgroundContainer {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 0 !important;
}
.layout-desktop .backgroundContainer.withBackdrop::after,
.layout-mobile .backgroundContainer.withBackdrop::after,
.layout-tv .backgroundContainer.withBackdrop::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
180deg,
rgba(0,0,0,0.00) 0%,
rgba(0,0,0,0.00) 35%,
rgba(0,0,0,0.40) 70%,
rgba(0,0,0,0.75) 100%
);
pointer-events: none;
z-index: 1;
}
.itemDetailPage,
.itemDetailPage > *,
.detailPageContent,
.detailPagePrimaryContainer,
.detailPageWrapperContainer,
.detailPageContent > *,
.detailVerticalSection,
.detailVerticalSection-extrabottompadding,
.detailSection,
.detailSectionContent,
.itemsContainer,
.scrollSlider,
.scrollSliderContainer,
.padded-bottom-page,
.detailPagePrimaryContent,
.sectionTitleContainer,
.detailRibbon,
.subtitleAudioContainer,
.detailPageRoot {
background-color: transparent !important;
background: transparent !important;
}
""" """
s = s.replace("</CustomCss>", patch + "</CustomCss>") s = s.replace("</CustomCss>", patch + "</CustomCss>")
open(p, "w").write(s) open(p, "w").write(s)

View file

@ -123,18 +123,24 @@ async def main():
await page.goto(target, wait_until="networkidle", timeout=30000) await page.goto(target, wait_until="networkidle", timeout=30000)
await asyncio.sleep(4) # let SPA paint backdrop await asyncio.sleep(4) # let SPA paint backdrop
# Probe key DOM elements # Probe key DOM elements (extended)
probe = await page.evaluate("""() => { probe = await page.evaluate("""() => {
const result = {}; const result = {};
const sel = ['.itemBackdrop', '.detailBackdrop', '.backdropContainer', const sel = ['.itemBackdrop', '.detailBackdrop', '.backdropContainer',
'.backgroundContainer', '.layout-desktop', '.backgroundContainer', '.layout-desktop',
'body', '#reactRoot', '.itemDetailPage', 'body', '#reactRoot', '.itemDetailPage',
'video', '.htmlvideoplayer', '.btnPlay', '.detailPagePrimaryContainer']; 'video', '.htmlvideoplayer', '.btnPlay', '.detailPagePrimaryContainer',
'.detailSection', '.detailVerticalSection', '.itemsContainer',
'.padded-bottom-page', '.mainAnimatedPages', '.pageContainer',
'.cardScalable', '.scrollSlider', '.sectionTitleContainer',
'.detailPageContent', '.detailPageWrapperContainer'];
for (const s of sel) { for (const s of sel) {
const el = document.querySelector(s); const els = document.querySelectorAll(s);
if (!el) { result[s] = '<absent>'; continue; } if (els.length === 0) { result[s] = '<absent>'; continue; }
const el = els[0];
const cs = getComputedStyle(el); const cs = getComputedStyle(el);
result[s] = { result[s] = {
count: els.length,
display: cs.display, display: cs.display,
opacity: cs.opacity, opacity: cs.opacity,
visibility: cs.visibility, visibility: cs.visibility,
@ -152,8 +158,14 @@ async def main():
return result; return result;
}""") }""")
# Two screenshots: top viewport + scrolled to mid-page (so fixed backdrop renders correctly)
screenshot = os.path.join(OUT, f"{URL.replace('https://','').replace('.','_')}-detail.png") screenshot = os.path.join(OUT, f"{URL.replace('https://','').replace('.','_')}-detail.png")
await page.screenshot(path=screenshot, full_page=False) await page.screenshot(path=screenshot, full_page=False)
# Scroll halfway down to verify pinned backdrop persists
await page.evaluate("() => window.scrollTo(0, document.body.scrollHeight * 0.5)")
await asyncio.sleep(1)
scrolled = os.path.join(OUT, f"{URL.replace('https://','').replace('.','_')}-scrolled.png")
await page.screenshot(path=scrolled, full_page=False)
print(f"[+] screenshot: {screenshot}") print(f"[+] screenshot: {screenshot}")
with open(os.path.join(OUT, "probe.json"), "w") as f: with open(os.path.join(OUT, "probe.json"), "w") as f:

View file

@ -539,6 +539,61 @@ These are the dead-ends. Future operators (and future me) should skip:
--- ---
## Iteration 2 — backdrop visible only on top viewport (2026-05-09 follow-up)
After INC1 (`:has()` transparent-scope) shipped and prod showed backdrop on
detail-page top, owner reported "in the middle of the More from Season 1
is black, it's hiding the artwork". Below-the-fold sections (Next Up, Seasons,
More Like This) showed solid black instead of continuing the backdrop.
### Root cause (INC2)
`.backdropContainer` defaults to non-fixed positioning — it scrolls out of
view. INC1 made wrappers transparent so backdrop showed through, but only
where the backdrop EXISTED in the DOM viewport. Once user scrolls down,
backdrop is above viewport, sections see body's `#000` bg.
### Fix INC2
Pin `.backdropContainer` + `.backgroundContainer` to `position: fixed; top:0;
height:100vh; z-index:0`. Added `::after` vertical gradient (transparent at
top → 75% black at bottom) so text remains readable as user scrolls into
backdrop area.
### Root cause (INC3)
INC2 alone didn't fix it visually — section wrappers (`.detailVerticalSection`,
`.scrollSliderContainer`, `.padded-bottom-page`, `.itemsContainer` etc) still
painted opaque bg from BLACK-PASS + finity. Pinned backdrop sat behind, but
sections occluded it section-by-section.
### Fix INC3
Extended transparent-scope to all detail-page sub-sections:
`.itemDetailPage > *`, `.detailPageContent`, `.detailPagePrimaryContainer`,
`.detailPageWrapperContainer`, `.detailVerticalSection*`, `.detailSection*`,
`.itemsContainer`, `.scrollSlider*`, `.padded-bottom-page`,
`.sectionTitleContainer`, `.detailRibbon`, `.subtitleAudioContainer`,
`.detailPageRoot`.
### Verification (INC2 + INC3)
Updated `bin/headless-test.py` to take TWO viewport screenshots: top-of-page
+ scrolled to 50% page height. With INC2/INC3 applied, scrolled screenshot
shows R&M backdrop persisting behind "Seasons" + "More Like This" sections
(previously: solid black).
### Lesson learned
When pinning a backdrop with `position:fixed`, transparency must extend
RECURSIVELY through every wrapper ON TOP of the backdrop layer, not just the
top-level page wrappers. Test with scrolled screenshot — full-page screenshot
in playwright stretches viewport and hides `position:fixed` issues.
`bin/headless-test.py` now takes both top + scrolled. Use both to bisect.
---
## Open follow-ups (for separate sessions) ## Open follow-ups (for separate sessions)
- **AV1+Opus playback** (Bug E): Chrome's AV1 DirectStream codec-tag mislabel - **AV1+Opus playback** (Bug E): Chrome's AV1 DirectStream codec-tag mislabel