Build the approved TV Updates settings UI #65

Merged
justinzeus merged 9 commits from fm/tv-updater-ui into main 2026-11-07 22:04:40 +01:00
Owner

Outcome

AI-assisted (fork-internal). Build the approved Wolfskin TV Updates screen, add Updates immediately after Requests, retain every existing Settings group, and put Classic settings (trial) last.

Base is main, following firstmate steering 004. The verified updater core is already in main through the household updater core review; this UI consumes its process-scoped controller. Final pushed head: 9061384ad1260e1b2f802725970c008fb3c158fb. No round-2 branch rebase or device/box action.

Applied decisions

  • Installed version comes from installedVersion; Auto-check uses the controller's existing app-wide preference. No second preference or updater metadata store.
  • Distinct checking, successful compatible up-to-date, empty channel, availability, known/unknown-length download, verified Ready, permission, Android handoff, cancellation, typed failure, offline and missing-installer surfaces. Checking has an animated track, not a spinner or loading sentence. Download and Install remain separate explicit actions.
  • Consume persisted release notes/date/size and structured Failed.operation. Check/download/install errors receive operation-correct headings without parsing reason strings. Permission/handoff dismissal uses the core's verified-file-preserving cancellation contract, including its independent installer-ownership guard.
  • Refresh installed identity on resume. The integrated core clears a completed install to Idle: a confirmed version-code increase now shows the approved Updated surface there too, without masking an ongoing check or claiming an unconfirmed up-to-date response. The initially blank Idle panel was reproduced natively before this fix.
  • Settings starts at Requests; Down then OK reaches Updates. Action Left/Right, remembered action ↔ Auto-check, Up to retained navigation, exact Settings Updates-row/scroll restoration on Back, and trapped/scrollable notes with Back restoring the notes button. The final classic row uses the actual LazyColumn scroller. Full classic About → Update and More/Advanced → Updates remain reachable.
  • Apply the approved card ring to ring-style controls; Settings rows remain filled-only. Progress is not focusable and byte emissions do not steal focus.

Choices I made

  • Scoped bundled DejaVu Sans regular/bold to Settings, Updates and their retained nav: this is the approved browser's resolved font, not a global app font replacement. The font license is an APK asset and was read from the built universal APK.
  • Keep Updates geometry in update-tokens.json, using the existing generator/drift guard. Add only missing motion tokens: 1800 ms scanning, 1600 ms sheen, and the linear curve needed by the approved progress treatment. Existing motion curves/durations, reduced motion and motionDuration remain authoritative.
  • Animate bounded panels/progress; suppress full-scene fades on Updates entry/return. The native Navigation3 frame series preserves the retained nav pixel-for-pixel.
  • Keep permanent tests behavior-focused. Temporary capture harnesses are archived with proof, not left in the test source set. The test Hilt module replaces the real UpdateModule instead of creating a competing production/fake binding.

Captures and visible differences

Native/approved comparison and motion proof

All 18 prescribed pairs: unretouched 1920×1080 originals, 3840×1080 side-by-side files, approved fixture data (1.1.0-140-g5162c299 → 1.1.1, 47.8 MB, 6 Nov 2026). Those sizes are fixture facts, not a measured release APK. Additional no-release and unknown-total outcomes are linked. Files are 0660; directories 0770.

The page records renderer/review heads, original hashes, per-label pixel deltas and causes. The retained fixture matrix is supplemented by three narrow native integration captures. The fixed post-install Idle image is pixel-identical to the existing same-data Updated native image: zero differing pixels.

Visible difference Measurement / cause
Glyph ink bounds Up to 8 px per label; Skia/Chrome advance, baseline and antialias rounding with the same DejaVu outlines. Every measured label is listed on the page.
Composited fills Native rest rows and secondary buttons are +1 RGB level; alpha-premultiplication rounding. Opaque palette swatches match.
Rounded/glyph edge coverage Individual edge-pixel coverage differs on the native pixel grid.
Progress stills Different scan/sheen phase at native 1000 ms versus approved near-cycle-start; real byte fraction is 42.0502% versus the browser fixture's rounded 42% (about 0.61 px fill).

No difference is waived. The approved-frame rule remains a merge gate: a visible unapproved deviation blocks merge.

Native motion proof: 113 checking, 24 state-transition and 101 downloading frames at manually clocked 16 ms steps; reduced-motion frames 1600 ms apart are pixel-identical. Another 24 entry and 24 return frames cover Navigation3 and exact Settings restoration.

The final served-file page was inspected in an isolated 1920×1080 Chromium session: dark scheme, 18 decoded pairs, zero broken images, working integration-image link. The clean remote route redirects an unauthenticated session to Authentik; authenticated remote-page verification is not claimed.

Verification

At final merged-main head 9061384ad1260e1b2f802725970c008fb3c158fb:

Check Cases / result Failures / errors / skips
Touched app/updater tests 62 passed 0 / 0 / 0
Touched shell focus/capture tests 21 passed 0 / 0 / 0
Scoped total 83 passed 0 / 0 / 0
One final-head make apk-debug Passed; four debug APKs —

App coverage includes 27 real-controller loopback/persistence cases, 2 session-stream cases, 3 manifest cases, 12 updater UI behavior cases, and 18 affected view-model cases. State/action boundaries, percentage validity, focus/Back behavior, toggle semantics, notes trapping and the refreshed-identity/checking precedence are covered without wording tests.

The APK gate inspected 1016 branding resources per artifact and preserved legal attribution. These are debug/stub-media builds, not distribution or playback proof. Per the released slim pilot, focused tests were followed by one final-head APK gate; the full make test suite was not rerun. Counts, source and native image hashes are linked from the proof page.

Questions for Justin

No additional product decisions beyond native-frame review. The measured rendering differences above are disclosed, not silently accepted or treated as an approval waiver.

Not in this change

  • Core service, channel, installer and release-tooling implementation: already owned by the merged core review; this change consumes that interface.
  • Distribution repository creation, real release publication or an optimized distribution build.
  • ADB/device/box access, real OEM permission/installer interaction, actual installation or retained-login/settings in-place update proof. Those belong to the later authorized walk.
## Outcome AI-assisted (fork-internal). Build the approved Wolfskin TV Updates screen, add Updates immediately after Requests, retain every existing Settings group, and put Classic settings (trial) last. Base is **main**, following firstmate steering 004. The verified updater core is already in main through the [household updater core review](https://git.justinvisser.org/justinzeus/wolfskin/pulls/60); this UI consumes its process-scoped controller. Final pushed head: `9061384ad1260e1b2f802725970c008fb3c158fb`. No round-2 branch rebase or device/box action. ## Applied decisions - Installed version comes from `installedVersion`; Auto-check uses the controller's existing app-wide preference. No second preference or updater metadata store. - Distinct checking, successful compatible up-to-date, empty channel, availability, known/unknown-length download, verified Ready, permission, Android handoff, cancellation, typed failure, offline and missing-installer surfaces. Checking has an animated track, not a spinner or loading sentence. Download and Install remain separate explicit actions. - Consume persisted release notes/date/size and structured `Failed.operation`. Check/download/install errors receive operation-correct headings without parsing reason strings. Permission/handoff dismissal uses the core's verified-file-preserving cancellation contract, including its independent installer-ownership guard. - Refresh installed identity on resume. The integrated core clears a completed install to Idle: a confirmed version-code increase now shows the approved Updated surface there too, without masking an ongoing check or claiming an unconfirmed up-to-date response. The initially blank Idle panel was reproduced natively before this fix. - Settings starts at Requests; Down then OK reaches Updates. Action Left/Right, remembered action ↔ Auto-check, Up to retained navigation, exact Settings Updates-row/scroll restoration on Back, and trapped/scrollable notes with Back restoring the notes button. The final classic row uses the actual LazyColumn scroller. Full classic About → Update and More/Advanced → Updates remain reachable. - Apply the approved card ring to ring-style controls; Settings rows remain filled-only. Progress is not focusable and byte emissions do not steal focus. ## Choices I made - Scoped bundled DejaVu Sans regular/bold to Settings, Updates and their retained nav: this is the approved browser's resolved font, not a global app font replacement. The font license is an APK asset and was read from the built universal APK. - Keep Updates geometry in `update-tokens.json`, using the existing generator/drift guard. Add only missing motion tokens: 1800 ms scanning, 1600 ms sheen, and the linear curve needed by the approved progress treatment. Existing motion curves/durations, reduced motion and `motionDuration` remain authoritative. - Animate bounded panels/progress; suppress full-scene fades on Updates entry/return. The native Navigation3 frame series preserves the retained nav pixel-for-pixel. - Keep permanent tests behavior-focused. Temporary capture harnesses are archived with proof, not left in the test source set. The test Hilt module replaces the real UpdateModule instead of creating a competing production/fake binding. ## Captures and visible differences **[Native/approved comparison and motion proof](https://pages.justinvisser.org/wolfskin/tv-updater-ui)** All 18 prescribed pairs: unretouched 1920×1080 originals, 3840×1080 side-by-side files, approved fixture data (`1.1.0-140-g5162c299` → `1.1.1`, 47.8 MB, 6 Nov 2026). Those sizes are fixture facts, not a measured release APK. Additional no-release and unknown-total outcomes are linked. Files are 0660; directories 0770. The page records renderer/review heads, original hashes, per-label pixel deltas and causes. The retained fixture matrix is supplemented by three narrow native integration captures. The fixed post-install Idle image is pixel-identical to the existing same-data Updated native image: **zero differing pixels**. | Visible difference | Measurement / cause | | --- | --- | | Glyph ink bounds | Up to 8 px per label; Skia/Chrome advance, baseline and antialias rounding with the same DejaVu outlines. Every measured label is listed on the page. | | Composited fills | Native rest rows and secondary buttons are +1 RGB level; alpha-premultiplication rounding. Opaque palette swatches match. | | Rounded/glyph edge coverage | Individual edge-pixel coverage differs on the native pixel grid. | | Progress stills | Different scan/sheen phase at native 1000 ms versus approved near-cycle-start; real byte fraction is 42.0502% versus the browser fixture's rounded 42% (about 0.61 px fill). | No difference is waived. The approved-frame rule remains a merge gate: a visible unapproved deviation blocks merge. Native motion proof: 113 checking, 24 state-transition and 101 downloading frames at manually clocked 16 ms steps; reduced-motion frames 1600 ms apart are pixel-identical. Another 24 entry and 24 return frames cover Navigation3 and exact Settings restoration. The final served-file page was inspected in an isolated 1920×1080 Chromium session: dark scheme, 18 decoded pairs, zero broken images, working integration-image link. The clean remote route redirects an unauthenticated session to Authentik; authenticated remote-page verification is not claimed. ## Verification At final merged-main head `9061384ad1260e1b2f802725970c008fb3c158fb`: | Check | Cases / result | Failures / errors / skips | | --- | --- | --- | | Touched app/updater tests | 62 passed | 0 / 0 / 0 | | Touched shell focus/capture tests | 21 passed | 0 / 0 / 0 | | **Scoped total** | **83 passed** | **0 / 0 / 0** | | One final-head `make apk-debug` | Passed; four debug APKs | — | App coverage includes 27 real-controller loopback/persistence cases, 2 session-stream cases, 3 manifest cases, 12 updater UI behavior cases, and 18 affected view-model cases. State/action boundaries, percentage validity, focus/Back behavior, toggle semantics, notes trapping and the refreshed-identity/checking precedence are covered without wording tests. The APK gate inspected 1016 branding resources per artifact and preserved legal attribution. These are debug/stub-media builds, not distribution or playback proof. Per the released slim pilot, focused tests were followed by **one** final-head APK gate; the full `make test` suite was not rerun. Counts, source and native image hashes are linked from the proof page. ## Questions for Justin No additional product decisions beyond native-frame review. The measured rendering differences above are disclosed, not silently accepted or treated as an approval waiver. ## Not in this change - Core service, channel, installer and release-tooling implementation: already owned by the merged core review; this change consumes that interface. - Distribution repository creation, real release publication or an optimized distribution build. - ADB/device/box access, real OEM permission/installer interaction, actual installation or retained-login/settings in-place update proof. Those belong to the later authorized walk.
justinzeus added 9 commits 2026-11-07 21:56:54 +01:00
Author
Owner

Review of the Settings Updates UI at head 9061384ad1260e1b2f802725970c008fb3c158fb (base main).

Code verdict: APPROVE (review/code-claude = success).
Design verdict: PASS (review/design-claude = success). No blocking finding.

Design. I compared all 18 approved and native frame pairs: Settings with Updates focused, Classic settings last, checking, up to date, available, notes, notes open, downloading, ready, permission, handing off, cancelled, failed (Download failed, 403), offline, no installer, updated and the Auto-check frames. Copy, layout, button set and order, focus ring and fills match. The only differences are glyph antialiasing, +1 RGB on composited fills and the animation phase, all disclosed in visible-differences.json. The shortened descriptions on existing Settings rows are the approved copy.

Behaviour, UI side.

  • The screen consumes the core's UpdateController state, release, installed-version and auto-check flows. UpdateFocus holds only focus state, so there is no second updater store. Auto-check goes through the controller's preference.
  • There is no install without an explicit press. Install is offered only in Ready, PermissionNeeded and InstallCancelled, never at download end, and the core re-verifies the retained file inside install().
  • Failed.operation drives the Check failed / Download failed / Install failed headline. The cancel actions map to cancel(), which keeps the verified file. Verified wording appears only in Ready and InstallCancelled. Up to date only for the core's UpToDate, distinct from no release, offline and failed.
  • Focus and Back: Requests, Down and OK reach Updates; Left/Right moves actions; Down enters Auto-check; Up returns to the remembered action and then the nav; notes trap every key and Back restores the notes button; Back pops to the exact Settings row and scroll; progress never takes focus. Classic is the last row via the real scroller. No dead end found. I did not review root Back, which is owned by the other PR.
  • Nothing publishes. generate_tokens.py --check passes.
  • Tests assert behaviour: 12 JVM cases (actions per outcome, percentage bounds, focus rules, notes trap, progress not stealing focus) and 6 native key-driven cases. The edits to existing shell tests only track the added row.

Non-blocking notes:

  1. Font and nav on Settings and Updates (worth asking Justin). The approved frames came from a browser mock that resolved to DejaVu Sans, a lowercase wolfskin wordmark and no selected-tab pill. The PR reproduces all three on Settings and Updates only, so the nav changes typeface, wordmark case and selection between those routes and Home, Discover, Movies and Shows. It follows frames he approved, so it is not a block, but it looks like a mock artifact. The switch lives in UpdateTypography and NavBar (hasSelectedTab).
  2. Idle renders as an empty panel with a lone Check for updates button until the entry check starts. The core awaits the restore, including re-hashing a retained APK, first, so on the armv7 box this could last seconds. Showing the Checking track in that gap would be safer.
  3. A download that throws an unclassified exception maps to InvalidMetadata and shows Verification failed, which is not accurate. This is a mapping nit.
  4. Updated to X relies on a version code remembered at first composition. A real install replaces the process, so a fresh screen shows Up to date instead. The text is never wrong, only less informative.
  5. Core behaviour, not this PR: after Back from HandingOff the controller returns to Ready but keeps the installer-active flag until the installer reports, so an immediate Install press does nothing.
  6. A click in the exact frame a state changes could index a stale action list. Narrow.
  7. I spot-checked the motion evidence rather than re-measuring it.

Full report (review 4): /root/.treehouse/firstmate-7bab20/2/firstmate/data/round2-review/report.md. Not run: Gradle/make, nothing on the box.

Review of the Settings Updates UI at head `9061384ad1260e1b2f802725970c008fb3c158fb` (base `main`). **Code verdict: APPROVE (`review/code-claude` = success).** **Design verdict: PASS (`review/design-claude` = success).** No blocking finding. **Design.** I compared all 18 approved and native frame pairs: Settings with Updates focused, Classic settings last, checking, up to date, available, notes, notes open, downloading, ready, permission, handing off, cancelled, failed (Download failed, 403), offline, no installer, updated and the Auto-check frames. Copy, layout, button set and order, focus ring and fills match. The only differences are glyph antialiasing, +1 RGB on composited fills and the animation phase, all disclosed in `visible-differences.json`. The shortened descriptions on existing Settings rows are the approved copy. **Behaviour, UI side.** - The screen consumes the core's `UpdateController` state, release, installed-version and auto-check flows. `UpdateFocus` holds only focus state, so there is no second updater store. Auto-check goes through the controller's preference. - There is no install without an explicit press. Install is offered only in Ready, PermissionNeeded and InstallCancelled, never at download end, and the core re-verifies the retained file inside `install()`. - `Failed.operation` drives the `Check failed` / `Download failed` / `Install failed` headline. The cancel actions map to `cancel()`, which keeps the verified file. `Verified` wording appears only in Ready and InstallCancelled. `Up to date` only for the core's `UpToDate`, distinct from no release, offline and failed. - Focus and Back: Requests, Down and OK reach Updates; Left/Right moves actions; Down enters Auto-check; Up returns to the remembered action and then the nav; notes trap every key and Back restores the notes button; Back pops to the exact Settings row and scroll; progress never takes focus. Classic is the last row via the real scroller. No dead end found. I did not review root Back, which is owned by the other PR. - Nothing publishes. `generate_tokens.py --check` passes. - Tests assert behaviour: 12 JVM cases (actions per outcome, percentage bounds, focus rules, notes trap, progress not stealing focus) and 6 native key-driven cases. The edits to existing shell tests only track the added row. **Non-blocking notes:** 1. **Font and nav on Settings and Updates (worth asking Justin).** The approved frames came from a browser mock that resolved to DejaVu Sans, a lowercase `wolfskin` wordmark and no selected-tab pill. The PR reproduces all three on Settings and Updates only, so the nav changes typeface, wordmark case and selection between those routes and Home, Discover, Movies and Shows. It follows frames he approved, so it is not a block, but it looks like a mock artifact. The switch lives in `UpdateTypography` and `NavBar` (`hasSelectedTab`). 2. Idle renders as an empty panel with a lone `Check for updates` button until the entry check starts. The core awaits the restore, including re-hashing a retained APK, first, so on the armv7 box this could last seconds. Showing the Checking track in that gap would be safer. 3. A download that throws an unclassified exception maps to `InvalidMetadata` and shows `Verification failed`, which is not accurate. This is a mapping nit. 4. `Updated to X` relies on a version code remembered at first composition. A real install replaces the process, so a fresh screen shows `Up to date` instead. The text is never wrong, only less informative. 5. Core behaviour, not this PR: after Back from HandingOff the controller returns to Ready but keeps the installer-active flag until the installer reports, so an immediate Install press does nothing. 6. A click in the exact frame a state changes could index a stale action list. Narrow. 7. I spot-checked the motion evidence rather than re-measuring it. Full report (review 4): `/root/.treehouse/firstmate-7bab20/2/firstmate/data/round2-review/report.md`. Not run: Gradle/make, nothing on the box.
justinzeus merged commit 6fc10b7af2 into main 2026-11-07 22:04:40 +01:00
Sign in to join this conversation.
No reviewers
No labels
exploring
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: justinzeus/wolfskin#65
No description provided.