wolfskin/docker/README.md
justinzeus 844530404d
All checks were successful
review/design-claude 13 fixes passed Sonnet re-review at cd625b42; merged head 84453040 = main merge + shared RecoveryToast reuse: 33/34 pairs byte-identical, st-048 adopts the shared toast, 830 tests green (firstmate check)
Integrate shared offline recovery and saved-state isolation
2026-11-05 22:59:45 +01:00

540 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` 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.
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 renders Wolfskin Home and Libraries.
Discover and Requests are integrated Wolfskin surfaces; title hand-offs remain labelled.
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/`.
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,
Jellyfin shelves, per-destination "Your …" shelves and the Discover continuation.
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.
Composing destinations bind `rememberedView(route)` before navigation effects run, so a
new See all grid cannot borrow or clamp Home's scroller. Re-entry restores the
remembered track before the page with `scrollToItem`, preserving exact indices and
pixel offsets rather than animating back. Android focus stays with the shell;
cards use its 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 shelves, an unvisited row selects the nearest visible card center even
when card widths differ. A visited row restores its column. Horizontal movement
keeps the track still while focus remains in its visible middle.
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.
`HomeReadFacade` is the only Home code importing Jellyfin API clients. Release
variants read the selected user's Resume, Next up, newest titles and destination
shelves, with app-private last-known-good projections through the shared atomic
disk cache. A missing saved library becomes a recovery panel; a saved library
stays visible when Jellyfin is unreachable. Playback resolves an actual film or
episode and checks Jellyfin before opening upstream's player. No season,
recommendation stub or different person's session is promoted to playback.
An unresolved person switch guards Home and both owned/upstream library content
before mounting any source read with the prior person's Jellyfin session.
Debug variants additionally package the locked fixture and local artwork under
`wolfskin-design/src/debug/assets/wolfskin-home/`; release variants exclude them.
The fixture provider supplies the approved household data and Home states without
network reads in tests. Conductarr sign-in is not implemented here: real signed-out
Home hides every rating and shows "Discover needs sign-in". No Seerr fallback is
used. Catalogue data and ratings in debug captures are fixture-backed.
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 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 `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.
## 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 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/`.
## 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.
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.
`TitleReadFacade` confines SDK reads, server/user-scoped last-good disk copies,
person credits and playback mutations to one boundary. Cached content appears
before refresh. Production ratings stay hidden until Conductarr sign-in exists.
Debug-only `wolfskin-titles.json` and local `wolfskin-titles/assets/` project the
locked household fixture, including portraits; release never packages them.
Those synthetic IDs are not invented Jellyfin playback IDs. Real held-title
playback uses the existing player; debug catalogue identities cannot play a file.
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. Conductarr sign-in and request
submission/receipt workflows belong to their separate slices; title pages do not
pretend those operations succeeded or fall back to Seerr.
`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, long-sheet visibility and full media-table geometry.
`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.
Production Conductarr TV sign-in/provider wiring remains outside these state
components: release Discover is signed out and release Requests is unavailable.
The generated client's identity-scoped disk cache is the Conductarr cache boundary;
no Seerr fallback, live-test server, handwritten HTTP or offline request queue is added.
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.
## 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.