Skip to content

feat(remote): recognize servers across addresses and fix SiloRemote routing - #342

Merged
Quick104 merged 1 commit into
mainfrom
t3code/788f8ca7
Sep 21, 2026
Merged

Quick104 merged 1 commit into
mainfrom
t3code/788f8ca7

Conversation

@Quick104

@Quick104 Quick104 commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A phone signed in through a network-plugin address (Tailscale) and an Apple TV signed in at the public address are the same server, but the clients derive server identity from the URL, so the remote-only picker hides the TV and a profile handoff forces the TV onto the phone's address even when it cannot reach it. Companion setup has the same reachability gap and reports every failure as auth_failed.

Testing the fix on shared-dev surfaced several pre-existing SiloRemote bugs: playback sometimes started on the phone while a TV was engaged, sending a second title to a TV failed, and the remote's scrubber intermittently did nothing.

Closes #341. Server contract: Silo-Server/silo-server#1271 (deployed to shared-dev). Sibling: Silo-Server/silo-android#352.

Solution

Identity across addresses

  • ServerEntry gains an optional verifiedServerId learned from GET /api/v2/system/identity when a server is added, on foreground refresh, and when pairing persists. Registry keys and credential slots are unchanged. ServerRegistry.serversMatch accepts equal identities alongside the existing origin rule.
  • SiloControl: the TXT record, hello, and handoff_offer carry serverIdentity, and the offer carries serverEndpoints from GET /api/v2/system/connections. Protocol version stays 2; older peers ignore the keys. The TV probes its own saved address, the phone's, then public and provider endpoints, and uses the first that answers with the expected identity. An address answering with another identity is refused (identity_mismatch); none reachable yields server_unreachable with provider help.
  • Companion pairing: pushServer carries serverIdentity and endpoints (protocol stays v1). The TV probes the pushed address and, when unreachable, shows help naming the provider from its manifest display name with an explicit "Use " choice. Nothing switches without the user. deviceStarted and serverResult always echo the pushed URL; the TV saves the address that worked. serverResult.error is now one of auth_failed, denied, expired, unreachable, identity_mismatch, and the phone summarises it.

SiloRemote routing and playback

  • SiloControlClient.remotePlaybackEngaged is the single "TV engaged" predicate (true through reconnect, false during a silent auto-resume probe). The mode button, mini-bar, and routing all read it.
  • AppRouter.presentPlayer routes every streaming play through an interceptor installed by the root view, so an engaged TV takes the request and the local player never opens. This covers the home rail play badge, deep links, and the stale "Try Anyway" alerts without per-site checks. A play during reconnect waits for the link instead of falling through. Offline plays prompt for phone or TV. PiP restore is skipped while a TV is engaged.
  • Playing a different title while the TV is mid-title asks before replacing it.
  • The phone accepts a reused handoff_ready with no challenge (previously every second title timed out), and the TV hands the temporary identity generation to a replacing player instead of ending it mid-load.
  • The remote scrubber commits on value settle rather than only on the slider's end-of-edit callback, which SwiftUI does not reliably deliver; a missed callback pinned the slider and swallowed every later drag.
  • Connecting to a server from Change Server pops the login stack so the setup screen no longer appears stuck.

Validation

  • iOS and tvOS compile.
  • Unit tests: identity matching, resolver, pairing protocol and both coordinators, SiloControl, wire compatibility, routing, contract state. 96 tests, 0 failures. New coverage: identity matching and persistence, probe classification, wire round-trips, candidate ordering, receiver unreachable/alternate/mismatch/legacy flows, companion push contents and error codes, routing interception and replace-confirmation.
  • Simulator, iOS against tvOS on shared-dev: TV listed by identity from a different hostname; send title; replace confirmation switches the TV; scrub while playing, while paused, and repeatedly while paused reaches the TV and tracking resumes; pause/play cycles keep the clock within half a second.
  • Physical iPhone 18 Pro against the tvOS simulator: companion setup, remote playback, and scrubbing exercised by the developer.
  • AccountSessionPersistenceTests fails on this simulator with keychain error -34018 both with and without this change; confirmed on a clean baseline checkout.

Risks and follow-up

  • Physical Apple TV runs against the Tailscale/public split in issue feat(pairing): handle different server addresses in SiloRemote and TV setup #341 have not been recorded yet. The bedroom TV needs this build for the replace-title path to work.
  • The identity is self-asserted; device-login approval remains the authorization gate, matching the server design.
  • Servers without the identity contract behave exactly as before.

AI disclosure

  • Harness: Claude Code in T3 Code
  • Model: claude-fable-5-1
  • Involvement: Fully AI-generated, human tested on device
  • Adversarial review: n/a

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added smarter remote playback routing to engaged TVs, including replacement and offline-play choices.
    • Added deployment identity verification and alternate server address support during pairing and TV handoff.
    • Added clearer pairing failure messages with retry, alternate-address, and cancellation options.
  • Bug Fixes
    • Improved remote control visibility and matching across server addresses.
    • Fixed playback scrubber previews remaining stuck after interrupted gestures.
    • Prevented Picture-in-Picture restoration while a TV is actively engaged.
    • Improved setup navigation after changing or adding a server.

…outing

Consume the server identity contract (Silo-Server/silo-server#1271) so a
phone on a network-plugin address and a TV on the public address recognize
one deployment, and fix a cluster of SiloRemote playback bugs found while
testing it.

Identity (#341):
- Registry entries learn a verified deployment identity; matching accepts
  equal identities alongside the existing origin rule. Registry keys and
  credential slots are unchanged.
- SiloControl hello, TXT record, and handoff offer carry the identity and
  the deployment's other addresses. The TV probes candidates and uses the
  first that answers with the expected identity; a different identity is
  refused.
- Companion pairing pushes identity and endpoints. The TV probes the pushed
  address, and on failure shows provider help with an explicit public
  fallback. Frames echo the pushed URL; the TV saves the address that
  worked. Failures now carry typed codes.

SiloRemote routing and playback:
- One "engaged" predicate drives the mode button, mini-bar, and routing.
- Every streaming play goes through the router, so an engaged TV (including
  mid-reconnect) always takes it; offline plays prompt.
- Playing a different title asks before replacing what the TV is showing.
- The phone accepts a reused handoff_ready without a challenge, and the TV
  hands the identity generation to a replacing player.
- The remote scrubber commits on value settle, so a missed end-of-edit
  callback no longer pins the slider and swallows later drags.
- Connecting to a server from the Change Server flow pops the stack.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick104 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 50402e0f-78cf-4bfe-906b-a802e792efa1

📥 Commits

Reviewing files that changed from the base of the PR and between c23903c and 0135568.

📒 Files selected for processing (31)
  • iosApp/Tests/PairingCoordinatorTests.swift
  • iosApp/Tests/PairingProtocolTests.swift
  • iosApp/Tests/RemotePlaybackRoutingTests.swift
  • iosApp/Tests/ServerIdentityMatchingTests.swift
  • iosApp/Tests/ServerIdentityResolverTests.swift
  • iosApp/Tests/SiloControlTests.swift
  • iosApp/iosApp/ContentView.swift
  • iosApp/iosApp/Control/SiloControlProtocol.swift
  • iosApp/iosApp/Control/iOS/NowPlayingShelf.swift
  • iosApp/iosApp/Control/iOS/SiloControlBrowser.swift
  • iosApp/iosApp/Control/iOS/SiloControlClient.swift
  • iosApp/iosApp/Control/iOS/SiloControlMiniBar.swift
  • iosApp/iosApp/Control/iOS/SiloControlModeButton.swift
  • iosApp/iosApp/Control/iOS/SiloControlRemoteView.swift
  • iosApp/iosApp/Control/iOS/SiloControlTargetPickerView.swift
  • iosApp/iosApp/Control/tvOS/RemotePlaybackIdentityManager.swift
  • iosApp/iosApp/Control/tvOS/TVControlReceiver.swift
  • iosApp/iosApp/Navigation/AppRouter.swift
  • iosApp/iosApp/Networking/HTTPClient.swift
  • iosApp/iosApp/Networking/ServerIdentity.swift
  • iosApp/iosApp/Networking/ServerIdentityResolver.swift
  • iosApp/iosApp/Networking/ServerRegistry.swift
  • iosApp/iosApp/Pairing/Companion/CompanionPairingCard.swift
  • iosApp/iosApp/Pairing/Companion/CompanionPairingCoordinator.swift
  • iosApp/iosApp/Pairing/PairingProtocol.swift
  • iosApp/iosApp/Pairing/Receiver/ReceiverPairingCoordinator.swift
  • iosApp/iosApp/Pairing/Receiver/TVPairingReceiverView.swift
  • iosApp/iosApp/Screens/Auth/AuthService.swift
  • iosApp/iosApp/Screens/Auth/ServerSetupViewModel.swift
  • iosApp/iosApp/Screens/Detail/ItemDetailView.swift
  • iosApp/iosApp/Screens/Player/iOS/PlayerPresentationRestoration.swift
 _______________________________________________________
< Your cache invalidation strategy is 'vibes and hope'. >
 -------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Quick104
Quick104 merged commit e350f4d into main Sep 21, 2026
4 of 6 checks passed
@Quick104
Quick104 deleted the t3code/788f8ca7 branch September 21, 2026 15:00
Quick104 added a commit that referenced this pull request Sep 23, 2026
The API v2 and diagnostics contract fixtures are now pinned to server commit `84ed9e596`. The sync script only vendors from an explicit ref, so anyone re-running it at that ref gets the same bytes.

This PR also splits `ContentView.body` so Xcode 26.3 can type-check it. The iOS job of the Apple regression workflow had failed on main since #342 with "unable to type-check this expression in reasonable time". Behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

feat(pairing): handle different server addresses in SiloRemote and TV setup

1 participant