feat: faster cold-start reconnect, auto-resolved settings conflicts, one-step Cloud Relay setup - #41
Conversation
Three layers, ordered by impact: - Request the iOS Local Network permission (NSLocalNetworkUsageDescription). Without it, iOS 14+ silently blocks direct TCP/QUIC dials to peers on the same Wi-Fi, forcing every connection through public relays — the slowest possible path. Local multicast discovery additionally needs the restricted multicast entitlement (Apple approval required); documented in docs/architecture.md. - Cache the last-known-good address of every successful outbound connection in the device config (bridge addresscache.go), so the first dial after a cold start goes straight to where the peer was last reachable instead of waiting for a global-discovery round trip. Only default-'dynamic' address lists are managed; user-set static addresses are never touched. - Fork patch 003: minConnectionLoopSleep 5s -> 1s (the initial dial rampup was clamped to 5s rounds while the discovery cache is still empty) and TCP dial timeout 10s -> 5s (a stale cached LAN address must fail over to the relay path quickly).
Reconnecting after a cold start is normal warm-up, not a failure — the UI now treats it that way on every surface, not just the header: - Startup grace: disconnects observed within 30s of engine start get a 60s grace window (measured from engine start); mid-session disconnects keep the 30s window. All devices are tracked so the Devices tab can show per-device state. - Header: keeps its positive title with a busy spinner and 'Connecting to <device>…' subtitle instead of switching the title to 'Reconnecting…'. - Dashboard: neutral 'Connecting to devices…' row while every disconnected device is still inside its grace window, instead of an orange '0 of N devices connected'. - Devices list + detail: spinner + 'Connecting…' during grace, then a neutral gray 'Offline' (moon glyph). The red ✕ 'Disconnected' treatment is gone; paused devices now read 'Paused'. - Engine start moved off the main actor: start() is async with an in-flight guard, so certificate/config/database loading no longer blocks the launch frame. New strings localized in en/de/es/zh-Hans; obsolete 'Reconnecting…' / 'Restoring connection…' / 'Disconnected' keys removed. Grace-period tests extended for the startup window and the per-device API.
…pies Conflicts on Obsidian app-state files (anything inside a .obsidian directory, any depth) are now resolved automatically with last-writer-wins: the newer version becomes the original, the conflict copy is removed, and a copy whose original disappeared is promoted instead of deleted so nothing is lost. Runs in the foreground poll (gated on the conflict scan actually containing a state conflict) and in the background-sync path before the conflict notification fires. Notes are exempt by design; opt-out toggle in Settings -> Conflicts. The home-screen issue banner, vault badges, and conflict notifications now count distinct conflicted files instead of conflict copies - with MaxConflicts=10 a single churn-prone file used to read as ten conflicts for what is one decision. New Go bridge API AutoResolveStateConflicts (+tests), Swift tests for state-conflict classification and distinct counting, localized in en/de/es/zh-Hans.
…d UI The one-line installer is the entire setup for most users, but README, the helper guide, and the in-app setup screen still presented it as one option among many. Now the curl one-liner is the centerpiece everywhere and every manual path is tucked behind expandable sections: - README: "One-step setup" with the installer framed front and center; dry-run, custom config path, and the manual guide collapsed into a details block. - notify/README: leads with the one-liner; Docker Compose, docker run, bootstrap.sh, prebuilt binaries, and the env-var reference each fold into their own details block under "Manual & advanced setup". - RelayServerSetupView: single "One step" section (run command, copy, self-activation explained in the footer); the docker run alternative and the full-guide link live in a collapsed DisclosureGroup. - RelayHomeView: the pitch and the "not active yet" state now say plainly that a single line on the server finishes the job. - Drops the unused "Step 3 — Confirm it works" strings; localized in English, German, Spanish, and Simplified Chinese.
|
Warning Review limit reached
More reviews will be available in 28 minutes and 55 seconds. Learn how PR review limits work. To continue reviewing without waiting, purchase usage credits in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThis v1.7.0 release combines three major features: automatic last-writer-wins conflict resolution for Obsidian ChangesState Conflict Auto-Resolution for Obsidian Files
Reconnect Grace-Window and Async Startup Lifecycle
Cloud Relay One-Step Setup Redesign and Release
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
ios/VaultSync/Services/SyncthingManager.swift (1)
548-578:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftMake async startup cancellation-aware before it marks the engine running.
start()awaits a detached bridge boot, but after that await it always setsisRunning = trueand starts the 2s poll loop. In this PR, the callers are unstructured lifecycle tasks inVaultSyncAppandOnboardingView, so if the app backgrounds during cold start, the foreground start can still finish off-scene and keep polling in the background anyway. That breaks the “bounded, cancellation-aware” lifecycle requirement and can leave foreground startup work running after the app has already handed control back to background orchestration.Possible direction
func start() async { guard !isRunning, !isStarting else { return } isStarting = true defer { isStarting = false } let configDir = Self.configDirectory() logger.info("Starting Syncthing with configDir: \(configDir)") BackgroundSyncService.lifecycleLock.withLock { $0.foregroundActive = true } let startError = await Task.detached(priority: .userInitiated) { SyncBridgeService.startSyncthing(configDir: configDir) }.value + guard !Task.isCancelled else { + BackgroundSyncService.lifecycleLock.withLock { $0.foregroundActive = false } + SyncBridgeService.stopSyncthing() + return + } + if let err = startError { BackgroundSyncService.lifecycleLock.withLock { $0.foregroundActive = false } logger.error("Failed to start Syncthing: \(err)") error = err userError = SyncUserError.from(rawMessage: err, fallbackTitle: L10n.tr("Could Not Start Sync")) return }That still needs the callers to retain/cancel their startup task on scene changes, but it closes the worst part of the lifecycle leak here.
As per coding guidelines, Swift background/lifecycle work should be bounded and cancellation-aware, and this path currently keeps going after the caller context can disappear.
Source: Coding guidelines
ios/VaultSync/Services/BackgroundSyncService.swift (1)
1053-1065:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep
.obsidianauto-resolution independent from the banner toggle.Line 1059 returns before Line 1064, so turning off Conflict Notifications also turns off background auto-resolution of
.obsidianstate conflicts. That couples an alert preference to a sync-behavior feature and leaves settings/plugin conflicts unresolved for users who muted banners.Suggested fix
guard !isSceneActive() else { return } + // Auto-resolution is a sync behavior, not a notification preference. + autoResolveStateConflictsIfEnabled() + // In-app toggle (default ON). Read the raw object so an absent key — // every install from before this feature shipped — reads as ON, not // false (UserDefaults.bool returns false for a missing key). Gating // here, before the per-folder conflict scan, also skips that disk I/O // when the user has turned banners off. let bannersEnabled = (UserDefaults.standard.object(forKey: conflictNotificationsEnabledKey) as? Bool) ?? true guard bannersEnabled else { return } - - // Resolve `.obsidian` state-file conflicts (last-writer-wins) before - // counting, so a background sync never wakes the user for conflicts - // the app can settle on its own. Same opt-out as the foreground poll. - autoResolveStateConflictsIfEnabled()🤖 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/BackgroundSyncService.swift` around lines 1053 - 1065, The current guard using conflictNotificationsEnabledKey causes bannersEnabled to short-circuit and skip calling autoResolveStateConflictsIfEnabled, coupling notification preference to auto-resolution; move or duplicate the call so autoResolveStateConflictsIfEnabled() runs regardless of bannersEnabled (i.e., invoke autoResolveStateConflictsIfEnabled() before the guard that checks bannersEnabled or call it unconditionally outside the bannersEnabled short-circuit), leaving bannersEnabled and the conflictNotificationsEnabledKey logic only to control UI/banner behavior.
🧹 Nitpick comments (1)
go/bridge/conflicts_test.go (1)
536-649: ⚡ Quick winAdd one equal-mtime fixture for the tie-break.
The new test covers older/newer/orphan paths well, but the production rule also treats equal mtimes as “discard the conflict copy” (
Afteronly). That boundary is worth pinning down explicitly on this silent-resolution path.🤖 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 `@go/bridge/conflicts_test.go` around lines 536 - 649, TestAutoResolveStateConflicts is missing an equal-mtime case to verify the tie-break rule (equal mtimes should discard the conflict copy); add a fixture in that test that writes an original and a matching .sync-conflict file (use mustWrite and setMtime to give them identical timestamps), then assert after calling AutoResolveStateConflicts("autoresolve") that the original file content remains and the conflict copy was removed (increment expected resolved count accordingly), referencing the existing variables origNewer/origOlder patterns and the AutoResolveStateConflicts result parsing in the test.
🤖 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.
Inline comments:
In `@CHANGELOG.md`:
- Line 18: Reword the CHANGELOG line to avoid implying the iOS Local Network
prompt alone guarantees direct same‑Wi‑Fi connections: change the clause about
asking for the iOS "Local Network" permission to say it "helps same‑Wi‑Fi peers
connect directly sooner" and add a short clarifier that direct connections also
depend on VaultSync's cached last‑known address and on multicast/broadcast
discovery (which requires the com.apple.developer.networking.multicast
entitlement from Apple), and keep the note that without that entitlement we fall
back to global discovery + direct LAN dial and relay fallback.
In `@docs/architecture.md`:
- Around line 39-43: Update the “Direct LAN (TCP/QUIC to a private address)”
paragraph to stop saying Local Network denial forces every connection through a
relay: explicitly state that denying the iOS Local Network permission
(NSLocalNetworkUsageDescription) prevents same‑LAN dials but does not prevent
direct WAN connections to reachable public addresses, and that relays are used
only for connections that cannot be established via LAN or WAN; keep the mention
of the Local Network prompt and the dependency on global discovery for peer LAN
addresses.
In `@docs/sync-filters-ux.md`:
- Around line 151-178: The document adds a v1.7.0 section but the top metadata
still reads "Current app: v1.4.0" and "Last updated: 2026-05-23"; update the
metadata to reflect the new section by changing "Current app" to v1.7.0 and set
"Last updated" to the present revision date (e.g., 2026-06-12) so the header
aligns with the new "6.6 Conflict auto-resolution (v1.7.0)" content.
In `@go/bridge/addresscache.go`:
- Around line 35-44: In startAddressCache, stop assuming events.Event.Data is a
map[string]string and replace the direct type assertion
ev.Data.(map[string]string) with the bridge's generic interface->string-map
decoder used elsewhere (i.e., the helper that converts interface{} payloads to
map[string]string), then pass the resulting map into the existing dialableURI
and cacheDeviceAddress calls; specifically locate the ev := <-sub.C() branch,
decode ev.Data via the project’s map-string helper instead of
ev.Data.(map[string]string), handle the ok/failure path as before, and then call
dialableURI(data["type"], data["addr"]) and cacheDeviceAddress(cfg, data["id"],
uri).
In `@ios/VaultSync/Services/SyncthingManager.swift`:
- Around line 367-390: The reconnectingRequiredDeviceIDs and
disconnectedRequiredDeviceIDs logic should exclude peers that are explicitly
paused (DeviceInfo.paused == true): compute the set of paused IDs from devices
(e.g., devices.filter { $0.paused }.map(\.deviceID) -> Set) and subtract those
paused IDs from the required set derived from folders.flatMap(\.deviceIDs)
before filtering disconnectedSince and building stale/unresolved sets; leave
unresolvedUnknown (required devices not present in devices) unchanged. Ensure
you update both computed properties (reconnectingRequiredDeviceIDs and
disconnectedRequiredDeviceIDs) and continue to use the existing helpers
(disconnectedSince, graceDeadline(firstDisconnected:), now(), devices, folders)
when applying the paused-ID exclusion.
In `@ios/VaultSync/Views/ContentView.swift`:
- Around line 381-388: The warming-up check incorrectly uses contains (which
returns true if any disconnected device is in grace) so change it to verify that
every disconnected device is still within its reconnect grace window; update the
isWarmingUp calculation (referencing isWarmingUp, connected, total,
syncthingManager.devices, and isWithinReconnectGrace(deviceID:)) to ensure
connected == 0 && total > 0 && all disconnected devices satisfy
syncthingManager.isWithinReconnectGrace(deviceID:).
In `@ios/VaultSync/Views/DeviceDetailView.swift`:
- Around line 53-70: The HStack in DeviceDetailView currently treats paused
devices as Offline; update the conditional to check device.paused before falling
back to the Offline branch and render a Paused UI state: when device.paused is
true show a suitable paused icon (e.g. "pause.circle.fill"), an accessible
Text(L10n.tr("Paused")), and use the paused status color (e.g.
Color.statusPaused or another appropriate color constant) with
.accessibilityHidden(true) on the icon; keep existing isConnecting and
device.connected branches unchanged so paused is distinct from Offline.
---
Outside diff comments:
In `@ios/VaultSync/Services/BackgroundSyncService.swift`:
- Around line 1053-1065: The current guard using conflictNotificationsEnabledKey
causes bannersEnabled to short-circuit and skip calling
autoResolveStateConflictsIfEnabled, coupling notification preference to
auto-resolution; move or duplicate the call so
autoResolveStateConflictsIfEnabled() runs regardless of bannersEnabled (i.e.,
invoke autoResolveStateConflictsIfEnabled() before the guard that checks
bannersEnabled or call it unconditionally outside the bannersEnabled
short-circuit), leaving bannersEnabled and the conflictNotificationsEnabledKey
logic only to control UI/banner behavior.
---
Nitpick comments:
In `@go/bridge/conflicts_test.go`:
- Around line 536-649: TestAutoResolveStateConflicts is missing an equal-mtime
case to verify the tie-break rule (equal mtimes should discard the conflict
copy); add a fixture in that test that writes an original and a matching
.sync-conflict file (use mustWrite and setMtime to give them identical
timestamps), then assert after calling AutoResolveStateConflicts("autoresolve")
that the original file content remains and the conflict copy was removed
(increment expected resolved count accordingly), referencing the existing
variables origNewer/origOlder patterns and the AutoResolveStateConflicts result
parsing in the test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3505241e-dc12-4478-8c95-3bd769e0cf51
📒 Files selected for processing (36)
CHANGELOG.mdREADME.mddocs/architecture.mddocs/sync-filters-ux.mddocs/troubleshooting.mdgo/bridge/addresscache.gogo/bridge/addresscache_test.gogo/bridge/conflicts.gogo/bridge/conflicts_test.gogo/bridge/syncthing.gogo/patches/README.mdgo/patches/syncthing/003-faster-cold-start-redial.patchios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Services/BackgroundSyncService.swiftios/VaultSync/Services/SyncBridgeService.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Views/ContentView.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Views/OnboardingView.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Views/RelayServerSetupView.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/de.lproj/InfoPlist.stringsios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/en.lproj/InfoPlist.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/es.lproj/InfoPlist.stringsios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/InfoPlist.stringsios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSyncTests/ConflictAutoResolveTests.swiftios/VaultSyncTests/ReconnectingGracePeriodTests.swiftios/VaultSyncTests/SetupChecklistViewModelTests.swiftios/project.ymlnotify/README.md
📜 Review details
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build & Test
🧰 Additional context used
📓 Path-based instructions (9)
**/*
⚙️ 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/de.lproj/InfoPlist.stringsios/VaultSync/en.lproj/InfoPlist.stringsgo/bridge/syncthing.goios/VaultSync/zh-Hans.lproj/InfoPlist.stringsios/VaultSync/Views/OnboardingView.swiftgo/patches/README.mdios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/es.lproj/InfoPlist.stringsdocs/architecture.mdios/VaultSyncTests/ConflictAutoResolveTests.swiftios/VaultSync/Services/SyncBridgeService.swiftdocs/troubleshooting.mdREADME.mdios/project.ymlios/VaultSync/Views/RelayHomeView.swiftgo/bridge/conflicts.goios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/RelayServerSetupView.swiftdocs/sync-filters-ux.mdgo/patches/syncthing/003-faster-cold-start-redial.patchnotify/README.mdgo/bridge/conflicts_test.goios/VaultSyncTests/ReconnectingGracePeriodTests.swiftCHANGELOG.mdgo/bridge/addresscache.gogo/bridge/addresscache_test.goios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSyncTests/SetupChecklistViewModelTests.swiftios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/Services/BackgroundSyncService.swiftios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/Views/ContentView.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/Localizable.strings
**
⚙️ 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/de.lproj/InfoPlist.stringsios/VaultSync/en.lproj/InfoPlist.stringsgo/bridge/syncthing.goios/VaultSync/zh-Hans.lproj/InfoPlist.stringsios/VaultSync/Views/OnboardingView.swiftgo/patches/README.mdios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/es.lproj/InfoPlist.stringsdocs/architecture.mdios/VaultSyncTests/ConflictAutoResolveTests.swiftios/VaultSync/Services/SyncBridgeService.swiftdocs/troubleshooting.mdREADME.mdios/project.ymlios/VaultSync/Views/RelayHomeView.swiftgo/bridge/conflicts.goios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/RelayServerSetupView.swiftdocs/sync-filters-ux.mdgo/patches/syncthing/003-faster-cold-start-redial.patchnotify/README.mdgo/bridge/conflicts_test.goios/VaultSyncTests/ReconnectingGracePeriodTests.swiftCHANGELOG.mdgo/bridge/addresscache.gogo/bridge/addresscache_test.goios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSyncTests/SetupChecklistViewModelTests.swiftios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/Services/BackgroundSyncService.swiftios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/Views/ContentView.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/Localizable.strings
go/bridge/**/*.go
⚙️ CodeRabbit configuration file
go/bridge/**/*.go: This code crosses the gomobile Swift-Go boundary. Verify exported signatures use only gomobile-safe primitive types,
preserve the JSON string contract, keep empty-string success conventions intact, and avoid breaking Swift decoding tests.
Review Syncthing lifecycle, locking, error strings, and noassets build assumptions carefully.
Files:
go/bridge/syncthing.gogo/bridge/conflicts.gogo/bridge/conflicts_test.gogo/bridge/addresscache.gogo/bridge/addresscache_test.go
**/*.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.
**/*.swift: Code must be written in Swift 6 and SwiftUI
Background tasks must use BGAppRefreshTask and BGContinuedProcessingTask (iOS 26+ when available)
Push notifications must be implemented via APNs silent push through Cloud Relay
VoiceOver and Dynamic Type accessibility must be supported throughout the application
Files:
ios/VaultSync/Views/OnboardingView.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSyncTests/ConflictAutoResolveTests.swiftios/VaultSync/Services/SyncBridgeService.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/RelayServerSetupView.swiftios/VaultSyncTests/ReconnectingGracePeriodTests.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSyncTests/SetupChecklistViewModelTests.swiftios/VaultSync/Services/BackgroundSyncService.swiftios/VaultSync/Views/ContentView.swiftios/VaultSync/Services/SyncthingManager.swift
ios/**/*.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/VaultSync/Views/OnboardingView.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSyncTests/ConflictAutoResolveTests.swiftios/VaultSync/Services/SyncBridgeService.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/RelayServerSetupView.swiftios/VaultSyncTests/ReconnectingGracePeriodTests.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSyncTests/SetupChecklistViewModelTests.swiftios/VaultSync/Services/BackgroundSyncService.swiftios/VaultSync/Views/ContentView.swiftios/VaultSync/Services/SyncthingManager.swift
**/*.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:
go/patches/README.mddocs/architecture.mddocs/troubleshooting.mdREADME.mddocs/sync-filters-ux.mdnotify/README.mdCHANGELOG.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.🌉 Go bridge (
go/bridge/)Minimal API exported via gomobile. Only prim...
Files:
docs/architecture.mddocs/troubleshooting.mddocs/sync-filters-ux.md
ios/project.yml
⚙️ CodeRabbit configuration file
ios/project.yml: This generates the Xcode project and Info.plist. Review changes for bundle ID,
entitlements, background modes, URL schemes, signing settings, and accidental secret exposure.
Files:
ios/project.yml
**/*.lproj/Localizable.strings
📄 CodeRabbit inference engine (README.md)
Localization must be provided in English, German, Spanish, and Simplified Chinese
Files:
ios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/Localizable.strings
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:29:58.144Z
Learning: Build requires Xcode 26+, Go 1.26+, gomobile, XcodeGen, and Make
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:29:58.144Z
Learning: Platform target is iOS/iPadOS 18 or later
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:29:58.144Z
Learning: License must be MPL-2.0
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:12.138Z
Learning: Embed Syncthing's Go reference implementation as an iOS library via gomobile with no reimplementation of the protocol in Swift to guarantee wire compatibility
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:12.138Z
Learning: In foreground sync mode, Syncthing runs unrestricted for immediate, continuous sync
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:12.138Z
Learning: Cloud Relay is a server → iPhone acceleration path (via APNs silent push), not a guarantee of symmetric real-time background sync
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:12.138Z
Learning: Implement asymmetric sync: Server → iPhone via `vaultsync-notify` spotting changes → Cloud Relay silent push → VaultSync wake and pull; iPhone → Server via foreground Syncthing with Shortcuts automation
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:12.138Z
Learning: Request `com.apple.developer.networking.multicast` entitlement from Apple to enable Syncthing's local (multicast/broadcast) discovery; until granted, find LAN peers via global discovery + direct LAN dial
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:12.138Z
Learning: The optional `notify/` sidecar watches Syncthing on the homeserver and sends APNs wake-ups per relay-spec.md protocol
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:40.397Z
Learning: Recommended presets must be silently applied when a new folder is added so that a fresh vault never syncs `workspace.json` even if the user instantly closes the sheet without tapping Done.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:40.397Z
Learning: The preset catalog includes: `workspace` (Workspace state: `.obsidian/workspace.json`, `.obsidian/workspace-mobile.json` — ON by default), `trash` (Trash: `.Trash` — ON by default), `git` (Git repository: `.git` — OFF, auto-on if scan finds it), `macos` (macOS metadata: `.DS_Store`, `._*` — OFF), `copilot` (Copilot index: `.copilot-index` — OFF, auto-on if scan finds it), `obsidianCache` (Obsidian app cache: `.obsidian/cache` — OFF).
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:40.397Z
Learning: Pattern matching in multi-vault setups automatically expands unanchored patterns (without leading `/`) to match at any depth via Syncthing, so `.git` covers both `Obsidian/.git` and `Obsidian/Vault1/.git` without requiring `**/` prefix in preset definitions.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:40.397Z
Learning: Conflict auto-resolution (v1.7.0) must only apply to files inside `.obsidian` directory (any depth); notes and files outside `.obsidian` keep the full manual conflict flow (diff view, Keep This/Other/Both, Skip Family) to prevent silent data loss on user content.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:40.397Z
Learning: Throughout the app, use 'Sync Filters' as section title, and 'Skip on this iPhone', 'Always skip on this iPhone', 'Choose what gets synced to this iPhone' for CTAs and copy. Avoid 'Ignore patterns' (Syncthing-jargon), 'Exclusions' (corporate-y), and 'Filter rules' (too abstract).
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:40.397Z
Learning: All new Sync Filters strings must be shipped in English, German, Spanish, and Simplified Chinese (the four shipping locales).
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:40.397Z
Learning: For users updating from current build, the 3 silent default patterns (`.Trash`, `.obsidian/workspace.json`, `.obsidian/workspace-mobile.json`) must stay on disk untouched and the derived state automatically shows 'Workspace state' and 'Trash' as ON. No migration sheet, no disk changes, no surprise.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:40.397Z
Learning: In Sync Filters home banner, vault badges, and notifications, count distinct conflicted files instead of conflict copies to accurately represent the number of files with conflicts rather than the total conflict copy count.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:40.397Z
Learning: Plugin caches should not be bundled under a single 'Plugin caches' preset toggle. Instead, ship specific presets per plugin (e.g. Dataview's `cache.db`, Copilot's `.copilot-index`) to avoid either missing real caches or accidentally excluding plugin data users want.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: When reporting Syncthing Not Running issues, force-close VaultSync and reopen it, keep it in the foreground for 20-30 seconds, confirm Device ID appears in onboarding or settings, tap Rescan Vault from the vault detail page, and reboot the device if still failing
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: vaultsync-notify reads the Syncthing API key from config.xml with no key to paste; ensure the helper runs as the correct user (matching config.xml ownership with -u <uid>:<gid>), point at the real config file with SYNCTHING_CONFIG=/path/to/config.xml if needed, and remove any hardcoded SYNCTHING_API_KEY override to fall back to auto-detection
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: When diagnosing Relay Unreachable issues, confirm RELAY_URL is set correctly in notify/.env (cloud value: https://relay.vaultsync.eu), verify homeserver egress rules and firewall configuration, and test relay connectivity with curl -fsSL https://relay.vaultsync.eu/api/v1/health
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: When vaultsync-notify logs show 401/403 permission errors, first check user permissions on config.xml and the uid/gid running the container, then verify SYNCTHING_CONFIG points to the correct file location, and recreate the container with docker compose up -d --force-recreate vaultsync-notify followed by docker compose run --rm vaultsync-notify --doctor
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: No Pending Shares appear when the desktop Syncthing folder is not shared to the iOS Device ID, the Device IDs do not match between desktop and app, or the desktop Syncthing is offline; verify folder sharing on desktop, confirm Device IDs match, ensure desktop Syncthing is online and the folder is not paused, and re-share the folder if needed
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: Silent APNs push wake-ups do not require notification permission but do require a valid APNs token and provisioned device; users should tap Retry APNs Registration in Cloud Relay diagnostics to confirm an APNs Token appears, then tap Retry Provisioning to rebind the token to device IDs
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: Obsidian Folder Not Found occurs when Obsidian has not been installed or opened on the device, or the wrong folder was picked; users should install/open Obsidian at least once, tap Connect Obsidian Folder, select On My iPhone → Obsidian (or the vault root containing .obsidian), and confirm VaultSync lists detected vaults
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: Bookmark Access Expired issues are fixed by tapping Reconnect to Obsidian and re-selecting the Obsidian folder in the Files picker, keeping VaultSync in the foreground during rescan, or removing the vault if its storage is truly gone (only affects this iPhone; other devices retain their notes)
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: Background Sync Not Working is usually caused by iOS controlling background time; fix by opening VaultSync and running manual rescan, clearing any Sync Issues first, reconnecting the Obsidian folder if access warnings appear, and for relay users confirming vaultsync-notify --doctor is green and Last Trigger Received is recent
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: Required Device Disconnected errors occur when desktop Syncthing is offline or the network path is blocked; fix by ensuring desktop Syncthing is running and online, confirming both devices can reach each other, checking the device still exists under Devices in VaultSync, and removing/re-adding the device if its ID changed
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-06-12T06:30:55.429Z
Learning: When capturing diagnostic information for issue reports, include VaultSync version and iOS version, screenshots of Sync Issues and Relay health & diagnostics, vaultsync-notify --doctor output if using the relay, and relevant vaultsync-notify log lines around the failure
📚 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/Views/OnboardingView.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Services/SyncBridgeService.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/RelayServerSetupView.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSync/Services/BackgroundSyncService.swiftios/VaultSync/Views/ContentView.swiftios/VaultSync/Services/SyncthingManager.swift
🪛 LanguageTool
go/patches/README.md
[grammar] ~38-~38: Ensure spelling is correct
Context: ...oopSleep` 5s→1s (the dial loop's initial rampup is otherwise clamped to 5s rounds while...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🔇 Additional comments (12)
ios/VaultSync/de.lproj/InfoPlist.strings (1)
3-3: LGTM!ios/VaultSync/en.lproj/InfoPlist.strings (1)
3-3: LGTM!ios/VaultSync/es.lproj/InfoPlist.strings (1)
3-3: LGTM!ios/VaultSync/zh-Hans.lproj/InfoPlist.strings (1)
3-3: LGTM!go/bridge/conflicts.go (1)
349-466: LGTM!ios/VaultSync/Views/DesignSystem.swift (1)
97-99: LGTM!Also applies to: 108-108, 116-116, 124-129
ios/VaultSyncTests/ConflictAutoResolveTests.swift (1)
1-92: LGTM!README.md (1)
67-92: LGTM!notify/README.md (1)
9-116: LGTM!docs/troubleshooting.md (1)
48-48: LGTM!ios/VaultSync/Views/RelayHomeView.swift (1)
63-65: LGTM!Also applies to: 179-179
ios/VaultSync/Views/RelayServerSetupView.swift (1)
64-67: LGTM!Also applies to: 69-89
Paused peers were leaking into the disconnected-warning machinery in three places, undermining the calm reconnect UX this release introduces: - SyncthingManager: reconnectingRequiredDeviceIDs and disconnectedRequiredDeviceIDs now exclude paused devices - an intentionally paused peer must neither read as "connecting" nor raise the required-device-disconnected issue after grace expires (+test). - ContentView: the dashboard warm-up state now requires that every disconnected non-paused device is still within its reconnect grace window (allSatisfy, was contains), so one device past grace no longer hides behind another device's spinner. - DeviceDetailView: paused devices render as "Paused" with a pause glyph instead of falling through to "Offline", mirroring the device list row. Docs accuracy fixes from the same review: CHANGELOG and architecture.md no longer overstate the Local Network permission (it is necessary for LAN dials, not sufficient for direct connections; denial does not force WAN-reachable peers through a relay), and sync-filters-ux.md metadata now matches its new v1.7.0 section. Not changed: addresscache.go's ev.Data.(map[string]string) assertion is correct - the in-process fork emits DeviceConnected with a literal map[string]string (lib/model/model.go), no JSON round trip involved.


Summary
Four areas, released together as 1.7.0 (build 30):
Faster reconnect after a cold start
NSLocalNetworkUsageDescription, localized) so same-Wi-Fi devicesconnect directly instead of detouring through a relay.
discovery round trip.
timeout drops from 10 s to 5 s so the relay fallback kicks in sooner.
Calm reconnect UX
device…"), neutral dashboard state, spinner + "Connecting…" in the Devices list.
start()is now async.Auto-resolved .obsidian conflicts
.obsidian/) resolve themselves, newest versionwins — note conflicts still wait for the user. Opt-out in Settings → Conflicts.
Cloud Relay as a one-step setup
docker run,prebuilt binaries, env vars) are folded into expandable sections.
docker runpath and the full-guide linklive in a collapsed "Manual & advanced setup" disclosure.
All user-facing copy localized in English, German, Spanish, and Simplified Chinese.
Test plan
go test ./bridge/greenxcodebuild teston iPhone 17 Pro simulator greenplutil -lintclean on all fourLocalizable.strings; key parity verified across languagesNotes
com.apple.developer.networking.multicast) still requires Apple approval; until thenlocal discovery falls back to global discovery + direct dial.
engine.
v1.7.0 Release: Faster Cold-Start Reconnect, Auto-Resolved Settings Conflicts, One-Step Cloud Relay Setup
Overview
This release delivers four main improvements: faster peer reconnection after app launch, calmer reconnect UX that avoids false alarms during warmup, automatic conflict resolution for Obsidian settings, and streamlined one-line Cloud Relay server setup.
User-Visible Sync Behavior Changes
Faster cold-start reconnect:
Calm reconnect UX:
Auto-resolved .obsidian conflicts:
.obsidian/) now auto-resolved using last-writer-wins strategy.obsidian/remain manualCloud Relay one-step setup:
curl -fsSL https://vaultsync.eu/notify.sh | shPrivacy & Security
Background Execution Impact
start()is now async—UI calls nowawait syncthingManager.start()Test Coverage
TestDialableURIvalidates cached-address URI generation across connection types (tcp/quic/relay), IPv6 zone handling;TestUpdatedAddressesverifies address cache replacement logic and idempotencyTestIsStateFilePathandTestAutoResolveStateConflictscover state-conflict detection and last-writer-wins resolution (resolved count, file cleanup, idempotency, error handling)ConflictAutoResolveTestsverify state-conflict classification and distinct-file counting; newReconnectingGracePeriodTestscover cold-start vs mid-session grace window behaviorSetupChecklistViewModelTestsupdated to await asyncstart()Localization
Implementation Details
addresscache.gosubscribes toDeviceConnectedevents, caches successful dial URIs per device in configconflicts.goimplements last-writer-wins scan and resolution for.obsidianpaths with JSON response envelope003-faster-cold-start-redial.patchapplied to Syncthing engine (connection loop sleep 5s→1s, TCP dial timeout 10s→5s)SyncthingManagertracks reconnect grace windows; newisAutoResolveStateConflictsEnabledsetting;BackgroundSyncServicecalls auto-resolution during conflict notifications