Fix TV artwork, episode layout and focus visibility #47

Merged
justinzeus merged 3 commits from fm/tv-fb1-visual into main 2026-11-06 22:04:18 +01:00
Owner

Tests are not green. Firstmate explicitly authorized review with the unresolved person-switch timeouts below. Real-box single-OK person switching must be verified by firstmate before anything ships.

TV feedback: artwork, episode layout and focus visibility

Implements the real-TV feedback against the approved revision-4 frames, with revision 5 documenting the requested deltas. AI-assisted implementation. No TV connection, ADB, installation or optimized-APK build was used.

Diagnosis and decisions

Area Diagnosis / evidence Change
Who’s watching gear The horizontal offset explicitly placed Settings under the final person. Interpret the requested placement as centered beneath the complete people row, independent of selection/count; Justin confirmed this interpretation in steering. Preserve Down-from-final-person, Up return, 1.08 scale and caption; use the people’s focus ring.
Focus visibility Home had separate retained-row restoration and an early positioned checkpoint; shelf scrolling used inconsistent rules, and Discover’s width calculation rounded the approved six poster slots down to five. Retain approved second-last-slot/lookahead behavior, clamp to real contents, settle absolute offsets, and mark vertical positioning only after scrolling finishes. Preserve the 168 px heading pin. Person’s final column no longer loses 8 px of poster width to end padding.
Episode artwork Read-only Jellyfin GETs: all ten The Bear S3 episodes have Primary tags, no episode Backdrop tags; Tomorrow’s source is 2100×1182 and the series backdrop is 3840×2160. These are aired files, not unaired placeholders. Primary → Thumb → already-read series Backdrop crop → missing-art treatment. No per-episode series reads.
Hero quality Home capped a full hero at 1280 px, Discover reused ordinary poster-sized art, title backdrops were capped, and TMDB hero sources used w780. The real Desperate Housewives backdrop is 2732×1536. Slot-sized Jellyfin URLs, suitable TMDB source tiers, panel-bounded Coil decoding and correct upright-poster requests; preserve image tags, quality and cache/concurrency limits.
Discover heading gap The previous 40 px heading-to-card gap was too tight in Series for you. Apply 60 px / 104 px heading band consistently to every Discover lane, including service and Hidden picks lanes; Home is unchanged.
Episode descriptions The header used show-level copy despite an episode context. Focused → explicitly opened → up-next episode overview; show overview only without episode context. Focused episode cards also show two ellipsised summary lines.
Series whitespace A fixed 900 px header pushed real Bear thumbnails to y=1088, outside the initial frame. Measure held-series copy, actions and caption, then add 40 px before Seasons. The recorded Bear header is 700 px, placing thumbnails at y=888. Film/loading/non-held title geometry is unchanged.

Dimensions and memory

Item Before After
Episode art 400×225 px 400×280 px (+24.4%)
Episode shelf 377 px 432 px; retains 68 px before Cast
Discover heading-to-card gap 40 px 60 px, all lanes
Full hero request/decode bound 1280×720 / undersized source paths 1920×1080; ARGB bound 7.91 MiB
Upright hero poster ordinary 256×384 poster path 420×630; RGB565 0.50 MiB where supported
Desperate Housewives JPEG, quality 96 1281×720; 438,329 bytes 1921×1080; 982,435 bytes

The one-pixel Jellyfin response excess is aspect-ratio rounding; native Coil decoding measured 1920×1079 for both comparison sources, so the old source was upscaled while the new source supplies real detail. Decode fitting prevents oversized off-ratio allocations. Memory cache stays 64 MiB, disk cache 128 MiB, image concurrency 4, JPEG quality 96. No 4K hero decode. These are bounds, not measured Xiaomi RSS; no claim of device playback/performance verification.

Specification revision 5 · native prototype delta board · source/measurement evidence

Committed sources: data/tv-spec/spec.md, data/tv-v2/feedback-2026-11-06/, the three token JSON files, generated Kotlin/CSS and drift manifest. The frozen revision-4 prototype is unchanged.

Native before / after — 1920×1080

Captured with Robolectric native graphics, not browser approximations. Bear frames use recorded real Jellyfin metadata through TitleReadFacade; other page frames use the existing native fixtures. All published PNGs have mode 0660. Pages are behind the existing sign-in gate.

Screen Before After
Who’s watching Picker before Picker after
Home Home before Home after
Discover Discover before Discover after
Series header Series before Series after
Focused episode and Cast Episode before Episode after
Person Person before Person after
Library (unchanged geometry check) Library before Library after

Hero source-quality comparison: same native renderer, real old/new Jellyfin responses; this isolates source resolution rather than pretending the renderer changed between these two captures.

Old response New response
Home hero old source Home hero new source

Focus sequence coverage and excluded timing case

HomeViewportTest exercises short/long poster, tile and wide shelves: first item, row end, Up/Down and OK into the shipped title surface followed by native Back. It checks full card width, ring clearance, ≥40 px screen-side margin, nav clearance and bottom visibility. Native scenario frames: poster 6, poster 12, tile 4, tile 12, wide 3, wide 12.

Known excluded case: a synthetic sequence disposes Home mid-animation, forcibly rewinds the retained lazy row to index 0, then restores logical selection. That interrupted/forced-rewind case still fails and was explicitly excluded by firstmate after two repair attempts. It is not presented as fixed or as normal Back behavior. No failing/ignored test or throwaway capture harness remains in the diff; no issue was filed for the excluded case.

Verification on 12fd12f754

  • Targeted native Home viewport, title keys, Discover projection/key/capture, watched-service/activity and real-artwork smoke runs passed.
  • Isolated PersonPickerSessionTest: 7 passed.
  • Default parallel make test: 696 app tests, 3 person-picker session timeouts.
  • Initial full unfiltered make test through the approved scratch serial overlay: Gradle workers=1, test forks=1 verified in output; 696 app tests, 4 person-picker session timeouts. The selected person and closed picker were correct, but sign-in/identity remained empty. No picker session source/test change, excluded test or ignored test was used to hide these failures.
  • The one authorized full serial rerun after clean ordered diagnostics also failed 4/696 app cases: launchRightThenOneOkKeepsPickerClosed, fastLeftLeftOkKeepsPickerClosed, avatarRightThenOneOkKeepsPickerClosed, gearAndBackThenOneOkKeepsPickerClosed. No SIGABRT occurred. The failure remains full-suite/order-dependent; a causal regression or baseline comparison is not yet established.
  • make apk-debug: passed, including branding checks for all four debug APK packages. Debug uses stub playback codecs; this is not optimized-release playback proof.
  • Older offline Robolectric SDK jars were seeded locally for the existing suite. The temporary SDK setup change was reverted; no SDK-cache/build-script workaround is committed.
  • Read-only Jellyfin GETs and native image decoding exercised the changed real-art path. No server/device state mutation.

Session-timeout investigation

The observed failure is after selectPerson clears the old identity but before setup creates the replacement model: models=1, empty signedInPerson, null identity, correct selected person and picker=false. Session, input routing and WolfskinApp.kt are unchanged; the picker change is offset/ring only. The only new test class, HomeViewportTest, belongs to the separate design-module test JVM and does not replace dispatchers or mutate a singleton. Full-run timestamps put the app Title classes after the picker, ruling out a preceding Title-class leak in that failed run.

Forced ordered diagnostics passed 139 design cases + 77 app cases, zero failures/skips: design viewport/Home/Discover/shell/captures, then app native capture and title classes, with all seven picker tests last in the same app JVM (identity logged for every class). The throwaway ordered suite was removed before the full gate. Ordered results. No lifecycle leak was reproduced by that sequence. The failed full log also showed GCLocker allocation retries, but their causal connection is unproven.

Gate decision: both full serial runs on this head reached 696 app tests and four PersonPickerSessionTest timeouts, with Gradle workers=1 / test forks=1. The Discover watch-claims and request-history work passed the same full serial suite on a different base state; that does not clear this head. No origin/main A/B was run; cause is not isolated. Firstmate explicitly chose review now rather than another baseline run. The four timeouts must be bisected on main as a follow-up; they are not fixed or waived as a known flake. Firstmate will verify real-box person switching (one OK per switch) before anything ships.

Device review after merge

Firstmate builds/installs Opt after merge. Check centered gear and its ring/navigation; short and long row ends/Up/Down/Back on Home, Discover, Library, Person and Title; Bear S3 stills, episode descriptions, initial thumbnail visibility and Cast clearance; Home/Discover/title hero sharpness; consistent Discover lane gaps.

> **Tests are not green.** Firstmate explicitly authorized review with the unresolved person-switch timeouts below. Real-box single-OK person switching must be verified by firstmate before anything ships. ## TV feedback: artwork, episode layout and focus visibility Implements the real-TV feedback against the approved revision-4 frames, with revision 5 documenting the requested deltas. AI-assisted implementation. No TV connection, ADB, installation or optimized-APK build was used. ### Diagnosis and decisions | Area | Diagnosis / evidence | Change | |---|---|---| | Who’s watching gear | The horizontal offset explicitly placed Settings under the final person. | Interpret the requested placement as centered beneath the complete people row, independent of selection/count; Justin confirmed this interpretation in steering. Preserve Down-from-final-person, Up return, 1.08 scale and caption; use the people’s focus ring. | | Focus visibility | Home had separate retained-row restoration and an early positioned checkpoint; shelf scrolling used inconsistent rules, and Discover’s width calculation rounded the approved six poster slots down to five. | Retain approved second-last-slot/lookahead behavior, clamp to real contents, settle absolute offsets, and mark vertical positioning only after scrolling finishes. Preserve the 168 px heading pin. Person’s final column no longer loses 8 px of poster width to end padding. | | Episode artwork | Read-only Jellyfin GETs: all ten The Bear S3 episodes have Primary tags, no episode Backdrop tags; Tomorrow’s source is 2100×1182 and the series backdrop is 3840×2160. These are aired files, not unaired placeholders. | Primary → Thumb → already-read series Backdrop crop → missing-art treatment. No per-episode series reads. | | Hero quality | Home capped a full hero at 1280 px, Discover reused ordinary poster-sized art, title backdrops were capped, and TMDB hero sources used w780. The real Desperate Housewives backdrop is 2732×1536. | Slot-sized Jellyfin URLs, suitable TMDB source tiers, panel-bounded Coil decoding and correct upright-poster requests; preserve image tags, quality and cache/concurrency limits. | | Discover heading gap | The previous 40 px heading-to-card gap was too tight in Series for you. | Apply 60 px / 104 px heading band consistently to **every** Discover lane, including service and Hidden picks lanes; Home is unchanged. | | Episode descriptions | The header used show-level copy despite an episode context. | Focused → explicitly opened → up-next episode overview; show overview only without episode context. Focused episode cards also show two ellipsised summary lines. | | Series whitespace | A fixed 900 px header pushed real Bear thumbnails to y=1088, outside the initial frame. | Measure held-series copy, actions and caption, then add 40 px before Seasons. The recorded Bear header is 700 px, placing thumbnails at y=888. Film/loading/non-held title geometry is unchanged. | ### Dimensions and memory | Item | Before | After | |---|---:|---:| | Episode art | 400×225 px | 400×280 px (+24.4%) | | Episode shelf | 377 px | 432 px; retains 68 px before Cast | | Discover heading-to-card gap | 40 px | 60 px, all lanes | | Full hero request/decode bound | 1280×720 / undersized source paths | 1920×1080; ARGB bound 7.91 MiB | | Upright hero poster | ordinary 256×384 poster path | 420×630; RGB565 0.50 MiB where supported | | Desperate Housewives JPEG, quality 96 | 1281×720; 438,329 bytes | 1921×1080; 982,435 bytes | The one-pixel Jellyfin response excess is aspect-ratio rounding; native Coil decoding measured 1920×1079 for both comparison sources, so the old source was upscaled while the new source supplies real detail. Decode fitting prevents oversized off-ratio allocations. Memory cache stays **64 MiB**, disk cache **128 MiB**, image concurrency **4**, JPEG quality **96**. No 4K hero decode. These are bounds, **not** measured Xiaomi RSS; no claim of device playback/performance verification. [Specification revision 5](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/spec.md) · [native prototype delta board](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/) · [source/measurement evidence](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/evidence.json) Committed sources: `data/tv-spec/spec.md`, `data/tv-v2/feedback-2026-11-06/`, the three token JSON files, generated Kotlin/CSS and drift manifest. The frozen revision-4 prototype is unchanged. ### Native before / after — 1920×1080 Captured with Robolectric native graphics, not browser approximations. Bear frames use recorded real Jellyfin metadata through `TitleReadFacade`; other page frames use the existing native fixtures. All published PNGs have mode 0660. Pages are behind the existing sign-in gate. | Screen | Before | After | |---|---|---| | Who’s watching | ![Picker before](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/before/picker.png) | ![Picker after](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/after/picker.png) | | Home | ![Home before](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/before/home.png) | ![Home after](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/after/home.png) | | Discover | ![Discover before](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/before/discover.png) | ![Discover after](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/after/discover.png) | | Series header | ![Series before](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/before/series.png) | ![Series after](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/after/series.png) | | Focused episode and Cast | ![Episode before](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/before/episodes.png) | ![Episode after](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/after/episodes.png) | | Person | ![Person before](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/before/person.png) | ![Person after](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/after/person.png) | | Library (unchanged geometry check) | ![Library before](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/before/library.png) | ![Library after](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/after/library.png) | Hero source-quality comparison: **same native renderer**, real old/new Jellyfin responses; this isolates source resolution rather than pretending the renderer changed between these two captures. | Old response | New response | |---|---| | ![Home hero old source](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/before/home-hero.png) | ![Home hero new source](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/after/home-hero.png) | ### Focus sequence coverage and excluded timing case `HomeViewportTest` exercises short/long poster, tile and wide shelves: first item, row end, Up/Down and OK into the shipped title surface followed by native Back. It checks full card width, ring clearance, ≥40 px screen-side margin, nav clearance and bottom visibility. Native scenario frames: [poster 6](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/coverage/viewport-Poster-6.png), [poster 12](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/coverage/viewport-Poster-12.png), [tile 4](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/coverage/viewport-Tile-4.png), [tile 12](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/coverage/viewport-Tile-12.png), [wide 3](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/coverage/viewport-Wide-3.png), [wide 12](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/coverage/viewport-Wide-12.png). **Known excluded case:** a synthetic sequence disposes Home mid-animation, forcibly rewinds the retained lazy row to index 0, then restores logical selection. That interrupted/forced-rewind case still fails and was explicitly excluded by firstmate after two repair attempts. It is not presented as fixed or as normal Back behavior. No failing/ignored test or throwaway capture harness remains in the diff; no issue was filed for the excluded case. ### Verification on 12fd12f7548014a8c4575901a501b46498f3b9a8 - Targeted native Home viewport, title keys, Discover projection/key/capture, watched-service/activity and real-artwork smoke runs passed. - Isolated `PersonPickerSessionTest`: **7 passed**. - Default parallel `make test`: 696 app tests, **3 person-picker session timeouts**. - Initial full unfiltered `make test` through the approved scratch serial overlay: Gradle workers=1, test forks=1 verified in output; 696 app tests, **4 person-picker session timeouts**. The selected person and closed picker were correct, but sign-in/identity remained empty. No picker session source/test change, excluded test or ignored test was used to hide these failures. - The one authorized full serial rerun after clean ordered diagnostics **also failed 4/696 app cases**: `launchRightThenOneOkKeepsPickerClosed`, `fastLeftLeftOkKeepsPickerClosed`, `avatarRightThenOneOkKeepsPickerClosed`, `gearAndBackThenOneOkKeepsPickerClosed`. No SIGABRT occurred. The failure remains full-suite/order-dependent; a causal regression or baseline comparison is not yet established. - `make apk-debug`: **passed**, including branding checks for all four debug APK packages. Debug uses stub playback codecs; this is not optimized-release playback proof. - Older offline Robolectric SDK jars were seeded locally for the existing suite. The temporary SDK setup change was reverted; no SDK-cache/build-script workaround is committed. - Read-only Jellyfin GETs and native image decoding exercised the changed real-art path. No server/device state mutation. #### Session-timeout investigation The observed failure is after `selectPerson` clears the old identity but before setup creates the replacement model: `models=1`, empty `signedInPerson`, null identity, correct selected person and `picker=false`. Session, input routing and `WolfskinApp.kt` are unchanged; the picker change is offset/ring only. The only new test class, `HomeViewportTest`, belongs to the separate design-module test JVM and does not replace dispatchers or mutate a singleton. Full-run timestamps put the app Title classes **after** the picker, ruling out a preceding Title-class leak in that failed run. Forced ordered diagnostics passed **139 design cases + 77 app cases**, zero failures/skips: design viewport/Home/Discover/shell/captures, then app native capture and title classes, with **all seven picker tests last in the same app JVM** (identity logged for every class). The throwaway ordered suite was removed before the full gate. [Ordered results](https://pages.justinvisser.org/wolfskin/tv-fb1-visual/ordered-sequence-results.json). No lifecycle leak was reproduced by that sequence. The failed full log also showed GCLocker allocation retries, but their causal connection is unproven. **Gate decision:** both full serial runs on this head reached 696 app tests and four `PersonPickerSessionTest` timeouts, with Gradle workers=1 / test forks=1. The [Discover watch-claims and request-history work](https://git.justinvisser.org/justinzeus/wolfskin/pulls/46) passed the same full serial suite on a **different base state**; that does not clear this head. **No `origin/main` A/B was run; cause is not isolated.** Firstmate explicitly chose review now rather than another baseline run. The four timeouts **must be bisected on main as a follow-up**; they are not fixed or waived as a known flake. Firstmate will verify real-box person switching (one OK per switch) **before anything ships**. ### Device review after merge Firstmate builds/installs Opt after merge. Check centered gear and its ring/navigation; short and long row ends/Up/Down/Back on Home, Discover, Library, Person and Title; Bear S3 stills, episode descriptions, initial thumbnail visibility and Cast clearance; Home/Discover/title hero sharpness; consistent Discover lane gaps.
justinzeus added 3 commits 2026-11-06 22:02:32 +01:00
Merge remote-tracking branch 'origin/main' into fm/tv-fb1-visual
All checks were successful
review/code-gpt Firstmate read the diff and evidence (native before/after frames, hero decode proof, episode art chain, gear centered per owner answer); apk-debug green; full serial suite NOT green (4 PersonPickerSessionTest timeouts, cause unisolated, disclosed in the PR body), accepted by firstmate with real-box person-switch verification before anything ships to the owner
review/design-claude Firstmate read the diff and evidence (native before/after frames, hero decode proof, episode art chain, gear centered per owner answer); apk-debug green; full serial suite NOT green (4 PersonPickerSessionTest timeouts, cause unisolated, disclosed in the PR body), accepted by firstmate with real-box person-switch verification before anything ships to the owner
12fd12f754
justinzeus merged commit 54c23e076a into main 2026-11-06 22:04:18 +01:00
Sign in to join this conversation.
No reviewers
No labels
exploring
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: justinzeus/wolfskin#47
No description provided.