# Wolfskin TV container builds Run from a full Git checkout with the upstream `v*` and `p*` tags available. The host needs Docker, GNU Make, Bash, Git, `realpath` (GNU coreutils), and `flock` (util-linux), not Java or the Android SDK. ```sh git fetch origin --tags make apk # clean, signed default release: three ABI splits + universal make test # app default/debug suite plus wolfskin-design JVM/capture suite make apk-debug # default debug, no secrets; MPV stub and no extra codec decoders ``` APKs remain under `app/build/outputs/apk/default/{release,debug}/`. Debug builds are not playback-complete and must not be distributed as releases. Upstream's `.github/workflows` and dependency version catalog are unchanged; release builds use the catalog's exact Wholphin extension version (currently `0.2.4`). Do not place local extension AARs in `app/libs`: upstream prefers them over the pinned GitHub Packages dependencies. Upload release assets renamed to `Wolfskin.apk` / `Wolfskin-.apk` for the in-app updater. Gradle outputs `Wholphin-default-release--[-].apk`; strip the flavor, version, and code as well as replacing the prefix. Upstream enables JVM unit tests only for debug variants; there is no `testDefaultReleaseUnitTest` task. `make test` uses the closest default-flavor task without changing upstream's test configuration. ### Full-suite test diagnostics The Gradle daemon heap (`org.gradle.jvmargs` in `gradle.properties`) is separate from the forked test-worker heap. The default test worker uses 512 MiB; changing the daemon's `-Xmx` does not increase it. A larger worker heap did not resolve the person-picker fixture's missing Compose snapshot notifications. Gradle 9.8.2 runs previously failed test classes first on the next invocation. An immediate green `make test` rerun can therefore hide an order-sensitive failure rather than prove a fix. For comparisons, clear only generated binary test-result history (`build/test-results//binary` in each module) before each run and record actual class-start order; XML file listing order is not execution order. Keep worker heap and parallelism unchanged between samples. `PersonPickerSessionTest` must also pass late in the full app JVM, not just alone or promoted to the front by previous failures. `SavedPeoplePickerTest` publishes pending Compose snapshots and pumps Android's main queue while awaiting the saved household, just like the session-picker await above. Its five-second deadline, blocked first network response and saved identity assertions are unchanged; the virtual frame clock alone does not drain an IO continuation queued on Android's host looper. ## Saved-person launch and DNS Launch restores an unprotected saved Jellyfin login from local storage before network validation. The existing Wolfskin picker paints the saved names and identity-derived colors, then refreshes the household in the background. PIN and require-login profiles still go through the existing protected sign-in flow. `Users/Me` and `System/Info/Public` now start concurrently; a late launch response cannot replace a different selected person. Name revalidation updates only the name column and reloads the saved user, preserving newer settings and PINs. Jellyfin, Conductarr, artwork, playback and crash-report clients share one `PersistentDns` instance through the application graph. It keeps system-ordered IPv6/IPv4 answers in memory and `noBackupFilesDir/wolfskin-dns.json` (at most 32 hosts). Saved answers return immediately for up to 24 hours and refresh off main after 60 seconds; failed background lookups back off for 2 seconds without extending the last successful timestamp. Unexpected resolver exceptions are contained at the background refresh and warm-up boundaries. Negative answers are not persisted or cached. Missing, oversized or corrupt hint files fall back to system resolution. Application creation warms the saved Jellyfin host and configured Conductarr host on parallel IO jobs; it does not change Android Private DNS. A connection-establishment failure before any HTTP request headers were sent gets one fresh system lookup and one retry. Responses, read failures and uncertain mutation outcomes are not replayed. Conductarr retains its existing no-automatic-replay policy after a request starts. A DNS refresh changes the next new connection; already-working pooled connections are not forcibly closed. OkHttp's derived WebSocket clients disable event listeners; those calls bypass the HTTP retry bookkeeping while retaining the shared DNS resolver. Device acceptance (not established by JVM tests): with saved people present, cold-launch six times more than 60 seconds apart and require the picker within 0.5 seconds on every launch. Press Left/Right then one OK; the selected Home must open without a second picker. Open the avatar picker, change person with one OK, and repeat after an offline launch to confirm saved people still paint. Compare timestamps of the launch pair of SDK log lines `GET https://jelly.justinvisser.org/Users/Me` and `GET https://jelly.justinvisser.org/System/Info/Public` (ignore later foreground `Users/Me` refreshes). The spike's two slow launch gaps were 1.508 and 1.509 seconds. The requests now overlap, so either may log first; the picker must not wait for either response. The minified SDK log tag is not stable across builds. ## Optimized side-by-side comparison The `optimized` build type inherits `release`: R8 optimization/minification, resource shrinking, non-debuggable code, dependency consumer keep rules and dependency baseline profiles. It changes only signing (the Android debug key), package (`org.justinvisser.wolfskin.tv.opt`) and launcher label (`Wolfskin (Opt)`). The launcher icon/banner and release-compatible version name are unchanged: the shared upgrade/updater parser rejects extra suffixes such as `-opt`. `make apk` remains the clean, release-key-signed production build. Debug and optimized containers share the persistent debug key described below; release signing is unchanged. Build all four default-flavor outputs in one invocation through the container wrapper, without a release keystore. On a host whose Gradle cache already holds the pinned native extensions and their metadata: ```sh bash docker/gradle.sh optimized --offline \ -PWholphinExtensionsUsername=JustinZeus \ -PWholphinExtensionsPassword=unused \ :app:assembleDefaultOptimized ``` The unused password is not a credential: offline resolution never contacts GitHub Packages. A cold cache instead needs a read-packages credential supplied through the existing `ORG_GRADLE_PROJECT_WholphinExtensionsUsername` and `ORG_GRADLE_PROJECT_WholphinExtensionsPassword` hooks inside the build container, or securely seeded cached artifacts. No release signing inputs are needed. Without native extension configuration, the existing dependency selection uses the MPV stub and omits ffmpeg/AV1, just like `make apk-debug`; that is not a playback-complete comparison. Inspect native libraries before installing. Outputs are in `app/build/outputs/apk/default/optimized/`: universal, arm64-v8a, armeabi-v7a and x86_64. Keep `app/build/outputs/mapping/defaultOptimized/mapping.txt` with those APKs to deobfuscate crashes. This variant shares release library variants and the release `debugHomeSnapshot` implementation, not `src/debug` or its preview JSON/artwork. `BuildConfig.DEBUG` remains false: no preview-fixture setting, debug StrictMode or Compose diagnostic stack traces. This is a fresh app sandbox: sign in with Jellyfin Quick Connect or the normal server/user flow again; it does not inherit the debug/release app's logins or preferences. **Wolfskin screens** defaults on, and the same Advanced switch can restore upstream screens. The same Conductarr door (`https://wolfskin-tv.justinvisser.org`) and URL override apply. The selected Jellyfin user's token is borrowed for Conductarr; no app-specific TV credential or embedded secret is added. All other settings retain release behavior, including update checks; a production release APK is not an update for this separate package/debug signer. `androidx.profileinstaller` and library-provided profiles already exist. There is no recorded app baseline-profile source or Macrobenchmark module. `src/optimized/baselineProfiles/baseline-prof.txt` adds a hand-maintained seed: application and activity classes plus their `onCreate` methods, `WolfskinAppKt`, `WolfskinScreenPreference` and `ConductarrPreference`. AGP merges it with library profiles, rewrites it through R8 and packages `assets/dexopt/baseline.prof` and `baseline.profm`. It is not a measured navigation profile or evidence of a speedup. To replace the seed, add an AndroidX Baseline Profile producer/Macrobenchmark module, record cold launch, Quick Connect/sign-in, person selection, Home, Libraries, Discover, title pages and playback on a suitable device, then rebuild and compare startup/frame timings. Device-driven recording and runtime checks remain separate from an APK build. The optimized source set explicitly registers that profile directory; a bare `src/optimized/baseline-prof.txt` is not consumed by this AGP configuration. Static verification: use `aapt2 dump badging` for package/label/SDKs and absence of `application-debuggable`; `apksigner verify --print-certs` for the debug signer; `unzip -l` for both dexopt assets, native ABI contents and absence of `assets/wolfskin-*`/`assets/library/` fixtures. Review R8 warnings and effective consumer rules rather than adding blanket keeps. JVM tests do not prove minified Hilt/Room/serialization/JNI runtime safety; exercise sign-in, real Conductarr reads, artwork, database reopen and each player on the comparison box. `help --task :app:testDefaultOptimizedUnitTest` confirms that this non-debug variant has no JVM unit-test task, just like release. The existing debug JVM `VersionCompareTests` receives the actual optimized version name from the variant API and checks it with the same `Version.fromString` used by upgrade/update handling. Build logic is otherwise checked through actual APK assembly and static inspection. ## Upstream v1.1.0 integration The release merge brings Kotlin `2.4.21`, Gradle `9.8.2`, AGP `9.5.0`, and ExoplayerHdrUtils `0.4.1`. The pinned SDK/JDK image and serialized Gradle wrapper remain compatible without changes; native Wholphin extensions remain `0.2.4`. There are no committed dependency lockfiles or Gradle verification metadata. Forgejo Actions stays disabled; upstream GitHub workflows remain dormant. The app database upgrades from Room schema 35 through 40 to 41 on first launch after installation. The upstream migrations add series track choices and a home settings source preference. Downgrading with the upgraded database is not supported: a TV installation is a one-way step and requires explicit approval. The optional companion plugin can supply Discover credentials before locally saved Seerr profiles; do not configure an administrator API key in place of the household's per-profile Jellyfin login, which Conductarr uses for request routing. Build only after committing a release merge: version names come from Git HEAD, not the working tree. An uncommitted merge still builds with the old version name. Neither an upstream merge nor its validation build authorizes a release or TV installation. ## Wolfskin shell Wolfskin UI lives in `app/src/wolfskin/java`, beside upstream source. The signed-in entry in `MainContent.kt` calls `WolfskinApp`; Wolfskin-only routes use the single `Destination.Wolfskin` subtype in the existing navigation stack. Other destinations continue through upstream `DestinationContent`. Full-screen player/settings pages cover the Wolfskin nav. The independent `:wolfskin-design` module owns the shell components, fixed person colours, focus policy, per-person root-view memory, palette and motion. Its `tokens.json` is the copied approved TV-spec source, retaining the locked prototype SHA and provenance. `shell-tokens.json` adds shared shell/grid/sheet measurements; `title-tokens.json`, `home-tokens.json` and `discover-tokens.json` add their screen measurements from the same locked prototype. Generate Kotlin, CSS and the checksum manifest together: ```sh python3 wolfskin-design/generate_tokens.py python3 wolfskin-design/generate_tokens.py --check ``` `preBuild` checks the manifest against the source JSON, generator and generated outputs. Editing any of them without regenerating fails the build. The shell uses the generated palette rather than the retired `WOLFSKIN` theme-enum value; the old numeric value is reserved. Compose output maps source weights 600/650 to the platform Bold (700) face, which resolves consistently in native captures; the approved JSON weights stay unchanged. Nav, picker names, labels and pill weights use that same mapping. Nav label colour fades for 200 ms ease-out; the blob retains its critically damped spring. Shell view accessors are read-only, including the first read from Discover's `snapshotFlow`. Construction and explicit person/navigation events retain root and destination memory; reading an unknown destination returns an unsaved default. Per-person focus, cards and scrollers still survive route changes, while reselecting the current root resets it. JVM regressions exercise both read-only snapshots and the signed-in asynchronous Discover host without a screen prewarming its memory. Settings pills have a separate 1 dp stroke (2 px on the stage), not a focus ring. The stroke is inset within the fixed 20 dp / 40 px height; horizontal border-box padding includes it. The 6 px dash / 4 px gap follows the browser's 3:2 stroke-width ratios. Resting borders are white at 22% opacity; focused borders use ink at 40%. The small gear centred beneath the complete people row opens Settings with Requests focused. Down from any avatar reaches it; Up returns to the originating avatar, including after a Settings visit. Left/Right avatar traversal is unchanged. The navigation requirement comes from Justin's words: “the settings icon moved, but pressing down only works from the last profile icon. I want it to always move there when pressing down in that screen”. The approved gear placement stays centred beneath the complete people row. Settings groups hand off to existing upstream pages; Requests opens the household Coming-in sheet described below. **Advanced settings → Wolfskin screens** is the persistent rollout switch: turning it off renders upstream Home and library destinations with the upstream Backdrop and top scrim, while retaining Wolfskin's nav, picker and Settings list. Native Up traversal stays in content until the first focusable row, then enters the nav. Turning the switch on renders Wolfskin Home and Libraries. Discover and Requests are integrated Wolfskin surfaces; title hand-offs remain labelled. Root content Back focuses the nav; another Back exits the Android task via `finishAndRemoveTask`, including Discover, Movies, Shows and Anime without a Home detour. Cold picker Back exits; a reopened picker dismisses to nav first. Pushed pages pop normally. Child dialogs, title menus, request sheets and unsaved-setting prompts keep Back priority. Singleton full-screen pages also exit; they cannot fall through to the older setup/user UI. Source — Justin: “pressing back enough times should close wolfskin, currently there is no way to go back to projectivity apart from weird methods via the old ui”. `rememberWolfskinBackHandler` owns Android Back at the app root, including a single-entry `NavDisplay`; full-screen hand-offs use the same `NavigationManager` exit event rather than exposing the parent setup stack. `WolfskinRootExitTest` mounts actual Wolfskin Home/Discover/library surfaces and covers picker, hardware Back and Coming-in sheet precedence. `WolfskinBackStackTest` mounts the real Updates and Home Settings pages to cover parent pops and unsaved edits. Saved Jellyfin identities use the existing upstream session. Conductarr supplies household labels, requester capabilities and library audiences after sign-in. The locked shell projection in `app/src/debug/assets/wolfskin-shell.json` is used only when the Advanced debug fixture switch is enabled; release excludes it. ### Household setup and sign-in recovery With **Wolfskin screens** enabled, `MainContent` intercepts both upstream setup destinations before their stock server/user selectors can render. Cold start, restart with automatic sign-in disabled, the old drawer's user action, legacy sign-out-equivalent state, setup fallbacks and intent failures all use Wolfskin's own picker. Entering it does not call `SwitchUserViewModel.init` or clear the remembered server/user preference. Setup reuses the supplied/current/recent household server, recovering the most recently used Room server if the legacy picker erased the preference. Saved people render without a network request. Only an install with no saved server gets the one-time Jellyfin URL field; it uses existing Jellyfin discovery and Room persistence, not a new compiled endpoint. Conductarr remains on its existing configured URL and enriches the signed-in shell as before. Selecting a protected profile still requires its PIN or server credentials. Expired credentials during saved-session revalidation or foreground refresh return to the picker without allowing a late failure to replace another person's session. A failed selection offers Wolfskin **Try again** and **Sign in** actions; hidden/unsaved profiles can use username/password sign-in through the setup picker's Settings gear. An outage never replaces the saved household endpoint. Turning **Wolfskin screens** off retains the unchanged upstream `SwitchServerContent` and `SwitchUserContent` setup screens for rollback. Debug fixtures still seed the existing fixture picker after authentication. Person selection immediately clears the old Conductarr identity and Jellyfin read gate before saved-login I/O. Detached clients are disposed exactly once on the application's IO scope, after the replacement (or signed-out) identity is published; a slow or failing socket close cannot block the picker or abort a switch. Cleanup outlives the replaced screen. Retry and session replacement use the same disposal owner. Superseded sign-ins are cancelled and serialized so a late reply cannot navigate back to an earlier person. The fixture-mode marker belongs to the singleton `WolfskinSession`, alongside its server and shell state, not to the user-scoped ViewModel. Switching people replaces the upstream app entry and ViewModel but retains the same-server, same-mode shell: the picker stays closed on Home after the first OK. A cold start still opens it, and the nav avatar explicitly reopens it. `PersonPickerSessionTest` drives the real shell and user-entry replacement for launch/avatar Left and Right, the initial person, gear and back, and fast Left-Left-OK; each selection emits one person action. Preference and people-fixture reads, Conductarr client construction (including Home/title retries), and private-cache directory initialization run on IO. `PersonSwitchThreadingTest` replays the real picker across Justin → Mindel → Shared → Justin while all closes are held pending, verifies retired clients cannot start reads, contains a failing close, and checks a burst of choices against a late sign-in reply. The thread-recording close fake rejects the main Looper; actual TLS/StrictMode verification remains a real-device check. Discover adds the picks hero, type chips, catalogue shelves, service pages, continuous See all grids, Hidden picks, request chooser and receipts. Its `discover-title/...` destination is an explicit title-family integration stub exposing only Discover's request and pick actions. Unheld titles have no trailer control until the pinned Conductarr title contract supplies a trailer. Named “Because you watched” shelves require watched contribution evidence. Seeds are grouped by media type and TMDB identity, never by caption; repeated contributions retain both watched/added flags. The current feed has no per-seed watch timestamp, so eligible shelves keep wire order. Added-only contributions remain available in the general For-you collections, but make no named shelf. Titleless contributions do not make a shelf. Recommendations remain acquisition suggestions, not a filter to the viewer's current library. The debug-only fixture and local art live in `app/src/debug/assets/wolfskin-discover/`. The Advanced preview switch selects a network-free fixture host; real providers are the default in debug and release. The mutation adapter accepts the selected person's `ConductarrClient` and uses only typed routes. Request submission first reads title receipts. Unknown outcomes are checked, never blindly re-sent, including after closing a sheet or choosing the same uncertain recipient again. Whole-series requests require season metadata. Discover hero and Restore text use explicit per-line CSS allocations so native paragraph overflow cannot enlarge the bottom-anchored stack. Restore subtitles retain the sheet's negative top margin and bottom gap. Recovery panels may overflow their fixed-height scroll row so sign-in copy never shrinks the retry button. Native regressions cover those three layout boundaries. Discover's native capture suite covers its 28 named acceptance states and emits text-layout metrics beside the PNGs. Data-driven Android key replays cover opening Discover in 4 presses, requesting for yourself in 7, requesting for Mindel in 9 and Not interested in 8. Additional regressions exercise held-OK safety, uncertain receipts, per-recipient outcomes, collection recovery and Undo, unknown seasons, Dutch decimal formatting and short-row/tail clamping. Discover reuses the Libraries grid, poster, ratings plate, See all and recovery cards, bounded image loader, caption separator token and the shell's single hold guard. Both frames share the 180 ms scroll tween. The TV keeps platform Roboto with the approved type sizes and weights. Browser system-font wrapping is not a per-title contract: no bundled font, forced title breaks or width compensation is applied. Discover's own stacked text preserves untrimmed CSS line boxes without changing merged shell/library typography. Hero, sheet and shared focused-content shadows use tokenized drop shadows, not Material elevation; their alpha animates in the draw phase. Native text-layout measurements accompany each acceptance capture; remaining font-driven wrapping and text-block positions must be listed explicitly in its comparison report. The asynchronous debug fixture seam hands loading focus back to the first loaded action when Discover is still the selected destination. A JVM regression releases the actual host's suspended fixture load and presses Android OK: it must open the hero rather than activate the nav. Fixture completion does not steal a different destination or nav selection. Native Roborazzi captures use Robolectric SDK 34 and `w960dp-h540dp-xhdpi` (1920×1080 at density 2.0). `make test` writes capture PNGs and the sampled Compose nav-spring curve to `build/wolfskin-captures/`. Home, Library and Discover captures use `home/`, `library/` and `discover/` subdirectories so shared spec state numbers cannot overwrite another renderer's image. Key replays exercise the Android event adapter, including the 6/8/10/11-press shell paths. The profile hand-off capture targets the real `HomeSettingsPage`; Playback is a separate press-count path. Robolectric SDK 28/30/34 runtimes resolve through Gradle's existing persistent dependency cache, then copy to `wolfskin-design/build/robolectric-sdk/`. Test workers use that local directory in offline mode instead of downloading Android JARs into the container's ephemeral home. A cache miss still needs Maven Central; rate-limited hosts can seed the existing cache from SHA512-verified local Maven artifacts without changing the wrapper or adding volumes. Default-flavour launcher artwork and English, Hungarian and Polish string overrides live in `app/src/default/res/`. Legacy Wolfskin strings in 25 translated `main` resource folders have not yet been moved; non-default `appstore`/`firetv` flavours therefore combine upstream icons with some legacy Wolfskin text. Those flavours and a full locale migration are not certified by this slice. Debug assembly runs `checkWolfskinBranding` against every packaged APK through AGP's public artifact provider and AAPT2. It checks all string, plural and array configurations, rejecting user-visible upstream branding while preserving the upstream legal attribution and project URL. The check does not depend on incremental merge files. ## Wolfskin Home Home is one lazy frame: a non-rotating resume → Next up → newest → pick hero, one merged **Continue watching** lane, newest and per-destination Jellyfin shelves, and the Discover continuation. The watch lane uses landscape tiles, prefers a resumable episode over another next-up episode from the same series, and sorts the chosen items by latest actual watch activity. The hero stays off the lane. The final action opens the real merged Discover screen at its top through the shell's root re-selection transition. Home's See all cards open `WolfskinSeeAllLists`, carrying the complete underlying list rather than the visible shelf window. Title content remains the later-slice target. All Home components and immutable screen models live in `:wolfskin-design`. The shell's remembered view owns vertical position, each row's selected card and actual horizontal `LazyListState`, and the bounded six-hour ratings session cache. Every `NavEntry` installs `ScreenEntry` with its own `rememberedView(route)` and active state before navigation effects run. Home, Library, Discover, title and person surfaces consume that entry's viewport; an outgoing page cannot borrow Home's scroller during Back. Only the active entry registers logical key handling or acquires Android focus. Re-entry restores the remembered horizontal track with `scrollToItem`, preserving exact indices and pixel offsets. Cards use the shell's logical focus without requests against uncomposed lazy items. Back preserves the viewport; returning to Home does not re-run focus-follow over a manually scrolled saved view. The next arrow re-enters visible content when scrolling moved focus off-screen. Pointing changes focus without activation, cards activate on key-up, and held OK events cannot activate twice. Between Home shelves, an unvisited row starts at its leftmost card. A visited row restores its column and first-visible position, including after leaving and returning to the Home tab; Home memory is not reset on tab leave. Library keeps standard grid navigation. Horizontal movement keeps the track still while focus remains in its visible middle. Title/person Android Back handlers use the same active-entry ownership flag, so outgoing pages cannot consume a second Back. The initial skeleton parks focus on the nav. When the first content arrives, an untouched fresh entry focuses hero button zero; intervening nav input prevents that automatic transfer. Home, Libraries and Discover use the same `ShellContentFocus` release/repeat guard and Back ladder. Landscape title scrims include the reserved state-line area inside their gradient rather than leaving an exposed image strip below it. Their top padding is the shared Home token (18 dp); opacity remains the approved 92%. The legacy opt-out Home and its presets also use one combined watch row. Saved local/server/web/plugin split rows migrate at their first position, preserving that row's display options and all unrelated rows. Old serialized row names stay decodable; settings no longer offer separate lanes or a Split action. `HomeReadFacade` confines Home SDK reads to the selected user. Resume and Next up publish independently of slow or failed library shelves; saved metadata still paints immediately, but a saved hero offers Details until watch data revalidates. Series chronology reads the latest actually watched episode, not the next-up episode's unwatched user data or premiere date. Enrichment is bounded to three concurrent series reads; absent history leaves the date unknown rather than inventing a timestamp. The disk projection preserves series IDs and watch dates, and still accepts older cache entries without either field. Home and title Play actions resolve the current Jellyfin item and its current checkpoint at the click, preserving millisecond positions. Series actions use live NextUp with resumable episodes enabled; an explicitly opened episode stays that episode, and Restart deliberately starts that displayed episode at zero. Played flags do not discard a positive checkpoint for an intentional rewatch. No season, recommendation stub or different person's session becomes playable. An unresolved person switch guards Home and both owned/upstream library content before mounting any source read with the prior person's Jellyfin session. Home and title watch reads refresh on entry and foreground. A successful PlaybackStopped acknowledgement publishes a revision for the captured server/user, so a return read that beat the stop write is refreshed again after that write. The acknowledgement also removes that user's persisted library grid snapshots instead of waiting for their ten-minute TTL; an in-memory revision bypasses any older snapshot that raced invalidation. Stop-report failures remain logged and do not publish a successful acknowledgement. External players refresh after a supported stop result; an external app that returns no position cannot supply a missing checkpoint. Another client's changes after the final read are outside that click's snapshot. Debug variants additionally package the locked fixture and local artwork under `wolfskin-design/src/debug/assets/wolfskin-home/`; release variants exclude them. The explicit fixture provider supplies approved household data and Home states without network reads in tests. Real signed-out Home hides every rating and shows "Discover needs sign-in"; signed-in Home reads the Conductarr continuation and ratings-only endpoint. No Seerr fallback is used. Home, Libraries and Discover reuse `TitleCard`, `Caption` and `LibrarySeeAllCard`; those screens and Requests share one `WolfskinImages` loader. Exact-slot requests use RGB565 for posters, disable crossfades, share a 64 MiB memory/128 MiB disk cache, limit image work to four reads and prefetch the next shelf. Rating plates start at 14 sp, shrink to 12 sp and drop trailing entries rather than wrap. Their uncached read gate is 350 ms; cached focus returns without that delay. Focus rings and native lift draw outside fixed slots. The Saved nav chip uses its own geometry; the Discover section uses a warning pill. Caption separators multiply the existing muted colour alpha by the shared 40% token. Home-only untrimmed styles and `exactLineBox` honor the declared CSS line boxes despite Android's intrinsic paragraph minimums; shell and library typography retain their existing defaults. The shared See all component uses its canonical locked 8px gap, not the generic 10px tail spacing. Focus lifts use the merged draw-only `FocusLift` implementation; the obsolete Home shadow helper and duplicate gap/lift measurements are removed. Vertical moves clamp to short target rows, including a final See all card. `make test` includes `HomeCaptureTest` (23 native acceptance states) and the data-driven paths in `wolfskin-home-presses.json`: play next 2, resume 3, Discover 4, deep-row Back/Down 9, Trending 13 and Trending's See all card 33 presses. Home captures are written to `build/wolfskin-captures/home/`. Compare those images side by side with the frozen spec captures; Compose Roboto/JPEG rasterisation, the approved button-scale consolidation and removal of prototype sample pills are recorded differences, not a claim of browser pixel equality. ## Requests under Settings The Requests row and Coming-in sheet share one immutable household history snapshot. Settings counts returned requests, including Ready, rather than calling every non-Ready row “open”. An unavailable empty snapshot has no count. Deleted rows read Removed; absent holdings read Not in any library; unreleased titles read Not out yet; holdings outside the recipient's libraries read In another library. Waiting for an import is not evidence of an active download. Downloading and Searching require explicit acquisition signals. Visibility-proof faults read Check needed; playback faults read File health needs attention, even when a library holding is present. Right expands the actual stage evidence and the relevant summary, including episode/source names when supplied by that summary. The activity feed currently supplies only a title-level summary for Avatar. Attention-needed rows sort first, then active acquisition/checks, waiting and settled history. The bounded window shows five rows, or four with readiness expanded. Left collapses; Up/Down collapse while moving. Opening a title preserves the request for Back, and closing the sheet restores its opener. Selecting a root or changing person clears the title-return link. Shared's authenticated actor always reads “by Shared”, never “by you”; absent actors have no “by” clause. The debug fixture switch loads `app/src/debug/assets/wolfskin-requests.json` and its eight local posters. Shared and focused-rating records also feed the native acceptance states; the identical projection is in design-test resources. No “sample” or “example stages” pill is drawn. The default provider instead maps Conductarr household activity, authenticated actors and display artwork into the same sheet. Release excludes all fixture assets. There is no Seerr fallback. Foreground refreshes once; an open sheet or unsettled requests refresh every 30 seconds while started. The immutable debug projection is loaded once, not decoded again on every poll. Thumbnails reuse `WolfskinImages` with a 96×144-pixel request, RGB565, no crossfade, the shared 64 MB memory/128 MB disk cache and four-read limit. Actor separator dots use the surrounding text colour; the 40% rule applies only to poster captions. Uncached focused ratings wait 350 ms; moving away cancels the read. Read ratings show immediately on return; sign-out clears visibility. This follows the frozen page and spec 6.12, resolving the contradictory no-dwell sentence in spec 7.8. Deterministic coroutine-clock tests cover cancellation, reentry, cached return, sign-out and refreshed locale scores; a thin Compose test verifies actual Dutch rendering and sign-out suppression. The approved nine-key Requests path is JSON data replayed through the real shell. The Requests-only harness starts at the Home-to-nav boundary; Home's actual hero and asynchronous entry have separate key replays. Sheet dimensions, padding, glyphs, scales, colours and durations are generated tokens. Scoped sheet line boxes retain CSS heights and baselines independently of native leading rounding; shared shell/library typography is unchanged. Platform Roboto remains required: the host browser used DejaVu Sans, so its width, wrapping, ellipsis and text-block positions are not a wrapping-parity target. For example, the 44-pixel “Coming in” ink is 239 pixels wide in the reference and 194 pixels in native captures. No per-title compensation or font bundle is used. Native comparisons must report those structural differences explicitly. Readiness glyphs sit inline on the text baseline with no flex gap, matching the frozen block-span cascade. Items wrap intrinsically: Roboto fits the expanded readiness into two lines where the reference uses three; no grouping is forced. The six rating marks retain the exact locked SVG source in `wolfskin-design/request-marks/`; native-size PNGs and their hashes are generated from the JSON dimensions with the locked contain fit (`.rr .rb` outranks `.crow img`). Inside Coming-in rows they retain the prototype's dark `surface3` chips with 12-pixel rounded corners. Loaded poster thumbnails have no placeholder fill beneath their clipped edge. ```sh uv run --with cairosvg==2.9.1 python wolfskin-design/generate_request_marks.py ``` `RequestsCaptureTest` writes `st-081`…`st-086`, `st-088` and `st-111` at 1920×1080, density 2.0. Title selection opens the shared native title destination by media type and TMDB identity. Back restores the exact Coming-in row and expansion; the temporary `request-title/...` placeholder is removed. TV sign-in, release and device deployment remain separate. `ActivityTruthProjectionTest` feeds recorded activity JSON through the TV adapter for Steins;Gate, Yu Yu Hakusho, Love is Blind, Spider-Man: Homecoming, deleted One Hour Photo, unreleased The Body, Avatar health and Partition waiting. `ConductarrTruthCaptureTest` renders those provider snapshots and Mindel's recorded recommendation feed on the native Compose surfaces at 1920×1080, writing `build/wolfskin-captures/truth/`. Artwork is deliberately omitted to keep these captures offline; missing-art rendering, tokens, focus, sheet geometry and shelf spacing are the shipped components. ## Libraries and See all Movies, Shows and Anime use the shell's retained view and a continuous six-column lazy grid, reduced to five columns while the letter rail is visible so scaled posters cannot overlap it. Up from the first row reaches Sort/Filter, then the nav; Back preserves the card and scroll. Sort cycles A–Z, Recently added and Rating. The letter rail exists only at 200 titles with A–Z selected. Card OK activates on key-up through the same shell hold guard, without a long-press menu. Down clamps a short final title row before entering the separate Try again / Show more row; an empty slot cannot activate recovery. An unavailable library hides its unknown item count. Library titles and compact subtitles remain visible at rest, with one-line, card-width ellipsis rather than a 640 px focused caption. The letter rail uses bold 24 px letters, with the focused letter bold 28 px. Library and Settings header initials use the muted colour over the person's fill; navigation and picker avatars retain their dark initials. The debug-only `wolfskin-library.json` and `library/assets/` contain the locked prototype's titles and local poster art. Release libraries read through `LibraryReadFacade`, with 200-item Jellyfin chunks and ten-minute per-server/person snapshots using the existing atomic disk cache. Saved data is painted before a quiet refresh; a failed refresh keeps it. Production ratings remain hidden before Conductarr sign-in and use the shared ratings-only provider afterwards. Home/Discover shelves open a full list through `WolfskinSeeAllLists.open(person, key, title, fullList, navigation, loadNextPage)`. The optional suspend page provider returns `LibraryPage(items, hasMore)` using the generated Conductarr client. The grid reads within ten cards of its end, deduplicates title identities, and pauses on Try again or Show more rather than looping. Title activation opens `title/{id}` through `Destination.Wolfskin` for prototype identities, or the existing typed media destination for real Jellyfin identities. Movies, series, episodes and people use the native title family; collections and music keep their upstream destinations. `LibraryCaptureTest` renders the sixteen approved library/See all states offline. `LibraryKeysTest` reads the approved JSON path and reaches Spirited Away in exactly 20 Android key presses. Paging and snapshot tests cover duplicate-only reads, failure/retry, stale copies, and person/server isolation. Library captures are written to `build/wolfskin-captures/library/`; Discover's independent captures use `discover/`. ### Roomy card spacing The generated design tokens use 40 px card gaps (44 px for cast), 84 px shelf heading bands, 68 px shelf bottom gaps and 524 px grid rows at 1920×1080. Posters remain 256×384 px; tiles are 400×225 px and wide cards 544×306 px, preserving six/four/three shelf slots. Library and See all toolbars occupy 136 px: chips stay at y 252–320 and the first poster row starts at y 372 (52 px clear). Discover and season-chip rows retain their 104 px height. Cards and cast circles draw a single 4 px accent stroke centred on the art edge, above the content, with no background gap. Card lift parameters are unchanged; cast portraits and episode tiles now receive that lift too. The stroke and art scale together at 1.08. Title shelves reserve vertical drawing room for the scaled ring. Before scaling the stroke, ring-to-neighbour clearances round to 28/22/16 px for poster/tile/wide cards; including stroke scale they are 27.6/21.8/16.1 px. Cards use the approved option-B bottom state line instead of a separate inset resume bar: watched is 3 px accent; resume is 6 px proportional accent over `color.progressTrack`; unwatched and Discover have no line. Opaque background backs the entire strip, including its bottom 2 px and the 2 px separator above the fill. Artwork is clipped out of the backing band before painting it so rounded-corner antialiasing cannot leak poster pixels into the lowest row. The separately drawn focus ring stays above the line. Film subtitles are year + duration + a 24 px watched check, episode subtitles are their own duration + check, and series subtitles are year + check with no single runtime. Resuming cards have no check. The poster caption band remains 96 px; Home poster captions are focus-only. Home landscapes retain titles on the art and reserve 64 px for focused metadata below it, making wide/tile shelf pitches 522/441 px; poster shelves remain 632 px and grids remain 524 px. Loading rows reserve the same blank caption bands and share one pulse layer; only art-sized blocks are painted, with no title/subtitle placeholder pills. `CardBottomPixelTest` samples full native bottom rows on four real posters at rest and individually focused, including white Annie Hall and dark Locke. `CardGeometryTest` measures 210/327 px caption fitting, card-width titles, 24 px checks, row pitches, pinned rows and ring clearances to 0.1 px, and checks that every reserved loading-caption pixel remains background-coloured. `CardCaptionMotionTest` checks visible native glyph pixels when a still-focused caption changes title directly or while moving across a Home shelf. The [native option-B proof gallery](https://pages.justinvisser.org/wolfskin/tv-card-meta-build/) contains the same illustrative watch-state data as the approved frames and records renderer/fixture artifacts; unapproved design changes block merge. `LibrarySpacingRegressionTest` retains the deep focused index across the 199↔200-title letter-rail boundary. `LibraryCaptureTest.spacingShowsFirstFocused`, `HomeCaptureTest.spacingNextUpFirstFocused` and `TitleCaptureTest.spacingCastFirstFocused` render the amended reference states at native resolution. Their files are `library/spacing-shows.png`, `home/spacing-next-up.png` and `st-114.png` under `build/wolfskin-captures/`. These captures are recordings, not automated reference-pixel assertions; pair measurements are a separate check. Platform Roboto metrics and fixture ordering are not browser pixel parity. ### Shared focus rings The same `focusRing` stroke now covers pill/icon buttons (hero, title, state, recovery and sheet actions), the nav avatar, picker people and gear. It is always accent, 4 px / 2 dp wide, centred on the surface edge with no background gap. The former outside-gap renderer is removed. The stroke draws after opaque content, so neither half can be covered by the background or portrait. Control tokens are `Focus.ringControlGap = 0` and `ringControlRing = 2.dp`; avatar and picker tokens use the same geometry. Existing scales remain 1.05 for buttons and 1.08 for picker people/gear; the nav avatar remains unscaled. Card and button lifts remain; avatars, people and gear gain no shadow. Existing clearance accommodates the smaller outside reach without layout movement. Filled-only chips, nav pills, Settings/sheet/Coming-in rows and the A–Z rail are unchanged. Selected-only chip/icon/radio borders and decorative borders are not focus rings. These distinctions are explicit in the inventory and reference states rather than silently adding rings to filled controls. `FocusRingTest.ringStraddlesOpaqueSurfaceAndDisappearsWithoutFocus` checks the two outside and two inside pixels against opaque content for pill, circle and card radii, including clearing the stroke on blur. The focused hero Details, nav avatar, picker, Discover chip, Library toolbar chip and title Request captures exercise the actual screens at 1920×1080. Browser and native glyph metrics differ; their ring offsets are measured relative to each surface edge. ### TV feedback corrections (spec revision 5) The [revision-5 amendment](../data/tv-spec/spec.md) and [native frame prototype delta](../data/tv-v2/feedback-2026-11-06/index.html) retain the approved frames except for the requested feedback changes: - The picker gear retains its approved whole-row centring and picker-person ring. Down from every avatar reaches it; Up returns to the originating avatar. - Shelf tracks share second-last-slot lookahead, count clamping and settled offsets. Home records completed frame positioning rather than an animation that may still be cancelled; first mount restores the intended shelf item. Discover uses all six approved poster slots. Person keeps the final column at full poster width. - Episode images choose episode Primary, then Thumb, then the already-read series Backdrop. Title episode cards now use 448×280 px, preserving the taller approved crop. Focused summaries ellipsise to one line, the show-name overlay is absent, and `S1 E3 · duration` plus the watched check share one line. Ready title headers measure their copy/actions and leave 40 px before the following section. Missing readiness lines collapse completely on every title kind; late lines expand/collapse over the 200 ms late-line token with ease-out, or snap with reduced motion. Already-pinned rows compensate in the same remeasure rather than bouncing through a second focus-follow animation. Poster-only movies floor that stack so Cast circles clear the poster by at least 20 px, including held movie fallbacks and short overviews. - Every Discover heading uses a 104 px band, including a 60 px title-to-card gap. Home heading geometry is unchanged. - Hero/backdrop decoding is bounded to 1920×1080; upright hero sheets request 420×630. The shared request builder replaces conflicting Jellyfin size/encoding parameters: cards use WebP quality 80, while hero backdrops/sheets retain JPEG quality 96. Poster/tile/wide/episode requests are 256×384, 400×225, 544×306 and 448×280; Jellyfin fill preserves aspect ratio before the UI crops. Tags and authentication query parameters remain intact. TMDB uses w342 for posters and portraits, w780 for landscape cards and w1280 for hero backdrops, never original. Lossy WebP decoding is guaranteed below minSdk 23; API 28+ additionally checks decoder support and requests JPEG if WebP is unavailable. The 64 MiB memory cache, 128 MiB disk cache and four image-read limit remain unchanged. `HomeViewportTest` covers short/long poster, tile and wide rows, both row directions, first focus and native Back from the actual title surface. Native proof records real Bear season-three metadata through the title read facade; the proof-only harness is not part of the shipped tests or APK. Episode lists request only ProviderIds and Overview. Next up reads only identity, then hydrates the selected playback target through the existing single-item read, including when Next up is empty. Media sources/streams and cast remain on detail reads for header track/version controls. Performer credits omit media sources, streams and ParentId but retain People because their cards show character roles. Movies/Shows grids retain ProviderIds and DateCreated; server-side SortName ordering does not require returning SortName on every item. ## Title pages and people `TitleSurface` owns the film/series/episode header, season chips, episode tiles, cast and related shelves. `PersonSurface` owns the real biography and continuous six-column filmography. Both reuse the shell/nav/back ladder, `LibraryPoster`, the adaptive `LibraryRatingPlate`, shared captions and the single bounded `WolfskinImages` loader. Image requests use exact slot sizes, RGB565 for opaque posters, no crossfade, four concurrent reads and next-row prefetch. The fixed title backdrop remains visible behind deep rows; only off-screen row images leave composition. The options row preserves complete labels beyond the copy-column measure. Version choices close after selection; Audio and subtitles stays open, including an explicit Off choice. Real source IDs use the Jellyfin SDK UUID parser, and stream indexes remain the server's indexes rather than positions in a list. Selections persist through the same playback repository as the upstream player. Marking a whole series asks first; an explicit episode remains the exact play and watched target even while another season is selected. Nested person/title visits retain their entry's focus and selection state. Choosing another episode from an episode page replaces only the top navigation entry, preserving its series/Home parent. Choosing the first episode from a series, related titles, cast and performer credits still push normal drill-ins. Down into season chips initially focuses the selected playable season, including offscreen later seasons; the row scrolls that chip into view and remembers subsequent deliberate browsing. Saved title watch data cannot enable Play/Resume or Restart before source revalidation. Hardware and remote Back share the sheet/picker/request-overlay precedence, then pop a pushed title/person from content **or** nav. Initial loading has no content target: Down stays in nav, while Back can still leave the title. A missing rating set emits no layout node, so it cannot reserve an invisible extra facts gap. The title Menu replaces the old More/media-info icon action and contains Media info plus Add for someone when requests are permitted. It is shared by held movie, series and episode pages; non-held pages expose Add for someone in the same menu while Request/status and Not interested/Show again stay in the row. Version, tracks, watched and trailer controls keep their existing positions. Back from Media info returns to the menu; Back from the menu restores its opening button. The compact menu fades in for 160 ms and out for 100 ms using the motion ease-out token, with reduced-motion fades capped at 80 ms. Only the bounded menu panel owns an alpha layer; the scrim is static. Opening another sheet immediately removes the menu panel and scrim, preventing double darkening. An empty menu closes if its last permitted action disappears. Controls use the shared 4 px straddle focus ring. Neither series nor episode details have a redundant Seasons shortcut; Down reaches the season chips directly. Ratings appear only in the header facts line, not again beside progress. Explicit episode pages use the opened episode's own set, including its `Ep` score; movie and series pages keep their own sets. Episode shelf skeletons use the same width, height and pitch as ready cards. Person filmography and related-title shelves remain posters; Home continuation tiles are a separate component and retain their series-name context. `TitleReadFacade` confines SDK reads, server/user-scoped last-good disk copies, person credits and playback mutations to one boundary. `ConductarrTitlePages` adds real detail, cast, related titles, performer/filmography, readiness/receipts and source ratings. A catalogue title gains held/play controls only after an exact TMDB lookup returns this Jellyfin user's actual item identity. Episode votes enrich those real episode identities; no catalogue stub becomes playable. Debug-only `wolfskin-titles.json` and local artwork remain behind the explicit Advanced fixture switch; release never packages or selects them. Held trailers use Jellyfin remote/local trailer data. YouTube URLs target an installed YouTube handler such as SmartTube, never a browser. No handler leaves the title visible with a calm message. Titles nobody holds have no Trailer button until Conductarr's trailer field exists. Title requests use the same actor-safe chooser and identity-lifetime uncertain-submission ledger as Discover. Signed-out and offline pages cannot submit; no Seerr fallback is used. `TitleCaptureTest` writes the 34 approved title/person/options frames plus native text weights, colours, line boxes and bounds at 1920×1080, density 2.0. The approved typeface ruling keeps platform Roboto, not the browser's DejaVu Sans: no per-title compensation and no wrapping-parity claim. Explicit CSS line boxes constrain title headers, sheets, cast and poster captions without changing shared type tokens. Fractional stack placement rounds only the accumulated position, preventing a one-pixel-per-row drift. Focused controls share the canonical lift shadow. `TitleKeysTest` replays real Android keys, including modal Back/hold, nearest-row entry, remembered columns and long-sheet visibility. `TitleRound2Test` covers menu Back/focus, preserved media info and household requests, movie stacking, the Mussolini resume example, request revocation, episode skeleton geometry, and native menu fade frames sampled in 16 ms steps. `TitleAsyncFocusTest` delays title and person replies through the real destination and view-model, then exercises content focus and hardware Back. Subtitles Off maps to the player's explicit disabled sentinel, not its unspecified default. `TitleLaunchKeysTest` starts at Who's watching and replays the full 4/8/15/11 sequences through the real Home hero/shelves and title/person surfaces. `title-press-paths.json` supplies every event; no prefix is skipped or padded. The shorter 1/5/6/2 title-entry replays remain as focused regressions. SDK cache, exact-episode, track-index, identity, locale-layout and trailer-handler tests run offline with in-process service replies and local art. ## Offline states and recovery Owned screens share `RecoveryToast`: the warning glyph, 760 px maximum width, 22/28 px padding, 14 px icon gap, 22 px corners and CSS-sized body line boxes. Network failures never expose transport exception text. Playback failure reads “Can’t reach Jellyfin to play. Check the network and try again.” and leaves the originating card focused; no simulated player hand-off panel is shipped. Retry failure reads “Still can’t reach it. The last saved view stays on screen.” Library headers and the nav publish the same saved-copy timestamp. Failed library refreshes retain cards and expose a reachable Try again tail. Discover retains its current catalogue after a failed refresh, adds a recovery tail and disables request and dismissal writes until recovery; a revoked sign-in discards that saved projection. Previously observed plates can remain visible offline, but moving to an uncached card cannot start a ratings read. `SavedPill` shares the compact cached-header treatment across Home, Libraries and Requests; the nav retains its larger wifi chip. Requests focus, observed ratings and retained rows are partitioned by selected person. A failed authenticated read preserves the last rows and their timestamp; Down from the final row reaches Try again and Up returns to the row. Signed-out data is not retained as an authenticated copy. Returning to a foreground person triggers that person's refresh rather than waiting for the prior person's polling timer. Home disk snapshots remain server/user scoped. A new identity starts with its own skeleton or saved copy, never the previous person's projection; late responses from an older read cannot overwrite the current identity. A disk-write failure does not turn a successful Jellyfin read into an outage. Skeleton-only Home, Library and Discover screens keep focus in the nav even when Down is pressed repeatedly. Library/Discover posters use the same title/glyph/160-degree gradient fallback as Home, including an actual Coil read failure. Discover hero image failures use the shared ambient background; missing artwork is not a successful data refresh. `RecoveryStateTest`, `RecoveryCaptureTest`, `HomeRecoveryTest` and `PlayerHandoffTest` exercise state transitions, person isolation, native recovery focus/geometry and the real upstream playback destination with offline fixtures. New native captures and rendered text/font metrics go to `build/wolfskin-captures/recovery/`; Dutch score captures configure the Android resource locale as well as the JVM locale. Conductarr transport and identity live outside the state components. A revoked sign-in discards the current person's authenticated projections; an outage keeps last-known-good (LKG) data and its Saved timestamp. No Seerr fallback or offline request queue is used. Title and performer pages use the same authenticated cache. ## Conductarr production wiring The default endpoint is `BuildConfig.CONDUCTARR_BASE_URL`: `https://wolfskin-tv.justinvisser.org`. Override **Conductarr server URL** beside **Wolfskin screens** in Advanced settings; blank restores the build default. HTTPS is required except for loopback fake servers. **Wolfskin preview fixtures** is a debug-only, off-by-default switch; it never sends HTTP mutations. `ConductarrSession` borrows the currently selected user's existing Jellyfin access token. It sends only `Authorization: MediaBrowser Token=""`, with no cookie, API key or second credential store. Person/server/token/endpoint changes discard the old client and in-memory providers. LKG files are isolated by person and client identity in app-private, backup-excluded storage. A 401 clears that identity's cache and returns to sign-in states; Try again reconstructs and verifies the client. Home publishes Jellyfin/LKG rows independently of Conductarr, including retry authentication, and paints new Jellyfin data before writing its disk copy. Production calls use 3-second connect, 5-second read and 8-second total limits. Request recipients include authorized Conductarr profiles without saved Jellyfin logins; that does not create selectable TV logins. Late name reads preserve the open chooser's selected recipient and footer focus. The launch picker publishes the current Jellyfin user and working navigation before the saved-login database read. Saved people and Conductarr household metadata fill that same shell later; slow activity or catalogue reads cannot hold its first usable frame. Discover details navigate to the shared title page, including cast, performer navigation and related titles, rather than the header-only design preview. Held items whose detail omits ProviderIds or People get an explicit Jellyfin field read before catalogue enrichment, without changing their playback identity. Scrolled navigation draws an opaque band; the unscrolled hero treatment is unchanged. Cached `NavDisplay` entries read the current shell and Discover composition locals, not the temporary launch shell captured when the entry was first created. Otherwise the screen can retain skeleton people, disconnected tab actions and an open picker even after the real session is ready. Only the current navigation entry may set the shared route or enable the outer host's Back handler; outgoing entries can remain composed during a transition. Nested title handlers retain their existing lifecycle-based behaviour. Artwork uses a fresh bounded Wolfskin loader, not the upstream loader's authenticated fetcher. The app's network interceptor removes Authorization from every non-Jellyfin artwork origin (scheme, host and port), including redirects; current Jellyfin artwork still authenticates. A failed or empty library-audience response keeps the existing mapping or Jellyfin name rule. Returning to the foreground retries household metadata without rebuilding the selected identity. An Anime audience grants a library grouping, not proof of owned titles. Household refresh enables Anime only after a one-title Jellyfin read confirms visible series in an accessible Anime library for the signed-in user. This runs after the live picker is published; failures retain that user's last confirmed availability. Empty Anime Home shelves remain omitted. When availability changes, focus follows the avatar or a surviving destination; removing Anime never leaves an out-of-range nav selection. If Anime was the root, both the shell and navigation stack replace it with Home while retaining pushed title/person pages. `AnimeRootRemovalTest` covers active-root Back, avatar focus, and the real navigation stack with one or two pushed pages. | Screen seam | Production supplier | | --- | --- | | People and nav | Operator, profiles, library audiences; saved Jellyfin users remain the only selectable logins | | Home continuation | Recommendation feed, catalogue options/discovery; Coming soon stays Discover-only | | Discover | Feed/contributions, services, New on, Trending, genres, Coming soon, parameterized service shelves, dismissal reads/writes | | Catalogue paging | Two-shelf read-ahead, distinct title identities, per-parameter disk pages, retry/duplicate-only tails; no page counters | | Request sheet | Capability-limited requesters, season metadata, POST receipt, receipt lookup before any resubmit; actor comes only from the token | | Requests | Household activity, stage precedence, actor/requester evidence and display posters; foreground/30-second refresh | | Ratings | Ratings-only read after 350 ms card dwell; immediate hero/title/Coming-in reads; per-type six-hour/300-entry cache; episode season votes | | Titles and people | Detail/enrichment/display, cast, related, real Jellyfin season episodes plus votes, performer/filmography, title activity and uncached receipts; shared request/hide workflows | The pinned client exposes exactly the 22 TV operations, including ratings and display. Conditional reads send ETags and reuse the saved body on 304. The fake server tests use representative wire JSON, never a live Conductarr or Jellyfin. `ConductarrSessionTest`, `ConductarrProjectionTest`, `ConductarrTitlePagesTest`, `ConductarrSurfaceTest`, `ConductarrHomeSurfaceTest` and `ConductarrTitleSurfaceTest` cover identity/cache isolation, 401/retry, saved offline data, actor-safe requests, ratings timing, title playback permissions/episode votes and native real-provider surfaces. The existing fixture screen tests remain in the normal `make test` gate. `WolfskinArtworkTest` exercises the actual Coil loader against two HTTP hosts and a same-host/different-port origin with an authenticated upstream parent, and verifies successful decoding with credentials only on Jellyfin. `ConductarrAudienceRecoveryTest` covers operator success plus failed/empty audiences, name-rule retention and foreground healing. `ShellStartupTimingTest` holds saved-login and real-wire activity reads pending while exercising the picker, early tabs and a person switch. `TitleReadFacadeTest` covers omitted held-film provider ids with authenticated cast/related enrichment. `DiscoverTitleTimingTest` delays the cast reply, exercises real remote Down/Back and writes native cast/related captures to `walk-fixes/`. `NavBandOcclusionTest` checks pixels across the entire scrolled navigation band. `FirstSignInNavigationTest` starts the full app host with no existing shell and immediate signed-in replies, then continues as Mindel and renders her six-film Movies grid and 29-series Shows grid from owner-prefixed Jellyfin views. It also checks that Continue closes the real session picker and selecting Home does not reopen it. `AnimeAvailabilityTest` reads the signed-in audience wire shape and checks Shared and Mindel with empty and nonempty Jellyfin libraries: no empty Anime nav item or Home shelf, while a real visible series enables both. Not done here: deploying the dedicated hostname or widening server authority; hardware installation/playback verification; releases, signing or version changes. Unheld trailers remain absent because the server contract has no trailer field. Saved Jellyfin logins are not fabricated from Conductarr profiles: a person without an upstream token remains signed out. Fixtures require explicit debug opt-in. ### Manual smoke after endpoint deployment (no build command) Use an already authorized test build and a sandbox that accepts its selected Jellyfin login. No token needs to be copied, displayed or entered into Wolfskin. 1. In Advanced settings, set the sandbox URL with fixtures **off**. Select Shared, then another saved person; verify their operator/requester and independent views. Its hostname must be in Conductarr's TV allowed-host list; arbitrary hosts or IP addresses are refused even with the correct token. 2. Open Home, Discover, a service, Hidden picks and Requests. Open a title, cast member and filmography credit; check real facts, related titles, episode votes, stage/actor labels, card dwell ratings and continuous See all loading. 3. Make the sandbox return 503 after a successful read. Reopen: saved content and Saved time remain, requests cannot submit, and Try again performs another read. 4. Return 401, then restore authentication. Verify ratings/request actions disappear and this person's saved Conductarr data is cleared. Try again restores data without changing Jellyfin login or copying a token. 5. Have the sandbox accept a Shared-on-behalf request but lose its POST response. Use Check again: the receipt says Shared, and the sandbox records one POST. 6. Restore the blank/default URL when the dedicated TV hostname and server's 22-operation authority are deployed. This checklist is manual, not a live gate. Title and person surfaces publish saved timestamps through the same per-person `RememberedView` partition and nav Saved indicator. Their timed notices render the shared `RecoveryToast`; failed Try again and playback use `RecoveryMessage` without transport details. A selected-person mismatch never renders the former person's title/person projection. Title and performer disk copies remain isolated by both server and authenticated user; native retry and cache-isolation regressions exercise these boundaries without a live service. ## TV fidelity and source gates Library/Discover page headings use the existing exact CSS line-box layout. Request sheets reserve the inherited inline text descent around their ratings; person rows and receipt panels therefore share the locked frame's vertical coordinates. Settings row title/facts also retain their CSS line boxes. These are local component fixes: shared typography tokens, Home and caption styling do not shift. Home ratings observations use independent six-hour, 300-entry title and series/season caches. A season retains each observed episode's own score and timestamp; changing a Jellyfin episode UUID does not lose the observation, and another episode or season never borrows that score. Episodes without a complete catalogue identity remain isolated by their Jellyfin identity. Cold library grids publish each cumulative 200-item SDK chunk before the next read completes. An existing complete saved copy stays visible throughout a refresh, and only a completed read replaces it on disk. A cold later-chunk failure keeps the already-readable titles with a retry tail, without saving an incomplete copy. Appended titles preserve the focused title even when the destination's sort inserts them before it. Catalogue row keys use the same media-type/TMDB identity as their cards. Undo restores a dismissed pick silently; Show again announces “Back in your picks.” Title pages own their own toast so the request adapter never duplicates it. The saved Discover retry tail is labelled More picks, not Try again twice. People retry retains an already signed-in Conductarr client and its uncertain submission ledger; only a signed-out identity is rebuilt. Home authorizes its Jellyfin read by the selected person's saved Jellyfin user ID, so a lost Conductarr operator or a renamed household profile does not hide that user's own library. A different saved user ID still fails closed. `make test` runs `OwnedSourceAuditTest` through the design module's JVM suite. It scans the owned Kotlin sources in `wolfskin-design/src/main/java` and `app/src/wolfskin`, rejecting untokened `.dp`/`.sp` conversions and numeric Color constructors outside generated tokens, token font sizes below 12 sp, raw household library labels, and page-counter/navigation wording. Its negative cases plant each violation independently. Existing native capture checks also verify rendered font sizes and wording. `LibraryProgressiveSurfaceTest` captures the usable first chunk while the final read is still suspended. ## Toolchain and disk use `Dockerfile` pins Temurin JDK `21.0.11+10` by image digest, command-line tools `23.0` and platform-tools `37.0.1` by archive and checksum, and build-tools `36.0.0` by SDK package version. The SDK manager registers platform `37.0` and its licenses; the build rejects any platform revision other than 2. An SDK revision upgrade therefore requires a deliberate Dockerfile update. Gradle comes from the committed wrapper. The image is built for `linux/amd64`; the default local image tag is `wolfskin-build:sdk37-jdk21`. Upstream's build timestamp means APKs are not byte-for-byte reproducible. Gradle downloads and caches live in the named volume `wolfskin-gradle`, not `/tmp`. Module outputs stay in the checkout. Builds run with the host UID/GID, `--no-daemon`, and at most four workers. The repository's Git metadata is mounted read-only, including when the checkout is a linked worktree. Override `WOLFSKIN_BUILD_IMAGE` to change the locally built image tag and `WOLFSKIN_GRADLE_VOLUME` to isolate a cache. `make build-image` builds only the image. Do not delete shared caches during another build. Every Gradle container holds a host-side exclusive lock for its entire run. Concurrent targets sharing a cache queue rather than timing out on Gradle's cross-container cache journal lock; a waiting invocation prints one line. The default lock is `/var/tmp/${WOLFSKIN_GRADLE_VOLUME}.gradle.lock` (using `wolfskin-gradle` when unset). Override `WOLFSKIN_GRADLE_LOCK_FILE` with a path whose parent directory already exists. All callers sharing a volume must use the same lock path on the Docker host. Do not delete the file while builds are running or waiting. The lock releases automatically when the invocation exits. ## Persistent debug signing `make apk-debug` and non-release `docker/gradle.sh` invocations (including `:app:assembleDefaultOptimized`) use one host keystore, outside the checkout: ```text /root/.treehouse/firstmate-7bab20/2/firstmate/data/signing/debug.keystore ``` Set `WOLFSKIN_DEBUG_KEYSTORE` to another file in a dedicated directory outside the checkout when building on another host, for example: ```sh WOLFSKIN_DEBUG_KEYSTORE="$HOME/.local/share/wolfskin-signing/debug.keystore" make apk-debug ``` The wrapper creates a missing key once using the build image's `keytool`: RSA 2048, 10,000-day validity, alias `androiddebugkey`, store/key passwords `android`, and subject `CN=Android Debug`. The containing directory is mode `0700`, the file mode `0600`. Existing keys are reused, never regenerated; generation errors abort the build rather than falling back to an ephemeral container key. The file is bind-mounted read-only at `/tmp/.android/debug.keystore`, with `ANDROID_USER_HOME=/tmp/.android`, for every non-release Gradle container. Keep and back up this file across clean builds, disposable worktrees, container rebuilds and Gradle cache resets. Never commit it. The standard debug passwords are public; this key is for local debug/optimized builds, not release signing. ### Preserve household TV data Install and update the household TV app only in place with `adb install -r`, using the same package and persistent debug signer. Its certificate SHA-256 is: ```text 182bac72e292b9ea4d3d4e6383f92eb2161fa28ba5afcff0337cb7d5a9d5341d ``` Never run `adb uninstall`, `pm uninstall` or `pm clear`, or switch signers on the household box: clearing/removing the app deletes saved people, sign-ins and settings, and a different signer cannot update the existing app in place. Do not delete or replace the persistent key. If an uninstall is unavoidable, use `adb uninstall -k ` to keep app data, then reinstall the same package with the same signer. The manifest sets `android:hasFragileUserData="true"` so Android 10+ can offer a **keep app data** choice during interactive uninstall. Choose it when available; older Android versions ignore the attribute. This is a retention prompt, not an automatic backup or a guarantee against data clearing. For an in-place update, compare `dumpsys package ` before and after: `firstInstallTime` must remain unchanged. The guarded installer that checks the signer and this timestamp lives in the operator's tooling, not this repo. ## Release credentials The default escrow is `/opt/stacks/ventoy/machine-parity/manifest-tank/secrets/wolfskin/`. Check `mountpoint -q /mnt/ventoy` before using that path. Set `WOLFSKIN_SECRETS_DIR` to use another existing directory. Its required files are: | File | Contents | | --- | --- | | `release.jks` | Separate Wolfskin PKCS12 keystore, RSA 4096, alias `wolfskin` | | `release.properties` | Java properties `storePassword` and `keyPassword` | | `github-packages-token` | GitHub token with `read:packages`, username `JustinZeus` | The directory must be mode `0700`, each file mode `0600`, owned by the building user. Never commit them or put their contents in logs. Back up the keystore and passwords together: losing the release key forces a reinstall. Do not reuse wolfskin-android's key or the gh CLI login token. Release builds mount escrow read-only. The token is loaded inside the container into upstream's `ORG_GRADLE_PROJECT_WholphinExtensionsUsername` and `ORG_GRADLE_PROJECT_WholphinExtensionsPassword` environment hooks, never a Docker command argument. The signing init script registers `wolfskin`; the nonsecret `release-signing.properties` is mounted as `local.properties` so upstream's `release.signing.config` hook selects it. Any host `local.properties` is hidden only for the duration of this container. Keystore and passwords stay outside the checkout and image. Missing credentials fail the release rather than silently shipping the MPV stub. ## Versioning and local release markers Upstream computes `versionName` from `git describe --tags --long --match='v*'`, dropping the leading `v`, and computes `versionCode` from the count of all local `v*` and `p*` tags. Preserve and fetch those tags; a shallow or incomplete checkout can give an incorrect version. Use the full `git describe` output as the human release name. After committing the intended release, with a clean checkout and fetched tags: ```sh make release-tag make apk ``` The helper creates one lightweight `pwolfskin-YYYY_MM_DD` tag in UTC, appending `_1`, `_2`, etc. on collision. Each marker adds one to `versionCode` without changing upstream's `v*`-based version name. It never pushes tags, publishes a release, or installs an APK. Do not delete published release markers. Tag creation and publication must be serialized between release operators. ## Inspecting release artifacts Use `apksigner verify --verbose --print-certs APK` and `aapt2 dump badging APK` inside the build image. Compare the signer SHA-256 with `keytool -list -v -alias wolfskin -keystore /path/to/release.jks` (enter its password privately). Inspect APK ZIP entries for real native MPV and ffmpeg libraries for each shipped ABI; a successful Java compilation alone does not prove that codec extensions are included.