Skip to content

feat: faster cold-start reconnect, auto-resolved settings conflicts, one-step Cloud Relay setup - #41

Merged
psimaker merged 6 commits into
mainfrom
fix/cold-start-reconnect
Jun 12, 2026
Merged

feat: faster cold-start reconnect, auto-resolved settings conflicts, one-step Cloud Relay setup#41
psimaker merged 6 commits into
mainfrom
fix/cold-start-reconnect

Conversation

@psimaker

@psimaker psimaker commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Four areas, released together as 1.7.0 (build 30):

Faster reconnect after a cold start

  • Request the iOS Local Network permission (NSLocalNetworkUsageDescription, localized) so same-Wi-Fi devices
    connect directly instead of detouring through a relay.
  • Cache the last successful outbound address per device and dial it immediately on launch instead of waiting for a
    discovery round trip.
  • Fork patch 003: the embedded engine retries failed dials every 1 s (was 5 s) while warming up, and the TCP dial
    timeout drops from 10 s to 5 s so the relay fallback kicks in sooner.

Calm reconnect UX

  • Reconnecting after a cold start is treated as normal warm-up: positive header with spinner ("Connecting to
    device…"), neutral dashboard state, spinner + "Connecting…" in the Devices list.
  • 60-second startup grace period before anything is reported as a problem (mid-session disconnects keep 30 s).
  • Disconnected peers read as neutral gray "Offline" (the red ✕ badge is gone), paused devices are labeled "Paused".
  • Engine startup work (certificates, config, index DB) moved off the UI thread; start() is now async.

Auto-resolved .obsidian conflicts

  • Conflicts in Obsidian's settings and plugin state (anything inside .obsidian/) resolve themselves, newest version
    wins — note conflicts still wait for the user. Opt-out in Settings → Conflicts.
  • Conflict counts now count conflicted files instead of every conflict copy.

Cloud Relay as a one-step setup

  • README and notify/README lead with the framed one-line installer; all manual paths (Docker Compose, docker run,
    prebuilt binaries, env vars) are folded into expandable sections.
  • The in-app setup screen is now a single "One step" section; the manual docker run path and the full-guide link
    live in a collapsed "Manual & advanced setup" disclosure.
  • Relay pitch and "not active yet" state say plainly that a single line on the server finishes the job.

All user-facing copy localized in English, German, Spanish, and Simplified Chinese.

Test plan

  • go test ./bridge/ green
  • Full xcodebuild test on iPhone 17 Pro simulator green
  • plutil -lint clean on all four Localizable.strings; key parity verified across languages
  • Local Network permission prompt needs verification on a physical device (simulator doesn't show it reliably)

Notes

  • The multicast entitlement (com.apple.developer.networking.multicast) still requires Apple approval; until then
    local discovery falls back to global discovery + direct dial.
  • The prebuilt xcframework was rebuilt locally to include fork patch 003 — ensure the App Store build uses the patched
    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:

  • App now caches the last successful outbound address per peer device and dials it immediately on launch, avoiding rediscovery delays
  • Syncthing dial timeout reduced from 10s to 5s; retry loop reduced from 5s to 1s intervals during startup
  • iOS Local Network permission requested on first launch to enable direct same-network connectivity without internet routing

Calm reconnect UX:

  • Cold-start reconnection treated as a "warmup" phase with positive spinner, "Connecting…" status, and neutral device states
  • 60-second startup grace period (vs. 30s mid-session); reconnecting devices shown as "Connecting…" instead of "Offline"
  • Paused devices labeled "Paused" to distinguish from disconnected peers
  • Device row UI refactored to show three states: "Connecting…" (during grace), "Connected" (with checkmark), or "Offline" (moon icon)

Auto-resolved .obsidian conflicts:

  • Sync-conflict copies of Obsidian state files (app settings, plugin state under .obsidian/) now auto-resolved using last-writer-wins strategy
  • Original file preferred if newer; orphan conflicts promoted if original missing
  • Conflict counts now reflect distinct conflicted files rather than individual conflict copies
  • User can opt out via Settings → Conflicts → "Auto-Resolve Settings Conflicts"
  • Conflicts outside .obsidian/ remain manual

Cloud Relay one-step setup:

  • README and in-app guides now promote single-line installer: curl -fsSL https://vaultsync.eu/notify.sh | sh
  • Manual setup paths (Docker Compose, prebuilt binaries, NAS) moved into collapsed "Manual & advanced setup" sections
  • Server setup header renamed to "One step — run this on your server"

Privacy & Security

  • NSLocalNetworkUsageDescription added to iOS app permissions to explain local network access (direct peer connectivity for faster syncing)
  • No new external data collection; Local Network permission is system-level iOS privacy control

Background Execution Impact

  • Syncthing bridge startup work moved off UI thread; start() is now async—UI calls now await syncthingManager.start()
  • Background sync conflict auto-resolution happens before computing conflict notifications (when feature enabled)
  • Folder rescans triggered on state-conflict resolution to refresh UI promptly
  • Address caching runs in detached background goroutine tied to engine lifecycle

Test Coverage

  • Go bridge tests: TestDialableURI validates cached-address URI generation across connection types (tcp/quic/relay), IPv6 zone handling; TestUpdatedAddresses verifies address cache replacement logic and idempotency
  • Go bridge tests: TestIsStateFilePath and TestAutoResolveStateConflicts cover state-conflict detection and last-writer-wins resolution (resolved count, file cleanup, idempotency, error handling)
  • iOS tests: ConflictAutoResolveTests verify state-conflict classification and distinct-file counting; new ReconnectingGracePeriodTests cover cold-start vs mid-session grace window behavior
  • Async startup test: SetupChecklistViewModelTests updated to await async start()
  • Note: Local Network permission prompt requires physical iOS device verification (simulator cannot trigger system permission UI)

Localization

  • User-facing copy localized for English, German, Spanish, and Simplified Chinese
  • Included: permission prompts, status messages, setup guides, settings labels, conflict auto-resolution descriptions

Implementation Details

  • Go: New addresscache.go subscribes to DeviceConnected events, caches successful dial URIs per device in config
  • Go: New conflicts.go implements last-writer-wins scan and resolution for .obsidian paths with JSON response envelope
  • Go patch: 003-faster-cold-start-redial.patch applied to Syncthing engine (connection loop sleep 5s→1s, TCP dial timeout 10s→5s)
  • iOS: SyncthingManager tracks reconnect grace windows; new isAutoResolveStateConflictsEnabled setting; BackgroundSyncService calls auto-resolution during conflict notifications
  • Build: xcframework rebuilt locally with patched Syncthing engine

psimaker added 5 commits June 12, 2026 07:23
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.
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@psimaker, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2992fd08-94c4-487f-aee1-8ae8d6f65ad2

📥 Commits

Reviewing files that changed from the base of the PR and between 8619ebc and a238b00.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • docs/architecture.md
  • docs/sync-filters-ux.md
  • ios/VaultSync/Services/SyncthingManager.swift
  • ios/VaultSync/Views/ContentView.swift
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSyncTests/ReconnectingGracePeriodTests.swift
📝 Walkthrough

Walkthrough

This v1.7.0 release combines three major features: automatic last-writer-wins conflict resolution for Obsidian .obsidian files (Go bridge + iOS UI), address caching for faster cold-start reconnect, and a redesigned reconnect grace-window UX with async startup and calmer status presentation. Cloud Relay setup is streamlined to one-step with manual/advanced options collapsible, and localization is updated across five languages.

Changes

State Conflict Auto-Resolution for Obsidian Files

Layer / File(s) Summary
Go Bridge conflict detection and resolution
go/bridge/conflicts.go, go/bridge/conflicts_test.go
Detects .obsidian conflict copies at any depth and applies last-writer-wins: promotes newer conflicts via rename, deletes stale ones, rescues orphans; returns JSON envelope with resolved count and error; comprehensive coverage including idempotency and error handling.
Address caching for faster cold-start reconnect
go/bridge/addresscache.go, go/bridge/addresscache_test.go, go/bridge/syncthing.go
Event-driven address cache subscribes to device-connected events, derives dial URIs for TCP/QUIC outbound connections, updates device config with last-known-good addresses to avoid discovery delay on subsequent cold starts; integrated into bridge startup lifecycle.
Swift bridge wrapper and iOS manager integration
ios/VaultSync/Services/SyncBridgeService.swift, ios/VaultSync/Services/SyncthingManager.swift
Adds SyncBridgeService.autoResolveStateConflicts(folderID:) wrapper and SyncthingManager.autoResolveStateConflictsKey / isAutoResolveStateConflictsEnabled user toggle; detects state conflicts via ConflictInfo.isStateConflict computed property.
Background sync conflict notification integration
ios/VaultSync/Services/BackgroundSyncService.swift
Auto-resolves .obsidian state conflicts during background sync before computing conflict counts for notifications; rescans folders when resolution occurs and switches conflict counting to distinct-file semantics by originalPath.
Settings and conflict display UI
ios/VaultSync/Views/SettingsView.swift, ios/VaultSync/Views/ContentView.swift
Adds conflict auto-resolution toggle in SettingsView; updates vault conflict displays and counts to use distinct originalPath values instead of conflict-copy counts across vault rows and detail sections.
Conflict auto-resolution test coverage
ios/VaultSyncTests/ConflictAutoResolveTests.swift
Validates state-file path detection, Swift-side state-conflict classification, and distinct-file conflict counting logic including mixed/notes-only/empty JSON payload scenarios.

Reconnect Grace-Window and Async Startup Lifecycle

Layer / File(s) Summary
Async startup and grace deadline calculation
ios/VaultSync/Services/SyncthingManager.swift
Converts start() to async method; initializes engineStartedAt on bridge startup and clears it on stop/reset; calculates grace deadlines distinguishing startup grace (90s) from mid-session short grace (30s) to drive reconnect classification.
Device connection status UI (grace-aware)
ios/VaultSync/Views/ContentView.swift, ios/VaultSync/Views/DeviceDetailView.swift, ios/VaultSync/Views/DesignSystem.swift
Refactors device-row presentation to show calm "Connecting…" spinner during grace windows, "Paused" for paused devices, and "Offline" otherwise; updates StatusRow to support indeterminate busy state with progress spinner; removes binary "Connected/Disconnected" distinction.
Dashboard warming-up indicator
ios/VaultSync/Views/ContentView.swift
Displays calm "Connecting to devices…" spinner and message during post-startup reconnect grace windows instead of showing "0 of N connected" as alert condition.
Header reconnect subtitle wording
ios/VaultSync/Views/ContentView.swift
Updates header subtitle to use "Connecting to …" wording during grace windows with device name when available; suppresses reconnecting state from changing overallStatus so grace-window reconnects don't trigger false problem alerts.
App startup paths: async start integration
ios/VaultSync/App/VaultSyncApp.swift, ios/VaultSync/Views/OnboardingView.swift, ios/VaultSyncTests/SetupChecklistViewModelTests.swift
Updates VaultSyncApp scene-phase and onboarding handlers to await async syncthingManager.start(); converts test checklist to async to support awaited startup.
Reconnect grace-window test coverage
ios/VaultSyncTests/ReconnectingGracePeriodTests.swift
Validates startup grace period vs mid-session short-grace behavior, tests isWithinReconnectGrace(deviceID:) helper, and verifies grace-window state classification across required/non-required devices.

Cloud Relay One-Step Setup Redesign and Release

Layer / File(s) Summary
Setup documentation reorganization
README.md, notify/README.md
Replaces quick-start/step-by-step sections with "One-step setup" and collapsible "Manual & advanced setup" structure; clarifies installer permissions, dry-run usage, Docker/binary alternatives, and NAS-specific guidance.
Cloud Relay UI copy updates
ios/VaultSync/Views/RelayHomeView.swift, ios/VaultSync/Views/RelayServerSetupView.swift
Emphasizes one-step setup with terminal icon; condenses server-setup footer to "One step left…"; consolidates manual setup under wrench-labeled disclosure group.
Multi-language localization strings
ios/VaultSync/*/Localizable.strings, ios/VaultSync/*/InfoPlist.strings
Adds/updates "Connecting…", "Offline" status strings across English, German, Spanish, Chinese (Simplified); replaces "Disconnected"/"Reconnecting…" keys; reorganizes Cloud Relay setup and conflict-resolution copy.
iOS permissions: Local Network access
ios/project.yml, ios/VaultSync/*/InfoPlist.strings
Adds NSLocalNetworkUsageDescription to iOS project configuration and Info.plist localized strings across five languages, explaining direct same-network device connectivity.
Version bump and release notes
CHANGELOG.md, ios/project.yml
Updates CHANGELOG.md with v1.7.0 release notes; bumps app and widget bundle versions from 1.6.0/28 to 1.7.0/30.
Architecture and configuration documentation
docs/architecture.md
Documents Syncthing connection paths (LAN, WAN, relays), iOS Local Network permission and multicast entitlement requirements, and address-cache behavior.
Syncthing patch: faster cold-start dial
go/patches/syncthing/003-faster-cold-start-redial.patch, go/patches/README.md
Reduces minConnectionLoopSleep from 5s to 1s and TCP dial timeout from 10s to 5s to accelerate reconnect; documents patch in release checklist.
Documentation link updates
docs/troubleshooting.md, docs/sync-filters-ux.md
Updates troubleshooting doc reference from old quick-start anchor to new one-step-setup; documents v1.7.0 conflict auto-resolution spec in sync-filters-ux.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • psimaker/vaultsync#24: Earlier StatusRow design-system extension work that this PR builds on for indeterminate busy-state spinner rendering.
  • psimaker/vaultsync#9: Related reconnect grace-period logic and "connecting" UI changes in SyncthingManager and device status presentation.
  • psimaker/vaultsync#23: Prior Cloud Relay activation/delivery setup screen work that the one-step redesign refines.

Poem

🔄 Conflicts fade with grace, old dials swift return,
Cold starts bloom to warmth, no more endless churn.
One line, one step—the server stands and waits,
While spinning wheels say "soon" at connection's gates.

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
No Private Note Leakage ⚠️ Warning BackgroundSyncService logs BridgeEvent diagnosticSummary via traceRelevantBridgeEvents; diagnosticSummary includes event data like item= and error=, which can contain synced file paths/filenames, u... Remove/disable these debug traces or sanitize diagnosticSummary (drop item/error fields) / log only IDs in release builds to avoid leaking Obsidian filenames via system logs.
Bounded Ios Background Work ⚠️ Warning BGTask expiration handlers only stop Syncthing, but performBackgroundSync still runs notifyConflictsIfAny→autoResolveStateConflictsIfEnabled without cancellation checks, and it ignores the bridge’s... Gate auto-conflict work (and conflict counting) on an expired/cancellation flag set in expirationHandler (or Task.isCancelled), and log/report autoResolveStateConflicts errors (redacted) instead of discarding them.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.
Bridge Contract Compatibility ✅ Passed Bridge contract intact: Go AutoResolveStateConflicts returns JSON {resolved,error} with error:"" on success, matching Swift decoding; conflict JSON fields still match ConflictStub; Go+Swift tests a...
Title check ✅ Passed The title follows conventional-commit style with 'feat:' prefix and clearly summarizes the three main features: faster cold-start reconnect, auto-resolved settings conflicts, and one-step Cloud Relay setup.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cold-start-reconnect

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 and usage tips.

@psimaker psimaker changed the title v1.7.0: faster cold-start reconnect, auto-resolved settings conflicts, one-step Cloud Relay setup feat: faster cold-start reconnect, auto-resolved settings conflicts, one-step Cloud Relay setup Jun 12, 2026

@coderabbitai coderabbitai 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.

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 lift

Make async startup cancellation-aware before it marks the engine running.

start() awaits a detached bridge boot, but after that await it always sets isRunning = true and starts the 2s poll loop. In this PR, the callers are unstructured lifecycle tasks in VaultSyncApp and OnboardingView, 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 win

Keep .obsidian auto-resolution independent from the banner toggle.

Line 1059 returns before Line 1064, so turning off Conflict Notifications also turns off background auto-resolution of .obsidian state 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 win

Add 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” (After only). 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7ff12ba and 8619ebc.

📒 Files selected for processing (36)
  • CHANGELOG.md
  • README.md
  • docs/architecture.md
  • docs/sync-filters-ux.md
  • docs/troubleshooting.md
  • go/bridge/addresscache.go
  • go/bridge/addresscache_test.go
  • go/bridge/conflicts.go
  • go/bridge/conflicts_test.go
  • go/bridge/syncthing.go
  • go/patches/README.md
  • go/patches/syncthing/003-faster-cold-start-redial.patch
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/VaultSync/Services/BackgroundSyncService.swift
  • ios/VaultSync/Services/SyncBridgeService.swift
  • ios/VaultSync/Services/SyncthingManager.swift
  • ios/VaultSync/Views/ContentView.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSync/Views/OnboardingView.swift
  • ios/VaultSync/Views/RelayHomeView.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/de.lproj/InfoPlist.strings
  • ios/VaultSync/de.lproj/Localizable.strings
  • ios/VaultSync/en.lproj/InfoPlist.strings
  • ios/VaultSync/en.lproj/Localizable.strings
  • ios/VaultSync/es.lproj/InfoPlist.strings
  • ios/VaultSync/es.lproj/Localizable.strings
  • ios/VaultSync/zh-Hans.lproj/InfoPlist.strings
  • ios/VaultSync/zh-Hans.lproj/Localizable.strings
  • ios/VaultSyncTests/ConflictAutoResolveTests.swift
  • ios/VaultSyncTests/ReconnectingGracePeriodTests.swift
  • ios/VaultSyncTests/SetupChecklistViewModelTests.swift
  • ios/project.yml
  • notify/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.strings
  • ios/VaultSync/en.lproj/InfoPlist.strings
  • go/bridge/syncthing.go
  • ios/VaultSync/zh-Hans.lproj/InfoPlist.strings
  • ios/VaultSync/Views/OnboardingView.swift
  • go/patches/README.md
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSync/es.lproj/InfoPlist.strings
  • docs/architecture.md
  • ios/VaultSyncTests/ConflictAutoResolveTests.swift
  • ios/VaultSync/Services/SyncBridgeService.swift
  • docs/troubleshooting.md
  • README.md
  • ios/project.yml
  • ios/VaultSync/Views/RelayHomeView.swift
  • go/bridge/conflicts.go
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • docs/sync-filters-ux.md
  • go/patches/syncthing/003-faster-cold-start-redial.patch
  • notify/README.md
  • go/bridge/conflicts_test.go
  • ios/VaultSyncTests/ReconnectingGracePeriodTests.swift
  • CHANGELOG.md
  • go/bridge/addresscache.go
  • go/bridge/addresscache_test.go
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSyncTests/SetupChecklistViewModelTests.swift
  • ios/VaultSync/en.lproj/Localizable.strings
  • ios/VaultSync/Services/BackgroundSyncService.swift
  • ios/VaultSync/de.lproj/Localizable.strings
  • ios/VaultSync/Views/ContentView.swift
  • ios/VaultSync/Services/SyncthingManager.swift
  • ios/VaultSync/es.lproj/Localizable.strings
  • ios/VaultSync/zh-Hans.lproj/Localizable.strings
**

⚙️ CodeRabbit configuration file

**:

VaultSync

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.

Download on the App Store



Stars
License: MPL-2.0
iOS 18+
CI

VaultSync welcome screen VaultSync home screen

🔭 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.strings
  • ios/VaultSync/en.lproj/InfoPlist.strings
  • go/bridge/syncthing.go
  • ios/VaultSync/zh-Hans.lproj/InfoPlist.strings
  • ios/VaultSync/Views/OnboardingView.swift
  • go/patches/README.md
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSync/es.lproj/InfoPlist.strings
  • docs/architecture.md
  • ios/VaultSyncTests/ConflictAutoResolveTests.swift
  • ios/VaultSync/Services/SyncBridgeService.swift
  • docs/troubleshooting.md
  • README.md
  • ios/project.yml
  • ios/VaultSync/Views/RelayHomeView.swift
  • go/bridge/conflicts.go
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • docs/sync-filters-ux.md
  • go/patches/syncthing/003-faster-cold-start-redial.patch
  • notify/README.md
  • go/bridge/conflicts_test.go
  • ios/VaultSyncTests/ReconnectingGracePeriodTests.swift
  • CHANGELOG.md
  • go/bridge/addresscache.go
  • go/bridge/addresscache_test.go
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSyncTests/SetupChecklistViewModelTests.swift
  • ios/VaultSync/en.lproj/Localizable.strings
  • ios/VaultSync/Services/BackgroundSyncService.swift
  • ios/VaultSync/de.lproj/Localizable.strings
  • ios/VaultSync/Views/ContentView.swift
  • ios/VaultSync/Services/SyncthingManager.swift
  • ios/VaultSync/es.lproj/Localizable.strings
  • ios/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.go
  • go/bridge/conflicts.go
  • go/bridge/conflicts_test.go
  • go/bridge/addresscache.go
  • go/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.swift
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSyncTests/ConflictAutoResolveTests.swift
  • ios/VaultSync/Services/SyncBridgeService.swift
  • ios/VaultSync/Views/RelayHomeView.swift
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSyncTests/ReconnectingGracePeriodTests.swift
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSyncTests/SetupChecklistViewModelTests.swift
  • ios/VaultSync/Services/BackgroundSyncService.swift
  • ios/VaultSync/Views/ContentView.swift
  • ios/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.swift
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSyncTests/ConflictAutoResolveTests.swift
  • ios/VaultSync/Services/SyncBridgeService.swift
  • ios/VaultSync/Views/RelayHomeView.swift
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSyncTests/ReconnectingGracePeriodTests.swift
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSyncTests/SetupChecklistViewModelTests.swift
  • ios/VaultSync/Services/BackgroundSyncService.swift
  • ios/VaultSync/Views/ContentView.swift
  • ios/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.md
  • docs/architecture.md
  • docs/troubleshooting.md
  • README.md
  • docs/sync-filters-ux.md
  • notify/README.md
  • CHANGELOG.md
docs/**

⚙️ CodeRabbit configuration file

docs/**: # Architecture

VaultSync 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.
  • BackgroundBGAppRefreshTask (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 → iPhone wake-ups via APNs silent push. See relay-spec.md.

VaultSync is intentionally asymmetric:

Direction Path
Server → iPhone vaultsync-notify spots 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 → iPhone acceleration path, not a guarantee of symmetric real-time background sync.

🌉 Go bridge (go/bridge/)

Minimal API exported via gomobile. Only prim...

Files:

  • docs/architecture.md
  • docs/troubleshooting.md
  • docs/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.strings
  • ios/VaultSync/de.lproj/Localizable.strings
  • ios/VaultSync/es.lproj/Localizable.strings
  • ios/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.swift
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSync/Services/SyncBridgeService.swift
  • ios/VaultSync/Views/RelayHomeView.swift
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSync/Services/BackgroundSyncService.swift
  • ios/VaultSync/Views/ContentView.swift
  • ios/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

Comment thread CHANGELOG.md Outdated
Comment thread docs/architecture.md Outdated
Comment thread docs/sync-filters-ux.md
Comment thread go/bridge/addresscache.go
Comment thread ios/VaultSync/Services/SyncthingManager.swift
Comment thread ios/VaultSync/Views/ContentView.swift Outdated
Comment thread ios/VaultSync/Views/DeviceDetailView.swift
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.
@psimaker
psimaker merged commit dd43508 into main Jun 12, 2026
13 checks passed
@psimaker
psimaker deleted the fix/cold-start-reconnect branch June 12, 2026 07:09
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