fix(ios): consent dialogs with visible Cancel on iOS 26; localized marker-missing folder error (#64, #65)#72
Conversation
…uidance (#65) When a vault's folder was moved, renamed, replaced, or deleted outside the app, Syncthing's raw English "folder marker missing (this indicates potential data loss, …)" text surfaced verbatim as the user-facing message in every locale: the bridge classifies it as "unknown_error", so SyncUserError.fromFolderStatus fell through to the default branch, which passes the engine message straight through. The paired guidance ("…then retry" and the "Rescan Failed Vaults" button) could not help: Syncthing refuses to scan a folder without its marker, by design. What could go wrong and why this is safe: - The new mapping is match-only presentation: no path is touched, nothing is re-pointed, re-accepted, or rescanned automatically (doctrine 002; safety rule 3 untouched). - Matched on the message ("marker missing") before the reason switch in fromFolderStatus and before the generic classifiers in from(rawMessage:), so both surfaces (folder status row, failed-rescan error) map identically; every other error keeps its existing mapping (regression-tested). - The rescan CTA hides only when every errored folder is a marker-missing case — any other folder error keeps the button. - The raw engine text stays available in technicalDetails. Strings added to all four .lproj files; troubleshooting.md gained the linked "Vault Folder Was Moved or Deleted" section; regression tests in SyncUserErrorTests reference #65. Fixes #65.
…ble Cancel (#64) On iOS 26, .confirmationDialog renders as a centered popover whose cancel-role button is not rendered at all — dismissal only by tapping outside. The merge-consent dialog (the #54 consent gate) and the vault-removal dialog therefore showed the destructive action as their only visible button, funneling users toward exactly the action those gates exist to slow down. iOS 18 rendered the same code as an action sheet with a visible Cancel. Recorded as decision 011. What could go wrong and why this is safe: - Pure presentation change: titles, consequence texts, button labels, roles, and the confirm handlers are unchanged; no accept logic and no localization keys touched. - .alert renders title, full message, destructive action, and Cancel on every supported OS version — verified empirically with before/after screenshots on iOS 26.5 and iOS 18.0 simulators. - The DEBUG-only UIAuditFixture launch-argument harness added here seeds the dialog/error states for that evidence; it is compiled out of release builds and skips all engine management while active (same reasoning as TestHost), so it can never affect shipping behaviour. The two remaining .confirmationDialog instances (device removal, conflict resolve) are converted in the follow-up commit per decision 011. Fixes #64.
…olve consent (#64) Completes the class-wide cleanup behind decision 011: on iOS 26 a .confirmationDialog renders as a centered popover without its cancel-role button. The device-removal dialog additionally declared no Cancel button at all, so on iOS 26 it offered destructive "Remove" as its only visible choice; the conflict-resolve dialog discards a version of a note on two of its three actions. What could go wrong and why this is safe: - Presentation-only: titles, messages, button labels, roles, and handlers are unchanged — except that device removal gains the explicit Cancel it always should have had (key exists in all four locales). keep-both keeps its non-destructive role. - Verified empirically via the UIAuditFixture harness: after-screenshots on iOS 26.5 show destructive action and Cancel rendered for both dialogs. Refs #64 / decision 011. The remaining low-priority UI-audit findings stay bundled in #71.
📝 WalkthroughWalkthroughConsent-related SwiftUI dialogs in ContentView, DeviceDetailView, and ConflictDiffView are converted from confirmationDialog to alert to ensure Cancel is always visible (iOS 26 fix). A new folderMarkerMissing error category adds localized recovery guidance and gates the Rescan CTA. Debug-only UI audit fixtures seed these states for verification, plus supporting docs and tests. ChangesConsent alert migration and folder marker missing handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 7✅ Passed checks (7 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ios/VaultSync/Views/SyncIssuesView.swift (1)
161-185: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRoute marker-missing folder errors to the moved/deleted guide
troubleshootingURL(for:)still hard-codes#bookmark-access-expiredfor.folderErrors, so a card that only contains folder-marker loss points to the wrong recovery doc. UsehasRescanableFolderErrorshere (or reuseSyncUserError.troubleshootingURL) so marker-missing-only cases land on#vault-folder-was-moved-or-deleted.🤖 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/Views/SyncIssuesView.swift` around lines 161 - 185, The troubleshooting link selection in troubleshootingURL(for:) is routing .folderErrors to the bookmark-access-expired anchor, which is the wrong recovery doc for marker-missing-only cases. Update the .folderErrors handling to use hasRescanableFolderErrors or reuse SyncUserError.troubleshootingURL so those cases point to the moved/deleted guide anchor instead. Keep the existing nil behavior for the other kinds in SyncIssuesView.
🤖 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.
Outside diff comments:
In `@ios/VaultSync/Views/SyncIssuesView.swift`:
- Around line 161-185: The troubleshooting link selection in
troubleshootingURL(for:) is routing .folderErrors to the bookmark-access-expired
anchor, which is the wrong recovery doc for marker-missing-only cases. Update
the .folderErrors handling to use hasRescanableFolderErrors or reuse
SyncUserError.troubleshootingURL so those cases point to the moved/deleted guide
anchor instead. Keep the existing nil behavior for the other kinds in
SyncIssuesView.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 222774a0-ee61-4194-b92d-e26aaa77a8aa
📒 Files selected for processing (16)
CHANGELOG.mddocs/decisions/011-consent-dialogs-alert-not-confirmation-dialog.mddocs/troubleshooting.mdios/VaultSync/App/UIAuditFixture.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Models/SyncUserError.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Views/ContentView.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSyncTests/SyncUserErrorTests.swift
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
- GitHub Check: Go Tests
⚠️ CI failures not shown inline (2)
GitHub Actions: Security / Go Lint: fix(ios): consent dialogs with visible Cancel on iOS 26; localized marker-missing folder error (#64, #65)
Conclusion: failure
##[group]Run unformatted="$(gofmt -l go/bridge notify)"
�[36;1munformatted="$(gofmt -l go/bridge notify)"�[0m
�[36;1mif [ -n "$unformatted" ]; then�[0m
�[36;1m echo "::error::These files need gofmt:"�[0m
GitHub Actions: Security / 1_Go Lint.txt: fix(ios): consent dialogs with visible Cancel on iOS 26; localized marker-missing folder error (#64, #65)
Conclusion: failure
##[group]Run unformatted="$(gofmt -l go/bridge notify)"
�[36;1munformatted="$(gofmt -l go/bridge notify)"�[0m
�[36;1mif [ -n "$unformatted" ]; then�[0m
�[36;1m echo "::error::These files need gofmt:"�[0m
🧰 Additional context used
📓 Path-based instructions (9)
**
⚙️ 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:
docs/decisions/011-consent-dialogs-alert-not-confirmation-dialog.mdios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/de.lproj/Localizable.stringsdocs/troubleshooting.mdios/VaultSync/App/UIAuditFixture.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Models/SyncUserError.swiftios/VaultSync/Views/ConflictDiffView.swiftCHANGELOG.mdios/VaultSyncTests/SyncUserErrorTests.swiftios/VaultSync/Views/ContentView.swift
docs/**
⚙️ CodeRabbit configuration file
docs/**: # ArchitectureVaultSync embeds Syncthing's Go reference implementation as an iOS library via gomobile — no reimplementation of the protocol in Swift, and guaranteed wire compatibility.
┌─────────────────────────────────┐ │ SwiftUI Frontend │ iOS-native UI, Swift 6 ├─────────────────────────────────┤ │ Swift ↔ Go Bridge │ thin API via gomobile │ │ → exported as .xcframework ├─────────────────────────────────┤ │ syncthing/lib (Go) │ protocol, discovery, sync └─────────────────────────────────┘ ↕ filesystem ┌─────────────────────────────────┐ │ Obsidian Vault (direct) │ Obsidian's iOS sandbox └─────────────────────────────────┘🔄 Sync strategy
- Foreground — Syncthing runs unrestricted: immediate, continuous sync.
- Background —
BGAppRefreshTask(requested ~15 min out; iOS decides the actual timing) +BGProcessingTask(overnight catch-up: multi-minute budget while charging with network) +BGContinuedProcessingTask(iOS 26+, longer runtime for user-initiated tasks). A ~30s grace window after backgrounding lets in-flight work finish.- Push (Cloud Relay) — optional. Near-realtime
server → iPhonewake-ups via APNs silent push. See relay-spec.md.VaultSync is intentionally asymmetric:
Direction Path Server → iPhone vaultsync-notifyspots outgoing changes → Cloud Relay silent push → VaultSync wakes and pulls.iPhone → Server iOS doesn't guarantee timely background execution for local edits. The reliable path is to open VaultSync and let embedded Syncthing run in the foreground — a Shortcuts automation can do that automatically whenever you leave Obsidian. Cloud Relay is a
server → iPhoneacceleration path, not a guarantee of symmetric real-time background sync.Connection paths & iOS network privacy
How peers are reached, fastest f...
Files:
docs/decisions/011-consent-dialogs-alert-not-confirmation-dialog.mddocs/troubleshooting.md
docs/decisions/**
⚙️ CodeRabbit configuration file
docs/decisions/**: # 001 — Two Syncthing folders never overlap on disk (three enforcement layers)Context: A server sharing more than one vault could hand the second share the same local folder as the first (1.6.0–1.7.0) or a subfolder inside an existing vault (1.7.1). Overlapping folders sync each other's content as their own — deleting the stray copy on any peer would have deleted the inner vault everywhere (
#45).Decision: The no-overlap invariant (equal, nested, or containing paths) is enforced in three independent layers, each with its own tests: the Go hard floor (
AcceptPendingFolder/AddFolderreject withfolderPathOverlapError), the Swift mapping (VaultManager.resolveSharePathreturnsnilrather than an overlapping path), and the launch shield (PathCollisionGuardpauses already-overlapping folders exactly once).Why: The single-layer version failed twice — both
#45bugs lived in the Swift mapping. The Go floor backstops future mapping bugs, the Swift layer turns a hard engine error into user guidance, and only the shield catches damage that predates the fix.Rejected alternative: Enforcing only in the Swift mapping — the proven failure mode; a bug there would silently re-open the hole with no backstop.
Links:
#45, PR#47(same-folder merge, 1.7.1), PR#51(nesting, 1.7.2).
docs/decisions/**: # 002 — Recovery from data damage is never automaticContext: When VaultSync detects existing damage (two vaults merged into one folder, or one nested inside another,
#45), an automatic repair would have to move, rename, delete, or re-accept user data whose true state only the user knows — after a merge, the app cannot tell which files belong to which vault.Decision: Recovery is always: pause the affected folders (exactly once — a folder the user deliberately resumes is never re-paused), explain the problem in a critical issue with concrete recovery steps, and let the user act.
Why: Sync propagates every local act...
Files:
docs/decisions/011-consent-dialogs-alert-not-confirmation-dialog.md
**/*
⚙️ CodeRabbit configuration file
**/*: VaultSync syncs private Obsidian notes through Syncthing. Treat data loss,
privacy leaks, security regressions, and broken sync behavior as high priority.
Do not nitpick formatting unless it affects maintainability, correctness, or public API clarity.
Flag any accidental logging, telemetry, crash reporting, or network transfer of note contents,
vault paths, filenames with private context, API keys, APNs tokens, relay keys, or security-scoped bookmark data.
Files:
docs/decisions/011-consent-dialogs-alert-not-confirmation-dialog.mdios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/de.lproj/Localizable.stringsdocs/troubleshooting.mdios/VaultSync/App/UIAuditFixture.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Models/SyncUserError.swiftios/VaultSync/Views/ConflictDiffView.swiftCHANGELOG.mdios/VaultSyncTests/SyncUserErrorTests.swiftios/VaultSync/Views/ContentView.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:
docs/decisions/011-consent-dialogs-alert-not-confirmation-dialog.mddocs/troubleshooting.mdCHANGELOG.md
ios/**/*.{swift,strings,stringsdict}
📄 CodeRabbit inference engine (README.md)
Support localization in English, German, Spanish, and Simplified Chinese
Files:
ios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/App/UIAuditFixture.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Models/SyncUserError.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSyncTests/SyncUserErrorTests.swiftios/VaultSync/Views/ContentView.swift
**/*.swift
📄 CodeRabbit inference engine (Custom checks)
For Swift background execution changes, pass if work is bounded, cancellation-aware, handles expiration callbacks, and records errors without leaking private vault data. Fail only when background work can continue unbounded, miss cleanup, or violate iOS background execution constraints.
Files:
ios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/App/UIAuditFixture.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Models/SyncUserError.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSyncTests/SyncUserErrorTests.swiftios/VaultSync/Views/ContentView.swift
ios/**/*.swift
📄 CodeRabbit inference engine (README.md)
ios/**/*.swift: Use Swift 6 and SwiftUI for iOS app development
Implement VoiceOver and Dynamic Type accessibility support throughout the app
UseBGAppRefreshTaskandBGContinuedProcessingTask(iOS 26+ when available) for background sync operations
Use APNs silent push notifications via Cloud Relay for server-to-iPhone wake-ups
Implement side-by-side diff resolution for Markdown file conflicts
Provide an activity timeline and diagnostics interface showing exactly what synced and when
Implement QR code pairing for Syncthing Device ID connection setup
Detect and list available Obsidian vaults automatically upon connection
Files:
ios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/App/UIAuditFixture.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Models/SyncUserError.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSyncTests/SyncUserErrorTests.swiftios/VaultSync/Views/ContentView.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/SyncIssuesView.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/App/UIAuditFixture.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Models/SyncUserError.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSyncTests/SyncUserErrorTests.swiftios/VaultSync/Views/ContentView.swift
ios/**/*.{swift,pbxproj}
📄 CodeRabbit inference engine (README.md)
Target iOS / iPadOS 18 or later as the minimum deployment target
Files:
ios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/App/UIAuditFixture.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Models/SyncUserError.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSyncTests/SyncUserErrorTests.swiftios/VaultSync/Views/ContentView.swift
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: If VaultSync shows "Sync Engine Not Running" and the Device ID is empty, force-close the app, reopen it, keep it in the foreground for 20–30 seconds, confirm the Device ID appears, rescan the vault, and reboot the device if needed.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: For `vaultsync-notify` 401/403 or permission-denied errors, run the helper as the user that owns `config.xml`, point `SYNCTHING_CONFIG` at the real Syncthing config if needed, and remove any incorrect `SYNCTHING_API_KEY` override before recreating the container and rerunning `--doctor`.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: If VaultSync reports "Relay Unreachable", verify internet access, check `RELAY_URL`, review firewall/VPN/proxy egress rules, test the relay health endpoint from the homeserver, and confirm the app's relay status and last trigger received update.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: If onboarding is stuck on "Confirm your first share" and no pending shares appear, verify the desktop Syncthing folder is shared to the iOS Device ID, confirm the Device ID matches, ensure Syncthing is online and the folder is not paused, then refresh or re-share the folder.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: If APNs wake-ups are not arriving, use the app's relay diagnostics to retry APNs registration, retry provisioning to rebind the token to the device IDs, and then verify a file change updates "Last Trigger Received".
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: If VaultSync says the Obsidian folder is not connected, make sure Obsidian has been opened at least once, reconnect the folder in the Files picker, choose the correct Obsidian folder or vault root containing `.obsidian`, and confirm vaults or pending shares appear.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: If a vault shows "Vault Folder Was Moved or Deleted", move the folder back if it was renamed or relocated; otherwise remove the vault in VaultSync and re-accept its share to create a fresh folder.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: If VaultSync cannot access a folder because a bookmark expired, reconnect to the Obsidian folder in the Files picker, keep the app in the foreground while rescanning, and remove the vault only if its storage is truly gone.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: If background sync works in the foreground but not when the app is closed, rescan manually, clear sync issues, reconnect the Obsidian folder if needed, verify relay health for relay users, and check the last sync timestamp after the next background window.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: If a required device is disconnected, ensure desktop Syncthing is running, confirm device connectivity over LAN/VPN/relay, verify the device still exists in VaultSync, and remove/re-add it if its ID changed.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-07T18:31:55.577Z
Learning: When filing an issue, include the VaultSync version, iOS version, screenshots of Sync Issues and Relay health & diagnostics, `vaultsync-notify --doctor` output if using the relay, and relevant `vaultsync-notify` log lines.
📚 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/SyncIssuesView.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/App/UIAuditFixture.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Models/SyncUserError.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Views/ContentView.swift
🪛 LanguageTool
CHANGELOG.md
[style] ~21-~21: This expression tends to be overused. Try opting for a more concise alternative.
Context: ...during the window asks you to try again in a moment. Nothing is ever moved automatically. L...
(IN_A_MOMENT)
🔇 Additional comments (23)
docs/decisions/011-consent-dialogs-alert-not-confirmation-dialog.md (1)
1-8: LGTM!ios/VaultSync/Views/ContentView.swift (4)
158-190: Alert conversions look correct.Destructive/cancel roles and dismissal semantics (
vaultPendingRemoval = nil/pendingMergeConfirmation = nil) match the pre-existing.confirmationDialogbehavior, and thepresenting:binding wiring is the correct overload for this use case.
91-100: 🎯 Functional Correctness | ⚡ Quick winFour
.alertmodifiers now chained on one view — worth a quick manual/screenshot check.
ContentView.bodynow attaches four separate.alert(...)modifiers directly on theTabView(pre-existing "Error"/"Note" alerts plus the two new ones). SwiftUI's olderAlert-struct API had a well-known "only the last alert in the chain fires" bug; this was fixed for the.alert(_:isPresented:...)family starting iOS 15, and the pre-existing two-alert chain presumably already works in production, so risk here is low — but since the UI-audit harness exists specifically to screenshot-verify dialog rendering, it would be worth confirming all four alerts (not just the new pair) still present correctly when triggered back-to-back on iOS 18/26, rather than relying on the general API guarantee alone.Also applies to: 158-190
24-26: LGTM!Also applies to: 219-238, 363-444
390-418: 🔒 Security & PrivacyNo issue:
_testSetFoldersand_testSetFolderStatusesare#if DEBUG-gated inSyncthingManager.> Likely an incorrect or invalid review comment.ios/VaultSync/Views/DeviceDetailView.swift (1)
94-119: LGTM!Good catch adding the previously-missing Cancel button (per decision 011, this dialog "declared no Cancel at all"). Fixture gating and alert wiring both look correct.
ios/VaultSync/Views/ConflictDiffView.swift (1)
78-87: LGTM!Destructive-role assignment correctly reflects actual data risk (keepBoth is non-destructive; keepThis/keepOther are), and the alert conversion preserves the existing confirm/cancel handlers.
Also applies to: 113-128
ios/VaultSync/App/UIAuditFixture.swift (1)
1-27: LGTM!ios/VaultSync/App/VaultSyncApp.swift (1)
64-72: LGTM!Also applies to: 122-126
ios/VaultSync/Models/SyncUserError.swift (2)
49-55: Clean mapping, matches tests and doctrine-002 stance.The marker-missing detection order (checked before other classifiers, in both
from(rawMessage:)andfromFolderStatus) and the dedicatedfolderMarkerMissinghelper correctly avoid surfacing the raw "potential data loss" engine string and steer users to manual recovery instead of rescan/retry, matching the linked tests.Also applies to: 138-144, 181-199
261-262: LGTM!ios/VaultSync/Services/SyncthingManager.swift (3)
1919-1929: LGTM!
529-540: LGTM!
2282-2285: 🩺 Stability & AvailabilityNo change needed for
_testSetFolderStatuses. It’s already inside the#if DEBUGtest-hooks block, so it won’t ship in release builds.> Likely an incorrect or invalid review comment.ios/VaultSync/de.lproj/Localizable.strings (1)
657-660: LGTM!ios/VaultSync/en.lproj/Localizable.strings (1)
657-660: LGTM!ios/VaultSync/es.lproj/Localizable.strings (1)
657-660: 📐 Maintainability & Code QualityNo issue here; zh-Hans includes the same four keys.
> Likely an incorrect or invalid review comment.ios/VaultSync/Views/SyncIssuesView.swift (1)
84-94: LGTM!ios/VaultSync/zh-Hans.lproj/Localizable.strings (1)
657-660: LGTM!docs/troubleshooting.md (1)
13-13: LGTM!New section and index entry are consistent with the
SyncUserError.troubleshootingURLanchor (#vault-folder-was-moved-or-deleted) and align with the manual-recovery doctrine (decision 002).Also applies to: 117-129
CHANGELOG.md (1)
20-23: LGTM!ios/VaultSyncTests/SyncUserErrorTests.swift (2)
64-134: LGTM!
FolderMarkerMissingMappingTestscorrectly exercises both the folder-status and raw-message paths, the anti-misdirection remediation check, and the dedicated troubleshooting anchor, matching thefromFolderStatus/from/troubleshootingURLimplementations shown in context.
136-179: LGTM!
RescanCTAAvailabilityTestscorrectly validates that marker-only errors hide the rescan path while a mix with a non-marker error keeps it available, matchinghasRescanableFolderErrors.


Fixes #64, fixes #65 — the two release-blocking findings from the 1.8.0 pre-release UI audit; ships in the pending 1.8.0 build.
all), conflict resolve. Wording unchanged. Verified with before/after screenshots on iOS 26.5 and iOS 18.0 via a new DEBUG-only UIAuditFixture
launch-argument harness (compiled out of release).
demoted to technical details; the rescan CTA is hidden when no rescan can help; troubleshooting.md gained the linked section. Regression tests
reference fix(ios): raw untranslated Syncthing 'folder marker missing' error shown in de/es/zh with wrong remediation #65 (red first, then green).
Verification: full Xcode test plan green (211 passed / 0 failed, iPhone 17 Pro, iOS 26.5), design-token lint green, Localizable key counts 4x621, no
real duplicate keys. No go/ changes — no xcframework rebuild required by this PR.
Follow-up (deliberately not here): a UI-test target for automated dialog-rendering regression tests; remaining audit polish stays in #71.
This change fixes two iOS release blockers.
It switches the merge, vault-removal, device-removal, and conflict-resolution consent flows from
.confirmationDialogto.alert, so Cancel and destructive actions remain visible on iOS 18/26. It also adds DEBUG-only UI audit fixtures to preseed those screens for screenshot verification without affecting release builds.For sync errors, raw Syncthing “folder marker missing” failures are now mapped to a localized “Vault Folder Was Moved or Deleted” message in English, German, Spanish, and Simplified Chinese. The app keeps the raw engine text in technical details, removes the rescan CTA when rescan can’t help, and links users to manual recovery steps.
User-visible sync behavior and recovery guidance were updated accordingly. Privacy/security impact is limited to clearer handling of potentially unsafe folder state; background execution behavior is unchanged outside DEBUG-only guards. Coverage was added with new tests for the error mapping and rescan-CTA availability, alongside the existing Xcode test plan verification.