feat(relay): show honest wake-up proof states - #98
Conversation
Query the verified per-homeserver Relay observation only in waiting and diagnostics views, with bounded polling and independent partial results. Keep Relay observation, local wake-up receipt, and sync progress as separate evidence without changing existing-user setup or sync mappings. (#91)
|
Warning Review limit reached
Next review available in: 20 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughCloud Relay now exposes per-device signal observation, bounded polling, and layered sync-proof state. The iOS app records background-sync milestones, presents more precise relay statuses, updates diagnostics, and documents the protocol and privacy behavior. ChangesRelay observation and proof hierarchy
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 7✅ Passed checks (7 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
ios/VaultSync/Services/RelayStatus.swift (1)
15-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winClarify the intentionally-narrow scope of
requiresPollingStop/wasRateLimited.Both computed properties filter by
requestedDeviceIDs, which is deliberately left empty when the global entitlement check fails (line 62, confirmed by the cited test assertingoutcome.requestedDeviceIDs.isEmptyfor that branch). That means a.verificationRequiredfailure set in the entitlement-invalid branch never tripsrequiresPollingStop— polling only stops there because the current caller (pollRelayObservationStatus'sshouldContinue) independently re-checks entitlement every iteration. That's correct today, but it's an easy trap for a future caller (e.g. a differently-scoped polling context) that relies onrequiresPollingStopalone and doesn't duplicate the entitlement check.A short doc comment noting "only reflects failures for devices actually queried this pass; global entitlement/subscription lapses must be checked by the caller's
shouldContinue" would prevent a future regression in this safety-critical proof-hierarchy code.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ios/VaultSync/Services/RelayStatus.swift` around lines 15 - 29, Add a concise documentation comment to RelayStatusCheckOutcome.requiresPollingStop and wasRateLimited explaining that they only consider failures for devices queried in the current pass, while global entitlement or subscription lapses must be checked by the caller’s shouldContinue logic. Preserve the existing requestedDeviceIDs filtering and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@ios/VaultSync/Services/RelayStatus.swift`:
- Around line 15-29: Add a concise documentation comment to
RelayStatusCheckOutcome.requiresPollingStop and wasRateLimited explaining that
they only consider failures for devices queried in the current pass, while
global entitlement or subscription lapses must be checked by the caller’s
shouldContinue logic. Preserve the existing requestedDeviceIDs filtering and
behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: bb27edb9-7d7d-4221-ada3-7b9fb706b9c2
📒 Files selected for processing (17)
CHANGELOG.mdPRIVACY.mddocs/architecture.mddocs/decisions/019-relay-proof-hierarchy.mddocs/relay-spec.mdios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/RelayService.swiftios/VaultSync/Services/RelayStatus.swiftios/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSyncTests/RelayStatusTests.swiftios/VaultSyncTests/TestSupport.swift
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
- GitHub Check: CodeRabbit
🧰 Additional context used
📓 Path-based instructions (10)
ios/**/*.{swift,strings,stringsdict}
📄 CodeRabbit inference engine (README.md)
Support localization in English, German, Spanish, and Simplified Chinese
Files:
ios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/es.lproj/Localizable.stringsios/VaultSyncTests/TestSupport.swiftios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/RelayStatus.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSyncTests/RelayStatusTests.swiftios/VaultSync/Services/RelayService.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Services/SubscriptionManager.swift
**
⚙️ CodeRabbit configuration file
**:![]()
VaultSync
Self-hosted Obsidian vault sync for iPhone and iPad.
Your notes sync peer-to-peer over Syncthing, straight into Obsidian's iOS sandbox — no note cloud, no account, no tracking.![]()
![]()
![]()
🔭 Why VaultSync
- Peer-to-peer & private — syncs directly between your own devices over Syncthing. No note cloud, no account, no tracking.
- Lands in Obsidian — files sync into Obsidian's iOS sandbox, where the app already looks for them.
- Pair by QR, resolve conflicts — connect your server in seconds; settle Markdown conflicts with side-by-side diffs.
- Server changes wake your iPhone — optional Cloud Relay nudges the app the moment your server updates, so incoming notes land eve...
Files:
ios/VaultSync/zh-Hans.lproj/Localizable.stringsdocs/decisions/019-relay-proof-hierarchy.mdios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/es.lproj/Localizable.stringsios/VaultSyncTests/TestSupport.swiftdocs/architecture.mdios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/RelayStatus.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftCHANGELOG.mdios/VaultSyncTests/RelayStatusTests.swiftios/VaultSync/Services/RelayService.swiftios/VaultSync/Views/RelayHomeView.swiftdocs/relay-spec.mdios/VaultSync/Services/SubscriptionManager.swiftPRIVACY.md
**/*
⚙️ CodeRabbit configuration file
**/*: VaultSync syncs private Obsidian notes through Syncthing. Treat data loss,
privacy leaks, security regressions, and broken sync behavior as high priority.
Do not nitpick formatting unless it affects maintainability, correctness, or public API clarity.
Flag any accidental logging, telemetry, crash reporting, or network transfer of note contents,
vault paths, filenames with private context, API keys, APNs tokens, relay keys, or security-scoped bookmark data.
Files:
ios/VaultSync/zh-Hans.lproj/Localizable.stringsdocs/decisions/019-relay-proof-hierarchy.mdios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/es.lproj/Localizable.stringsios/VaultSyncTests/TestSupport.swiftdocs/architecture.mdios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/RelayStatus.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftCHANGELOG.mdios/VaultSyncTests/RelayStatusTests.swiftios/VaultSync/Services/RelayService.swiftios/VaultSync/Views/RelayHomeView.swiftdocs/relay-spec.mdios/VaultSync/Services/SubscriptionManager.swiftPRIVACY.md
docs/decisions/**/*
📄 CodeRabbit inference engine (docs/decisions/019-relay-proof-hierarchy.md)
docs/decisions/**/*: Model StoreKit verification, verified provisioning, backend reachability, per-homeserver v1 observation, local wake-up receipt, background-sync start, and observed sync progress as independent evidence.
A valid debounced v1 signal may update Relay observation, but must not set APNs delivery or sync success.
Normal UI should use plain-language waiting states, while Diagnostics should name the technical boundary and keep multi-homeserver results separate.
Do not treat /health, provisioning, or Relay observation as active delivery, because each skips a stronger downstream leg.
Do not treat silent-push receipt as completed sync; later sync progress must remain independently unproven until observed.
Files:
docs/decisions/019-relay-proof-hierarchy.md
docs/**
⚙️ CodeRabbit configuration file
docs/**: # ArchitectureVaultSync embeds Syncthing's Go reference implementation as an iOS library via gomobile — no reimplementation of the protocol in Swift, and guaranteed wire compatibility.
┌─────────────────────────────────┐ │ SwiftUI Frontend │ iOS-native UI, Swift 6 ├─────────────────────────────────┤ │ Swift ↔ Go Bridge │ thin API via gomobile │ │ → exported as .xcframework ├─────────────────────────────────┤ │ syncthing/lib (Go) │ protocol, discovery, sync └─────────────────────────────────┘ ↕ filesystem ┌─────────────────────────────────┐ │ Obsidian Vault (direct) │ Obsidian's iOS sandbox └─────────────────────────────────┘🔄 Sync strategy
- Foreground — Syncthing runs unrestricted: immediate, continuous sync.
- Background —
BGAppRefreshTask(requested ~15 min out; iOS decides the actual timing) +BGProcessingTask(overnight catch-up: multi-minute budget while charging with network) +BGContinuedProcessingTask(iOS 26+, longer runtime for user-initiated tasks). A ~30s grace window after backgrounding lets in-flight work finish.- Push (Cloud Relay) — optional. Near-realtime
server → iPhonewake-ups via APNs silent push. See relay-spec.md.VaultSync is intentionally asymmetric:
Direction Path Server → iPhone vaultsync-notifyspots outgoing changes → Cloud Relay silent push → VaultSync wakes and pulls.iPhone → Server iOS doesn't guarantee timely background execution for local edits. The reliable path is to open VaultSync and let embedded Syncthing run in the foreground — a Shortcuts automation can do that automatically whenever you leave Obsidian. Cloud Relay is a
server → iPhoneacceleration path, not a guarantee of symmetric real-time background sync.Connection paths & iOS network privacy
How peers are reached, fastest f...
Files:
docs/decisions/019-relay-proof-hierarchy.mddocs/architecture.mddocs/relay-spec.md
docs/decisions/**
⚙️ CodeRabbit configuration file
docs/decisions/**: # 001 — Two Syncthing folders never overlap on disk (three enforcement layers)Context: A server sharing more than one vault could hand the second share the same local folder as the first (1.6.0–1.7.0) or a subfolder inside an existing vault (1.7.1). Overlapping folders sync each other's content as their own — deleting the stray copy on any peer would have deleted the inner vault everywhere (
#45).Decision: The no-overlap invariant (equal, nested, or containing paths) is enforced in three independent layers, each with its own tests: the Go hard floor (
AcceptPendingFolder/AddFolderreject withfolderPathOverlapError), the Swift mapping (VaultManager.resolveSharePathreturnsnilrather than an overlapping path), and the launch shield (PathCollisionGuardpauses already-overlapping folders exactly once).Why: The single-layer version failed twice — both
#45bugs lived in the Swift mapping. The Go floor backstops future mapping bugs, the Swift layer turns a hard engine error into user guidance, and only the shield catches damage that predates the fix.Rejected alternative: Enforcing only in the Swift mapping — the proven failure mode; a bug there would silently re-open the hole with no backstop.
Links:
#45, PR#47(same-folder merge, 1.7.1), PR#51(nesting, 1.7.2).
docs/decisions/**: # 002 — Recovery from data damage is never automaticContext: When VaultSync detects existing damage (two vaults merged into one folder, or one nested inside another,
#45), an automatic repair would have to move, rename, delete, or re-accept user data whose true state only the user knows — after a merge, the app cannot tell which files belong to which vault.Decision: Recovery is always: pause the affected folders (exactly once — a folder the user deliberately resumes is never re-paused), explain the problem in a critical issue with concrete recovery steps, and let the user act.
Why: Sync propagates every local act...
Files:
docs/decisions/019-relay-proof-hierarchy.md
**/*.md
⚙️ CodeRabbit configuration file
**/*.md: Review public documentation for technical accuracy, privacy/security claims, App Store-facing wording,
setup correctness, and consistency with the free app plus optional Cloud Relay subscription model.
Files:
docs/decisions/019-relay-proof-hierarchy.mddocs/architecture.mdCHANGELOG.mddocs/relay-spec.mdPRIVACY.md
**/*.swift
📄 CodeRabbit inference engine (Custom checks)
For Swift background execution changes, pass if work is bounded, cancellation-aware, handles expiration callbacks, and records errors without leaking private vault data. Fail only when background work can continue unbounded, miss cleanup, or violate iOS background execution constraints.
Files:
ios/VaultSyncTests/TestSupport.swiftios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/RelayStatus.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSyncTests/RelayStatusTests.swiftios/VaultSync/Services/RelayService.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Services/SubscriptionManager.swift
ios/**/*.swift
📄 CodeRabbit inference engine (README.md)
ios/**/*.swift: Use Swift 6 and SwiftUI for iOS app development
Implement VoiceOver and Dynamic Type accessibility support throughout the app
UseBGAppRefreshTaskandBGContinuedProcessingTask(iOS 26+ when available) for background sync operations
Use APNs silent push notifications via Cloud Relay for server-to-iPhone wake-ups
Implement side-by-side diff resolution for Markdown file conflicts
Provide an activity timeline and diagnostics interface showing exactly what synced and when
Implement QR code pairing for Syncthing Device ID connection setup
Detect and list available Obsidian vaults automatically upon connection
Files:
ios/VaultSyncTests/TestSupport.swiftios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/RelayStatus.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSyncTests/RelayStatusTests.swiftios/VaultSync/Services/RelayService.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Services/SubscriptionManager.swift
⚙️ CodeRabbit configuration file
ios/**/*.swift: Focus on Swift 6 strict concurrency, Sendable/MainActor correctness, Task cancellation,
retain cycles, memory pressure, SwiftUI observation state, StoreKit/APNs flows, and iOS background execution limits.
Pay special attention to BGAppRefreshTask and BGContinuedProcessingTask behavior, expiration handling,
bounded work, and cleanup when the app is suspended or terminated.
Files:
ios/VaultSyncTests/TestSupport.swiftios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/RelayStatus.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSyncTests/RelayStatusTests.swiftios/VaultSync/Services/RelayService.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Services/SubscriptionManager.swift
ios/**/*.{swift,pbxproj}
📄 CodeRabbit inference engine (README.md)
Target iOS / iPadOS 18 or later as the minimum deployment target
Files:
ios/VaultSyncTests/TestSupport.swiftios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/RelayStatus.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSyncTests/RelayStatusTests.swiftios/VaultSync/Services/RelayService.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Services/SubscriptionManager.swift
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T08:16:59.280Z
Learning: Use `BGAppRefreshTask`, `BGProcessingTask`, and, on iOS 26+, `BGContinuedProcessingTask` according to their intended background-sync roles, while allowing roughly 30 seconds for in-flight work after backgrounding.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T08:16:59.280Z
Learning: Treat Cloud Relay as an optional server-to-iPhone acceleration path, not as guaranteed symmetric real-time background synchronization.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T08:16:59.280Z
Learning: Keep entitlement, relay provisioning, relay reachability, relay observation, push receipt, background sync start, and local sync progress as separate proofs; none automatically implies the next.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T08:16:59.280Z
Learning: Use direct LAN, direct WAN, and Syncthing relay connection paths in that priority order, respecting iOS Local Network permission and multicast entitlement requirements.
📚 Learning: 2026-06-10T18:47:10.724Z
Learnt from: psimaker
Repo: psimaker/vaultsync PR: 38
File: ios/VaultSync/Views/ContentView.swift:605-611
Timestamp: 2026-06-10T18:47:10.724Z
Learning: In the SwiftUI codebase under ios/VaultSync, do not flag missing localization for SwiftUI string literals used as Text("…") or DisclosureGroup("…") titles/labels. In SwiftUI, these string literals are treated as LocalizedStringKey and resolve via the app’s Localizable.strings automatically—so they only need attention if the corresponding key is actually missing. Only require an explicit localization helper (e.g., L10n.tr(…)) when the string is not being passed through SwiftUI’s LocalizedStringKey path (e.g., plain String values provided to non-SwiftUI APIs).
Applied to files:
ios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/RelayStatus.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Services/RelayService.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Services/SubscriptionManager.swift
🪛 LanguageTool
docs/decisions/019-relay-proof-hierarchy.md
[style] ~4-~4: The words ‘observation’ and ‘observed’ are quite similar. Consider replacing ‘observed’ with a different word.
Context: ...-up receipt, background-sync start, and observed sync progress as independent evidence. ...
(VERB_NOUN_SENT_LEVEL_REP)
docs/relay-spec.md
[style] ~310-~310: The words ‘observation’ and ‘observed’ are quite similar. Consider replacing ‘observed’ with a different word.
Context: ...ush receipt, background-sync start, and observed local sync progress as separate evidenc...
(VERB_NOUN_SENT_LEVEL_REP)
🔇 Additional comments (21)
ios/VaultSyncTests/RelayStatusTests.swift (1)
35-53: LGTM!Also applies to: 55-77, 79-93, 95-106, 108-141, 143-171, 173-179, 181-201, 203-217, 219-232, 234-252, 254-280, 282-299, 301-322
ios/VaultSyncTests/TestSupport.swift (1)
32-33: LGTM!CHANGELOG.md (1)
9-20: LGTM!PRIVACY.md (1)
3-3: LGTM!Also applies to: 18-30, 47-47
docs/architecture.md (1)
35-45: LGTM!docs/decisions/019-relay-proof-hierarchy.md (1)
1-11: LGTM!docs/relay-spec.md (1)
3-3: LGTM! The new/statuswire contract (request fields, response shapes, verification rules) matches the iOS client'smakeStatusRequest/decodeStatusResponseimplementation, and the polling cadence description (immediate + 15/30/60/120s, 5-attempt cap) matchesRelayStatusPollingPolicy.waitingView.Also applies to: 72-84, 154-159, 174-210, 222-225, 255-255, 266-266, 301-313
ios/VaultSync/App/AppDelegate.swift (1)
62-81: LGTM!markBackgroundSyncStartedis recorded unconditionally as an attempt marker, andmarkSyncProgressObservedis only set on.synced, keeping the proof ladder from overclaiming — consistent with decision 019.ios/VaultSync/Services/RelayService.swift (1)
136-136: LGTM! Dropping the raw response body fromlogger.errorwhile still preserving it in on-devicerecordLastErrordiagnostics is a sensible privacy-hardening tweak, and the newfetchStatus/makeStatusRequest/decodeStatusResponsetrio correctly gates on a verified entitlement, matches the documented/statuswire contract, and validatestriggerObserved == (lastObservedAt != nil)before returning.Also applies to: 183-280, 426-434
ios/VaultSync/Services/RelayStatus.swift (1)
1-14: LGTM!RelayStatusChecking/RelayStatusPollingare correctly@MainActor-isolated, the bounded-poll loop'sretryDelays[attempts - 1]indexing is safe for all attempt counts,Task.sleepcancellation is handled via thedo/catchbreak, andRelayStatusPresentation.status's priority ordering (local wake-up → failure → observation staleness) matches the documented proof hierarchy.Also applies to: 30-276
ios/VaultSync/Services/SubscriptionManager.swift (4)
45-47: 🗄️ Data Integrity & Integration | ⚡ Quick winNew observation/failure caches never clear on deprovision — stale proof survives a resubscribe.
relayServerObservations/relayStatusFailuresare written bycheckRelayObservationStatusbut nothing resets them.deprovisionRelay()(line 748) already resetsrelayProvisionStatusesand clears the "connected-celebrated" flag on every transition to inactive, but leaves these two new dictionaries untouched. After a lapse + resubscribe,RelayDiagnosticsView's "Server Signal Observation" section can briefly show a pre-lapselastTriggerObservedAt/checkedAtas if it were current — exactly the kind of stale-evidence-presented-as-fresh problem this PR is otherwise careful to avoid.🧹 Proposed fix
private func deprovisionRelay() async { let deviceIDs = loadStoredDeviceIDs() ensureProvisionStateEntries(for: deviceIDs) for deviceID in relayProvisionStatuses.keys { relayProvisionStatuses[deviceID] = .notAttempted } + // Clear relay-observation evidence too, so a later resubscribe never + // presents a pre-lapse timestamp as current. + relayServerObservations = [:] + relayStatusFailures = [:] RelayProvisionStatusStore.save(
61-61: LGTM!Also applies to: 77-91
472-529: 🩺 Stability & Availability | ⚡ Quick winConcurrent
checkRelayObservationStatuscalls can overwrite each other's fresher results.
checkRelayObservationStatussnapshotsrelayServerObservations/relayStatusFailuresat call start, awaits the network round-trip, then assigns the whole dictionaries back from that snapshot + its own outcome (lines 492-493).relayStatusCheckInFlightis set/cleared but never checked, so it's cosmetic, not a guard.This is reachable in practice, not just theoretically:
pollRelayObservationStatus(called from bothRelayHomeView.taskandRelayDiagnosticsView.task) retries in a loop, andRelayDiagnosticsViewalso exposes a "Check Relay Status" button that callscheckRelayObservationStatusdirectly. Since aNavigationLinkpush doesn't cancel the source view's.task, andRelayHomeVieweven adds a dedicated link intoRelayDiagnosticsViewspecifically while its own poll loop is active (.relayObservedWaitingForWakeUp, lines 180-193 inRelayHomeView.swift), two overlapping calls against different device-ID subsets are a normal user flow, not an edge case. Whichever call's network round-trip finishes last wins and can clobber a device's freshly-observed data with the stale snapshot the losing call started with. It self-heals on the next successful check, but it works against the exact goal of this PR (accurate, non-stale proof state).🔒 Proposed reentrancy guard
`@discardableResult` func checkRelayObservationStatus( homeserverDeviceIDs: [String] ) async -> RelayStatusCheckOutcome { + guard !relayStatusCheckInFlight else { + // A concurrent check is already in flight (Home + Diagnostics can both + // poll at once) — don't race it with a second snapshot/overwrite. + return RelayStatusCheckOutcome( + observations: relayServerObservations, + failures: relayStatusFailures, + requestedDeviceIDs: [] + ) + } relayStatusCheckInFlight = true defer { relayStatusCheckInFlight = false } let outcome = await RelayStatusChecking.run(Note the polling loop can also produce a similar artifact on plain task cancellation:
RelayStatusPolling.run'scheck()call isn't itself cancellation-aware mid-flight, so a check already in flight when the view disappears can still complete and write a spurious.temporarilyUnavailablefailure. The guard above incidentally helps here too, since a fresh call after re-appearing won't race the tail of the cancelled one.
531-570: LGTM! Priority-basedrelayUserStatusaggregation andrelayProofSnapshot's field set map cleanly onto the independent-evidence-layers model (StoreKit verification, provisioning, backend health, relay observation, silent-push receipt, background-sync start, sync progress kept separate, none implying the next).ios/VaultSync/Views/RelayDiagnosticsView.swift (1)
16-31: LGTM! The keyed.task, the two new "Last background sync start"/"Last observed sync progress" rows, the per-deviceobservationSection, andstatusFailureText/runDiagnostics(checkObservationStatus:)are all consistent and correctly localized across en/de/es/zh-Hans. (See the companion note onSubscriptionManager.checkRelayObservationStatusfor a cross-view concurrency concern that affects this screen's polling but originates there.)Also applies to: 289-306, 334-381, 542-562
ios/VaultSync/Views/RelayHomeView.swift (2)
29-31: LGTM!relayUserStatus-driven state (celebration, active, and the newrelayObservedWaitingForWakeUpdiagnostics prompt) reads correctly and keeps the messaging honest about delivery vs. sync completion.Also applies to: 43-50, 127-132, 142-145, 155-156, 169-193
234-277: 📐 Maintainability & Code Quality | 💤 Low valueSix near-identical branches — consider collapsing on badge style.
Every case except
.wakeUpReceivedrepeats the sameStatusBadge(...) + Text(...)pair, differing only in theStatusBadgestyle. A small helper keeps this from drifting as moreRelayUserStatuscases get added.♻️ Illustrative consolidation
`@ViewBuilder` private var relayWaitingStatus: some View { - switch relayUserStatus { - case .checking: - StatusBadge(.starting, text: relayUserStatus.userFacingTitle) - .font(.headline) - Text(relayUserStatus.userFacingDetail) - .font(.subheadline) - .foregroundStyle(.secondary) - case .waitingForFirstSignal: - StatusBadge(.attention, text: relayUserStatus.userFacingTitle) - ... - ... - case .wakeUpReceived: - EmptyView() - } + if relayUserStatus == .wakeUpReceived { + EmptyView() + } else { + StatusBadge(badgeStyle(for: relayUserStatus), text: relayUserStatus.userFacingTitle) + .font(.headline) + Text(relayUserStatus.userFacingDetail) + .font(.subheadline) + .foregroundStyle(.secondary) + } +} + +private func badgeStyle(for status: RelayUserStatus) -> StatusBadge.Style { + switch status { + case .checking, .relayObservedWithinGrace: return .starting + case .waitingForFirstSignal, .relayObservedWaitingForWakeUp, .statusUnavailable: return .attention + case .quietCanBeNormal: return .paused + case .wakeUpReceived: return .starting // unreachable, guarded above + } }ios/VaultSync/de.lproj/Localizable.strings (1)
727-761: LGTM! New relay-observation strings are complete and consistent with the other locales.ios/VaultSync/en.lproj/Localizable.strings (1)
727-761: LGTM! Base English strings for the new relay-observation/proof-hierarchy section are complete and match what's referenced fromSubscriptionManager.swift,RelayDiagnosticsView.swift, andRelayHomeView.swift.ios/VaultSync/es.lproj/Localizable.strings (1)
727-761: LGTM! Translations are complete and match the English key set 1:1, including format specifiers.ios/VaultSync/zh-Hans.lproj/Localizable.strings (1)
727-761: LGTM! Translations are complete and match the English key set 1:1, including format specifiers.


Query the verified per-homeserver Relay observation only in waiting and diagnostics views, with bounded polling and independent partial results. Keep Relay observation, local wake-up receipt, and sync progress as separate evidence without changing existing-user setup or sync mappings. (#91)
What & why
Component(s)
Testing
cd go && make patch && go test -tags noassets ./bridgecd notify && go test ./...xcodebuild testSummary