# 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, 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. ## 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 shell measurements from that same prototype. Generate the committed Kotlin, CSS and 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. 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 under the final person opens Settings with Requests focused. 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 restores the labelled placeholders until the corresponding screen slices ship. Debug people, tabs and initials come from the shell-only projection of the locked prototype fixture in `app/src/debug/assets/wolfskin-shell.json`; release resources do not package it. Saved Jellyfin identities use existing upstream services. The shell makes no Conductarr calls and does not add Conductarr sign-in. 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/`. 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. ## Requests under Settings The Requests row and Coming-in sheet share one immutable household snapshot. Ready requests do not count as open. Rows sort Failed, Downloading/Almost ready, Checking/Searching, Waiting, Ready. The bounded window shows five rows, or four with readiness expanded. Right expands Request/Download/Library/Playback; 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”. The debug-only provider loads `app/src/debug/assets/wolfskin-requests.json` and its eight local posters, projected from the locked prototype fixture. Six stage rows feed the running debug shell; the Shared and focused-rating records feed native acceptance states. The identical projection is in design-test resources. No “sample” or “example stages” pill is drawn. Release builds package none of these assets and show Requests unavailable until Conductarr sign-in supplies the generated-client data path. There is no Seerr fallback or handwritten HTTP. 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. 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. Home's hero content is not implemented here: its launch-to-nav shell suffix is the acceptance seam. Sheet dimensions, padding, glyphs, scales, colours and durations are generated tokens. Single-line sheet text retains CSS-style line-box leading. 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. 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, avoiding differences in older upstream conversions: ```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 currently uses an explicitly labelled `request-title/...` destination until the independent title screen is integrated; this slice does not claim to implement title pages, TV sign-in, release or device deployment. ## 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. ## 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.