Skip to content

feat(ui): radical redesign — design tokens, TabView shell, actionable onboarding & Relay funnel#24

Merged
psimaker merged 22 commits into
mainfrom
redesign/vault-os-foundation
May 31, 2026
Merged

feat(ui): radical redesign — design tokens, TabView shell, actionable onboarding & Relay funnel#24
psimaker merged 22 commits into
mainfrom
redesign/vault-os-foundation

Conversation

@psimaker

@psimaker psimaker commented May 31, 2026

Copy link
Copy Markdown
Owner

What & why

Radically redesigns the iOS app UI — a "Vault OS" identity on a Calm-Native foundation (the direction a 16-agent design jury converged on). It turns a structurally-uniform-but-visually-improvised SwiftUI app (two color literals, no design system, prose-only setup, a fractured relay flow) into a coherent, tokenized, navigable, localized experience.

20 focused commits; iOS build + full test suite + the design-token lint and the notify Go suite all green; key screens verified live in the simulator; review passes hardened correctness, a11y, translations, and dead code.

Highlights

  • Design-token foundation in the shared Theme.swift (P3, automatic dark mode): one brand accent + a six-meaning semantic status palette + spacing/radius scale, replacing the previous two Color literals. New canonical SyncStatus registry (symbol + color + label).
  • Status is never color-alone anymore (symbol + label everywhere) — fixes a systemic VoiceOver / color-blind gap; the AccentColor is filled so the app-wide tint is the brand, not the leaked system blue.
  • Shared component kit (StatusBadge, StatusRow, DetailRow, ActionCard, MonoField, SyncStatusHeader, vaultCard) now actually adopted across the views that hand-rebuilt those patterns.
  • TabView shell (Sync · Devices · Relay) with a persistent Liquid-Glass status header, replacing one overloaded scrolling hub. The scenePhase-coupled Syncthing lifecycle is untouched.
  • Actionable onboarding pager — steps launch the real task (FolderPicker / device pairing / QR) and turn green when satisfied, instead of describing setup in prose. The Add-Device form is a single shared AddDeviceSheet.
  • Dedicated Cloud Relay tab with two distinct shapes: a decluttered, honestly-framed pitch when not subscribed (Relay = a tiny private wake-up on the already-free P2P sync, not cloud storage; the framing detail sits behind an info button), and an activation-focused control center when subscribed (server-setup step up front until wake-ups arrive; diagnostics, server setup, and Manage Subscription live here). A single canonical SubscribePlanPicker (monthly-first low-commitment entry, yearly framed as "Save N%" from StoreKit prices, prominent price/term/auto-renew + Terms & Privacy links, and a no-device guard) addresses the recorded App Store 3.1.2(a) risk. The old Settings → Cloud Relay section is deleted.
  • Honesty: the dashboard no longer claims "Cloud Relay active" purely from a StoreKit entitlement — it reflects actual delivery/health and otherwise routes the user to finish setup. The faked 2s rescan timer is replaced by real engine state.
  • Conflict resolution uses full-width ≥44pt buttons; all three actions confirm before mutating files (Keep Both previously didn't).
  • Widget can no longer silently report "all good" on an unknown status (unknown → attention, regression-tested) and now has VoiceOver labels.
  • Localization complete: the redesign's new strings are translated to de/es/zh with full key parity (583 keys per catalog), relay terminology unified, and a curly-apostrophe key mismatch that broke a translated string fixed.
  • Real Obsidian vaults on the home screen instead of the raw sync folder.
  • notify sidecar fix: a relay 4xx for an inactive subscription no longer kills the helper (it crash-looped under restart: unless-stopped) — it logs and stays alive.
  • Guardrail: ios/scripts/design-token-lint.sh, wired as a fast CI gate, fails if views reintroduce raw status colors (including return-position / ternary / switch-expression forms) instead of the tokens.

Component(s)

  • go (bridge / Syncthing)
  • ios (app / widget)
  • notify (relay)
  • docs / CI

Testing

  • cd go && make patch && go test -tags noassets ./bridge
  • cd notify && go test ./... — green.
  • iOS build / xcodebuild test — build + full suite green on the iOS 26.5 simulator; ios/scripts/design-token-lint.sh passes.

Vault OS Foundation UI Redesign

Overview

This PR radically redesigns the iOS app UI around a shared design-token system ("Vault OS" identity) and reorganizes the information architecture into a three-tab shell. The changes unify sync, device, and relay states with semantic colors and icons (never color-only), introduce reusable components for consistency, consolidate relay features into a dedicated tab, and fix a notify sidecar crash-loop on relay subscription errors.

User-Visible Changes

  • App Shell: Single hub replaced by a TabView with persistent "Sync," "Devices," and "Relay" tabs, plus a persistent sync-status header (Liquid-Glass style) showing overall status, icon, and subtitle.
  • Design System: Semantic status palette (success, syncing, starting, attention, error, info, inactive) applied throughout UI—colors always paired with icons and labels, never color-only. Brand accent (vaultAccent) is filled and used consistently.
  • Onboarding: Now actionable—launches real setup tasks (grant Obsidian access, add Syncthing device) and verifies completion live, replacing static pager.
  • Relay Tab: Consolidates pitch and control center. New canonical SubscribePlanPicker (App Store framing, yearly savings badge). Relay state gated on actual delivery confirmation; no longer shows false "all good" states.
  • Conflict Resolution: Full-width ≥44pt action buttons with confirmation dialogs for all mutations (keep local, keep remote, keep both).
  • Devices & Folders: Status now derives from canonical registry (icon + semantic color + label).
  • Settings: Cloud Relay section removed; consolidated into dedicated Relay tab.

Sync Behavior & State Management

  • SyncStatus registry unifies statuses (synced, syncing, starting, attention, error, paused) with stable wire encoding, symbol names, semantic colors, and localized labels. Defaults to attention for unknown values (safer than silent/green).
  • Rescan behavior switched from local timer-based approach to actual folder engine scan state (status?.state == "scanning").
  • Conflict filtering now scopes to vault subdirectories via pathPrefix parameter on ConflictListView.

Background & Widget Behavior

  • Widget: Now avoids falsely reporting "all good" when status is unknown; adds accessibilityElement(children: .combine) and explicit VoiceOver labels on status icons.
  • Notify Sidecar: Fixed to not crash-loop on relay subscription state (4xx) errors. Now distinguishes subscription-inactive (400/401/402/403) from fatal misconfiguration (404), implementing exponential backoff for transient failures and a slower recheck cadence when subscription is inactive.

Privacy & Configuration

  • No new privacy implications; relay delivery confirmation gating prevents false reporting of disabled/unpaired servers.
  • Design-token lint script (ios/scripts/design-token-lint.sh) added to CI to prevent regressions (bans raw .red/.green/.orange/.blue literals in views).

Test Coverage

  • Added SyncStatusTests covering wire decoding, round-tripping, urgency logic, and invariants.
  • Added VaultConflictAttributionTests validating vault-to-conflict path matching and edge cases.
  • Added three notify integration tests: TestRunServiceSurvivesInactiveSubscription, TestRunServiceResumesAfterSubscriptionActivates, TestRunServiceExitsOnFatalTrigger.
  • Added relay_test.go suite covering HTTP status classification, retry logic, and outcome mapping.
  • All iOS builds, notify tests, and design-token lint pass.

Localization

  • English, German, Spanish, and Simplified Chinese localization updated for new strings and redesigned UI terminology (including onboarding, relay, and sync labels). Key mismatch fixed.

Components Introduced

  • StatusBadge: Icon + label for sync status.
  • StatusRow: Title/subtitle with optional status glyph and trailing content.
  • DetailRow: Title/value with optional monospaced rendering.
  • ActionCard: Status + message + optional primary action.
  • MonoField: Tap-to-copy monospaced field with haptics and "copied" feedback.
  • SyncStatusHeader: Busy spinner or status icon with title/subtitle.
  • vaultCard() modifier: Consistent card surface with optional accent strip.
  • AddDeviceSheet: Consolidated device-add workflow (QR scan, name entry, add/reset).
  • SubscribePlanPicker: Canonical relay subscription picker (App Store framing, device guard).
  • RelayHomeView: Unified relay pitch and control center.

psimaker added 20 commits May 31, 2026 16:41
…et parity

Phase 1 of the radical redesign (Vault-OS identity on a Calm-Native foundation).
Foundation only — no navigation/structure changes yet.

- Theme.swift becomes the shared design-token layer (compiled into app + widget):
  P3 / dark-aware brand accent, a six-meaning semantic status palette, and an
  8pt spacing + continuous-radius scale, replacing the previous two literals.
- Add a canonical SyncStatus registry (symbol + color + localized label) keyed by
  genuine sync state, incl. a distinct "starting" state. Uses String(localized:)
  so it resolves in both the app and the widget bundle (no L10n dependency).
- Fill AccentColor with the refined teal so the app-wide tint is the brand instead
  of the leaked system blue; also set an explicit .tint on the WindowGroup.
- Widget: decode the snapshot status through SyncStatus.fromWire — unknown values
  now resolve to "attention", never silently to the green "all good" branch
  (closes the documented widget-lies bug). CTA retinted off system blue.
- Add SyncStatus.fromWire contract tests (unknown -> attention regression guard).

Build + full test suite green on iOS 26.5 simulator.
…tionCard, MonoField)

Phase 2a — the reusable building blocks every later phase consumes, each
replacing a pattern the audit found hand-rebuilt across many views:

- StatusBadge / StatusRow: status as icon + text label (never color alone),
  driven by the SyncStatus registry.
- DetailRow: the title+value row duplicated across five views.
- ActionCard: icon + title + plain-language message + a real >=44pt primary
  action, replacing the prose "go to Settings" remediation blocks.
- MonoField: copyable monospaced field (Device IDs, .stignore globs, docker run)
  with haptic + confirmation — the Vault OS "monospace-as-identity" signature.
- vaultCard(): one card surface (continuous radius + optional status-accent bar)
  replacing the three divergent card looks.

Additive and app-only. Build green; not yet wired into screens.
Phase 2b (partial) — replace ad-hoc .green/.orange/.blue status colors with the
semantic tokens so status reads consistently and resolves correctly in dark mode:

- PendingSharesView: attention/info tokens for the failure vs "Ready" states;
  bumped the status badge off .caption2.
- SetupChecklistView: success/attention tokens for the progress tint and the
  per-item status color.
- RelayServerSetupView: success token for the "helper running" banner; removed
  the dead `private let teal` the audit flagged.

ContentView and OnboardingView are intentionally left for their Phase 3/4
rebuilds. Build green.
- Add SyncStatusHeader: a pinned material header (safeAreaInset) that states one
  glanceable truth, driven by a new canonical `overallStatus` (SyncStatus) for
  glyph + color while keeping the contextual wording in syncStatusText. Adopts
  Liquid Glass automatically on iOS 26.
- Give the hub a real navigation title ("VaultSync") instead of the empty one.
- Remove the now-redundant in-list status row (folded into the header) and its
  duplicate syncStatusIcon/syncStatusColor; relocate stale/background warnings.
- Retint the whole hub onto semantic status tokens (devices connected, errors,
  folder errors, conflict badge, vault state, "Shared With", device connectivity),
  killing the .green/.orange/.red/.blue + hand-rolled slate-opacity usage.
- Raise actionable/remediation text off .caption2 to the .footnote floor.

Build green. No navigation restructure yet (TabView is 3b).
…s (Phase 3b)

- Replace the single overloaded scrolling hub with a TabView: a "Sync" tab
  (status header + issues + Obsidian + pending shares + vaults) and a "Devices"
  tab (paired peers + Add Device), giving the app the HIG-correct bottom-tab
  shell and thumb-zone navigation it lacked.
- Keep all @State, sheets, alerts and onChange handlers in ContentView, attached
  at the shell level so cross-tab triggers (e.g. "Add Device" from a Sync-tab
  issue) still present correctly. The scenePhase-coupled Syncthing lifecycle in
  VaultSyncApp is untouched.
- Add-Device sheet grows with Dynamic Type ([.medium, .large] instead of medium-only).

The dedicated Relay tab + cross-linked funnel is Phase 4. Build + tests green.
…se 2b)

Retint the remaining views so status reads consistently and resolves correctly
in dark mode app-wide:

- SyncActivityView: error/info tokens for event severity.
- DeviceDetailView: success/inactive for connectivity.
- LineDiffView: unify the diff colors onto status tokens — fixes the mixed-hue
  bug (Color.green background vs systemGreen foreground) the audit flagged, and
  gives dark-mode-correct, contrast-validated add/remove highlights.
- SettingsView + RelayDiagnosticsView + CloudRelayUpsellView: success/attention/
  error tokens for relay subscription, delivery, health, APNs, and error states.

Only OnboardingView remains on raw colors; it is rebuilt in Phase 4. Build green.
Rebuild onboarding from prose-that-points-elsewhere into a real pager whose steps
launch the actual task — the audit's highest-leverage single change ("setup is
described but not doable" / "the checklist lies").

- TabView(.page) pager: a Welcome page (value pitch) + a live "Let's get your
  vault synced" page with three actionable step cards.
- Each step runs the real task inline and self-verifies against live manager
  state, turning green when its prerequisite is met:
  - Connect Obsidian folder -> FolderPicker
  - Add your computer/server -> Device ID form + QR scanner
  - Sync your first vault -> info + auto-green when a folder arrives
- Page dots + a persistent bottom bar (Continue -> Open VaultSync); tokens/radii
  throughout; Add-Device sheet grows with Dynamic Type.
- Animate the onboarding->hub swap in VaultSyncApp (was an abrupt no-transition flip).

Welcome page visually verified in the simulator; build green.
Replace the three disconnected relay altitudes (paywall / Docker wall / engineer
dump) with one progressively-disclosed Relay tab, and unify the subscribe UI.

- SubscribePlanPicker: the single canonical subscription picker — yearly first and
  highlighted as "Best value", monthly below, restore, and prominent price / term /
  auto-renew disclosure with Terms of Use + Privacy Policy links (App Store 3.1.2(a)).
  Replaces the two previously duplicated, opposite-ordered blocks.
- RelayHomeView: the new Relay tab. Not subscribed -> pitch + SubscribePlanPicker.
  Subscribed -> a status badge + a three-step spine (Subscribe -> run the server
  helper -> verify delivery) that links into RelayServerSetupView and
  RelayDiagnosticsView, finally cross-linking the funnel that had no links.
- ContentView: add the Relay tab (Sync / Devices / Relay); the dashboard relay row
  and the one-time aha-moment now select the tab instead of presenting a sheet.
- Delete CloudRelayUpsellView (superseded by the Relay tab).
- SettingsView: list yearly before monthly to match the canonical order.

Build + tests green.
…tus motion (Phase 5)

- ConflictDiffView: replace the tiny .caption2 bottom-bar icons (which looked like
  a fake tab bar) with a real bottom action bar of full-width >=44pt buttons. ALL
  three actions now confirm before mutating files — "Keep Both" previously mutated
  with no confirmation. Keep This / Keep Other are destructive-styled and confirmed;
  Keep Both confirms with a non-destructive message. Labels are now localized; the
  diff legend uses the semantic status tokens.
- ContentView: the Vault Detail rescan no longer fakes completion with a fixed 2s
  Task.sleep — the busy state now reflects the folder's real "scanning" engine state.
- SyncStatusHeader: add contentTransition(.symbolEffect(.replace)) so the status
  glyph morphs (e.g. syncing arrows -> checkmark) on completion; .pulse while
  syncing stays, both reduce-motion-safe.

Build + tests green.
…l (Phase 6)

- Widget: add VoiceOver support (it had zero) — decorative status glyphs are
  hidden and each widget family is exposed as one combined, readable element
  (status + last sync + files + action).
- L10n.tr: add a `value: key` fallback so a missing translation renders readable
  English instead of the bare key, matching the widget's helper.
- Add ios/scripts/design-token-lint.sh — fails if views reintroduce raw status
  colors (.red/.green/.orange/.blue) or hardcoded literals instead of Theme.swift
  tokens — and wire it as a fast CI gate before the macOS build. The guardrail
  immediately caught two missed .orange usages in ContentView's Obsidian section,
  now retinted to the attention token.

Lint passes; build green. Note: the bulk de/es/zh translation of the newly
added (and the pre-existing 44 unlocalized) strings is a separate translation
pass — they are now all extractable keys and fall back to English meanwhile.
…tings relay removed

Addresses review feedback on the Relay tab content:

- The Relay tab now has two deliberately different shapes:
  - Not subscribed: a decluttered, focused pitch (single hero + one honest
    framing line) instead of the busy icon-title-paragraph + three benefit rows.
    Framing repositions Relay as a tiny private wake-up on top of the already-free
    P2P sync — explicitly "not cloud storage" — so it doesn't read as an Obsidian
    Sync competitor.
  - Subscribed: an operational control center. Activation is the real lever, so
    when wake-ups aren't arriving the server-setup step is front and center;
    otherwise a calm "active" status. Diagnostics, server setup, and Manage
    Subscription all live here now.
- SubscribePlanPicker: Monthly is listed first (low-commitment entry, avoids
  annual sticker shock); Yearly follows, framed as "Save N%" (computed from
  StoreKit prices) rather than a scary annual total. Adds a no-device guard — with
  no paired homeserver it guides to the Devices tab instead of letting the user
  pay for nothing to wake.
- Delete the entire Cloud Relay section from Settings; the subscribe guard, manage
  subscription, server setup, and diagnostics it held are absorbed by the Relay
  tab. The tab refreshes relay diagnostics on appear.

Build + full test suite + design-token lint green.
The "already syncs free / not cloud storage" reassurance line is no longer
always-on in the pitch — it's now a subtle "How is this private?" info button
that reveals the text in a popover, decluttering the hero further. Dropped the
hero's accessibility-combine so the button stays independently actionable for
VoiceOver.

Build + design-token lint green.
…ology

- Add the 36 redesign keys that were English-only (tab labels Sync/Relay,
  Relay-tab pitch + subscribed states, onboarding setup steps, plan picker,
  conflict "keep both" copy) to all four catalogs with real de/es/zh
  translations. Parity restored: 583 keys each.
- Fix ConflictDiffView's primary "Keep This Device's Version" button: it used
  a curly apostrophe so it never matched the straight-apostrophe catalog key
  and fell back to English in every language despite existing translations.
- Unify "wake-up" terminology: de -> "Weck-Signal" (was Wecksignal/Aufweck-
  vorgänge/Weckrufe), es -> "señal de activación" (was señal de aviso/
  activaciones). Fix es "silent push" (push silencioso), "Health Endpoint"
  (Endpoint de salud), and de Synchronisierung/Synchronisation drift.
- zh: normalize quote marks to full-width "" (were 「」 / straight ASCII).
… kit adoption

Correctness
- ContentView: the dashboard "Cloud Relay active" claim was driven solely by
  isRelaySubscribed (a StoreKit entitlement check), so a subscribed-but-unprovisioned
  device showed green "active" while nothing was delivered. Gate it on
  relayDeliveryLikelyWorking; otherwise route the user to finish server setup.
- ContentView: fold the folder-state glyph/color mapping onto the SyncStatus
  registry (one source of truth) — fixes the divergent error glyph
  (exclamationmark.circle vs the canonical xmark.octagon) and the non-semantic
  .vaultAccent fill; unknown states stay neutral rather than a false "attention".

Dead code / duplication
- Extract the byte-for-byte-duplicated Add-Device form (ContentView + Onboarding)
  into a shared AddDeviceSheet.
- ContentView: drop the unused @Environment colorScheme; collapse the teal/vaultTeal
  alias onto vaultAccent; unify the empty-device strings.
- SettingsView: remove the dead "Error" alert and its unused alertMessage/showAlert
  state (left over from the relay-section removal).
- SetupChecklistView: dedup the optional-badge across both Dynamic-Type branches and
  remove the unreachable statusText("") branch.

Design-system adoption (the kit was 0-call-site dead code)
- Wire StatusRow (device rows), DetailRow (vault detail), ActionCard (pending-shares
  banner), MonoField (Device ID), and .vaultCard() (pending-shares + checklist
  surfaces) into the views that had hand-rebuilt them.
- DesignSystem: MonoField "copied" state now reverts after 1.5s instead of latching
  forever; vaultCard's accent strip is clipped to the card shape so it follows the
  leading corners instead of being rounded on all four of its own.

Tokens / color
- RelayDiagnosticsView: the .green/.red/.blue returned from the status-color helpers
  evaded the design-token lint (return-position blind spot) — map them onto
  statusSuccess/statusError/statusInfo.
- Tokenize stray radii/spacing (checklist, pending shares, relay command box);
  LineDiffView uses .foregroundStyle over the deprecated .foregroundColor.

Accessibility
- OnboardingView: the step card combined its children AND overrode the label, hiding
  both the description and the action button from VoiceOver — combine only the text,
  keep the button as its own element.
- DeviceDetailView: stop overriding the Status row's label so LabeledContent keeps the
  "Status" field name.
- SubscribePlanPicker: fold the "best value / save N%" badge into the recommended
  card's VoiceOver label; hide the in-progress spinner.
- PendingSharesView: drop the redundant "Applying share" spinner label.
…rn blind spot

- RelayHomeView: the "How is this private?" popover showed only a truncated first
  line ("Dein Vault synchronisiert bereits kostenl…"). It sized the text to a
  single line; force multi-line wrapping at a fixed width with fixedSize(vertical)
  so the popover grows to the full text.
- design-token-lint.sh: also flag return-position / ternary / switch-expression
  status colors (return .red, cond ? .green : .red, case .x: .orange) — the blind
  spot that let helper functions evade the lint. Tightening it immediately caught
  two more missed colors in SyncIssuesView's severity helper, now retinted to
  statusError / statusAttention.

Build + full test suite + (tightened) design-token lint green.
The relay declines triggers with HTTP 400/401/403 when a device's
subscription is expired, cancelled, or not yet provisioned. doTrigger
classified those as fatal, so fireTrigger exited the process — and with
`restart: unless-stopped` that turned any normal subscription lapse into
a crash-restart loop.

Treat the relay's subscription-gating codes (400/401/402/403) as a
recoverable subscriptionInactiveError: log and keep running, and re-check
on a slow cadence so delivery resumes automatically once the subscription
is active again, without hammering the relay. 404 stays fatal (wrong
RELAY_URL); other 4xx and 5xx remain transient retries. fireTrigger now
returns a triggerOutcome enum instead of (delivered, fatal) bools.

Add unit and end-to-end runService tests (survival, auto-resume, fatal
exit) and document the trigger error contract in docs/relay-spec.md.
…older

The "Obsidian Vaults" section rendered syncthingManager.folders directly, so a
whole-directory sync (one folder rooted at the Obsidian directory, labelled
"Obsidian") showed a single "Obsidian" row instead of the vaults inside it.
VaultManager.detectedVaults already discovered the real vaults (subfolders with
.obsidian/) but was only used to pick an empty-state message.

Derive the displayed list from the detected vaults mapped onto whichever
Syncthing folder syncs them: a folder rooted at the Obsidian directory expands
into one row per vault; per-vault folders still map 1:1. Path comparison is
symlink/trailing-slash tolerant. Conflicts are attributed per vault by
folder-relative path so a shared folder's conflicts no longer appear duplicated
across vault rows. The detail view is vault-centric (name + subpath) with a
note that sync filters and devices apply to the whole directory.

Adds VaultConflictAttributionTests for the path-prefix boundary logic.
zh:
- Monthly subscription period rendered "1 月" (reads as "January").
  Change month singular 月 -> 个月.
- Standardize 重扫 -> 重新扫描 (9x; Apple-standard "rescan").
- QR Code -> 二维码 (Scan button, camera-permission body, InfoPlist).
- "Present" 已提供 -> 存在 (APNs-token diagnostic; antonym of 缺失).
- "immediate" 即时 -> 目前 in "No immediate relay problems detected".
- Restore 前 ("ago") in "...more than %d %@ ago", parallel to the sibling.
- Symmetric Keep This / Keep Other (both on 版本).
- Full-width colon in "%@: %@".

de:
- Healthy/Unhealthy "Gesund/Ungesund" (medical sense) ->
  "Funktionsfähig/Nicht funktionsfähig" for a service-health status
  (incl. the "...is not healthy" body line).
- "No folders syncing yet" calque -> "Es werden noch keine Ordner synchronisiert".
- Unify contribution wording to "Unterstützung".
- "Last Trigger Received" -> noun-phrase label "Letzter empfangener Trigger".

es:
- "Last wake-up" -> consistent "señal de activación".
- "Best value" "La mejor opción" -> "Mejor valor".

Add missing keys Starting / Paused (SyncStatus badge) and Node modules
(Go-bridge detected-pattern label) to all four catalogs — these were
rendering the English fallback in de/es/zh. Parity restored: 587 keys each.
Catalogs (all four, parity 587 -> 549):
- Drop 38 unused string keys verified to have zero Swift references
  (neither L10n.tr/fmt nor bare SwiftUI string literals):
  the removed onboarding-overview page (11 onboarding.overview.* keys
  + 2 onboarding.accessibility hints) and ~25 superseded Cloud Relay
  redesign strings (Subscribe Monthly/Yearly, Make incoming sync instant,
  Not Now, Finish setup, the long pitch variants, etc.).

Swift (verified no callers; test-only symbols left intact):
- Theme: Color.vaultViolet (never wired), VaultSpacing.xxl (unused token).
- SubscriptionManager: relayProductID + availableProduct (legacy
  single-product aliases; UI reads monthlyProduct/yearlyProduct directly).
- VaultManager: stopAccess() (+ its now-orphaned // MARK: - Lifecycle).

Builds clean for iphonesimulator.
Shorten translation values for width-constrained controls so they no
longer truncate/wrap on iPhone (English keys unchanged):

de — tight .controlSize(.small) SyncIssuesView buttons + SyncStatus pills:
- "Run Foreground Rescan": "Erneuten Scan im Vordergrund ausführen" -> "Im Vordergrund scannen"
- "Add or Reconnect Device": "Gerät hinzufügen oder neu verbinden" -> "Gerät verbinden"
- "Rescan Failed Vaults": "Fehlgeschlagene Vaults erneut scannen" -> "Fehler-Vaults neu scannen"
- "Sync Error" -> "Sync-Fehler", "Sync Issue" -> "Sync-Problem",
  "Needs Attention" -> "Aktion nötig" (status pills; also widget catalog)

es:
- "Add or Reconnect Device" -> "Añadir o reconectar"
- "Run Foreground Rescan" -> "Reescanear ahora"
- "Timed out" -> "Sin respuesta"
- "Reconnect Obsidian Directory" (alert action) -> "Reconectar Obsidian"

zh:
- "Accept First Pending Share" -> "接受待处理共享"
- "Add or Reconnect Device" -> "添加或重连设备"
@coderabbitai

coderabbitai Bot commented May 31, 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 5 minutes and 14 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more 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: 4469ad1a-2933-4c58-9d06-a2eddab7fd81

📥 Commits

Reviewing files that changed from the base of the PR and between 75ccb26 and b8467ff.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.md
  • ios/VaultSync/Models/SyncUserError.swift
  • ios/VaultSync/ViewModels/SetupChecklistViewModel.swift
  • ios/VaultSync/Views/AddDeviceSheet.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSync/Views/OnboardingView.swift
  • ios/VaultSync/Views/SubscribePlanPicker.swift
  • ios/VaultSync/de.lproj/Localizable.strings
  • ios/VaultSync/en.lproj/Localizable.strings
  • ios/VaultSync/es.lproj/Localizable.strings
  • ios/VaultSync/zh-Hans.lproj/Localizable.strings
  • ios/VaultSyncWidget/VaultSyncWidget.swift
📝 Walkthrough

Walkthrough

This PR unifies the iOS app's design system by centralizing colors, spacing, and status semantics into a reusable token library, introduces a comprehensive component toolkit, refactors major views to use the new system, and adds subscription-inactive retry handling to the relay backend with corresponding tests.

Changes

iOS Design System & Relay Subscription Redesign

Layer / File(s) Summary
Design Token System & SyncStatus Registry
ios/VaultSync/Resources/Theme.swift, ios/VaultSync/Resources/Assets.xcassets/AccentColor.colorset/Contents.json, ios/VaultSync/Resources/L10n.swift, ios/VaultSyncTests/SyncStatusTests.swift
Dynamic Display-P3 colors with light/dark trait support, semantic status palette (statusSuccess, statusError, statusAttention, etc.), shared spacing/radius scales, and canonical SyncStatus enum with wire encoding, localized labels, and urgency classification. Tests validate decoding, round-trip encoding, and required metadata on all statuses.
Reusable Design System Components
ios/VaultSync/Views/DesignSystem.swift
Seven new reusable components: StatusBadge (icon+label), StatusRow (title/subtitle/status/trailing), DetailRow (title/value with optional monospace), ActionCard (status card with action), MonoField (tap-to-copy pasteboard), SyncStatusHeader (animated status with spinner/symbol), and vaultCard() modifier for consistent card surfaces.
Main App Navigation & VaultSyncApp
ios/VaultSync/App/VaultSyncApp.swift, ios/VaultSync/Views/ContentView.swift
Root scene applies vaultAccent tint and onboarding-driven animation. ContentView refactored from single-page navigation to TabView with Sync/Devices/Relay tabs. Global sheets (Add Device, Settings, Obsidian picker) moved to shell for cross-tab access. New vault-row model with canonical path canonicalization and per-vault conflict filtering. Status colors and component usage throughout.
Onboarding & Setup Flow Redesign
ios/VaultSync/Views/OnboardingView.swift, ios/VaultSync/Views/SetupChecklistView.swift
OnboardingView refactored from enum-driven screens to two-page TabView with live completion state derived from vaultManager/syncthingManager. Step cards become interactive, launching real Obsidian picker and Add Device sheet. SetupChecklistView updated with semantic colors (statusSuccess/statusAttention), shared design constants, and .vaultCard() styling.
Relay UI Redesign: Subscription & Diagnostics
ios/VaultSync/Views/RelayHomeView.swift, ios/VaultSync/Views/SubscribePlanPicker.swift, ios/VaultSync/Views/SettingsView.swift, ios/VaultSync/Views/RelayServerSetupView.swift, ios/VaultSync/Views/RelayDiagnosticsView.swift
New RelayHomeView with unsubscribed (pitch + SubscribePlanPicker) and subscribed (status + management) states. SubscribePlanPicker displays StoreKit products with savings calculation and restore-purchases flow. CloudRelayUpsellView removed entirely. Settings no longer shows relay UI. RelayServerSetupView and RelayDiagnosticsView migrate to semantic status colors.
Detail Views & Supporting Components
ios/VaultSync/Views/AddDeviceSheet.swift, ios/VaultSync/Views/DeviceDetailView.swift, ios/VaultSync/Views/ConflictDiffView.swift, ios/VaultSync/Views/ConflictListView.swift, ios/VaultSync/Views/PendingSharesView.swift, ios/VaultSync/Views/LineDiffView.swift, ios/VaultSync/Views/SyncActivityView.swift, ios/VaultSync/Views/SyncIssuesView.swift, ios/VaultSyncTests/VaultConflictAttributionTests.swift
New AddDeviceSheet encapsulates device-add workflow. Detail views refactored to use semantic colors and new design components. ConflictListView adds vault-scoped filtering via pathPrefix. ConflictListView adds belongs(toVault:) helper. New test suite validates vault-conflict attribution with path normalization and prefix matching.
Widget & Asset Updates
ios/VaultSyncWidget/VaultSyncWidget.swift
Widget snapshot derives status from canonical SyncStatus.fromWire(). Small/medium/accessory views use vaultAccent instead of hardcoded blue. Accessibility improvements: status icons marked hidden, container elements combined for accessibility.
Relay Subscription-Inactive Handling & Tests
notify/main.go, notify/relay.go, notify/main_test.go, notify/relay_test.go
notify/main.go classifies relay outcomes (delivered/fatal/retry/subscriptionInactive) instead of boolean pairs. inactiveRecheckInterval used for slower polling when subscription inactive. notify/relay.go maps HTTP 400/401/402/403 to non-fatal, non-retried subscriptionInactiveError. Comprehensive test suite validates classification, immediate return on subscription-inactive (no retries), fatal on 404, and automatic recovery when subscription activates.
Localization & CI Enforcement
ios/VaultSync/{en,de,es,zh-Hans}.lproj/Localizable.strings, ios/VaultSync/zh-Hans.lproj/InfoPlist.strings, ios/scripts/design-token-lint.sh, .github/workflows/ci.yml, docs/relay-spec.md
Comprehensive localization updates across all locales for redesigned flows, status labels, and onboarding messaging. New design-token-lint.sh scans for disallowed raw system colors and hardcoded literals, integrated into CI as design-lint job. Relay-spec.md documents subscription-inactive and fatal error handling. SubscriptionManager and VaultManager legacy API removals.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Possibly Related PRs

  • psimaker/vaultsync#23: Related to Cloud Relay UI surface changes; both PRs modify relay/upsell views in ios/VaultSync/Views/ and coordinate subscription flow updates.

Poem

🎨 A palette refined, a system in place,
From tokens to colors, each surface with grace.
New components emerge, reusable and bright,
Where semantic hues guide the user's delight.
The relay now rests when its subscription grows slow,
While buttons and cards in design harmony glow. ✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch redesign/vault-os-foundation

@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 (1)
ios/VaultSync/en.lproj/Localizable.strings (1)

513-516: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace the stale “Settings → Cloud Relay” route text.

These strings still send users to Settings, but this redesign moved relay subscription/setup into the dedicated Relay tab and removed the old Settings entry. That leaves the activation funnel pointing at a dead path.

Suggested copy update
-"Enable Cloud Relay in Settings if you want changes pushed the moment they happen."
+"Enable Cloud Relay on the Relay tab if you want changes pushed the moment they happen."

-"Set up the server helper from Settings → Cloud Relay → Set Up Your Server."
+"Set up the server helper from the Relay tab → Set Up Your Server."

-"Optional: turn on Cloud Relay later for instant updates — you’ll find it in Settings."
+"Optional: turn on Cloud Relay later for instant updates — you’ll find it on the Relay tab."

Please mirror the same route fix in the localized copies too.

Also applies to: 557-557

🤖 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/en.lproj/Localizable.strings` around lines 513 - 516, The
displayed route "Settings → Cloud Relay" is stale; update the four strings
containing that phrase (the entries beginning "Enable Cloud Relay in
Settings...", "Cloud Relay — finish server setup", "You’re subscribed...
vaultsync-notify helper...", and "Set up the server helper from Settings → Cloud
Relay → Set Up Your Server.") to point to the new Relay tab instead (e.g.,
change "Settings → Cloud Relay → Set Up Your Server" to "Relay → Set Up Your
Server" or "Relay tab → Set Up Your Server" as appropriate), and apply the same
replacement to the other occurrence noted (line 557) and all localized copies so
the activation funnel points to the Relay tab rather than Settings.
🧹 Nitpick comments (2)
notify/relay.go (1)

259-267: 💤 Low value

Consider using errors.As in isFatal for consistency.

isSubscriptionInactive correctly uses errors.As, but isFatal uses a direct type assertion. While errors aren't wrapped today, this inconsistency could cause subtle bugs if error wrapping is added later. Low priority since the current code works.

 func isFatal(err error) bool {
-	_, ok := err.(*fatalError)
-	return ok
+	var e *fatalError
+	return errors.As(err, &e)
 }
🤖 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 `@notify/relay.go` around lines 259 - 267, The isFatal function currently uses
a direct type assertion against *fatalError; change it to use errors.As like
isSubscriptionInactive does so wrapped errors are detected too: in isFatal,
declare a variable var fe *fatalError and return errors.As(err, &fe) instead of
the direct assertion, keeping behavior consistent with isSubscriptionInactive
and ensuring future wrapped fatalError values are recognized.
ios/VaultSync/Views/DesignSystem.swift (1)

69-91: ⚡ Quick win

Adjust StatusRow accessibility: current trailing is non-interactive; guard against future interactive trailing

  • ios/VaultSync/Views/DesignSystem.swift sets .accessibilityElement(children: .combine) on the whole StatusRow, but its current call site (ios/VaultSync/Views/ContentView.swift) passes trailing as Text(...) only, so there are no interactive descendants being flattened today.
  • Because StatusRow accepts generic trailing: () -> Trailing, consider switching to .accessibilityElement(children: .contain) (or constraining/documenting trailing to non-interactive views) to prevent future Button/Link/etc. from becoming hard to reach with VoiceOver.
🤖 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/DesignSystem.swift` around lines 69 - 91, The StatusRow
view currently flattens all children with .accessibilityElement(children:
.combine), which can hide interactive descendants passed via the generic
trailing: () -> Trailing closure; update StatusRow to use
.accessibilityElement(children: .contain) instead (replace the .combine call) so
any future interactive trailing content (e.g., Button, Link) remains reachable
by VoiceOver, or alternatively document/constrain trailing to non-interactive
views if you prefer.
🤖 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 @.github/workflows/ci.yml:
- Line 72: In the design-lint job's checkout step, replace the floating action
ref "actions/checkout@v6" with the pinned full commit SHA for that release and
add a with block setting persist-credentials: false; specifically update the
checkout step used in the design-lint job (the actions/checkout invocation) to
reference the exact commit SHA and include persist-credentials: false to prevent
the default GitHub token from being persisted.

In `@ios/VaultSync/Views/AddDeviceSheet.swift`:
- Around line 48-50: The Button's disabled check currently uses deviceID.isEmpty
which allows whitespace-only IDs; update the Button(s) (the "Add" Button and any
similar controls around the same block) to disable when
deviceID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty and also add an
early-guard at the start of addDevice() to return if the trimmed ID is empty
(use the same trimming check) so mutations cannot be attempted with
whitespace-only input.

In `@ios/VaultSync/Views/DesignSystem.swift`:
- Around line 30-40: StatusBadge currently hides the icon and replaces its
accessibilityLabel with text, which loses the status semantic
(paused/attention/error) for VoiceOver; update the StatusBadge accessibility so
when text is provided it preserves the status meaning by exposing both the
visible text and the status.label (e.g. set accessibilityLabel to "\(text) —
\(status.label)" or keep accessibilityLabel as text and call
accessibilityValue(status.label)) so VoiceOver announces the badge text and the
status; modify the StatusBadge view (references: StatusBadge, status.label,
text, .accessibilityHidden, .accessibilityLabel, .accessibilityElement) to
include the status semantics alongside the override text.

In `@ios/VaultSync/Views/SubscribePlanPicker.swift`:
- Around line 155-163: The yearlySavingsText computed property currently returns
"Best value" when percent <= 0; change this so that when there is no positive
discount it returns nil instead of L10n.tr("Best value"). Specifically, in
yearlySavingsText (using subscriptionManager.monthlyProduct and
subscriptionManager.yearlyProduct and the percent computation), replace the
guard that currently does "guard percent > 0 else { return L10n.tr("Best value")
}" with a guard that returns nil for non-positive percent so the badge is only
shown when percent > 0.
- Around line 43-63: The Restore Purchases button is only disabled by
isRestoring but can be tapped while subscriptionManager.purchaseInProgress is
true; update the .disabled modifier to disable when either isRestoring or
subscriptionManager.purchaseInProgress, and add a guard at the start of the Task
(or before setting isRestoring) to return early if
subscriptionManager.purchaseInProgress or isRestoring is true so
subscriptionManager.restorePurchases() is not invoked during an ongoing
purchase; reference the isRestoring state, the
subscriptionManager.purchaseInProgress flag, and the
subscriptionManager.restorePurchases() call to locate where to add the checks.

In `@ios/VaultSync/zh-Hans.lproj/Localizable.strings`:
- Line 113: The Chinese translation for the localization key "Last successful
sync was more than %d %@ ago." is ungrammatical; update the value in
zh-Hans.lproj Localizable.strings (the string whose key is "Last successful sync
was more than %d %@ ago.") to a natural Chinese phrase that correctly orders the
placeholders and connector words, for example use a form like "距今上次成功同步已超过
%d%@。" or "上次成功同步距今已超过 %d%@。" so the numeric (%d) and unit (%@) render together
correctly and the sentence reads naturally in the UI.

In `@ios/VaultSyncWidget/VaultSyncWidget.swift`:
- Around line 42-52: buttonLabel currently branches on the raw wire string
(`status == "syncing"`) which misses aliases; change the branching to use the
canonical decoded status on the model (use `entry.snapshot.syncStatus` /
`SyncStatus.fromWire(status)`) instead of the raw `status` string so aliases
like `"scanning"` are handled; update the logic in `buttonLabel` (and the nearby
lines ~249-253) to compare against the canonical enum/case (e.g. the `.syncing`
case or an `isSyncing` helper on `SyncStatus`) and remove reliance on the raw
string.

---

Outside diff comments:
In `@ios/VaultSync/en.lproj/Localizable.strings`:
- Around line 513-516: The displayed route "Settings → Cloud Relay" is stale;
update the four strings containing that phrase (the entries beginning "Enable
Cloud Relay in Settings...", "Cloud Relay — finish server setup", "You’re
subscribed... vaultsync-notify helper...", and "Set up the server helper from
Settings → Cloud Relay → Set Up Your Server.") to point to the new Relay tab
instead (e.g., change "Settings → Cloud Relay → Set Up Your Server" to "Relay →
Set Up Your Server" or "Relay tab → Set Up Your Server" as appropriate), and
apply the same replacement to the other occurrence noted (line 557) and all
localized copies so the activation funnel points to the Relay tab rather than
Settings.

---

Nitpick comments:
In `@ios/VaultSync/Views/DesignSystem.swift`:
- Around line 69-91: The StatusRow view currently flattens all children with
.accessibilityElement(children: .combine), which can hide interactive
descendants passed via the generic trailing: () -> Trailing closure; update
StatusRow to use .accessibilityElement(children: .contain) instead (replace the
.combine call) so any future interactive trailing content (e.g., Button, Link)
remains reachable by VoiceOver, or alternatively document/constrain trailing to
non-interactive views if you prefer.

In `@notify/relay.go`:
- Around line 259-267: The isFatal function currently uses a direct type
assertion against *fatalError; change it to use errors.As like
isSubscriptionInactive does so wrapped errors are detected too: in isFatal,
declare a variable var fe *fatalError and return errors.As(err, &fe) instead of
the direct assertion, keeping behavior consistent with isSubscriptionInactive
and ensuring future wrapped fatalError values are recognized.
🪄 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: 7592fca3-50f3-4999-a984-a9ec1429e137

📥 Commits

Reviewing files that changed from the base of the PR and between 04c7aed and 75ccb26.

📒 Files selected for processing (40)
  • .github/workflows/ci.yml
  • docs/relay-spec.md
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/VaultSync/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
  • ios/VaultSync/Resources/L10n.swift
  • ios/VaultSync/Resources/Theme.swift
  • ios/VaultSync/Services/SubscriptionManager.swift
  • ios/VaultSync/Services/VaultManager.swift
  • ios/VaultSync/Views/AddDeviceSheet.swift
  • ios/VaultSync/Views/CloudRelayUpsellView.swift
  • ios/VaultSync/Views/ConflictDiffView.swift
  • ios/VaultSync/Views/ConflictListView.swift
  • ios/VaultSync/Views/ContentView.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSync/Views/LineDiffView.swift
  • ios/VaultSync/Views/OnboardingView.swift
  • ios/VaultSync/Views/PendingSharesView.swift
  • ios/VaultSync/Views/RelayDiagnosticsView.swift
  • ios/VaultSync/Views/RelayHomeView.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/Views/SetupChecklistView.swift
  • ios/VaultSync/Views/SubscribePlanPicker.swift
  • ios/VaultSync/Views/SyncActivityView.swift
  • ios/VaultSync/Views/SyncIssuesView.swift
  • ios/VaultSync/de.lproj/Localizable.strings
  • ios/VaultSync/en.lproj/Localizable.strings
  • ios/VaultSync/es.lproj/Localizable.strings
  • ios/VaultSync/zh-Hans.lproj/InfoPlist.strings
  • ios/VaultSync/zh-Hans.lproj/Localizable.strings
  • ios/VaultSyncTests/SyncStatusTests.swift
  • ios/VaultSyncTests/VaultConflictAttributionTests.swift
  • ios/VaultSyncWidget/VaultSyncWidget.swift
  • ios/VaultSyncWidget/de.lproj/Localizable.strings
  • ios/scripts/design-token-lint.sh
  • notify/main.go
  • notify/main_test.go
  • notify/relay.go
  • notify/relay_test.go
💤 Files with no reviewable changes (3)
  • ios/VaultSync/Views/CloudRelayUpsellView.swift
  • ios/VaultSync/Services/SubscriptionManager.swift
  • ios/VaultSync/Services/VaultManager.swift
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
**/*

⚙️ 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/VaultSyncWidget/de.lproj/Localizable.strings
  • ios/VaultSync/zh-Hans.lproj/InfoPlist.strings
  • ios/VaultSyncTests/VaultConflictAttributionTests.swift
  • docs/relay-spec.md
  • ios/VaultSync/Views/SyncActivityView.swift
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/scripts/design-token-lint.sh
  • ios/VaultSync/Views/AddDeviceSheet.swift
  • ios/VaultSync/Resources/L10n.swift
  • ios/VaultSyncTests/SyncStatusTests.swift
  • ios/VaultSync/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
  • ios/VaultSync/Views/ConflictListView.swift
  • ios/VaultSync/Views/RelayHomeView.swift
  • ios/VaultSync/Views/LineDiffView.swift
  • ios/VaultSync/Views/ConflictDiffView.swift
  • ios/VaultSync/Views/SyncIssuesView.swift
  • ios/VaultSync/Views/PendingSharesView.swift
  • ios/VaultSync/Views/SubscribePlanPicker.swift
  • notify/relay.go
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSyncWidget/VaultSyncWidget.swift
  • ios/VaultSync/Views/RelayDiagnosticsView.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSync/Resources/Theme.swift
  • ios/VaultSync/Views/SetupChecklistView.swift
  • notify/main.go
  • notify/main_test.go
  • notify/relay_test.go
  • ios/VaultSync/Views/OnboardingView.swift
  • ios/VaultSync/en.lproj/Localizable.strings
  • ios/VaultSync/de.lproj/Localizable.strings
  • ios/VaultSync/es.lproj/Localizable.strings
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/zh-Hans.lproj/Localizable.strings
  • ios/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.

**/*.swift: Follow Swift API Design Guidelines for Swift code
Use Swift strict concurrency where applicable

Files:

  • ios/VaultSyncTests/VaultConflictAttributionTests.swift
  • ios/VaultSync/Views/SyncActivityView.swift
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/VaultSync/Views/AddDeviceSheet.swift
  • ios/VaultSync/Resources/L10n.swift
  • ios/VaultSyncTests/SyncStatusTests.swift
  • ios/VaultSync/Views/ConflictListView.swift
  • ios/VaultSync/Views/RelayHomeView.swift
  • ios/VaultSync/Views/LineDiffView.swift
  • ios/VaultSync/Views/ConflictDiffView.swift
  • ios/VaultSync/Views/SyncIssuesView.swift
  • ios/VaultSync/Views/PendingSharesView.swift
  • ios/VaultSync/Views/SubscribePlanPicker.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSyncWidget/VaultSyncWidget.swift
  • ios/VaultSync/Views/RelayDiagnosticsView.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSync/Resources/Theme.swift
  • ios/VaultSync/Views/SetupChecklistView.swift
  • ios/VaultSync/Views/OnboardingView.swift
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/Views/ContentView.swift
ios/**/*.swift

📄 CodeRabbit inference engine (README.md)

ios/**/*.swift: Use SwiftUI for iOS user interface development
Background execution must use BGAppRefreshTask and BGContinuedProcessingTask for iOS 26+

Files:

  • ios/VaultSyncTests/VaultConflictAttributionTests.swift
  • ios/VaultSync/Views/SyncActivityView.swift
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/VaultSync/Views/AddDeviceSheet.swift
  • ios/VaultSync/Resources/L10n.swift
  • ios/VaultSyncTests/SyncStatusTests.swift
  • ios/VaultSync/Views/ConflictListView.swift
  • ios/VaultSync/Views/RelayHomeView.swift
  • ios/VaultSync/Views/LineDiffView.swift
  • ios/VaultSync/Views/ConflictDiffView.swift
  • ios/VaultSync/Views/SyncIssuesView.swift
  • ios/VaultSync/Views/PendingSharesView.swift
  • ios/VaultSync/Views/SubscribePlanPicker.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSyncWidget/VaultSyncWidget.swift
  • ios/VaultSync/Views/RelayDiagnosticsView.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSync/Resources/Theme.swift
  • ios/VaultSync/Views/SetupChecklistView.swift
  • ios/VaultSync/Views/OnboardingView.swift
  • ios/VaultSync/Views/SettingsView.swift
  • ios/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/VaultSyncTests/VaultConflictAttributionTests.swift
  • ios/VaultSync/Views/SyncActivityView.swift
  • ios/VaultSync/Views/DeviceDetailView.swift
  • ios/VaultSync/App/VaultSyncApp.swift
  • ios/VaultSync/Views/AddDeviceSheet.swift
  • ios/VaultSync/Resources/L10n.swift
  • ios/VaultSyncTests/SyncStatusTests.swift
  • ios/VaultSync/Views/ConflictListView.swift
  • ios/VaultSync/Views/RelayHomeView.swift
  • ios/VaultSync/Views/LineDiffView.swift
  • ios/VaultSync/Views/ConflictDiffView.swift
  • ios/VaultSync/Views/SyncIssuesView.swift
  • ios/VaultSync/Views/PendingSharesView.swift
  • ios/VaultSync/Views/SubscribePlanPicker.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSyncWidget/VaultSyncWidget.swift
  • ios/VaultSync/Views/RelayDiagnosticsView.swift
  • ios/VaultSync/Views/DesignSystem.swift
  • ios/VaultSync/Resources/Theme.swift
  • ios/VaultSync/Views/SetupChecklistView.swift
  • ios/VaultSync/Views/OnboardingView.swift
  • ios/VaultSync/Views/SettingsView.swift
  • ios/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/relay-spec.md
.github/workflows/**/*.yml

⚙️ CodeRabbit configuration file

.github/workflows/**/*.yml: Review CI for correct Go test tags, Xcode/iOS simulator assumptions, secret scoping,
dependency integrity, permissions, Docker publishing safety, and unnecessary privilege escalation.

Files:

  • .github/workflows/ci.yml
**/*.go

📄 CodeRabbit inference engine (README.md)

Follow standard Go conventions for Go code

Files:

  • notify/relay.go
  • notify/main.go
  • notify/main_test.go
  • notify/relay_test.go
notify/**

📄 CodeRabbit inference engine (README.md)

Cloud Relay authentication uses Syncthing Device ID, which the sidecar reads automatically — no relay key needed

Files:

  • notify/relay.go
  • notify/main.go
  • notify/main_test.go
  • notify/relay_test.go
notify/**/*.go

⚙️ CodeRabbit configuration file

notify/**/*.go: Review goroutine lifecycle, context cancellation, HTTP timeouts, signal handling, debounce behavior,
Syncthing REST API polling, relay API calls, error classification, and API-key handling.
Flag leaked request bodies, note metadata, Syncthing API keys, relay keys, or APNs-related secrets.

Files:

  • notify/relay.go
  • notify/main.go
  • notify/main_test.go
  • notify/relay_test.go
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T20:28:45.570Z
Learning: vaultsync-notify must be stateless except for configuration; no persistent storage required
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T20:28:45.570Z
Learning: Identity in the relay system is based on Syncthing Device IDs; no user accounts or API keys required
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T20:28:45.570Z
Learning: The central relay must be horizontally scalable with stateless request handling and a shared database
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T20:28:45.570Z
Learning: vaultsync-notify works identically regardless of cloud vs self-hosted relay; the gate for features is the central relay accepting provisioned Device IDs, not feature gates in the container
🪛 zizmor (1.25.2)
.github/workflows/ci.yml

[warning] 72-72: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 72-72: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🔇 Additional comments (47)
ios/VaultSyncWidget/VaultSyncWidget.swift (1)

149-152: LGTM!

Also applies to: 158-158, 192-192, 201-201, 209-209, 224-224, 231-231

ios/VaultSync/App/VaultSyncApp.swift (1)

56-57: LGTM!

ios/VaultSync/Views/ContentView.swift (13)

18-18: LGTM!

Also applies to: 30-36


39-95: LGTM!


97-155: LGTM!


168-168: LGTM!


232-244: LGTM!

Also applies to: 246-284, 287-287, 291-291, 316-316, 323-323


332-344: LGTM!

Also applies to: 349-349, 360-360, 369-369, 375-375


405-430: LGTM!


467-468: LGTM!

Also applies to: 472-472, 496-496


629-634: LGTM!

Also applies to: 640-701


703-740: LGTM!


742-761: LGTM!


765-779: LGTM!

Also applies to: 818-818, 824-824, 863-863, 880-901


935-935: LGTM!

Also applies to: 950-958

ios/VaultSync/Views/OnboardingView.swift (8)

12-18: LGTM!

Also applies to: 23-25


29-68: LGTM!


75-84: LGTM!


86-116: LGTM!


119-181: LGTM!


183-233: LGTM!


235-279: LGTM!


281-352: LGTM!

ios/VaultSync/Views/SetupChecklistView.swift (4)

12-12: LGTM!

Also applies to: 21-22


68-68: LGTM!

Also applies to: 71-71, 81-81, 84-84


104-104: LGTM!

Also applies to: 107-107, 109-109


122-148: LGTM!

docs/relay-spec.md (1)

151-163: LGTM!

ios/scripts/design-token-lint.sh (1)

1-37: LGTM!

notify/main.go (1)

83-86: LGTM!

The outcome-based classification is a clean design that correctly separates:

  • Transient failures (retry on debounce cadence)
  • Subscription-inactive (slow recheck, no crash)
  • Fatal misconfiguration (exit 1)

Timer lifecycle is handled correctly—when entering the debounceCh case the timer has already fired, so the reassignment at lines 301–302 or 310–311 doesn't leak. Shutdown uses a fresh timeout context, and the package-level inactiveRecheckInterval for test injection is documented appropriately.

Also applies to: 173-179, 181-181, 228-237, 286-312, 486-544

notify/main_test.go (1)

267-502: LGTM!

Good regression coverage for the crash-loop scenario. I appreciate that tests modifying inactiveRecheckInterval are intentionally serial to avoid races on the package-level variable, and t.Cleanup ensures servers are properly closed.

notify/relay.go (1)

82-119: LGTM!

The status-code classification is sensible—treating 400/401/402/403 as subscription-state (self-resolving) rather than fatal prevents crash loops while still failing fast on genuine misconfigurations (404). The ProbeTrigger change correctly treats an inactive subscription as "endpoint reachable" for the doctor diagnostic.

Also applies to: 159-165, 203-226, 242-267

notify/relay_test.go (1)

1-240: LGTM!

Comprehensive test coverage for the status-code classification. The triggerStub helper is clean, and the table-driven tests in TestDoTriggerClassifiesStatusCodes make the expected behavior explicit. TestTriggerRetriesTransientThenSucceeds directly exercises the retry loop, which is a good complement to the classification-only tests.

ios/VaultSync/zh-Hans.lproj/InfoPlist.strings (1)

2-2: LGTM!

ios/VaultSyncWidget/de.lproj/Localizable.strings (1)

3-3: LGTM!

ios/VaultSync/Views/RelayHomeView.swift (1)

25-186: LGTM!

ios/VaultSync/Views/RelayServerSetupView.swift (1)

31-35: LGTM!

Also applies to: 93-103

ios/VaultSync/Views/SettingsView.swift (1)

17-19: LGTM!

Also applies to: 164-168

ios/VaultSync/Views/RelayDiagnosticsView.swift (1)

31-38: LGTM!

Also applies to: 47-49, 83-85, 124-125, 163-165, 349-380

ios/VaultSync/Views/PendingSharesView.swift (1)

18-24: LGTM!

Also applies to: 73-138

ios/VaultSync/Views/SyncIssuesView.swift (1)

60-66: LGTM!

ios/VaultSync/Views/ConflictDiffView.swift (1)

93-97: LGTM!

Also applies to: 109-110, 151-190, 226-228, 322-323

ios/VaultSync/Views/ConflictListView.swift (1)

5-8: LGTM!

Also applies to: 15-18, 72-79

ios/VaultSync/Views/DeviceDetailView.swift (1)

16-23: LGTM!

Also applies to: 37-37

ios/VaultSync/Views/LineDiffView.swift (1)

34-39: LGTM!

Also applies to: 117-118, 125-126

ios/VaultSync/Views/SyncActivityView.swift (1)

19-19: LGTM!

ios/VaultSyncTests/VaultConflictAttributionTests.swift (1)

10-17: LGTM!

Also applies to: 19-42

Comment thread .github/workflows/ci.yml
Comment thread ios/VaultSync/Views/AddDeviceSheet.swift
Comment thread ios/VaultSync/Views/DesignSystem.swift
Comment thread ios/VaultSync/Views/SubscribePlanPicker.swift
Comment thread ios/VaultSync/Views/SubscribePlanPicker.swift
Comment thread ios/VaultSync/zh-Hans.lproj/Localizable.strings Outdated
Comment thread ios/VaultSyncWidget/VaultSyncWidget.swift
psimaker added 2 commits May 31, 2026 22:45
…ADME

1.5.0 hasn't shipped yet, so the redesign folds into the existing entry rather
than a new version (no bump). Added the redesign highlights (design system,
tabbed home, do-it onboarding, dedicated Cloud Relay tab, never-color-only
status, conflict affordance, honest progress), plus the notify sidecar
crash-loop fix, the widget false-"all good" fix, and the de/es/zh translation
pass. Updated the now-stale "Settings → Cloud Relay" references to the new
Relay tab and refreshed the README "What's New" to lead with the redesign.
- Stale activation route: in-app strings + all four catalogs no longer send users
  to "Settings → Cloud Relay" (removed in the redesign) — they now point to the
  Relay tab (checklist, onboarding, and the relay-unreachable / auth remediations).
- SubscribePlanPicker: yearlySavingsText returns nil instead of a false "Best value"
  when the yearly plan has no real discount; Restore Purchases is disabled and
  guarded while a purchase is in progress.
- AddDeviceSheet: disable Add and guard addDevice() against whitespace-only IDs.
- StatusBadge: expose the status meaning as accessibilityValue when its label is
  overridden, so VoiceOver keeps the severity.
- Widget: buttonLabel branches on the canonical decoded status (handles "scanning").
- zh: fix the ungrammatical "more than %d %@ ago" stale-sync phrasing.

Deliberately skipped nitpicks: errors.As in notify isFatal (no wrapping today),
StatusRow .combine->.contain (combine is correct for the current non-interactive
trailing), and CI checkout SHA-pin/persist-credentials (matches the repo's existing
unpinned convention — better as a separate repo-wide change).

Key parity 549 across en/de/es/zh; build + full test suite + design-token lint green.
@psimaker

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@psimaker psimaker merged commit 8df3231 into main May 31, 2026
11 checks passed
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