Skip to content

Feat/multi location hosts sync readiness - #85

Merged
gajendraxdev merged 6 commits into
zync-sh:mainfrom
gajendraxdev:feat/multi-location-hosts-sync-readiness
Jul 12, 2026
Merged

Feat/multi location hosts sync readiness#85
gajendraxdev merged 6 commits into
zync-sh:mainfrom
gajendraxdev:feat/multi-location-hosts-sync-readiness

Conversation

@gajendraxdev

@gajendraxdev gajendraxdev commented Jul 12, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Browse local and provider-hosted connections together with All, Local, and Remote filters, search, location indicators, and actions to keep remote hosts on your device.
    • Added improved vault status cues, attention badges, and secure-to-vault item counts.
    • Added recovery support for restoring sync collections after a local reset.
  • Bug Fixes

    • Improved filtered restores, tunnel and backup recovery, unlock errors, warning banners, and busy-state handling.
    • Sync status now refreshes consistently after vault and provider actions.
  • Style

    • Updated provider icons, sidebar layouts, labels, and warning colors for clearer navigation.

Add host catalog merge/filter/search by logicalId, session inventory cache,
and Keep/materialize restore path for provider hosts. Include unit tests and
clearer jump-host missing guidance that is provider-neutral.
Wire the sidebar to the host catalog with All/Local/Remote filters, sticky
search chrome, provider location chips, remote Keep / Keep-and-open rows, and
session inventory refresh. Local-only chips stay hidden; multi-location badges
remain for provider-backed hosts.
Introduce a single sync readiness store for Google OAuth and collection
encryption so Sync and Backup and All Hosts stay consistent. Notify on
collection unlock and setup, improve key-wrap recovery error handling, and
refresh vault sidebar attention, theme-token warning banners, and recovery
key presentation.
Document Unreleased host catalog, shared provider readiness, vault sidebar
attention, and related fixes with commit hashes.
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@gajendraxdev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5f7c3ce-821a-4f69-9b04-334ff8a55c91

📥 Commits

Reviewing files that changed from the base of the PR and between 342ce68 and 0d9dab7.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • src-tauri/src/sync/collection.rs
  • src-tauri/src/sync/commands.rs
  • src/components/layout/Sidebar.tsx
  • src/components/layout/sidebar/VaultNavSection.tsx
  • src/components/settings/tabs/vault/VaultSyncCard.tsx
  • src/features/connections/domain/hostInventoryCache.ts
  • src/features/connections/domain/hostMaterialize.ts
  • src/features/connections/presentation/useHostCatalog.ts
  • src/store/connectionSlice.ts
📝 Walkthrough

Walkthrough

This change adds provider-backed host cataloging, cached remote inventory, selective restore and collection-key recovery, shared sync readiness state, provider-aware sidebar actions, vault attention indicators, and updated provider icons and warning styles.

Changes

Host synchronization and recovery

Layer / File(s) Summary
Remote key recovery and selective restore
src-tauri/src/sync/collection.rs, src-tauri/src/sync/commands.rs, src/vault/syncPassphrase.ts
Remote key-wrap blobs support recovery after local wipes, while filtered restores fetch targeted hosts and credentials and include jump-server dependencies.
Host catalog, cache, and materialization
src/features/connections/domain/*, src/features/connections/presentation/useHostCatalog.ts, src/store/connectionSlice.ts, tests/hostCatalog.test.mjs
Local connections and provider inventory are merged, filtered, searched, cached, materialized, invalidated, and tested through the new catalog flow.
Shared sync readiness
src/vault/useSyncReadinessStore.ts, src/vault/syncIpc.ts, src/components/settings/tabs/vault/hooks/*, src/components/settings/tabs/vault/VaultSyncCard.tsx
OAuth and collection readiness are centralized, refreshed after sync lifecycle operations, and used for provider action gating.
Provider-aware sidebar
src/components/layout/Sidebar.tsx, src/components/layout/sidebar/*
The sidebar displays local and remote catalog entries, location chips, remote materialization actions, host filters, inventory states, and vault attention indicators.
Provider icons and presentation updates
src/components/icons/providerIcons.tsx, src/components/layout/{CommandPalette,TabBar}.tsx, src/components/settings/*, src/components/vault/RecoveryKeyModal.tsx
Google and local icons replace generic cloud icons, while vault warnings, setup messages, modal copy, and theme styling are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • zync-sh/zync#57: Related sidebar and ConnectionItem context-menu wiring changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes around multi-location host catalog support and shared sync readiness.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Multi-location host catalog, Keep/materialize flow, and shared sync readiness

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds a unified host catalog merging local connections with provider inventory by stable
 logicalId.
• Wires All Hosts to All/Local/Remote filters, sticky search, location chips, and remote Keep
 actions.
• Centralizes Google OAuth + encryption readiness in one store shared by Sync & Backup and All
 Hosts.
Diagram

graph TD
  Sidebar["Sidebar UI"] --> Hook["Host catalog hook"] --> Domain["Host catalog domain"]
  Hook --> Readiness["Sync readiness store"] --> IPC["Sync IPC"] --> Rust["Rust sync commands"] --> Drive[("Google Drive")]
  Sidebar --> Materialize["Materialize hosts"] --> IPC
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Persist provider host inventory cache to disk (instead of sessionStorage)
  • ➕ Survives app restarts; fewer empty Remote views after relaunch
  • ➕ Could enable richer offline UX with last-fetched timestamps
  • ➖ Needs eviction/versioning and careful privacy review (metadata persistence)
  • ➖ More complex failure modes vs session-only cache
2. Move catalog merge/filtering to the Rust backend
  • ➕ Single canonical catalog shape for all UIs; less TS duplication
  • ➕ Backend could enforce consistent logicalId computation
  • ➖ Reduces UI flexibility for local-only UX tweaks (search/filter/chips)
  • ➖ Adds IPC payload size/latency; more backend coupling for presentation concerns

Recommendation: Current approach is well-structured: keep merge/filter/search as pure TS domain logic, keep readiness as a shared Zustand store to prevent Sync/Hosts divergence, and keep high-cost operations (inventory listing and targeted restore) in Rust. Consider disk persistence only if users need Remote host visibility across restarts; otherwise sessionStorage is a good privacy/complexity tradeoff.

Files changed (40) +2885 / -403

Enhancement (18) +2456 / -271
collection.rsAdd remote key-wrap backup/recovery for collection encryption +128/-4

Add remote key-wrap backup/recovery for collection encryption

• Introduces RemoteCollectionKeyWrapV1 and helpers to serialize/apply a provider-stored key wrap, enabling passphrase recovery after local wipes. Updates setup_manifest to accept remote wrap and improves unrecoverable/mismatch error messaging; updates tests accordingly.

src-tauri/src/sync/collection.rs

commands.rsOptimize Keep restore via filtered object downloads; wire key-wrap support +287/-44

Optimize Keep restore via filtered object downloads; wire key-wrap support

• Adds helpers to skip full provider listings when restoring specific logicalIds (Keep/Keep-and-open), constructing object names directly to reduce latency. Integrates new key-wrap structures and improves restore scope handling for filtered restores.

src-tauri/src/sync/commands.rs

providerIcons.tsxAdd shared provider icons (Google mark/Drive, local device) +174/-0

Add shared provider icons (Google mark/Drive, local device)

• Introduces GoogleMarkIcon (mono/color), GoogleDriveIcon, and LocalDeviceIcon for consistent branding and theme-friendly UI chrome.

src/components/icons/providerIcons.tsx

CommandPalette.tsxUse Google mark icon for Sync & Backup command +3/-2

Use Google mark icon for Sync & Backup command

• Replaces the generic Cloud icon with GoogleMarkIcon for the Sync & Backup entry.

src/components/layout/CommandPalette.tsx

Sidebar.tsxAll Hosts: multi-location catalog UI with filters, refresh, and Keep actions +414/-110

All Hosts: multi-location catalog UI with filters, refresh, and Keep actions

• Replaces legacy search/count logic with useHostCatalog; adds sticky toolbar (search + All/Local/Remote + refresh), inventory hints/empty states, and renders provider-only rows with Keep/Keep-and-open actions alongside local tree rows.

src/components/layout/Sidebar.tsx

TabBar.tsxUse Google mark icon for sync tabs/menus +4/-3

Use Google mark icon for sync tabs/menus

• Replaces Cloud icon usages with GoogleMarkIcon for Sync & Backup tab/menu items.

src/components/layout/TabBar.tsx

ConnectionItem.tsxShow location chips on local host rows +34/-20

Show location chips on local host rows

• Adds optional 'locations' prop and renders HostLocationChips (hiding local-only) to indicate multi-location membership. Adjusts row spacing/hover chrome to match the new All Hosts layout.

src/components/layout/sidebar/ConnectionItem.tsx

HostLocationChips.tsxNew location chip/badge component (local/google/git/etc.) +138/-0

New location chip/badge component (local/google/git/etc.)

• Adds compact icon-only chips with tooltips for host location tags. Uses theme tokens for chip chrome and supports hiding a lone Local badge.

src/components/layout/sidebar/HostLocationChips.tsx

RemoteHostItem.tsxProvider-only host row with Keep and Keep-and-open actions +185/-0

Provider-only host row with Keep and Keep-and-open actions

• Adds a row component for remote-only catalog entries with hover actions, busy state handling, and location chips, styled to match local connection rows.

src/components/layout/sidebar/RemoteHostItem.tsx

SplitSidebarActionButton.tsxAdd attention states and badges for sidebar action buttons +47/-7

Add attention states and badges for sidebar action buttons

• Extends split action button with attention styling (setup/secure/locked/ready) plus optional badges and titles; updates toggle chevron behavior.

src/components/layout/sidebar/SplitSidebarActionButton.tsx

VaultNavSection.tsxVault nav attention badge + popup menu +216/-59

Vault nav attention badge + popup menu

• Reworks Vault sidebar nav into a compact popup menu using ZPortal. Computes actionable secure-to-vault candidate count to drive attention/badge state and refines vault status dot coloring.

src/components/layout/sidebar/VaultNavSection.tsx

AddConnectionModal.tsxReorder and relabel host creation/import entry points +16/-16

Reorder and relabel host creation/import entry points

• Updates the initial choice UI to emphasize New host, with Import Config and Import File options and refreshed copy/icons.

src/components/modals/AddConnectionModal.tsx

SyncProviderSetupSteps.tsxUse branded Google icon in provider setup steps +12/-6

Use branded Google icon in provider setup steps

• Replaces Cloud icon with GoogleMarkIcon for the Connect step, using color when active.

src/components/settings/tabs/vault/SyncProviderSetupSteps.tsx

hostCatalog.tsHost catalog model and pure merge/filter/search helpers +180/-0

Host catalog model and pure merge/filter/search helpers

• Introduces HostCatalogEntry keyed by logicalId, plus merge/filter/search utilities and helpers for local-only/remote-only projections.

src/features/connections/domain/hostCatalog.ts

hostInventoryCache.tsSession cache for provider inventory metadata +83/-0

Session cache for provider inventory metadata

• Adds memory + sessionStorage caching of last-known provider host inventory so location chips and Remote filtering remain useful when the collection is locked; includes cache clearing utilities and a connections-cleared event constant.

src/features/connections/domain/hostInventoryCache.ts

hostMaterialize.tsKeep/materialize helper for provider hosts +76/-0

Keep/materialize helper for provider hosts

• Adds a helper to materialize provider hosts onto this device via connectionsRestore, with clear error handling and optional bundle restore for tunnels/snippets.

src/features/connections/domain/hostMaterialize.ts

useHostCatalog.tsHook to build and refresh the host catalog view model +297/-0

Hook to build and refresh the host catalog view model

• Adds a hook that combines local connections with provider inventory, applies filter/search, maintains inventory status (cached/locked/not_configured/error), integrates session inventory caching, and refreshes based on shared readiness + sync-status events.

src/features/connections/presentation/useHostCatalog.ts

useSyncReadinessStore.tsNew shared sync readiness Zustand store +162/-0

New shared sync readiness Zustand store

• Introduces a shared store that refreshes OAuth + collection status together, derives readiness/labels, guards against out-of-order refresh responses, and attaches a single global listener for sync status change events.

src/vault/useSyncReadinessStore.ts

Bug fix (10) +128 / -47
SettingsModal.tsxClarify clear-connections behavior re: provider inventory +4/-1

Clarify clear-connections behavior re: provider inventory

• Updates the “Connections cleared” toast to note that provider host inventory is cleared for the session and requires refresh to reappear.

src/components/settings/SettingsModal.tsx

VaultTab.tsxTheme-token secure-to-vault warning banner styling +7/-5

Theme-token secure-to-vault warning banner styling

• Switches secure-to-vault banner colors from hard-coded Tailwind amber/dark variants to '--color-app-*' tokens for correct themed rendering.

src/components/settings/tabs/VaultTab.tsx

SyncCollectionSetupModal.tsxTheme-token banners for sync setup errors and vault prerequisites +5/-2

Theme-token banners for sync setup errors and vault prerequisites

• Converts warning/error copy to themed bordered banners using app CSS vars, improving readability across themes.

src/components/settings/tabs/vault/SyncCollectionSetupModal.tsx

SyncDomainRow.tsxUse theme tokens for domain gate copy +3/-1

Use theme tokens for domain gate copy

• Updates gate message styling to use app muted tokens rather than hard-coded amber text.

src/components/settings/tabs/vault/SyncDomainRow.tsx

VaultStatusCard.tsxImprove status icon contrast in dark mode +2/-2

Improve status icon contrast in dark mode

• Tweaks emerald/amber text colors to ensure legibility in light and dark themes.

src/components/settings/tabs/vault/VaultStatusCard.tsx

VaultSyncCard.tsxSync & Backup UI polish and gating correctness +27/-19

Sync & Backup UI polish and gating correctness

• Replaces Cloud with GoogleMarkIcon, moves warning colors to theme tokens, and fixes action blocking so OAuth connect doesn’t incorrectly block domain operations. Simplifies provider gate reason messaging to focus on setup/unlock state.

src/components/settings/tabs/vault/VaultSyncCard.tsx

RecoveryKeyModal.tsxTheme-token warning banner for recovery key presentation +18/-4

Theme-token warning banner for recovery key presentation

• Updates the one-time recovery key warning banner to use app CSS vars for consistent theme rendering.

src/components/vault/RecoveryKeyModal.tsx

connectionSlice.tsClear provider inventory cache on connection wipe; provider-neutral jump-host guidance +11/-1

Clear provider inventory cache on connection wipe; provider-neutral jump-host guidance

• Clears all host inventory caches and broadcasts a connections-cleared event when local connections are wiped. Updates jump-host failure copy to reflect multi-location restore paths (keep the jump host on this device).

src/store/connectionSlice.ts

syncIpc.tsEmit sync status change events for collection lifecycle calls +45/-12

Emit sync status change events for collection lifecycle calls

• Wraps collection setup/unlock/lock/forget-key/recovery-key/ttl IPC calls to invoke notifySyncStatusChanged so readiness and inventory refreshes are consistent across UI surfaces.

src/vault/syncIpc.ts

syncPassphrase.tsHandle new key-wrap unrecoverable error code +6/-0

Handle new key-wrap unrecoverable error code

• Adds formatting for the sync_collection_key_unrecoverable error to present actionable guidance after local reset scenarios.

src/vault/syncPassphrase.ts

Refactor (7) +154 / -85
FolderItem.tsxAlign FolderItem wiring with updated connection row props +1/-0

Align FolderItem wiring with updated connection row props

• Minor adjustment to support updated ConnectionItem props and sidebar layout changes.

src/components/layout/sidebar/FolderItem.tsx

SidebarSection.tsxSidebarSection action variant, fill layout, and icon support +86/-17

SidebarSection action variant, fill layout, and icon support

• Adds an action-style header variant (Vault/Port Forwarding look), optional icon, and a fill mode to keep headers sticky while content scrolls within the section.

src/components/layout/sidebar/SidebarSection.tsx

locationIcons.tsxRe-export provider icons for sidebar consumers +2/-0

Re-export provider icons for sidebar consumers

• Adds a stable re-export layer for Google provider icons used in sidebar modules.

src/components/layout/sidebar/locationIcons.tsx

types.tsAdd optional getLocations hook for connection row chips +3/-0

Add optional getLocations hook for connection row chips

• Extends ConnectionItemProps with a 'getLocations(conn)' callback used to attach host location tags to local rows.

src/components/layout/sidebar/types.ts

useConnectionsRestore.tsPatch shared readiness store on restore instead of local state mutation +10/-14

Patch shared readiness store on restore instead of local state mutation

• Replaces setState-based googleSync updates with patchGoogleSync to keep lastSync/lastError consistent with the shared readiness store.

src/components/settings/tabs/vault/hooks/useConnectionsRestore.ts

useVaultPanelActions.tsMigrate Sync & Backup status state to shared readiness store +50/-54

Migrate Sync & Backup status state to shared readiness store

• Replaces local googleSync/googleCollection state with useSyncReadinessStore selectors and refresh, ensuring Sync & Backup and All Hosts share the same readiness truth and event-driven refresh behavior.

src/components/settings/tabs/vault/hooks/useVaultPanelActions.ts

index.tsExport host catalog helpers from domain barrel +2/-0

Export host catalog helpers from domain barrel

• Updates domain exports to include the host catalog module for UI consumers.

src/features/connections/domain/index.ts

Documentation (2) +22 / -0
CHANGELOG.mdDocument multi-location hosts and shared sync readiness +17/-0

Document multi-location hosts and shared sync readiness

• Adds Unreleased entries covering the host catalog, shared readiness store, sidebar UX changes, and key-wrap/Keep-related fixes.

CHANGELOG.md

syncProviderGate.tsDocument readiness ownership and discourage re-deriving status in UI +5/-0

Document readiness ownership and discourage re-deriving status in UI

• Adds documentation clarifying the separation between pure readiness helpers and the shared readiness store that owns live state.

src/vault/syncProviderGate.ts

Other (3) +125 / -0
package.jsonAdd host catalog test script +1/-0

Add host catalog test script

• Adds 'test:host-catalog' to compile agent-tests and run the new host catalog unit tests.

package.json

hostCatalog.test.mjsHost catalog unit tests (merge/filter/search/logicalId) +121/-0

Host catalog unit tests (merge/filter/search/logicalId)

• Adds node:test coverage for logicalId selection, local+remote merging, filters, remote-only helpers, and skipping the local shell pseudo-connection.

tests/hostCatalog.test.mjs

tsconfig.agent-tests.jsonInclude host catalog + syncIpc in agent-tests build +3/-0

Include host catalog + syncIpc in agent-tests build

• Adds hostCatalog/types and syncIpc to the agent-tests TS project so the new tests compile under the agent test pipeline.

tsconfig.agent-tests.json

@qodo-code-review

qodo-code-review Bot commented Jul 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Key-wrap errors masked ✓ Resolved 🐞 Bug ☼ Reliability
Description
download_remote_collection_key_wrap treats any provider error whose message/code contains "not
found" as a missing wrap file, which can suppress real provider failures (auth/network/permission)
and mislead setup into proceeding as if the wrap simply doesn't exist. This can surface later as an
incorrect "unrecoverable" setup error instead of the real underlying provider failure.
Code

src-tauri/src/sync/commands.rs[R2951-2956]

+        Err(error) if error.code == "provider_object_not_found" => return Ok(None),
+        Err(error) => {
+            // Older collections may not have a wrap file yet.
+            if error.message.contains("not found") || error.code.contains("not_found") {
+                return Ok(None);
+            }
Relevance

⭐⭐⭐ High

Team accepted narrowing broad substring-based error classification to avoid misclassification (PR
#80).

PR-#80

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The current error handling path returns Ok(None) for broad "not found" matches, which can hide
non-missing-file provider errors; setup_manifest later distinguishes recoverable vs unrecoverable
based on whether wrap metadata exists.

src-tauri/src/sync/commands.rs[2939-2958]
src-tauri/src/sync/collection.rs[231-267]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`download_remote_collection_key_wrap` currently returns `Ok(None)` not only for the explicit `provider_object_not_found` case, but also for any error whose message contains "not found" or whose code contains "not_found". This can incorrectly downgrade real provider failures (e.g., HTTP 404/401 surfaced as provider_http_failed with "Not Found" in the message) into "wrap missing", which later causes misleading sync setup failures.

### Issue Context
Passphrase-recovery depends on correctly distinguishing "wrap file absent" from "provider access failed".

### Fix Focus Areas
- src-tauri/src/sync/commands.rs[2939-2964]

### Suggested fix
- Remove the substring-based checks on `error.message` / `error.code`.
- Treat *only* the explicit missing-object condition (`error.code == "provider_object_not_found"`) as `Ok(None)`.
- For any other error, return `Err(sync_error_to_string(&error))` so callers see the real provider/auth/network failure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Inventory cache leaks accounts ✓ Resolved 🐞 Bug ⛨ Security
Description
Host inventory caching is keyed only by provider and is still shown when the provider is
disconnected, so remote host rows from a previous Google account can remain visible after
disconnect/account switch within the same session. This is explicitly at odds with the cache
module’s own comment that disconnect should drop the cache.
Code

src/features/connections/domain/hostInventoryCache.ts[R22-24]

+function storageKey(provider: SyncProvider): string {
+  return `zync:host-inventory-cache:v1:${provider}`;
+}
Relevance

⭐⭐ Medium

Affected cache file path not found; limited direct precedent, though team accepts privacy/security
hardening in other areas (PR #58).

PR-#58

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cache key includes only provider (no identity), and the catalog logic intentionally applies
cached inventory when disconnected; the module comment states disconnect should clear the cache.

src/features/connections/domain/hostInventoryCache.ts[20-24]
src/features/connections/domain/hostInventoryCache.ts[55-56]
src/features/connections/presentation/useHostCatalog.ts[141-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The host inventory cache key is `zync:host-inventory-cache:v1:${provider}` (provider-only), and `useHostCatalog` will continue to apply cached inventory even when `readiness.isConnected` is false. If a user disconnects and reconnects with a different Google account in the same session, the sidebar can still show the prior account’s remote host inventory.

### Issue Context
The cache docstring indicates it should be dropped on provider disconnect, but there is no disconnect/account-identity-aware invalidation.

### Fix Focus Areas
- src/features/connections/domain/hostInventoryCache.ts[20-64]
- src/features/connections/presentation/useHostCatalog.ts[130-147]

### Suggested fix
Implement one (or both) of:
1) **Account-scoped cache key**: incorporate a stable provider identity into `storageKey` (e.g., connected email or provider account id) so switching accounts naturally uses a different cache namespace.
2) **Disconnect invalidation**: when readiness transitions to disconnected (or on a SYNC_STATUS_CHANGED_EVENT indicating disconnected), call `clearHostInventoryCache(provider)` and clear `inventory` state so provider-only rows/chips do not persist post-disconnect.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Jump-host IDs inconsistent ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new filtered restore loop normalizes IDs for checking local presence but uses raw
(non-normalized) IDs for fetched/pending dedupe, so IDs that differ only by case/whitespace can be
re-fetched or not recognized as already present. This is inconsistent with the rest of the file’s
host-id matching helpers, which normalize host IDs before comparisons.
Code

src-tauri/src/sync/commands.rs[R2114-2164]

+    // Filtered restore (Keep / Keep-and-open): also pull jump-host chain so connect works.
+    let mut pending: HashSet<String> = initial_filter.unwrap_or_default();
+    let mut fetched: HashSet<String> = HashSet::new();
+    let mut by_logical_id: HashMap<String, HostSyncRecord> = HashMap::new();
+    let mut scanned = 0u64;
+    let mut skipped = 0u64;
+    let mut failed = 0u64;
+
+    for _depth in 0..10 {
+        let to_fetch: HashSet<String> = pending
+            .difference(&fetched)
+            .cloned()
+            .collect();
+        if to_fetch.is_empty() {
+            break;
+        }
+
+        let collected = collect_remote_host_records(
+            provider_impl,
+            app,
+            kind,
+            manifest,
+            secret_key,
+            Some(&to_fetch),
+        )
+        .await
+        .map_err(|message| {
+            record_sync_error(provider_data_dir, kind, error_code, message.clone());
+            message
+        })?;
+
+        scanned = scanned.saturating_add(collected.scanned);
+        skipped = skipped.saturating_add(collected.skipped);
+        failed = failed.saturating_add(collected.failed);
+
+        pending.clear();
+        for (record, _revision) in collected.records {
+            fetched.insert(record.logical_id.clone());
+            if let Some(jump_id) = record
+                .jump_server_id
+                .as_ref()
+                .map(|id| id.trim().to_string())
+                .filter(|id| !id.is_empty())
+            {
+                let jump_key = normalize_host_connection_id(&jump_id);
+                let already_local = local_ids.contains(&jump_key) || local_ids.contains(&jump_id);
+                let already_batch =
+                    fetched.contains(&jump_id) || by_logical_id.contains_key(&jump_id);
+                if !already_local && !already_batch {
+                    pending.insert(jump_id);
+                }
Relevance

⭐⭐⭐ High

Team previously accepted ID/field normalization to prevent duplicates/inconsistency (e.g.,
trim/normalize IDs in PR #58).

PR-#58

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The filtered restore loop uses raw string sets for fetched/pending while the file defines
normalization helpers and uses them elsewhere for host-id equality semantics.

src-tauri/src/sync/commands.rs[2114-2167]
src-tauri/src/sync/commands.rs[1266-1302]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`prepare_connections_restore_scope`'s filtered restore path mixes normalized and raw IDs: it computes `jump_key = normalize_host_connection_id(&jump_id)` for local checks, but tracks `pending`, `fetched`, and `by_logical_id` keys using the raw strings. This can cause inconsistent dedupe / dependency discovery when the same logical host id appears with different casing/whitespace.

### Issue Context
Other host-id matching helpers in this file normalize IDs prior to set membership checks.

### Fix Focus Areas
- src-tauri/src/sync/commands.rs[1266-1302]
- src-tauri/src/sync/commands.rs[2114-2167]

### Suggested fix
- Store and compare *normalized* ids in `pending`/`fetched` (e.g., `HashSet<String>` containing `normalize_host_connection_id(...)`).
- When you need to fetch an object by name, either:
 - ensure the canonical stored logical ids are already normalized (then use normalized ids for `hosts_object_name`), or
 - keep a mapping from normalized->original requested id for object-name construction.
- Use the same normalization for `by_logical_id` keys when checking `already_batch`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Remote wrap unversioned ✓ Resolved 🐞 Bug ≡ Correctness
Description
apply_remote_key_wrap_to_manifest applies remote wrap fields without validating the wrap format
version, so a future/garbled wrap payload could be accepted and then cause hard-to-diagnose unwrap
failures. The wrap structure already carries a version field, but it is never checked before use.
Code

src-tauri/src/sync/collection.rs[R141-150]

+    if wrap.sync_collection_id != manifest.sync_collection_id {
+        return Err(SyncError::new(
+            "sync_collection_key_wrap_mismatch",
+            "Remote key wrap belongs to a different sync collection.",
+        ));
+    }
+    manifest.key_policy_mode = wrap.key_policy_mode;
+    manifest.key_wrap_salt = Some(base64_data(wrap.key_wrap_salt.clone())?);
+    manifest.key_wrap_nonce = Some(base64_data(wrap.key_wrap_nonce.clone())?);
+    manifest.key_wrap_ciphertext = Some(base64_data(wrap.key_wrap_ciphertext.clone())?);
Relevance

⭐⭐ Medium

Sync files not found in repo; only general history shows input validation often accepted (PR #33,
#38).

PR-#33
PR-#38

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrap object includes version but the apply function only checks sync_collection_id and then
writes the wrap fields into the manifest.

src-tauri/src/sync/collection.rs[86-107]
src-tauri/src/sync/collection.rs[137-163]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`apply_remote_key_wrap_to_manifest` consumes a deserialized `RemoteCollectionKeyWrapV1` but does not verify the `version` field before applying key-wrap parameters into the manifest. This makes the flow fragile if the remote file is corrupted or if a future version is introduced.

### Issue Context
The remote wrap object is parsed from provider bytes in `download_remote_collection_key_wrap` and then applied to the local manifest for passphrase recovery.

### Fix Focus Areas
- src-tauri/src/sync/collection.rs[86-164]

### Suggested fix
- In `apply_remote_key_wrap_to_manifest`, add a guard:
 - If `wrap.version != REMOTE_KEY_WRAP_VERSION`, return a `SyncError` like `sync_collection_key_wrap_version_unsupported`.
- (Optional defensive check) also ensure `wrap.provider == manifest.provider` to prevent applying a wrap that clearly claims a different provider.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src-tauri/src/sync/commands.rs Outdated
Comment thread src-tauri/src/sync/collection.rs
Comment thread src/features/connections/domain/hostInventoryCache.ts Outdated
Comment thread src-tauri/src/sync/commands.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/settings/tabs/vault/VaultSyncCard.tsx (1)

250-278: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

OAuth connect still blocks Upload/Restore isCollectionActionBlocked still includes isSyncing, and SyncDomainsGrouped uses isProviderDomainActionDisabled to disable both sync and restore actions. That keeps Upload/Restore blocked during Google OAuth connect.

🤖 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 `@src/components/settings/tabs/vault/VaultSyncCard.tsx` around lines 250 - 278,
Remove isSyncing from isCollectionActionBlocked so Google OAuth connection does
not disable Upload/Restore actions. Keep isSyncing included only in the
provider-specific action gating, preserving blocking for collection setup,
unlock, lock, recovery-key regeneration, and domain operations.
🧹 Nitpick comments (5)
src/components/layout/Sidebar.tsx (1)

253-257: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No-op ternary in treeRoot memo — both branches return searchTerm.

hostFilter === 'all' || hostFilter === 'local' ? searchTerm : searchTerm always evaluates to searchTerm regardless of hostFilter. This looks like leftover code from an incomplete refactor — please confirm whether a real per-filter distinction was intended (e.g. skipping search re-application for remote), otherwise simplify to remove the dead conditional and the now-unnecessary hostFilter dependency.

♻️ Suggested simplification (if no distinction is intended)
     const treeRoot = useMemo(
-        () => buildTree(treeConnections, folders, hostFilter === 'all' || hostFilter === 'local' ? searchTerm : searchTerm),
-        [treeConnections, folders, searchTerm, hostFilter],
+        () => buildTree(treeConnections, folders, searchTerm),
+        [treeConnections, folders, searchTerm],
     );
🤖 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 `@src/components/layout/Sidebar.tsx` around lines 253 - 257, In the treeRoot
useMemo, remove the no-op hostFilter ternary and pass searchTerm directly to
buildTree. Remove hostFilter from that memo’s dependency array, unless a real
per-filter search distinction is required; if one is intended, implement the
distinct remote behavior explicitly.
src/components/layout/sidebar/VaultNavSection.tsx (1)

19-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate branching logic between resolveVaultAttention and statusDotClass.

Both functions branch on the same (status, error, securableCount) triple with parallel logic. Consider deriving statusDotClass output from resolveVaultAttention's result (or merging into one function) to avoid maintaining two copies of the same state machine.

🤖 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 `@src/components/layout/sidebar/VaultNavSection.tsx` around lines 19 - 76,
Consolidate the duplicated vault-state branching by deriving status-dot output
from resolveVaultAttention, or by merging both results into a single shared
resolver. Ensure the resulting logic still distinguishes in-use errors, unlocked
vaults with securable credentials, locked vaults, and unconfigured vaults while
preserving each existing className and title.
src/features/connections/domain/hostMaterialize.ts (1)

59-72: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider suppressing the success toast when loadConnections fails.

If loadConnections throws, the user sees both an error toast ("local host list failed to refresh") and a success toast ("restore succeeded") simultaneously. While the comment on line 59 correctly notes the restore itself succeeded, surfacing both toasts at once may confuse users. Consider skipping the success toast (or downgrading it to a warning) when the reload fails.

♻️ Suggested adjustment
   // Restore succeeded — reload is best-effort and must not report as restore failure.
+  let reloadFailed = false;
   try {
     await options.loadConnections();
   } catch (error) {
+    reloadFailed = true;
     const message = error instanceof Error ? error.message : String(error);
     options.showToast(
       'error',
       message || 'Host was saved, but the local host list failed to refresh. Reload the app if hosts look stale.',
     );
   }
 
-  if (!options.silentSuccess) {
+  if (!options.silentSuccess && !reloadFailed) {
     options.showToast('success', formatConnectionsRestoreSuccessMessage(result));
   }
🤖 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 `@src/features/connections/domain/hostMaterialize.ts` around lines 59 - 72,
Track whether options.loadConnections() completes successfully in the restore
flow around formatConnectionsRestoreSuccessMessage. When reloading fails, keep
the error toast but suppress the subsequent success toast; retain the existing
success toast when reload succeeds, respecting options.silentSuccess.
src/store/connectionSlice.ts (1)

405-410: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant clearAllHostInventoryCaches() call.

clearAllHostInventoryCaches() is called directly here and again via the CONNECTIONS_CLEARED_EVENT listener in useHostCatalog.ts (line 242). The second call is a no-op since caches are already cleared. Not harmful, but consider removing the direct call here and relying solely on the event-driven cleanup to keep a single owner of cache lifecycle.

🤖 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 `@src/store/connectionSlice.ts` around lines 405 - 410, Remove the direct
clearAllHostInventoryCaches() invocation from the connection-clearing flow and
rely on the CONNECTIONS_CLEARED_EVENT dispatch to trigger the existing
useHostCatalog.ts listener. Preserve the event dispatch and all other connection
cleanup behavior.
src/features/connections/presentation/useHostCatalog.ts (1)

64-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

PRIMARY_PROVIDER is hardcoded to 'google'.

The HostLocationTag type comment says "Extensible when more providers ship," but this hook only supports Google. When additional providers are added, this hook, the cache, and the readiness checks will need refactoring to iterate over multiple providers. Consider extracting the provider as a parameter or config now to avoid a larger refactor later.

🤖 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 `@src/features/connections/presentation/useHostCatalog.ts` at line 64, Make the
provider used by the host catalog flow configurable instead of hardcoding
PRIMARY_PROVIDER to 'google'. Thread the selected SyncProvider through the
relevant hook, cache, and readiness-check logic, preserving Google as the
default for current callers while allowing future providers without refactoring
those paths.
🤖 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 `@src/components/settings/tabs/vault/VaultSyncCard.tsx`:
- Around line 250-278: Remove isSyncing from isCollectionActionBlocked so Google
OAuth connection does not disable Upload/Restore actions. Keep isSyncing
included only in the provider-specific action gating, preserving blocking for
collection setup, unlock, lock, recovery-key regeneration, and domain
operations.

---

Nitpick comments:
In `@src/components/layout/Sidebar.tsx`:
- Around line 253-257: In the treeRoot useMemo, remove the no-op hostFilter
ternary and pass searchTerm directly to buildTree. Remove hostFilter from that
memo’s dependency array, unless a real per-filter search distinction is
required; if one is intended, implement the distinct remote behavior explicitly.

In `@src/components/layout/sidebar/VaultNavSection.tsx`:
- Around line 19-76: Consolidate the duplicated vault-state branching by
deriving status-dot output from resolveVaultAttention, or by merging both
results into a single shared resolver. Ensure the resulting logic still
distinguishes in-use errors, unlocked vaults with securable credentials, locked
vaults, and unconfigured vaults while preserving each existing className and
title.

In `@src/features/connections/domain/hostMaterialize.ts`:
- Around line 59-72: Track whether options.loadConnections() completes
successfully in the restore flow around formatConnectionsRestoreSuccessMessage.
When reloading fails, keep the error toast but suppress the subsequent success
toast; retain the existing success toast when reload succeeds, respecting
options.silentSuccess.

In `@src/features/connections/presentation/useHostCatalog.ts`:
- Line 64: Make the provider used by the host catalog flow configurable instead
of hardcoding PRIMARY_PROVIDER to 'google'. Thread the selected SyncProvider
through the relevant hook, cache, and readiness-check logic, preserving Google
as the default for current callers while allowing future providers without
refactoring those paths.

In `@src/store/connectionSlice.ts`:
- Around line 405-410: Remove the direct clearAllHostInventoryCaches()
invocation from the connection-clearing flow and rely on the
CONNECTIONS_CLEARED_EVENT dispatch to trigger the existing useHostCatalog.ts
listener. Preserve the event dispatch and all other connection cleanup behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73476610-30fd-4fd5-a131-47338733c1de

📥 Commits

Reviewing files that changed from the base of the PR and between d22c7b9 and 342ce68.

📒 Files selected for processing (40)
  • CHANGELOG.md
  • package.json
  • src-tauri/src/sync/collection.rs
  • src-tauri/src/sync/commands.rs
  • src/components/icons/providerIcons.tsx
  • src/components/layout/CommandPalette.tsx
  • src/components/layout/Sidebar.tsx
  • src/components/layout/TabBar.tsx
  • src/components/layout/sidebar/ConnectionItem.tsx
  • src/components/layout/sidebar/FolderItem.tsx
  • src/components/layout/sidebar/HostLocationChips.tsx
  • src/components/layout/sidebar/RemoteHostItem.tsx
  • src/components/layout/sidebar/SidebarSection.tsx
  • src/components/layout/sidebar/SplitSidebarActionButton.tsx
  • src/components/layout/sidebar/VaultNavSection.tsx
  • src/components/layout/sidebar/locationIcons.tsx
  • src/components/layout/sidebar/types.ts
  • src/components/modals/AddConnectionModal.tsx
  • src/components/settings/SettingsModal.tsx
  • src/components/settings/tabs/VaultTab.tsx
  • src/components/settings/tabs/vault/SyncCollectionSetupModal.tsx
  • src/components/settings/tabs/vault/SyncDomainRow.tsx
  • src/components/settings/tabs/vault/SyncProviderSetupSteps.tsx
  • src/components/settings/tabs/vault/VaultStatusCard.tsx
  • src/components/settings/tabs/vault/VaultSyncCard.tsx
  • src/components/settings/tabs/vault/hooks/useConnectionsRestore.ts
  • src/components/settings/tabs/vault/hooks/useVaultPanelActions.ts
  • src/components/vault/RecoveryKeyModal.tsx
  • src/features/connections/domain/hostCatalog.ts
  • src/features/connections/domain/hostInventoryCache.ts
  • src/features/connections/domain/hostMaterialize.ts
  • src/features/connections/domain/index.ts
  • src/features/connections/presentation/useHostCatalog.ts
  • src/store/connectionSlice.ts
  • src/vault/syncIpc.ts
  • src/vault/syncPassphrase.ts
  • src/vault/syncProviderGate.ts
  • src/vault/useSyncReadinessStore.ts
  • tests/hostCatalog.test.mjs
  • tsconfig.agent-tests.json

Strict missing-object handling for collection key-wrap download, validate
wrap version and provider before apply, and normalize jump-chain host ids
during filtered restore. Scope host inventory cache by account, clear it on
disconnect, stop OAuth connect from blocking domain upload/restore, and
tighten materialize toasts and vault nav attention resolution.
Record Unreleased fixes for key-wrap validation, jump-chain id normalize,
account-scoped inventory cache, and domain action gating.
@gajendraxdev
gajendraxdev merged commit c201a42 into zync-sh:main Jul 12, 2026
4 checks passed
@qodo-code-review qodo-code-review Bot mentioned this pull request Jul 16, 2026
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