Merge remote-tracking branch 'origin/main' into fm/tv-fb1-visual
All checks were successful
review/code-gpt Firstmate read the diff and evidence (native before/after frames, hero decode proof, episode art chain, gear centered per owner answer); apk-debug green; full serial suite NOT green (4 PersonPickerSessionTest timeouts, cause unisolated, disclosed in the PR body), accepted by firstmate with real-box person-switch verification before anything ships to the owner
review/design-claude Firstmate read the diff and evidence (native before/after frames, hero decode proof, episode art chain, gear centered per owner answer); apk-debug green; full serial suite NOT green (4 PersonPickerSessionTest timeouts, cause unisolated, disclosed in the PR body), accepted by firstmate with real-box person-switch verification before anything ships to the owner

This commit is contained in:
justinzeus 2026-11-06 20:39:32 +01:00
commit 12fd12f754
22 changed files with 11163 additions and 53 deletions

View file

@ -1,8 +1,8 @@
# 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.
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
@ -35,8 +35,9 @@ 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; neither its
signing flow nor the existing debug build changes.
`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
@ -212,6 +213,14 @@ destination is an explicit title-family integration stub exposing only Discover'
request and pick actions. Unheld titles have no trailer control until the pinned
Conductarr title contract supplies a trailer.
Named “Because you watched” shelves require watched contribution evidence.
Seeds are grouped by media type and TMDB identity, never by caption; repeated
contributions retain both watched/added flags. The current feed has no per-seed
watch timestamp, so eligible shelves keep wire order. Added-only contributions
remain available in the general For-you collections, but make no named shelf.
Titleless contributions do not make a shelf. Recommendations remain acquisition
suggestions, not a filter to the viewer's current library.
The debug-only fixture and local art live in
`app/src/debug/assets/wolfskin-discover/`. The Advanced preview switch selects a
network-free fixture host; real providers are the default in debug and release.
@ -350,14 +359,24 @@ 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 Requests row and Coming-in sheet share one immutable household history snapshot.
Settings counts returned requests, including Ready, rather than calling every
non-Ready row “open”. An unavailable empty snapshot has no count. Deleted rows
read Removed; absent holdings read Not in any library; unreleased titles read
Not out yet; holdings outside the recipient's libraries read In another library.
Waiting for an import is not evidence of an active download. Downloading and
Searching require explicit acquisition signals. Visibility-proof faults read
Check needed; playback faults read File health needs attention, even when a
library holding is present. Right expands the actual stage evidence and the
relevant summary, including episode/source names when supplied by that summary.
The activity feed currently supplies only a title-level summary for Avatar.
Attention-needed rows sort first, then active acquisition/checks, waiting and
settled history. The bounded window shows five rows, or four with readiness
expanded. Left collapses; Up/Down collapse while moving. Opening a title preserves
the request for Back, and closing the sheet restores its opener. Selecting a root
or changing person clears the title-return link. Shared's authenticated actor
always reads “by Shared”, never “by you”; absent actors have no “by” clause.
The debug fixture switch loads `app/src/debug/assets/wolfskin-requests.json` and
its eight local posters. Shared and focused-rating records also feed the native
@ -409,6 +428,15 @@ type and TMDB identity. Back restores the exact Coming-in row and expansion;
the temporary `request-title/...` placeholder is removed. TV sign-in, release
and device deployment remain separate.
`ActivityTruthProjectionTest` feeds recorded activity JSON through the TV adapter
for Steins;Gate, Yu Yu Hakusho, Love is Blind, Spider-Man: Homecoming, deleted
One Hour Photo, unreleased The Body, Avatar health and Partition waiting.
`ConductarrTruthCaptureTest` renders those provider snapshots and Mindel's recorded
recommendation feed on the native Compose surfaces at 1920×1080, writing
`build/wolfskin-captures/truth/`. Artwork is deliberately omitted to keep these
captures offline; missing-art rendering, tokens, focus, sheet geometry and shelf
spacing are the shipped components.
## Libraries and See all
Movies, Shows and Anime use the shell's retained view and a continuous six-column
@ -844,6 +872,41 @@ 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

View file

@ -35,5 +35,36 @@ if [[ $mode == release ]]; then
exec ./gradlew "$@"
' bash "${gradle_arguments[@]}" "$@"
else
debug_keystore=$(realpath -m "${WOLFSKIN_DEBUG_KEYSTORE:-/root/.treehouse/firstmate-7bab20/2/firstmate/data/signing/debug.keystore}")
case "$debug_keystore" in
"$root"|"$root"/*)
printf 'Debug keystore must be outside the checkout: %s\n' "$debug_keystore" >&2
exit 1
;;
esac
signing_directory=$(dirname "$debug_keystore")
mkdir -p "$signing_directory"
chmod 700 "$signing_directory"
if [[ ! -e $debug_keystore ]]; then
temporary_directory=$(mktemp -d "$signing_directory/.debug-keystore.XXXXXX")
trap 'rm -rf -- "$temporary_directory"' EXIT
if ! docker run --rm --platform linux/amd64 --user "$(id -u):$(id -g)" \
--mount "type=bind,src=$temporary_directory,dst=/run/signing" \
"$image" keytool -genkeypair -noprompt -keystore /run/signing/debug.keystore \
-storetype JKS -storepass android -keypass android -alias androiddebugkey \
-keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug"; then
printf 'Failed to generate persistent debug keystore: %s\n' "$debug_keystore" >&2
exit 1
fi
chmod 600 "$temporary_directory/debug.keystore"
mv "$temporary_directory/debug.keystore" "$debug_keystore"
fi
if [[ ! -s $debug_keystore ]]; then
printf 'Missing or empty persistent debug keystore: %s\n' "$debug_keystore" >&2
exit 1
fi
chmod 600 "$debug_keystore"
arguments+=(--env ANDROID_USER_HOME=/tmp/.android
--mount "type=bind,src=$debug_keystore,dst=/tmp/.android/debug.keystore,readonly")
docker run "${arguments[@]}" "$image" ./gradlew "${gradle_arguments[@]}" "$@"
fi