Skip to content

Downloads overhaul, iPhone Duo and player rework (1.8.0) - #425

Merged
jlipworth merged 142 commits into
mainfrom
downloads/overhaul
Nov 5, 2026
Merged

jlipworth merged 142 commits into
mainfrom
downloads/overhaul

Conversation

@jlipworth

Copy link
Copy Markdown
Owner

Summary

Lands the 1.8.0 downloads/overhaul branch on main. The branch has 142 commits and main has not moved since it branched, so this merges with no conflicts.

Linked issue

Refs #403 (acceptance and landing). Refs #419, #420, #421, #422, #423.

Affected surface

  • Platforms: Apple Vision Pro / iPhone / iPad / iPhone Duo / Mac. Apple TV: build only, since downloads are compiled out there.
  • Backends: Plex / Jellyfin / Emby

Verification evidence

  • Commands run and results: running; results posted below. Planned with scripts/native-test-matrix.py affected --base origin/main.
  • Simulator / host / device evidence: internal TestFlight 1.8.0 builds 1523–1653 have been used on device by the maintainer. Per the maintainer:
    • download, pause, resume and delete, the row states and colours, and offline play work as far as they know;
    • Cinema return is low priority and largely unchanged by this branch.
  • Not verified (and why):

scripts/check-sensitive-paths.py --base origin/main matched 15 paths, and I reviewed each one:

  • skill mirrors and CLAUDE.md: docs/tooling text;
  • project.pbxproj: only the MARKETING_VERSION bump;
  • .gitignore: Duo sim/artifact entries;
  • pyproject.toml / uv.lock: numpy added as a dev dependency;
  • mkdocs.yml: one nav line;
  • scripts/ci-hygiene.sh: the reducer guard.

Privacy checklist

  • No tokens, client identifiers, passwords, or signing/team identifiers.
  • No real server hostnames, IPs, or URLs. The new connection-route tests use generic RFC 1918 and CGNAT fixture addresses.
  • No usernames, media titles, filenames, library paths, or personal home-directory paths.
  • Security-sensitive changes were discussed privately first (n/a).

AI assistance

Claude Opus via Claude Code and GPT via Codex wrote most of the implementation, docs and tooling under maintainer direction. The maintainer did the device testing.

After merge (#403)

  • Annotated tag v1.8.0 on the merge commit.
  • Archive docs/plans/2026-10-16-downloads-overhaul.md and repair its links.
  • Delete the stack branches and downloads/overhaul.
  • Close out the overhaul worktree and its simulators.

Downloads overhaul P1, verified and integrated into downloads/overhaul.
Downloads overhaul P2, verified and integrated into downloads/overhaul.
Downloads overhaul P3a, verified and integrated into downloads/overhaul.
Downloads overhaul P3b, verified and integrated into downloads/overhaul.
…385)

Downloads overhaul P4, verified and integrated into downloads/overhaul.
…387)

Downloads overhaul P5, verified and integrated into downloads/overhaul.
…kes (#388)

Downloads overhaul P6, verified and integrated into downloads/overhaul.
….8.0 (#389)

Downloads overhaul P7, verified and integrated into downloads/overhaul. Preserve the recorded build 1513 distribution history alongside the 1.8.0 source milestone. Device acceptance remains pending.
Maintainer-approved follow-up to the 1.8.0 row redesign. The target is
the `downloads/overhaul` integration branch, not `main`.

**What changed**
- Finished rows show one summary line instead of the old `Downloaded:
<bitrate> avg` line plus a status line. The order is status, resolution,
average bitrate, then size plus extras:
  - `Downloaded • 1080p • 5.7 Mbps avg • 1.6 GB + 12 MB extras`
  - `Not verified • 1080p • 6.1 Mbps avg • 1.8 GB`

Unknown pieces are omitted. The line is a single line with tail
truncation, and VoiceOver reads the full text.
- No status line has a leading icon any more. Every text line starts at
the same edge. Tone colour and words carry the state, and the poster
badge is unchanged.
- In-progress, paused, failed and waiting rows keep their `Requested: …`
line and progress bar.
- This applies everywhere the shared row is used: the Offline library,
the iOS/visionOS sheet and the Mac sheet.
- `docs/DOWNLOADS-OFFLINE.md` and `TESTING-CHECKLIST.md` are updated.

**Verification**
- PMSKit: 1,752 tests pass.
- macOS hosted: 981 tests pass.
- Hygiene passes.
- P7 parity tests pass.
- iPhone hosted: 967/968 pass.
`SeasonPlannerIntegrationTests.aRefreshOutageDefersAPlannedRowInsteadOfFailingIt`
timed out once at `:392`; this PR doesn't touch that test or the code it
covers.
- Rendered PNGs of the complete, unverified and downloading rows were
checked at normal and narrow widths.
Targets the `downloads/overhaul` integration branch. This is a test-only
change.

**Root cause:** `aRefreshOutageDefersAPlannedRowInsteadOfFailingIt`
inferred completion from transient supervision state:
- its one-second retry could overtake the waiter;
- supervision ended before deferral persistence finished.

This made it time out once on the iPhone simulator.

**Fix:**
- Waits now use exact-generation completion acknowledgements, with
controlled backoff time.
- Matching waits found in the same file were converted too.
- No assertion was weakened. In a red control, omitting the deferral
fails the kept assertions.

**Verification:**
- PMSKit: 1,640 tests pass.
- macOS hosted: 914 tests pass.
- iPhone season suite: 3 of 3 runs pass, 11 tests each.
- Hygiene passes.
Targets the `downloads/overhaul` integration branch, not `main`. Refs
#379. The issue closes when this lands on `main`.

After a wake (scene foreground, background-session wake, or a long gap
between callbacks), the Downloads header shows **"Updating speed…"**
until every active transfer has a sample in one common post-wake window.
Before this change it summed a partial set of per-item rates and
presented that as the batch speed.

- The #224 protection against inflated catch-up rates is kept.
- No persisted state is added.
- Tests use an injected clock: partial freshness hides the number, full
freshness shows the sum, the catch-up guard holds, and the single-item
case is covered.

**Verification:** PMSKit 1,651 · macOS hosted 914 · hygiene. Physical
headset comparison is still pending.
Closes #404. Targets the `downloads/overhaul` integration branch.

### What changed
- New setting: **Settings ▸ Downloads ▸ Simultaneous downloads**, with
choices 1–5 (default 3, the previous fixed cap). It is available on
visionOS, iOS/iPadOS and macOS; tvOS has no Downloads settings.
- `DownloadAdmissionPolicy` no longer hard-codes the caps. It takes
`DownloadAdmissionLimits`:
  - total cap: the setting, clamped to 1–5 when read;
  - static-file lane: `max(1, total - 1)`;
  - server preparation and live-forward lanes: still 1 each.
- `DownloadAdmissionCoordinator` reads the stored value at every gate,
including relaunch recovery. It watches only this key and starts an
admission pass when the value changes.
- Lowering the setting never cancels running transfers. Raising it
starts waiting rows when lanes have room.
- Updated `docs/DOWNLOADS-OFFLINE.md`, `docs/SETTINGS.md` and
`TESTING-CHECKLIST.md`.

### Verification
| Check | Result |
|---|---|
| PMSKit hermetic tests | 1,642 pass |
| LabstreamMacTests | 918 pass |
| Hygiene tooling tests | 386 pass |

Also included: policy pins (clamping, lane caps for each total, existing
work kept when lowered) and integration tests for lowering and raising
the setting.

### Open gates
- Live-backend and physical-device acceptance: not run. They are tracked
in #403.
…ness wait

#405 added a snapshot-builder call using the old downloadSpeed argument
that #406 renamed to aggregateSpeedBytesPerSecond; each PR passed alone.

failedHeadRetriesBeforeQueuedSuccessorWithoutPoisoningIt waited 1 s for the
head/successor retry to drain, which expired once under full iPhone-suite
load. The wait checks liveness, not speed, so it now allows 10 s.
Prefer Pause All while any work is active or requested. Pause current store attempts, withdraw waiting retries, clear retry handoffs, and park Emby preparation through the row-pause path. Keep paused first-start intents distinct from pending admission and disable bulk controls during startup recovery.

Closes #410
Retain exact-attempt refill intents across inactivity, count pending finished bodies as planner coverage, and release mutation ownership at replacement resume. Establish connectivity eligibility at baseline and end result observations at actual stop edges. Retire terminal health bookkeeping.

Add fake-clock and held-I/O regression coverage for the review interleavings, multi-row timing, stop-edge reports, pause, deletion, and backend sign-out. Update the download behavior and device checklist.

Validated with hermetic PMSKit tests, the macos-hosted affected lane using private temporary directories, and ci-hygiene. No simulators booted.
Schedule poster-only hydration for queued, season-planned, and restored active rows using the existing attempt/source-fenced cache. Preserve side-asset accounting and defer heavier assets to their existing paths.

Closes #412
… work

scheduleEarlyPosters runs on every records refresh; rows already offered now skip
attempt admission and media-item construction. Refs #412
…409)

A sibling's ordinary continuation can publish the attempt while a health refill waits
in backoff. Publication neither halts the range nor revokes admission, so the intent
kept polling every 30 s for the rest of the process and wrote a diagnostic each time.
Retirement and refill execution now also require a nonterminal lifecycle.
The restricted Offline surface mounted its status banner as a top safeAreaInset outside the
NavigationStack. On iPhone it clipped the large title and covered the Sign In/Settings toolbar,
on iPad it still covered the toolbar, and on visionOS it never rendered.

The status is now a "Server unreachable" navigation subtitle (iOS/iPadOS/macOS) or a toolbar
label (visionOS, which has no navigation subtitle), and Reconnect is a prominent toolbar action
whose accessibility hint carries the full explanation. A DEBUG --ui-testing-fixture offline
launch mounts the surface without a server, and a mobile UI test asserts Reconnect, Sign In
and Settings stay hittable.
With no job running, AVPlayer's init.mp4 and target-segment requests could reach Emby
together: init started a job from zero, the target stalled for about 20 s, and every later
segment restarted the encoder, dropping audio each time. Fetch the segment containing the
target first so the job already runs there when init arrives.

The soak runner now reads prewarm and target-warm facts from the diagnostics JSONL (NSLog
does not reach the unified log for the hosted dev app) and fails a run on a restart storm.

Mac soak, 20 deep-seek runs each: restart-storm runs 6 -> 4, audio-queue resyncs 61 -> 23,
target warm ready 40/40, no render-pipeline failures, every probe passed. The remaining storms
are an Emby-side position reset after a mid-file start.

Refs #420 #421
…oder logs

scripts/scrub-server-logs.py discovers the concrete hosts, addresses, credentials,
identifiers, account/device names, library paths, titles, release names and search
terms in server and ffmpeg logs (including base64/gzip payloads), replaces them with
the repository placeholders, re-encodes payloads, and fails closed when a re-scan of
the output still finds anything. Optional --since/--until/--play-session windows pull
in the matching transcoder logs. Reports carry category counts only.

Closes #423
…e in the iOS player overlay

On the unfolded iPhone Duo the scrim was pinned to the safe frame, leaving undimmed stripes
under the status strip and over the home-indicator band, and the bottom group stacked its
authored 28 pt margin on top of the bottom safe inset. The scrim now covers the full picture
in overlay and book; the bottom margin counts from the picture edge (at least 8 pt past the
safe edge).
The menu row was six unlabeled circles with Quality hidden behind More. It is now Quality,
Subtitles, Audio, Chapters, Speed and Stats as labeled glass capsules: beside the title when they
fit, else on their own row under the scrubber, else icon-only circles in the same order. Quality
is always present (dimmed when the source only plays at original quality); More is gone. The
transport adds smaller −30/+30 circles outside ±10 when the row has room. Verified on the Duo
simulator folded (678 pt: labeled row + ±30) and by the full mobile fixture UI suite.
Stats colors: green now means only "plays as-is" on every platform; copy and
encode are neutral gray (told apart by title and link label), matching the
iOS design spec instead of cyan/orange on visionOS, tvOS and macOS.

Connection: any custom DNS name was always "Unknown". Opening Stats now
resolves the host on the device and classifies the address against this
device's interfaces: same physical subnet is Local; a tunnel subnet, the
100.64.0.0/10 overlay range or Tailscale's IPv6 range is VPN; other private
addresses are VPN while a tunnel carries an address, else Local; public is
Remote. The host-only answer stays as the fallback, and the host never
leaves the resolver.
The soak defaulted to the downloads-overhaul staged app and bundle suffix, so it would
not find the app from main or another worktree. Mirror deploy-macos-to-host.sh: the
LABSTREAM_MAC_BUNDLE_ID_SUFFIX env, else the current branch, sanitized the same way.
… main

- Releases: one current-status summary, a corrected product table (latest processed is
  1.8.0 build 1653, not 1.7.2 build 1513) and newest-first build tables for 1.8.0 and 1.7.2.
- Downloads plan: status, P7 (#389) and P7.7 marked done; admission caps follow #408/#414.
- iPhone Duo plan: player spec revised for labeled menus/±30 (ff57f33), list+detail
  remnants removed, journal entry for the 2026-10-30..11-02 follow-ups; dated file name.
- Downloads/offline, Settings, CODE-MAP, macOS, iOS, playback and the checklist: compact
  iPhone/iPad rows, no detail page, #414 prep slots and render deadline, #416 speed, new
  download files, Stats palette/VPN checks.
- scripts/README: duo-appresize, research harness, signal host, lease and soak flags.
- Index pages, README, CLAUDE.md: Duo, headless playback testing, Xcode lease pointers;
  dated research note name.
@jlipworth

Copy link
Copy Markdown
Owner Author

Local checks on head 85da348a, run 2026-11-05:

  • native-test-matrix.py affected --base origin/main:
    • pmskit-correctness: 1800 tests passed.
    • Builds: visionOS, mobile, tvOS (build-for-testing) and macOS all succeeded.
    • macos-hosted: the first run failed with 1 issue in 988 tests. DownloadSignOutPauseTests.coordinatedSignOutPausesEveryBackendWithoutDeletingRecordsOrFiles caught EPERM (Cocoa 513) while removing a download-test-<UUID> fixture root under parallel load. That suite then passed 3 out of 3 times in isolation, and a full rerun passed all 988 tests. I'm treating it as a fixture-cleanup flake (see Tests: downloads fixtures leak hidden temp directories, slowing startup-recovery tests #411), not a sign-out defect.
  • scripts/ci-hygiene.sh: ok. Links, anchors and Mermaid (15/15) all pass.
  • visionOS sim smoke: clean build, install UUID matched, and the app stayed alive. It reached Plex Home with Continue Watching and On Deck. The log showed no faults.
  • iPhone sim smoke: clean build, install UUID matched, and the app stayed alive on the sign-in screen. This worktree's iPhone sim is not signed in. The log showed no faults.
  • Not run: the iPhone/Duo/tvOS hosted and UI-smoke lanes (they need the simulator lease), and any live-backend or hardware acceptance.

@jlipworth
jlipworth merged commit 0b7f3fc into main Nov 5, 2026
5 checks passed
@jlipworth
jlipworth deleted the downloads/overhaul branch November 5, 2026 15:39
jlipworth added a commit that referenced this pull request Nov 5, 2026
Records the first 1.8.0 TestFlight build from `main` (`27b6bf5a`, after
#425 and #427): it processed VALID on all four platforms and is in
Labstream Internal.

Also records that the four App Store Connect drafts were renamed from
1.7.2 to 1.8.0 with build 1654 selected (nothing submitted), and adds a
note that `--build-number auto` is below the integration-branch builds
until `main`'s commit count passes 1654.

Docs only. `scripts/ci-hygiene.sh` passed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant