Add TV offline recovery and per-person saved-state isolation #35

Merged
justinzeus merged 1 commit from fm/tv-slice8-states into main 2026-11-05 22:43:13 +01:00
Owner

Outcome

Add the cross-screen TV state sweep: retained last-known-good (LKG) views, reachable recovery, Saved timestamps, missing-art fallbacks and per-person isolation. Home is included after its merge; only the Title family remains for the short integration PR.

AI-assisted (fork-internal)

Verified head: a01c0a78fb3f2adb4d8ebfd05c9da6976dc5bf3c, branch fm/tv-slice8-states, base main. Fetched/merged origin/main immediately before the final gate pair; it was already current. Not a release or TV-install request.

Refs: approved TV specification, sections 3.3, 4.9, 6.13, 7.9, 10.1 and 12; locked Opus prototype. Existing visual sign-offs: shell/navigation, Libraries/See all, Home, Requests, Discover.

Download the exact-head proof archive: native captures, three reference pairs/diffs, text/pixel measurements, source audit, frame inventory, all final JUnit XML and both gate logs; no APK. Archive SHA256: 190adfabea70ed98cb02f3a9f978f336c124ff58948fa79a30012b72ed3ceed0.

Changed

  • Shared RecoveryToast uses the locked warning glyph, 760 px maximum width, 22/28 px padding, 14 px icon gap, 22 px corners, shadow and actual CSS body-line allocation. Network failures use the exact approved words, never exception text.
  • Discover keeps its existing catalogue on failed refresh, retains the original save time, exposes a recovery tail and blocks request/dismissal writes while offline. Revoked sign-in discards the saved projection. Collection and recipient retries use the same refresh transition; uncached rating plates cannot start reads offline.
  • Library cached headers propagate Saved to the nav; failed reads retain cards and add a reachable retry tail. Compact SavedPill is reused by Home, Libraries and Requests. Nav Saved padding includes the 2 px border rather than silently narrowing its border box.
  • Requests state/observed ratings are per person. Failed authenticated refresh keeps saved rows/time; Down reaches Try again after the last row and Up returns. A refresh already in flight publishes only to the person who started it. A person switch starts the new person's foreground refresh.
  • Home paints disk LKG before a source read; an identity change cannot paint the previous person or accept their late response. A disk-write failure does not turn a successful Jellyfin read into an outage. Owned Libraries now share the existing unresolved-person sign-in guard.
  • Skeleton-only Home, Library and Discover content retains nav focus on repeated Down. Library/Discover poster failures reuse Home's title/glyph/160-degree gradient fallback; Discover backdrop failure uses the shared ambient background.

Done bar — all eight items

Item Evidence and screen applicability
1. Tokens only Source audit covers 69 owned Kotlin source files, excluding the generated token file from the literal prohibition: no numeric .dp/.sp or Color(0x in screens. Token drift check passes. Applies to shell/settings, Home, Libraries/See all, Discover/service/request sheets and Requests.
2. State images Final native inventory contains 80 unique st-* IDs at 1920×1080/density 2.0, plus the Home extra capture. All merged-screen contract IDs exist; title-nav st-060 belongs to the 32 deferred Title states. Existing family sign-offs are inherited, not falsely re-claimed as a new review of all 113 frames. Fresh side-by-side inspection covers the changed Saved frames st-015/st-017 and actual offline-player st-090. The accepted st-089 exception is below.
3. Focus/Back ShellKeysTest; HomeKeysTest/HomeSeeAllNavigationTest; LibraryKeysTest; DiscoverKeysTest/DiscoverLoadingFocusTest; RequestsKeysTest. Added RecoveryStateTest and native RecoveryCaptureTest cover skeleton nav retention, saved-error recovery tails, Requests recovery rather than accidental last-title activation, and per-person isolation. PlayerHandoffTest proves actual full-screen hand-off and Back restoration.
4. Press counts Existing real-Android-event replay suites all remain green: shell 6/8/10/11; Home's approved JSON paths; Libraries' 20-press Spirited Away path; Discover 4/7/9/8 paths; Requests' existing 9-key shell replay. No new physical-TV/end-to-end installation claim.
5. Wording/type floor No raw library-name or pagination-label literals in the owned-source scan. Native recovery captures also inspect rendered strings. Generated type styles have a 12 sp / 24 px minimum across 29 declarations; new native captures assert the rendered font floor.
6. Sources down/LKG Per-screen matrix below. Added tests use packaged fixtures, local files and injected offline facade results, never a live server. Home and library disk-cache tests plus generated-client cache tests cover the disk boundary; UI tests cover retained data, exact failure wording and focus.
7. Frame/memory numbers Deferred for every screen by approved p1. Existing bounded image loader, exact decode slots, RGB565 policy and concurrency are reused; no TV profiling or largeHeap decision added.
8. Dutch scores Native HomeRatingsUiTest.dutchDecimalPlateStaysOnOneLine, RecoveryCaptureTest.dutchLibraryScoreRendersCommaWithoutBreakingThePlate, RecoveryCaptureTest.dutchDiscoverHeroScoreRendersComma, and RequestsKeysTest.cachedRatingsRenderInTheTelevisionLocale pass. TMDB 7.8 displays 7,8; RT percentages are unchanged. Discover's test configures Android resources to nl-NL as well as the JVM locale. These are native Robolectric checks, not an ADB/emulator or physical-TV session.

Per-screen source-failure and recovery proof

Screen Conductarr down Jellyfin down / person isolation Captures/tests
Who's watching, nav, Settings Loading/error picker retains Continue as/Try again; Settings remains a local hand-off list. No catalogue network dependency is invented for these local surfaces. Saved Jellyfin identities remain the shell input. Requests and root-view state are partitioned by person; a mismatched Jellyfin session cannot mount another person's owned library. st-001–004, st-010/011/014, st-091–094; ShellCaptureTest/ShellKeysTest; RecoveryStateTest person-switch regression.
Home Signed-out, unavailable/no-copy, saved-copy and partial-picks fixture states remain; library content stays usable independently. Real authenticated read path publishes disk LKG before failed refresh, keeps it on retry, rejects a late prior-person response and does not mislabel disk failure as server failure. Offline Play stays on the focused Resume card. st-005–020, st-096/099–103 and extra Home capture; HomeRecoveryTest; HomeCaptureTest; actual st-090 capture and PlayerHandoffTest.
Movies, Shows, Anime; See all Existing cached/uncached rating-offline frames remain; no new rating read starts where the source is unavailable. LibrarySnapshotStoreTest proves saved-before-failure, explicit refresh and server/person separation. Native saved-grid/error cases show header/nav Saved and reachable retry, or the no-copy recovery panel. st-021–028/030/035/039/095/097/098/112/113; LibraryCaptureTest/LibraryKeysTest/LibrarySnapshotStoreTest; new library-saved, library-unavailable and missing-art captures.
Discover, service, request sheets Failed refresh keeps catalogue/focus and Saved time, exposes Try again and blocks writes; no-copy error retry draws the exact Still-cannot-reach toast. Sign-out does not reuse authenticated data. Service/collection retries share this transition. Catalogue surfaces have no Jellyfin read dependency; a Jellyfin failure does not replace their Conductarr catalogue with a library error. Playback remains the real hand-off boundary. st-029–041, existing Discover-owned st-058/059, st-071–080/087/105/110; DiscoverCaptureTest/DiscoverKeysTest; RecoveryStateTest; new discover-saved/unavailable/missing-art captures. Existing title-destination stubs are not full Title-family delivery.
Requests under Settings Failed authenticated read keeps rows/time; focus can move to Try again and Back closes the sheet. Retry failure uses the shared exact toast; no-copy unavailable remains actionable. Requests does not read Jellyfin library data. Person B cannot see person A's retained Requests/ratings/Saved chip; already-started refresh cannot publish to a newly selected person. st-081–086/088/111; RequestsCaptureTest/RequestsKeysTest; ComingInRatingsGateTest; new requests-saved capture and RecoveryStateTest.

Cache boundary: release Conductarr TV sign-in/provider wiring is still unavailable, as in the merged slices. Release Discover remains signed out; release Requests remains honestly unavailable. This PR does not claim production authenticated catalogue/Requests reads or introduce fake network fallbacks. The existing generated client's identity-scoped disk cache is reused as the Conductarr boundary. Added UI state tests exercise the approved fixture seam; Home/Library use their existing real disk stores. No request is queued offline, and no Seerr fallback or handwritten HTTP is introduced.

Player acceptance and native comparisons

State Evidence
st-089 — successful player hand-off Stand-in by spec 6.13; no native pair required. Firstmate resolved the inconsistent brief: prove the real full-screen playback destination and Back restoring the exact card instead of shipping/capturing a simulated panel. PlayerHandoffTest.successfulPlaybackUsesTheRealFullscreenDestinationAndBackKeepsTheResumeCard passes, including real Destination.Playback, resume position, fullScreen and the original retained view/card after Back. No upstream-player fixture capture or playback/decoder certification is claimed.
st-090 — Play while offline Actual Home action → injected unreachable Jellyfin facade → unchanged Home/focus and exact toast: “Can’t reach Jellyfin to play. Check the network and try again.” PlayerHandoffTest.failedPlaybackReadLeavesSavedHomeFocusedAndDrawsTheExactOfflineToast passes. Separate actual-person guard test proves another person's session cannot resolve/start playback.

Every pair is frozen prototype left, native Compose right; each half 1920×1080 at density 2.0. The accepted shell compare script is reused. Mean RGB is absolute channel error on 0–255; >12 is the percentage whose maximum channel error exceeds 12; nav is the top 150 pixels, not isolated geometry.

Pair Mean RGB Pixels >12 Nav mean RGB Explanation
st-015 — cached Home 7.7583 7.6758% 7.1007 Saved includes its 2 px border in horizontal padding; retained Home layout. Inherited approved hero ink: prototype 674×65, native 550×64 px.
st-017 — saved Home Discover continuation 7.7583 7.6758% 7.1007 Same Saved correction; cached data and original Home line-box layout remain. Same approved hero font residual.
st-090 — actual offline Play 8.6169 9.0751% 4.8735 Shared toast and actual retained Home. Native Roboto fits “and try” on the first toast line; frozen DejaVu Sans wraps after “network”. Native hero/overview/pill advances differ; no per-title/width compensation.

Native-resolution paint/layout probes

Probe Frozen reference Native/current Meaning
Toast border box / body origin CSS x1064, y150, max width760; body x1136 Native assertions x1064/y150/w760; body x1136 Fixed the non-font 6 px gap error; actual icon gap14, padding22/28, radius22.
Toast fill scan, exclusive bounds [1065,150,1824,266] [1064,150,1824,267] JPEG/rounded-edge threshold and 116.8→117 px integer line allocation retain at most one paint-edge pixel; not a pixel-equality claim.
Toast type/leading 28 px / 400, 36.4 px CSS leading 14 sp / 400, 18.2 sp leading; two lines Exact line allocation, not an inflated native paragraph.
Origin button ring at y672 3 pixels within RGB tolerance30 4 pixels within the same threshold Nominal CSS/native ring remains4 px; opaque coverage differs with JPEG/fractional edges. Approved native control scale1.05 versus prototype1.06. No doubled-density stroke.
Hero title ink (min RGB200 probe) [102,384,847,463]: 745×79 px [98,386,686,467]: 588×81 px Platform font ink/advance residual; 84 px/800, CSS85.68 px line allocated as86 px natively. Raw paragraph diagnostics are not substituted for that allocated box.
Avatar initial contexts Dark nav initial; muted header initial Nav RGB11/11/16; Shared library header median RGB221/240/219 Existing context-specific initial colours remain, not one global colour override.
Caption separator/facts, fixed st-021 probes Peak channel81 /199 Peak channel79 /184 Existing muted alpha is multiplied by0.4; no bright full-strength dot. Exact rectangles/thresholds are retained in the raw report.

Approved font ruling is unchanged: platform Roboto, frozen browser DejaVu Sans; no bundled font, size fudge, title-specific wrapping or ellipsis-parity claim. The three pairs above were inspected side by side. Existing family sign-offs and their explicitly accepted residuals remain the visual baseline for unchanged frames.

Final commands and results

Command/scenario Observed result on the verified head
python3 wolfskin-design/generate_tokens.py --check Passed; approved source tokens untouched; supplemental/generated files agree.
Owned-screen source audit Passed: 69 source files, zero literal/wording violations; 29 generated type declarations, minimum12 sp.
ONE final make test Passed in 3m 7s: 569 app + 167 design + 17 client = 753 tests; zero failures/errors/skips. Every pre-existing test retained; no assertion weakened.
ONE final make apk-debug Passed in 1m 11s: four debug APKs; each packaged branding check scanned 1013 resources. Compile proof only; no APK attached or distributed.
Native smoke Actual player success/Back, offline failure and mismatched-person guard; Home disk/LKG/late-response tests; recovery focus, missing art and locale cases passed.

Initial targeted attempts exposed the known Maven429 runtime fetch and new harness setup issues (Android resource locale, paused Robolectric main looper, outer-versus-padded semantics bounds). Those were repaired before the final pair. The approved already-seeded Robolectric cache used a checkout-scoped untracked init hook; that hook was removed after both green gates. No tracked dependency/wrapper/Dockerfile workaround, shared daemon/volume administration, release, signing or version configuration change occurred.

Upstream files touched

None under app/src/main. No upstream player, manifest, hook, destination or preference implementation changed. The 29 changed files are confined to app/src/wolfskin, new fork tests, wolfskin-design (including generated token artifacts), and existing feature documentation docker/README.md.

Not done / integration boundary

  • The 32 individually named Title-family states below require the short sweep after the independent Title implementation merges; no Home state is deferred anymore. This PR does not claim all 113 states are already signed off.
  • Authenticated production Conductarr supplier/sign-in wiring remains outside the fixture-backed merged slices, as described above.
  • No physical-TV/emulator/ADB interaction, install, release build/tag/asset, signing or explicit version change. No PR merge or Actions/status mutation. Firstmate arranges review and any merge; release/install still require Justin's word.

Evidence is retained under /root/.treehouse/firstmate-7bab20/2/firstmate/data/tv-slice8-states/: native captures, reference pairs/diffs, layout/pixel measurements, source audit, inventory, all final JUnit XML and both gate logs.

Deferred Title-family state sweep

Home merged during this task and is now in scope; no Home state is deferred for an unmerged branch. The following 32 Title-family states require the short integration sweep after the Title implementation merges. Existing Discover-owned st-058/st-059 are not counted as full Title-family delivery.

State Locked state Remaining integration proof
st-042 Held movie (Justin) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-043 Held film, two versions: options row (Locke, note 3) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-044 Version list (Locke, sample files) Version-read error/loading still has Close/Back; selected source and origin focus survive recovery.
st-045 Version 2 remembered on reopening (Locke) Saved version choice stays per person/title after reopening and an offline refresh.
st-046 Audio and subtitles (sample) Track-read error/loading remains closable; no cross-person track state.
st-047 More → Media info (sample) Media-info error/loading remains closable and returns to More.
st-048 Trailer: sample hand-off (open question) Real installed-app trailer hand-off; no stand-in screen; no-handler recovery stays on title.
st-049 Mark as watched, toggled Optimistic watched failure reverts with a reachable recovery and no stale request state.
st-050 Series: mark the whole series (confirm) Series confirmation remains focus-trapped through a failure and restores the opener.
st-051 Episode page: Restart + options (Mindel resuming) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-052 Cast row (Locke, portraits) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-053 Cast row on a series, after Seasons (Andor) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-054 Cast: no photo (initials, Spirited Away) Missing portrait uses initials at the cast slot; focus ring stays on the circle.
st-055 Cast loading (placeholder circles) Loading cast is not focusable; Down skips it without a dead end.
st-056 Cast unavailable: row absent, Down skips to More like this Unavailable cast is absent; Down reaches More like this.
st-057 Person page (Tom Holland: real credits + sample fill) Person metadata/credits LKG remains per viewer; missing portrait initials and continuous-grid recovery.
st-060 Title page: nav focused (note 5) Title nav focus, cached Saved chip, root underline and Back/card restoration.
st-061 Title page loading, no saved copy (skeleton) No-LKG skeleton keeps nav focus, including repeated Down; LKG never becomes a skeleton.
st-062 Title page error, no saved copy (Try again) No-LKG metadata error exposes Try again; retry failure uses the exact shared toast.
st-063 Not signed in: a pick’s page without requests Sign-out hides ratings and request actions; recovery never borrows another person’s cache.
st-064 Held series with next up (Andor) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-065 Resume an episode (Mindel) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-066 Not here: request Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-067 Not here: Trailer focused (round 4, sample hand-off) Unavailable not-held trailer field stays absent; installed-app/no-handler behavior once supplied.
st-068 Requested for you (Searching) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-069 Missing artwork Missing and failed poster/backdrop reads use the shared imagery fallbacks.
st-070 Offline: request needs Conductarr Offline requests become Try again, never queue a write; saved metadata and focus survive retries.
st-104 Poster in More like this (title page) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-106 Title page: held film with RT critics (Shared) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-107 Title page: series + Up next “Ep” (Justin, Andor) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-108 Title page: resuming an episode (Mindel) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
st-109 Title page: not here, no ratings (request) Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show.
## Outcome Add the cross-screen TV state sweep: retained last-known-good (LKG) views, reachable recovery, Saved timestamps, missing-art fallbacks and per-person isolation. Home is included after its merge; only the Title family remains for the short integration PR. AI-assisted (fork-internal) Verified head: `a01c0a78fb3f2adb4d8ebfd05c9da6976dc5bf3c`, branch `fm/tv-slice8-states`, base `main`. Fetched/merged `origin/main` immediately before the final gate pair; it was already current. **Not a release or TV-install request.** Refs: [approved TV specification](https://pages.justinvisser.org/wolfskin/tv-spec), sections 3.3, 4.9, 6.13, 7.9, 10.1 and 12; [locked Opus prototype](https://pages.justinvisser.org/wolfskin/tv-prototype-v2-opus/). Existing visual sign-offs: [shell/navigation](https://git.justinvisser.org/justinzeus/wolfskin/pulls/29), [Libraries/See all](https://git.justinvisser.org/justinzeus/wolfskin/pulls/30), [Home](https://git.justinvisser.org/justinzeus/wolfskin/pulls/31), [Requests](https://git.justinvisser.org/justinzeus/wolfskin/pulls/32), [Discover](https://git.justinvisser.org/justinzeus/wolfskin/pulls/33). [Download the exact-head proof archive](https://git.justinvisser.org/attachments/f6d55c2d-6bca-4bfb-9e67-dc6147a41afa): native captures, three reference pairs/diffs, text/pixel measurements, source audit, frame inventory, all final JUnit XML and both gate logs; no APK. Archive SHA256: `190adfabea70ed98cb02f3a9f978f336c124ff58948fa79a30012b72ed3ceed0`. ## Changed - Shared `RecoveryToast` uses the locked warning glyph, 760 px maximum width, 22/28 px padding, 14 px icon gap, 22 px corners, shadow and actual CSS body-line allocation. Network failures use the exact approved words, never exception text. - Discover keeps its existing catalogue on failed refresh, retains the original save time, exposes a recovery tail and blocks request/dismissal writes while offline. Revoked sign-in discards the saved projection. Collection and recipient retries use the same refresh transition; uncached rating plates cannot start reads offline. - Library cached headers propagate Saved to the nav; failed reads retain cards and add a reachable retry tail. Compact `SavedPill` is reused by Home, Libraries and Requests. Nav Saved padding includes the 2 px border rather than silently narrowing its border box. - Requests state/observed ratings are per person. Failed authenticated refresh keeps saved rows/time; Down reaches Try again after the last row and Up returns. A refresh already in flight publishes only to the person who started it. A person switch starts the new person's foreground refresh. - Home paints disk LKG before a source read; an identity change cannot paint the previous person or accept their late response. A disk-write failure does not turn a successful Jellyfin read into an outage. Owned Libraries now share the existing unresolved-person sign-in guard. - Skeleton-only Home, Library and Discover content retains nav focus on repeated Down. Library/Discover poster failures reuse Home's title/glyph/160-degree gradient fallback; Discover backdrop failure uses the shared ambient background. ## Done bar — all eight items | Item | Evidence and screen applicability | |---|---| | 1. Tokens only | Source audit covers **69 owned Kotlin source files**, excluding the generated token file from the literal prohibition: no numeric `.dp`/`.sp` or `Color(0x` in screens. Token drift check passes. Applies to shell/settings, Home, Libraries/See all, Discover/service/request sheets and Requests. | | 2. State images | Final native inventory contains **80 unique `st-*` IDs** at 1920×1080/density 2.0, plus the Home extra capture. All merged-screen contract IDs exist; title-nav st-060 belongs to the 32 deferred Title states. Existing family sign-offs are inherited, not falsely re-claimed as a new review of all 113 frames. Fresh side-by-side inspection covers the changed Saved frames st-015/st-017 and actual offline-player st-090. The accepted st-089 exception is below. | | 3. Focus/Back | ShellKeysTest; HomeKeysTest/HomeSeeAllNavigationTest; LibraryKeysTest; DiscoverKeysTest/DiscoverLoadingFocusTest; RequestsKeysTest. Added RecoveryStateTest and native RecoveryCaptureTest cover skeleton nav retention, saved-error recovery tails, Requests recovery rather than accidental last-title activation, and per-person isolation. PlayerHandoffTest proves actual full-screen hand-off and Back restoration. | | 4. Press counts | Existing real-Android-event replay suites all remain green: shell 6/8/10/11; Home's approved JSON paths; Libraries' 20-press Spirited Away path; Discover 4/7/9/8 paths; Requests' existing 9-key shell replay. No new physical-TV/end-to-end installation claim. | | 5. Wording/type floor | No raw library-name or pagination-label literals in the owned-source scan. Native recovery captures also inspect rendered strings. Generated type styles have a **12 sp / 24 px minimum** across 29 declarations; new native captures assert the rendered font floor. | | 6. Sources down/LKG | Per-screen matrix below. Added tests use packaged fixtures, local files and injected offline facade results, never a live server. Home and library disk-cache tests plus generated-client cache tests cover the disk boundary; UI tests cover retained data, exact failure wording and focus. | | 7. Frame/memory numbers | Deferred for every screen by approved p1. Existing bounded image loader, exact decode slots, RGB565 policy and concurrency are reused; no TV profiling or largeHeap decision added. | | 8. Dutch scores | Native `HomeRatingsUiTest.dutchDecimalPlateStaysOnOneLine`, `RecoveryCaptureTest.dutchLibraryScoreRendersCommaWithoutBreakingThePlate`, `RecoveryCaptureTest.dutchDiscoverHeroScoreRendersComma`, and `RequestsKeysTest.cachedRatingsRenderInTheTelevisionLocale` pass. TMDB 7.8 displays **7,8**; RT percentages are unchanged. Discover's test configures Android resources to nl-NL as well as the JVM locale. These are native Robolectric checks, not an ADB/emulator or physical-TV session. | ### Per-screen source-failure and recovery proof | Screen | Conductarr down | Jellyfin down / person isolation | Captures/tests | |---|---|---|---| | Who's watching, nav, Settings | Loading/error picker retains Continue as/Try again; Settings remains a local hand-off list. No catalogue network dependency is invented for these local surfaces. | Saved Jellyfin identities remain the shell input. Requests and root-view state are partitioned by person; a mismatched Jellyfin session cannot mount another person's owned library. | st-001–004, st-010/011/014, st-091–094; ShellCaptureTest/ShellKeysTest; RecoveryStateTest person-switch regression. | | Home | Signed-out, unavailable/no-copy, saved-copy and partial-picks fixture states remain; library content stays usable independently. | Real authenticated read path publishes disk LKG before failed refresh, keeps it on retry, rejects a late prior-person response and does not mislabel disk failure as server failure. Offline Play stays on the focused Resume card. | st-005–020, st-096/099–103 and extra Home capture; HomeRecoveryTest; HomeCaptureTest; actual st-090 capture and PlayerHandoffTest. | | Movies, Shows, Anime; See all | Existing cached/uncached rating-offline frames remain; no new rating read starts where the source is unavailable. | LibrarySnapshotStoreTest proves saved-before-failure, explicit refresh and server/person separation. Native saved-grid/error cases show header/nav Saved and reachable retry, or the no-copy recovery panel. | st-021–028/030/035/039/095/097/098/112/113; LibraryCaptureTest/LibraryKeysTest/LibrarySnapshotStoreTest; new library-saved, library-unavailable and missing-art captures. | | Discover, service, request sheets | Failed refresh keeps catalogue/focus and Saved time, exposes Try again and blocks writes; no-copy error retry draws the exact Still-cannot-reach toast. Sign-out does not reuse authenticated data. Service/collection retries share this transition. | Catalogue surfaces have no Jellyfin read dependency; a Jellyfin failure does not replace their Conductarr catalogue with a library error. Playback remains the real hand-off boundary. | st-029–041, existing Discover-owned st-058/059, st-071–080/087/105/110; DiscoverCaptureTest/DiscoverKeysTest; RecoveryStateTest; new discover-saved/unavailable/missing-art captures. Existing title-destination stubs are not full Title-family delivery. | | Requests under Settings | Failed authenticated read keeps rows/time; focus can move to Try again and Back closes the sheet. Retry failure uses the shared exact toast; no-copy unavailable remains actionable. | Requests does not read Jellyfin library data. Person B cannot see person A's retained Requests/ratings/Saved chip; already-started refresh cannot publish to a newly selected person. | st-081–086/088/111; RequestsCaptureTest/RequestsKeysTest; ComingInRatingsGateTest; new requests-saved capture and RecoveryStateTest. | **Cache boundary:** release Conductarr TV sign-in/provider wiring is still unavailable, as in the merged slices. Release Discover remains signed out; release Requests remains honestly unavailable. This PR does not claim production authenticated catalogue/Requests reads or introduce fake network fallbacks. The existing generated client's identity-scoped disk cache is reused as the Conductarr boundary. Added UI state tests exercise the approved fixture seam; Home/Library use their existing real disk stores. No request is queued offline, and no Seerr fallback or handwritten HTTP is introduced. ## Player acceptance and native comparisons | State | Evidence | |---|---| | **st-089 — successful player hand-off** | **Stand-in by spec 6.13; no native pair required.** Firstmate resolved the inconsistent brief: prove the real full-screen playback destination and Back restoring the exact card instead of shipping/capturing a simulated panel. `PlayerHandoffTest.successfulPlaybackUsesTheRealFullscreenDestinationAndBackKeepsTheResumeCard` passes, including real Destination.Playback, resume position, fullScreen and the original retained view/card after Back. No upstream-player fixture capture or playback/decoder certification is claimed. | | **st-090 — Play while offline** | Actual Home action → injected unreachable Jellyfin facade → unchanged Home/focus and exact toast: **“Can’t reach Jellyfin to play. Check the network and try again.”** `PlayerHandoffTest.failedPlaybackReadLeavesSavedHomeFocusedAndDrawsTheExactOfflineToast` passes. Separate actual-person guard test proves another person's session cannot resolve/start playback. | Every pair is frozen prototype left, native Compose right; **each half 1920×1080 at density 2.0**. The accepted shell compare script is reused. Mean RGB is absolute channel error on 0–255; >12 is the percentage whose maximum channel error exceeds 12; nav is the top 150 pixels, not isolated geometry. | Pair | Mean RGB | Pixels >12 | Nav mean RGB | Explanation | |---|---:|---:|---:|---| | [st-015 — cached Home](https://git.justinvisser.org/attachments/bf37b655-4003-4fb3-9694-013484af94b3) | 7.7583 | 7.6758% | 7.1007 | Saved includes its 2 px border in horizontal padding; retained Home layout. Inherited approved hero ink: prototype 674×65, native 550×64 px. | | [st-017 — saved Home Discover continuation](https://git.justinvisser.org/attachments/479dd52d-26f2-4781-9a49-3939ae3fde8d) | 7.7583 | 7.6758% | 7.1007 | Same Saved correction; cached data and original Home line-box layout remain. Same approved hero font residual. | | [st-090 — actual offline Play](https://git.justinvisser.org/attachments/65db1746-0dd9-4768-ad83-33488b2e9b18) | 8.6169 | 9.0751% | 4.8735 | Shared toast and actual retained Home. Native Roboto fits “and try” on the first toast line; frozen DejaVu Sans wraps after “network”. Native hero/overview/pill advances differ; no per-title/width compensation. | ### Native-resolution paint/layout probes | Probe | Frozen reference | Native/current | Meaning | |---|---|---|---| | Toast border box / body origin | CSS x1064, y150, max width760; body x1136 | Native assertions x1064/y150/w760; body x1136 | Fixed the non-font 6 px gap error; actual icon gap14, padding22/28, radius22. | | Toast fill scan, exclusive bounds | [1065,150,1824,266] | [1064,150,1824,267] | JPEG/rounded-edge threshold and 116.8→117 px integer line allocation retain at most one paint-edge pixel; not a pixel-equality claim. | | Toast type/leading | 28 px / 400, 36.4 px CSS leading | 14 sp / 400, 18.2 sp leading; two lines | Exact line allocation, not an inflated native paragraph. | | Origin button ring at y672 | 3 pixels within RGB tolerance30 | 4 pixels within the same threshold | Nominal CSS/native ring remains4 px; opaque coverage differs with JPEG/fractional edges. Approved native control scale1.05 versus prototype1.06. No doubled-density stroke. | | Hero title ink (min RGB200 probe) | [102,384,847,463]: 745×79 px | [98,386,686,467]: 588×81 px | Platform font ink/advance residual; 84 px/800, CSS85.68 px line allocated as86 px natively. Raw paragraph diagnostics are not substituted for that allocated box. | | Avatar initial contexts | Dark nav initial; muted header initial | Nav RGB11/11/16; Shared library header median RGB221/240/219 | Existing context-specific initial colours remain, not one global colour override. | | Caption separator/facts, fixed st-021 probes | Peak channel81 /199 | Peak channel79 /184 | Existing muted alpha is multiplied by0.4; no bright full-strength dot. Exact rectangles/thresholds are retained in the raw report. | Approved font ruling is unchanged: **platform Roboto**, frozen browser **DejaVu Sans**; no bundled font, size fudge, title-specific wrapping or ellipsis-parity claim. The three pairs above were inspected side by side. Existing family sign-offs and their explicitly accepted residuals remain the visual baseline for unchanged frames. ## Final commands and results | Command/scenario | Observed result on the verified head | |---|---| | `python3 wolfskin-design/generate_tokens.py --check` | Passed; approved source tokens untouched; supplemental/generated files agree. | | Owned-screen source audit | Passed: 69 source files, zero literal/wording violations; 29 generated type declarations, minimum12 sp. | | **ONE final `make test`** | **Passed in 3m 7s: 569 app + 167 design + 17 client = 753 tests; zero failures/errors/skips.** Every pre-existing test retained; no assertion weakened. | | **ONE final `make apk-debug`** | **Passed in 1m 11s**: four debug APKs; each packaged branding check scanned 1013 resources. Compile proof only; no APK attached or distributed. | | Native smoke | Actual player success/Back, offline failure and mismatched-person guard; Home disk/LKG/late-response tests; recovery focus, missing art and locale cases passed. | Initial targeted attempts exposed the known Maven429 runtime fetch and new harness setup issues (Android resource locale, paused Robolectric main looper, outer-versus-padded semantics bounds). Those were repaired before the final pair. The approved already-seeded Robolectric cache used a checkout-scoped untracked init hook; that hook was removed after both green gates. No tracked dependency/wrapper/Dockerfile workaround, shared daemon/volume administration, release, signing or version configuration change occurred. ## Upstream files touched **None under `app/src/main`.** No upstream player, manifest, hook, destination or preference implementation changed. The 29 changed files are confined to `app/src/wolfskin`, new fork tests, `wolfskin-design` (including generated token artifacts), and existing feature documentation `docker/README.md`. ## Not done / integration boundary - The 32 individually named Title-family states below require the short sweep after the independent Title implementation merges; no Home state is deferred anymore. This PR does not claim all 113 states are already signed off. - Authenticated production Conductarr supplier/sign-in wiring remains outside the fixture-backed merged slices, as described above. - No physical-TV/emulator/ADB interaction, install, release build/tag/asset, signing or explicit version change. No PR merge or Actions/status mutation. Firstmate arranges review and any merge; release/install still require Justin's word. Evidence is retained under `/root/.treehouse/firstmate-7bab20/2/firstmate/data/tv-slice8-states/`: native captures, reference pairs/diffs, layout/pixel measurements, source audit, inventory, all final JUnit XML and both gate logs. ## Deferred Title-family state sweep Home merged during this task and is now in scope; no Home state is deferred for an unmerged branch. The following 32 Title-family states require the short integration sweep after the Title implementation merges. Existing Discover-owned st-058/st-059 are not counted as full Title-family delivery. | State | Locked state | Remaining integration proof | |---|---|---| | st-042 | Held movie (Justin) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-043 | Held film, two versions: options row (Locke, note 3) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-044 | Version list (Locke, sample files) | Version-read error/loading still has Close/Back; selected source and origin focus survive recovery. | | st-045 | Version 2 remembered on reopening (Locke) | Saved version choice stays per person/title after reopening and an offline refresh. | | st-046 | Audio and subtitles (sample) | Track-read error/loading remains closable; no cross-person track state. | | st-047 | More → Media info (sample) | Media-info error/loading remains closable and returns to More. | | st-048 | Trailer: sample hand-off (open question) | Real installed-app trailer hand-off; no stand-in screen; no-handler recovery stays on title. | | st-049 | Mark as watched, toggled | Optimistic watched failure reverts with a reachable recovery and no stale request state. | | st-050 | Series: mark the whole series (confirm) | Series confirmation remains focus-trapped through a failure and restores the opener. | | st-051 | Episode page: Restart + options (Mindel resuming) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-052 | Cast row (Locke, portraits) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-053 | Cast row on a series, after Seasons (Andor) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-054 | Cast: no photo (initials, Spirited Away) | Missing portrait uses initials at the cast slot; focus ring stays on the circle. | | st-055 | Cast loading (placeholder circles) | Loading cast is not focusable; Down skips it without a dead end. | | st-056 | Cast unavailable: row absent, Down skips to More like this | Unavailable cast is absent; Down reaches More like this. | | st-057 | Person page (Tom Holland: real credits + sample fill) | Person metadata/credits LKG remains per viewer; missing portrait initials and continuous-grid recovery. | | st-060 | Title page: nav focused (note 5) | Title nav focus, cached Saved chip, root underline and Back/card restoration. | | st-061 | Title page loading, no saved copy (skeleton) | No-LKG skeleton keeps nav focus, including repeated Down; LKG never becomes a skeleton. | | st-062 | Title page error, no saved copy (Try again) | No-LKG metadata error exposes Try again; retry failure uses the exact shared toast. | | st-063 | Not signed in: a pick’s page without requests | Sign-out hides ratings and request actions; recovery never borrows another person’s cache. | | st-064 | Held series with next up (Andor) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-065 | Resume an episode (Mindel) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-066 | Not here: request | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-067 | Not here: Trailer focused (round 4, sample hand-off) | Unavailable not-held trailer field stays absent; installed-app/no-handler behavior once supplied. | | st-068 | Requested for you (Searching) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-069 | Missing artwork | Missing and failed poster/backdrop reads use the shared imagery fallbacks. | | st-070 | Offline: request needs Conductarr | Offline requests become Try again, never queue a write; saved metadata and focus survive retries. | | st-104 | Poster in More like this (title page) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-106 | Title page: held film with RT critics (Shared) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-107 | Title page: series + Up next “Ep” (Justin, Andor) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-108 | Title page: resuming an episode (Mindel) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. | | st-109 | Title page: not here, no ratings (request) | Conductarr-down and Jellyfin-down LKG/no-LKG paths, per-person isolation, recovery focus/Back, Saved timestamp and native recapture; Dutch score/plate check where ratings show. |
justinzeus added 1 commit 2026-11-05 22:29:59 +01:00
Add TV offline recovery and per-person state isolation
All checks were successful
review/design-claude Claude Sonnet cross-family review: pass, no blockers
a01c0a78fb
Author
Owner

Design review (Claude Sonnet, cross-family): PASS, zero blockers. Reviewed head a01c0a78fb3f2adb4d8ebfd05c9da6976dc5bf3c. Read and measure only; no Gradle run.

Checked

  • st-090 toast measured against the locked frame at 1920x1080: top 150, right edge 1823 (right 96), width 760, warn glyph same size and x, line pitch 36 vs 37 px, 3200 ms. The fill is 1 px taller (117 vs 116), as the PR states. The wrap differs (Roboto fits "and try" on line 1), which is font-only.
  • st-015 Saved chip: 2 px border, colour, 22 px side padding, wifi icon and gap all match. The chip is 212 px wide against 233 px in the reference (text 129 vs 150 px). That is the Roboto vs DejaVu advance difference only. st-017 is byte-identical to st-015 in the locked set.
  • Done-bar tests read. They fail when the behaviour breaks: the exact toast strings, a kept catalogue and focus with writes blocked, nothing-focusable keeps focus on the nav, Down reaches Try again and never opens the last title.
  • Per-person partition: Home disk key is server:user (digest in the path), a revision guard drops an older person's late response, Discover state is rebuilt per person, Requests state is a map keyed by person, Libraries are per person. I found no cross-person leak.
  • Token-literal grep on the head: 0 hits. generate_tokens.py --check passes and the checksums verify. No existing test was changed, nothing under app/src/main was touched, and no public design API was removed.

Non-blocking

  1. The Saved Discover recovery card reads "Try again stopped loading" (lane title "Try again" feeds the DiscoverShelf.kt:95 caption). A one-string fix.
  2. The lint grep is a one-off script outside the repo, not a durable gate.
  3. Library and Requests retry toasts have no assertion; the code is correct.
  4. The "Back keeps the card" assertion is partly tautological.
  5. The Library header Saved pill has no visible capture.

Full report: /root/.treehouse/firstmate-7bab20/2/firstmate/data/review-wolfskin-35/report.md

**Design review (Claude Sonnet, cross-family): PASS, zero blockers.** Reviewed head `a01c0a78fb3f2adb4d8ebfd05c9da6976dc5bf3c`. Read and measure only; no Gradle run. **Checked** - `st-090` toast measured against the locked frame at 1920x1080: top 150, right edge 1823 (right 96), width 760, warn glyph same size and x, line pitch 36 vs 37 px, 3200 ms. The fill is 1 px taller (117 vs 116), as the PR states. The wrap differs (Roboto fits "and try" on line 1), which is font-only. - `st-015` Saved chip: 2 px border, colour, 22 px side padding, wifi icon and gap all match. The chip is 212 px wide against 233 px in the reference (text 129 vs 150 px). That is the Roboto vs DejaVu advance difference only. `st-017` is byte-identical to `st-015` in the locked set. - Done-bar tests read. They fail when the behaviour breaks: the exact toast strings, a kept catalogue and focus with writes blocked, nothing-focusable keeps focus on the nav, Down reaches Try again and never opens the last title. - Per-person partition: Home disk key is `server:user` (digest in the path), a revision guard drops an older person's late response, Discover state is rebuilt per person, Requests state is a map keyed by person, Libraries are per person. I found no cross-person leak. - Token-literal grep on the head: 0 hits. `generate_tokens.py --check` passes and the checksums verify. No existing test was changed, nothing under `app/src/main` was touched, and no public design API was removed. **Non-blocking** 1. The Saved Discover recovery card reads "Try again stopped loading" (lane title "Try again" feeds the `DiscoverShelf.kt:95` caption). A one-string fix. 2. The lint grep is a one-off script outside the repo, not a durable gate. 3. Library and Requests retry toasts have no assertion; the code is correct. 4. The "Back keeps the card" assertion is partly tautological. 5. The Library header Saved pill has no visible capture. Full report: `/root/.treehouse/firstmate-7bab20/2/firstmate/data/review-wolfskin-35/report.md`
justinzeus merged commit abec496305 into main 2026-11-05 22:43:13 +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#35
No description provided.