Skip to content

fix(ios): rank sync routes by health, not interface presence#813

Merged
arul28 merged 1 commit into
mainfrom
ade/accounts-connections-c97b1711
Jul 14, 2026
Merged

fix(ios): rank sync routes by health, not interface presence#813
arul28 merged 1 commit into
mainfrom
ade/accounts-connections-c97b1711

Conversation

@arul28

@arul28 arul28 commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Phase 1 — Accounts / Machine-Directory / Connection-Reliability initiative

Fixes a diagnosed iOS remote-connect reliability bug. Standalone and independently shippable — no account/identity dependency.

The bug

iOS ordered connection routes by interface presence, not route health:

  • relay was appended dead-last whenever a tailnet interface existed;
  • a dead tailnet host expanded into a 213-port sweep (8787–8999) before relay was ever dialed;
  • reconnect re-preferred tailnet on a 20s timer with no memory of the last-good route.

Net effect: slow, flaky reconnects away from home — the exact case the relay exists to serve.

The fix (ports desktop's proven model to iOS)

  • Rank by reachability → kind (lan < tailnet < relay) → last-succeeded-at within kind. Relay is first-class (never presence-demoted); a live LAN always beats a sticky relay, so returning to a home network can no longer lock the phone onto the cloud relay.
  • Bounded port fallback to 9 ports (was 213).
  • Last-good stickiness persisted per-endpoint (lastSucceededAt), stamped at the hello health gate.
  • Subtle Connected in 0.3s · tailnet pill so the improvement is felt.
  • Removed the 20s tailnet-preference heuristic and now-dead plumbing (syncRelayAwareEndpointAttempts wrapper, preferTailnet boolean).

Invariants preserved

Health-gated connect (.connected only after a valid hello RPC), DPoP, and pairing are unchanged.

Testing

  • 70 SyncService XCTest cases pass, including new regression tests: relay reached before the fallback sweep regardless of interface; live LAN ahead of a sticky relay + last-good within kind; winning-route-only success stamping; socket-open-without-hello does not connect.
  • iOS Simulator xcodebuild build green.

Reviewed via a two-track independent review (correctness/security + maintainability) that caught and fixed a relay-lock-in regression before this PR was opened.

🤖 Generated with Claude Code

ADE   Open in ADE  ·  ade/accounts-connections-c97b1711 branch  ·  PR #813

Greptile Summary

This PR updates iOS sync reconnect routing to choose routes by health instead of interface presence. The main changes are:

  • Adds persisted per-endpoint last-success state to host connection profiles.
  • Ranks LAN, tailnet, and relay attempts by liveness, route kind, and last successful connect time.
  • Bounds direct port fallback to the default 9-port set before alternate attempts.
  • Stamps the winning route after the hello health gate succeeds.
  • Adds a connected-route chip in Settings showing route kind and connect duration.
  • Updates tests for route ranking, endpoint-state stamping, bounded fallback, and the new route chip.

Confidence Score: 5/5

Safe to merge with low risk.

The routing changes are localized, keep the existing hello-gated connection contract, and store new profile state as optional Codable fields for compatibility.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Reviewed the iOS XCTest attempt log to confirm the run command, working directory, availability checks, attempted XCTest invocation, and the exit code 127 blocker.
  • Opened ios-sync-regression-test-scan.md to verify the exact test function names, line ranges, and assertion snippets for the requested SyncService regression cases.
  • Verified that none of the executable tests contradicted the PR's stated behavior.
  • Identified missing Xcode tooling as an environment blocker that prevented XCTest execution rather than indicating a product defect.
  • Recorded and prepared the two artifacts for reviewer inspection, ensuring the log and regression evidence are available.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
apps/ios/ADE/Models/RemoteModels.swift Adds optional persisted endpoint health state to host profiles while preserving legacy decoding compatibility.
apps/ios/ADE/Services/SyncService.swift Reworks iOS reconnect candidate ordering around bounded port fallback, route health ranking, success stamping after hello, and connection timing metrics.
apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift Passes route timing and route kind state into the settings snapshot and removes the old tailnet-preference reconnect plumbing.
apps/ios/ADE/Views/Settings/SettingsConnectionHeader.swift Adds the connected-route chip formatting and updates reconnect actions to the simplified callback.
apps/ios/ADE/Views/Settings/SettingsPairingSection.swift Updates saved-host reconnect buttons to use health-ranked reconnect behavior instead of forcing tailnet preference.
apps/ios/ADETests/ADETests.swift Covers bounded port fallback, ranked route ordering, endpoint-state stamping, and the settings route chip behavior.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as Settings UI
participant Sync as SyncService
participant Rank as syncRankedEndpointAttempts
participant Route as LAN/Tailnet/Relay route
participant Profile as HostConnectionProfile

UI->>Sync: reconnectIfPossible()
Sync->>Sync: collect live + saved direct addresses
Sync->>Sync: collect saved relay candidates
Sync->>Rank: direct attempts, relay routes, endpointStates, live routes
Rank-->>Sync: ranked primary attempts, then bounded fallback ports
loop until hello succeeds
    Sync->>Route: open websocket attempt
    Route-->>Sync: socket open or failure
    Sync->>Route: hello health gate
    Route-->>Sync: hello_ok or failure
end
Sync->>Profile: stamp winning endpoint lastSucceededAt
Sync->>UI: publish route kind + connect duration
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI as Settings UI
participant Sync as SyncService
participant Rank as syncRankedEndpointAttempts
participant Route as LAN/Tailnet/Relay route
participant Profile as HostConnectionProfile

UI->>Sync: reconnectIfPossible()
Sync->>Sync: collect live + saved direct addresses
Sync->>Sync: collect saved relay candidates
Sync->>Rank: direct attempts, relay routes, endpointStates, live routes
Rank-->>Sync: ranked primary attempts, then bounded fallback ports
loop until hello succeeds
    Sync->>Route: open websocket attempt
    Route-->>Sync: socket open or failure
    Sync->>Route: hello health gate
    Route-->>Sync: hello_ok or failure
end
Sync->>Profile: stamp winning endpoint lastSucceededAt
Sync->>UI: publish route kind + connect duration
Loading

Reviews (1): Last reviewed commit: "fix(ios): rank sync routes by health, no..." | Re-trigger Greptile

Summary by CodeRabbit

  • New Features

    • Connection profiles now remember endpoint health and recent successful routes.
    • Reconnection automatically prioritizes the most reliable available direct or relay route.
    • Connection settings display connection duration and route type, such as LAN, tailnet, or relay.
  • Improvements

    • Live-discovered connection addresses are prioritized over older cached addresses.
    • Connection discovery and fallback attempts are more focused and efficient.
    • Reconnect actions now use streamlined automatic route selection.

iOS remote-connect ordered routes by interface presence, not route
health: relay was appended dead-last whenever a tailnet interface was
present, and a dead tailnet host expanded into a 213-port sweep
(8787-8999) before relay was ever dialed. Reconnect re-preferred tailnet
on a 20s timer with no memory of the last-good route.

Port desktop's proven model to iOS:
- rank candidates by reachability, then kind (lan<tailnet<relay), then
  last-succeeded-at within kind. Relay is first-class (never presence-
  demoted) and a live LAN always beats a sticky relay, so returning home
  can no longer lock the phone onto the cloud relay.
- bound port fallback to 9 ports (was 213)
- persist per-endpoint lastSucceededAt, stamped at the hello health gate
- add a subtle "Connected in 0.3s / tailnet" pill so the win is felt
- remove the 20s tailnet-preference heuristic and now-dead plumbing

Health-gated connect (hello RPC), DPoP, and pairing are unchanged. 70
SyncService tests pass; iOS Simulator build is green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Jul 14, 2026 6:54pm

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 79a97a2b-3190-4cc3-8533-3a2e99bcb901

📥 Commits

Reviewing files that changed from the base of the PR and between 80b9ab4 and 6d55db3.

📒 Files selected for processing (6)
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift
  • apps/ios/ADE/Views/Settings/SettingsConnectionHeader.swift
  • apps/ios/ADE/Views/Settings/SettingsPairingSection.swift
  • apps/ios/ADETests/ADETests.swift

📝 Walkthrough

Walkthrough

The iOS connection stack now persists endpoint success history, ranks direct and relay routes, records connection timing and route metadata, simplifies reconnect APIs, and displays the latest connection telemetry in Settings.

Changes

Connection health and telemetry

Layer / File(s) Summary
Endpoint state contract
apps/ios/ADE/Models/RemoteModels.swift
HostConnectionProfile persists optional per-endpoint success timestamps while remaining compatible with older profiles.
Endpoint discovery and ranking
apps/ios/ADE/Services/SyncService.swift
Direct and relay candidates are deduplicated and ranked using live availability, route kind, and persisted success history.
Reconnect and connection lifecycle
apps/ios/ADE/Services/SyncService.swift
Reconnect APIs no longer accept tailnet preference flags, and connection timing state is started, published, or cleared across connection transitions.
Successful connection persistence
apps/ios/ADE/Services/SyncService.swift
Successful hello_ok responses record duration, route kind, endpoint success state, and updated relay/tailnet profile data.
Settings connection presentation
apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift, apps/ios/ADE/Views/Settings/SettingsConnectionHeader.swift, apps/ios/ADE/Views/Settings/SettingsPairingSection.swift
Settings snapshots and connected status pills expose connection duration and route kind, while reconnect actions use simplified callbacks.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • arul28/ADE#210: Also removes the legacy tailnet-preference reconnect flow in SyncService.
  • arul28/ADE#254: Also changes iOS connection state handling and Settings connection presentation.

Suggested labels: ios

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main iOS routing change: ranking sync routes by health instead of interface presence.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/accounts-connections-c97b1711

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.

@arul28
arul28 merged commit 8ae1734 into main Jul 14, 2026
33 checks passed
@arul28
arul28 deleted the ade/accounts-connections-c97b1711 branch July 14, 2026 22:10
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