Wire real Conductarr data into Wolfskin TV #36

Merged
justinzeus merged 7 commits from fm/tv-conductarr-wiring into main 2026-11-06 01:41:48 +01:00
Owner

Outcome

Wire the default Wolfskin TV surfaces to Conductarr using the currently selected Jellyfin user's existing access token. Debug fixtures remain explicit opt-in; this is not a fixture-only TV build.

AI-assisted (fork-internal)

Refs: TV specification, especially §§3.1, 3.3–3.6, 7.8–7.9 and 8; production seams, configuration and manual smoke. The server's dedicated-hostname TV authority remains a separate deployment: Conductarr TV operation authorization.

Seams wired

Surface Real provider
Who's watching / navigation Operator, profile names and library audiences; selectable logins still come from upstream Jellyfin users
Home Jellyfin/LKG paints independently; recommendation feed and catalogue continuation merge afterwards; no Coming soon lane on Home
Discover / services Picks and contributions, options/chips, New on, Trending, genres, Coming soon, service shelves, paging and dismissal GET/POST/DELETE
Request chooser Authorized profile names, including recipients without a saved TV login; known seasons, receipt lookup before POST, identity-lifetime uncertain-submission guard; actor is server evidence, never the selected beneficiary
Requests Household activity, stage precedence, actors/requesters and display artwork, foreground/poll refresh and saved recovery
Ratings Ratings-only route, 350 ms focused-card dwell, immediate prominent/title/Coming-in reads, canonical source precedence, positive-vote rules and episode-season scores
Titles / people Actual merged title routes: detail/enrichment/display, cast, related, seasons/episode votes, performer/filmography, title activity and receipts; shared request/hide workflows
Playback boundary Catalogue ownership never invents a playable UUID; an exact current-user Jellyfin TMDB/type lookup must resolve a held item first

Authentication, cache and configuration

  • Default: BuildConfig.CONDUCTARR_BASE_URL = https://wolfskin-tv.justinvisser.org.
  • Advanced settings beside the Wolfskin switch: Conductarr server URL; blank restores the default. HTTPS except loopback fake servers.
  • Wolfskin preview fixtures is debug-only and off by default. Release/default uses the real session; no upstream token means the existing signed-out states.
  • Only Authorization: MediaBrowser Token="<token>"; no second credential store, API key or cookie. Person/token/server/endpoint changes replace the client and in-memory providers.
  • Backup-excluded on-disk last-known-good data is partitioned by person and client identity. A 401 clears authenticated data/actions; offline keeps Saved data without an offline mutation queue.
  • Production call bounds: connect 3 s, read 5 s, total 8 s. Home does not await Conductarr authentication or a disk write before publishing available Jellyfin rows.
  • Pin OpenAPI to Conductarr 7e8f3f0a1c9a88d67b2a9518c1161db84ade531a; expose exactly the 22 TV operations, including ratings and display, with ETag/If-None-Match/304 reuse.

Test inventory

Loopback fake HTTP only; no live Conductarr/Jellyfin or device access:

  • ConductarrSessionTest: exact credential header, person/client replacement, disk separation even for equal tokens, offline reopen, 401 and retry.
  • ConductarrProjectionTest: Home/Discover/services/dismissals/activity projections, actor-safe uncertain requests, ratings dwell supplier and conditional reads.
  • ConductarrTitlePagesTest: no invented playback, saved title facts, cross-season episode votes, 401 rating removal and source/vote boundaries.
  • ConductarrSurfaceTest, ConductarrHomeSurfaceTest, ConductarrTitleSurfaceTest: native shipped surfaces from real HTTP-shaped data, request-on-behalf receipt, saved Requests, and sign-out recovery. Fake/decoded one-pixel art keeps these transport smoke captures off external image hosts; the existing fixture capture suites retain full approved artwork.
  • TitleReadFacadeTest: reject an unrelated TMDB identity or media type before granting catalogue playback; existing held title/person/playback regressions remain.
  • WolfskinArtworkTest: actual Coil image decoding against Jellyfin, another hostname and a same-host/different-port server; an authenticated upstream parent cannot leak Authorization, and Jellyfin still receives its credential.
  • ConductarrAudienceRecoveryTest: operator 200 plus audiences 503 without saved data, empty audience success, retained Jellyfin destination mappings and successful foreground recovery without replacing the client.
  • Existing screen/fixture tests are retained. Root make test also selects the client's contract/route/cache tests and loopback socket smoke.

Upstream files changed

Within the fork's permitted upstream touch points:

  • app/build.gradle.kts: base-URL BuildConfig default and fake-server test dependency.
  • app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt: Advanced URL override and debug fixture switch.

Other production changes are in fork-owned app/src/wolfskin, debug/default flavor resources, wolfskin-design, conductarr-client and existing fork tests/docs.

Not done / manual gate

  • No deployment of the dedicated hostname or expanded server authority; the default endpoint must be deployed before real TV data can work there.
  • No ADB, hardware installation/playback verification, release/tag/asset publishing, signing or version change.
  • Unheld trailers remain absent because the server contract has no trailer field. Signed-out users receive no ratings/request actions; fixture content appears only with explicit debug opt-in.
  • The clearly named Manual smoke after endpoint deployment (no build command) section in docker/README.md covers person switching, real title/person traversal, Saved/401 recovery and an uncertain Shared-on-behalf request with one POST.

Review blockers fixed

  • Artwork credential isolation: construct a fresh bounded loader, supply the authenticated call factory from the app module, and strip Authorization outside the currently selected Jellyfin origin (scheme, host and port), including redirects. Networking and concurrency interception share one Coil component registry so neither overwrites the other.
  • Libraries after unavailable metadata: failed or empty audience reads no longer install an authoritative empty list. Existing mappings or Jellyfin's name-based destinations remain usable; foreground entry retries household metadata without rebuilding the current identity or its request ledger.

Final verification

Run once each on final merged head ec32346c71f6fcbb966653959ab31acbff3db399; no source changes after the gates. The artwork fixture saves and restores Coil's process singleton, including when earlier screen tests initialized it.

Command Result
make test PASS, 2m 56s, 112 tasks; unfiltered app/design suites plus client contract/cache tests and loopback socket smoke
make apk-debug PASS, 1m 03s, 97 tasks; universal, arm64-v8a, armeabi-v7a and x86_64 debug APKs

The loopback client smoke observed profile data, disk reopen, ETag 304 reuse and
HTTP 503 retaining the last good profile in three requests. Packaged branding
checks passed for all four APKs, inspecting 1016 string/plural/array resources
per APK with legal attribution preserved.

Native Home, saved Requests and Shared-on-behalf receipt captures were opened
and visually inspected. A measured Home diagnosis established Jellyfin-only
publication at 3.577 s and real catalogue Ready at 4.657 s; the apparent stall
was Robolectric Main-loop scheduling. Home retry authentication and disk writes
were also moved off the first-paint path, and initial-loading Discover read-ahead
no longer warms a fallback hero's ratings before the focused-card dwell.

Builds and captures are preserved for firstmate under
/root/.treehouse/firstmate-7bab20/2/firstmate/data/tv-conductarr-wiring/
(apk-build-b/, captures/, and home-layer-diagnostic.txt). Build B contains the reviewed fixes and its checksum manifest. The older apk/ bundle is superseded and explicitly marked DO NOT INSTALL.

## Outcome Wire the default Wolfskin TV surfaces to Conductarr using the currently selected Jellyfin user's existing access token. Debug fixtures remain explicit opt-in; this is not a fixture-only TV build. AI-assisted (fork-internal) Refs: [TV specification](https://pages.justinvisser.org/wolfskin/tv-spec), especially §§3.1, 3.3–3.6, 7.8–7.9 and 8; [production seams, configuration and manual smoke](https://git.justinvisser.org/justinzeus/wolfskin/src/branch/fm/tv-conductarr-wiring/docker/README.md#conductarr-production-wiring). The server's dedicated-hostname TV authority remains a separate deployment: [Conductarr TV operation authorization](https://git.justinvisser.org/justinzeus/conductarr/pulls/921). ## Seams wired | Surface | Real provider | | --- | --- | | Who's watching / navigation | Operator, profile names and library audiences; selectable logins still come from upstream Jellyfin users | | Home | Jellyfin/LKG paints independently; recommendation feed and catalogue continuation merge afterwards; no Coming soon lane on Home | | Discover / services | Picks and contributions, options/chips, New on, Trending, genres, Coming soon, service shelves, paging and dismissal GET/POST/DELETE | | Request chooser | Authorized profile names, including recipients without a saved TV login; known seasons, receipt lookup before POST, identity-lifetime uncertain-submission guard; actor is server evidence, never the selected beneficiary | | Requests | Household activity, stage precedence, actors/requesters and display artwork, foreground/poll refresh and saved recovery | | Ratings | Ratings-only route, 350 ms focused-card dwell, immediate prominent/title/Coming-in reads, canonical source precedence, positive-vote rules and episode-season scores | | Titles / people | Actual merged title routes: detail/enrichment/display, cast, related, seasons/episode votes, performer/filmography, title activity and receipts; shared request/hide workflows | | Playback boundary | Catalogue ownership never invents a playable UUID; an exact current-user Jellyfin TMDB/type lookup must resolve a held item first | ## Authentication, cache and configuration - Default: `BuildConfig.CONDUCTARR_BASE_URL = https://wolfskin-tv.justinvisser.org`. - Advanced settings beside the Wolfskin switch: **Conductarr server URL**; blank restores the default. HTTPS except loopback fake servers. - **Wolfskin preview fixtures** is debug-only and off by default. Release/default uses the real session; no upstream token means the existing signed-out states. - Only `Authorization: MediaBrowser Token="<token>"`; no second credential store, API key or cookie. Person/token/server/endpoint changes replace the client and in-memory providers. - Backup-excluded on-disk last-known-good data is partitioned by person and client identity. A 401 clears authenticated data/actions; offline keeps Saved data without an offline mutation queue. - Production call bounds: connect 3 s, read 5 s, total 8 s. Home does not await Conductarr authentication or a disk write before publishing available Jellyfin rows. - Pin OpenAPI to Conductarr `7e8f3f0a1c9a88d67b2a9518c1161db84ade531a`; expose exactly the 22 TV operations, including ratings and display, with ETag/If-None-Match/304 reuse. ## Test inventory Loopback fake HTTP only; no live Conductarr/Jellyfin or device access: - `ConductarrSessionTest`: exact credential header, person/client replacement, disk separation even for equal tokens, offline reopen, 401 and retry. - `ConductarrProjectionTest`: Home/Discover/services/dismissals/activity projections, actor-safe uncertain requests, ratings dwell supplier and conditional reads. - `ConductarrTitlePagesTest`: no invented playback, saved title facts, cross-season episode votes, 401 rating removal and source/vote boundaries. - `ConductarrSurfaceTest`, `ConductarrHomeSurfaceTest`, `ConductarrTitleSurfaceTest`: native shipped surfaces from real HTTP-shaped data, request-on-behalf receipt, saved Requests, and sign-out recovery. Fake/decoded one-pixel art keeps these transport smoke captures off external image hosts; the existing fixture capture suites retain full approved artwork. - `TitleReadFacadeTest`: reject an unrelated TMDB identity or media type before granting catalogue playback; existing held title/person/playback regressions remain. - `WolfskinArtworkTest`: actual Coil image decoding against Jellyfin, another hostname and a same-host/different-port server; an authenticated upstream parent cannot leak Authorization, and Jellyfin still receives its credential. - `ConductarrAudienceRecoveryTest`: operator 200 plus audiences 503 without saved data, empty audience success, retained Jellyfin destination mappings and successful foreground recovery without replacing the client. - Existing screen/fixture tests are retained. Root `make test` also selects the client's contract/route/cache tests and loopback socket smoke. ## Upstream files changed Within the fork's permitted upstream touch points: - `app/build.gradle.kts`: base-URL BuildConfig default and fake-server test dependency. - `app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt`: Advanced URL override and debug fixture switch. Other production changes are in fork-owned `app/src/wolfskin`, debug/default flavor resources, `wolfskin-design`, `conductarr-client` and existing fork tests/docs. ## Not done / manual gate - No deployment of the dedicated hostname or expanded server authority; the default endpoint must be deployed before real TV data can work there. - No ADB, hardware installation/playback verification, release/tag/asset publishing, signing or version change. - Unheld trailers remain absent because the server contract has no trailer field. Signed-out users receive no ratings/request actions; fixture content appears only with explicit debug opt-in. - The clearly named **Manual smoke after endpoint deployment (no build command)** section in `docker/README.md` covers person switching, real title/person traversal, Saved/401 recovery and an uncertain Shared-on-behalf request with one POST. ## Review blockers fixed - **Artwork credential isolation:** construct a fresh bounded loader, supply the authenticated call factory from the app module, and strip Authorization outside the currently selected Jellyfin origin (scheme, host and port), including redirects. Networking and concurrency interception share one Coil component registry so neither overwrites the other. - **Libraries after unavailable metadata:** failed or empty audience reads no longer install an authoritative empty list. Existing mappings or Jellyfin's name-based destinations remain usable; foreground entry retries household metadata without rebuilding the current identity or its request ledger. ## Final verification Run once each on final merged head `ec32346c71f6fcbb966653959ab31acbff3db399`; no source changes after the gates. The artwork fixture saves and restores Coil's process singleton, including when earlier screen tests initialized it. | Command | Result | | --- | --- | | `make test` | **PASS**, 2m 56s, 112 tasks; unfiltered app/design suites plus client contract/cache tests and loopback socket smoke | | `make apk-debug` | **PASS**, 1m 03s, 97 tasks; universal, arm64-v8a, armeabi-v7a and x86_64 debug APKs | The loopback client smoke observed profile data, disk reopen, ETag 304 reuse and HTTP 503 retaining the last good profile in three requests. Packaged branding checks passed for all four APKs, inspecting 1016 string/plural/array resources per APK with legal attribution preserved. Native Home, saved Requests and Shared-on-behalf receipt captures were opened and visually inspected. A measured Home diagnosis established Jellyfin-only publication at 3.577 s and real catalogue Ready at 4.657 s; the apparent stall was Robolectric Main-loop scheduling. Home retry authentication and disk writes were also moved off the first-paint path, and initial-loading Discover read-ahead no longer warms a fallback hero's ratings before the focused-card dwell. Builds and captures are preserved for firstmate under `/root/.treehouse/firstmate-7bab20/2/firstmate/data/tv-conductarr-wiring/` (`apk-build-b/`, `captures/`, and `home-layer-diagnostic.txt`). Build B contains the reviewed fixes and its checksum manifest. The older `apk/` bundle is superseded and explicitly marked **DO NOT INSTALL**.
justinzeus added 5 commits 2026-11-06 00:43:34 +01:00
Author
Owner

Code review (Claude) of head 09f1d72f30f6035910a7adf7749f52680a2c2d9f: BLOCK, two small fixes.

Checked by reading the code and tests (no builds, no live calls): token handling, person identity and cache isolation, request safety, failure modes, contract pin, regressions, smoke note.

Blocking

  1. Token leak through the image loader. The Wolfskin Coil loader is derived from the app's singleton loader, whose OkHttp client is @AuthOkHttpClient (MainActivity.kt:103-105,260, AppModule.kt:101-122). That client adds Authorization: MediaBrowser Client=..., Token="<jellyfin token>" to every request with no host check. This PR makes https://image.tmdb.org/... (ConductarrProjection.catalogueImage) and Conductarr-supplied poster_url/profile_url the default artwork source, so the token goes to those hosts. Fix: in WolfskinImages.loader, strip Authorization unless the host is the Jellyfin server; add a two-host MockWebServer test.
  2. Failed audiences read hides every Jellyfin library. ConductarrProvider.audiences() returns emptyList() on failure; WolfskinApp.loadHousehold assigns it to libraries.audiences; LibraryReadFacade.destination() then returns null for every library, so Home library rows and the Movies/Shows/Anime grids are empty (no automatic retry). Spec 3.x/7.9 says name rule until audiences are known and Jellyfin must always work. Fix: keep audiences = null when the read failed; add a test (operator 200, audiences 503, no LKG).

Verified OK: exact Authorization: MediaBrowser Token="<token>" only, no redirects followed, HTTPS-or-loopback, 32-hex token, no logging, noBackup disk store; person switch rebuilds client and provider, cache keyed by person + base URL + token (test proves A is never served to B); actor is the token's operator, requester limited to capabilities.requesters, dismissals use the operator profile; post-request-intent looks up receipts first and never resubmits an uncertain outcome (test: GET, POST, GET, one POST); 401 flips every screen to signed-out; timeouts 3/5/8 s; Jellyfin Home rows publish without awaiting Conductarr; route allowlist is exactly the 22 operations and the contract test pins operationIds; pinned openapi equals Conductarr main (PR 921 merge 9efd508f) apart from description/security text; no existing test deleted or weakened; upstream edits only app/build.gradle.kts and AppPreference.kt.

Non-blocking: request ledger ignores seasons and RETIRED receipts skip the POST; RetryPeople resets the ledger; failed person switch leaves a null identity; extra sequential Jellyfin calls and repeated Home reloads; Home person guard relies on name==requester after a 401; orphan LKG files after sign-out; smoke note should say the override host must be in the server's allowed_hosts.

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

**Code review (Claude) of head `09f1d72f30f6035910a7adf7749f52680a2c2d9f`: BLOCK, two small fixes.** Checked by reading the code and tests (no builds, no live calls): token handling, person identity and cache isolation, request safety, failure modes, contract pin, regressions, smoke note. **Blocking** 1. **Token leak through the image loader.** The Wolfskin Coil loader is derived from the app's singleton loader, whose OkHttp client is `@AuthOkHttpClient` (`MainActivity.kt:103-105,260`, `AppModule.kt:101-122`). That client adds `Authorization: MediaBrowser Client=..., Token="<jellyfin token>"` to every request with no host check. This PR makes `https://image.tmdb.org/...` (`ConductarrProjection.catalogueImage`) and Conductarr-supplied `poster_url`/`profile_url` the default artwork source, so the token goes to those hosts. Fix: in `WolfskinImages.loader`, strip `Authorization` unless the host is the Jellyfin server; add a two-host MockWebServer test. 2. **Failed audiences read hides every Jellyfin library.** `ConductarrProvider.audiences()` returns `emptyList()` on failure; `WolfskinApp.loadHousehold` assigns it to `libraries.audiences`; `LibraryReadFacade.destination()` then returns null for every library, so Home library rows and the Movies/Shows/Anime grids are empty (no automatic retry). Spec 3.x/7.9 says name rule until audiences are known and Jellyfin must always work. Fix: keep `audiences = null` when the read failed; add a test (operator 200, audiences 503, no LKG). **Verified OK**: exact `Authorization: MediaBrowser Token="<token>"` only, no redirects followed, HTTPS-or-loopback, 32-hex token, no logging, noBackup disk store; person switch rebuilds client and provider, cache keyed by person + base URL + token (test proves A is never served to B); actor is the token's operator, requester limited to `capabilities.requesters`, dismissals use the operator profile; `post-request-intent` looks up receipts first and never resubmits an uncertain outcome (test: GET, POST, GET, one POST); 401 flips every screen to signed-out; timeouts 3/5/8 s; Jellyfin Home rows publish without awaiting Conductarr; route allowlist is exactly the 22 operations and the contract test pins operationIds; pinned openapi equals Conductarr `main` (PR 921 merge `9efd508f`) apart from description/security text; no existing test deleted or weakened; upstream edits only `app/build.gradle.kts` and `AppPreference.kt`. **Non-blocking**: request ledger ignores seasons and RETIRED receipts skip the POST; `RetryPeople` resets the ledger; failed person switch leaves a null identity; extra sequential Jellyfin calls and repeated Home reloads; Home person guard relies on name==requester after a 401; orphan LKG files after sign-out; smoke note should say the override host must be in the server's `allowed_hosts`. Full report: `/root/.treehouse/firstmate-7bab20/2/firstmate/data/review-wolfskin-wiring/report.md`.
justinzeus added 2 commits 2026-11-06 01:38:05 +01:00
Isolate the artwork test singleton lifecycle
All checks were successful
review/code-claude B1 (artwork credential scoped to the exact Jellyfin scheme/host/port by a network interceptor, fresh ImageLoader, redirect-safe, 2-host test) and B2 (audiences null keeps the name rule, foreground recovery, test) verified by firstmate reading the 9-file delta; final gates green
ec32346c71
justinzeus merged commit bfe3dbbb16 into main 2026-11-06 01:41:48 +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#36
No description provided.