All checks were successful
review/code-gpt Build-script only (docker/gradle.sh + README): persistent debug keystore outside the checkout, readonly bind-mount, fails loudly; firstmate read the diff; lane showed identical signer SHA-256 across two optimized builds and the debug build
909 lines
56 KiB
Markdown
909 lines
56 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, `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-<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.
|
||
|
||
## 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 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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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,
|
||
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 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 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 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.
|
||
|
||
## 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.
|
||
|
||
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 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.
|
||
|
||
Poster progress uses the independent 18 px title-card inset, not caption spacing:
|
||
the track remains 220 px wide on a 256 px poster. Library focus-follow restarts
|
||
when the effective column count, focused row or header geometry changes, retaining
|
||
deep focus across live snapshots that add or remove the letter rail.
|
||
`LibrarySpacingRegressionTest` renders both progress geometry and the unchanged
|
||
focused index across the 199↔200-title 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.
|
||
|
||
## 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. `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, 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.
|
||
|
||
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="<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.
|
||
|
||
**One-time reinstall:** the first build with the stable key still needs one
|
||
uninstall+reinstall on the box because the signer changes once; later ones
|
||
install in place with `adb install -r`, retaining sign-ins and settings.
|
||
The initial uninstall removes app data, so sign in again once after that switch.
|
||
Deleting/replacing the persistent key or choosing a different override changes
|
||
the signer again and requires another reinstall.
|
||
|
||
|
||
## 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.
|