All checks were successful
review/design-claude B1-B3 re-measured by firstmate from the lane measurement file against the locked numbers (all within 2 px); delta is the three scoped fixes plus an additive main merge; N1/N2 3-4 px offsets remain as listed follow-ups
353 lines
20 KiB
Markdown
353 lines
20 KiB
Markdown
# 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-<abi>.apk` for the
|
||
in-app updater. Gradle outputs
|
||
`Wholphin-default-release-<versionName>-<versionCode>[-<abi>].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` and `discover-tokens.json` add 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.
|
||
|
||
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.
|
||
|
||
The debug-only fixture and local art live in
|
||
`app/src/debug/assets/wolfskin-discover/`. Release builds remain on the explicit
|
||
Conductarr sign-in state. The fixture host cannot construct a network client:
|
||
debug request outcomes and dismissals stay in memory. The real mutation adapter
|
||
accepts an injected `ConductarrClient` and uses only its typed routes; it is not
|
||
wired to a live server. Unknown request outcomes force a fresh title-requests read
|
||
instead of blindly submitting again, including after closing a sheet or choosing
|
||
the same uncertain recipient from another chooser. Whole-series requests require every season.
|
||
|
||
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/`. 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.
|
||
Thumbnails reuse `LibraryImages` 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. 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. 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 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.
|
||
|
||
## Libraries and See all
|
||
|
||
Movies, Shows and Anime use the shell's retained view and a continuous six-column
|
||
lazy grid. Up from its 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.
|
||
|
||
Caption separators use 40% of the existing muted colour's alpha. The rail follows
|
||
the locked prototype CSS: bold 24 px letters, with the focused letter bold 28 px,
|
||
overriding the spec table's generic caption style for this component.
|
||
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 until
|
||
Conductarr sign-in exists.
|
||
|
||
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 pushes the explicitly named `library-title/{id}` destination for
|
||
the title-screen implementation; this slice does not implement title pages.
|
||
|
||
`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. Captures are written to
|
||
the same `build/wolfskin-captures/` directory as the shell captures.
|
||
|
||
## 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.
|