Skip to content

Add configuration synchronization with S3 and WebDAV support#96

Open
mrhuangyong wants to merge 22 commits into
codeErrorSleep:mainfrom
mrhuangyong:main
Open

Add configuration synchronization with S3 and WebDAV support#96
mrhuangyong wants to merge 22 commits into
codeErrorSleep:mainfrom
mrhuangyong:main

Conversation

@mrhuangyong

@mrhuangyong mrhuangyong commented Jun 2, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Added cross-device configuration synchronization with S3 and WebDAV backend support.
    • Implemented AES-256-GCM encryption for secure sync with password protection.
    • Added new Sync settings panel for configuring providers, passwords, and sync intervals.
    • Enabled automatic synchronization of database connections, saved queries, AI provider configurations, and user settings.
    • Added manual sync controls: sync now, force push, and force pull options.
    • Included sync status dashboard displaying last sync time, device information, and sync results.
  • Documentation

    • Added comprehensive sync architecture and implementation specifications.

mrhuangyong and others added 22 commits June 2, 2026 09:08
Design for cross-device configuration synchronization supporting
S3 and WebDAV backends with end-to-end encryption (PBKDF2 + AES-256-GCM),
hybrid auto/manual sync mode, and Last-Write-Wins conflict resolution.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10-task plan covering Rust backend (crypto, S3, WebDAV providers,
SyncManager, Tauri commands) and React frontend (sync settings panel,
API layer). Follows TDD with bite-sized commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add sha2, hmac, pbkdf2, hex dependencies for sync crypto.
Create sync_state table migration for storing sync configuration,
device ID, and sync metadata. Add get/set/delete CRUD methods.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Define SyncProvider trait with test_connection/put/get/delete.
Add SyncConfig, SyncStatus, SyncResult, SyncSnapshot types.
Include build_provider factory and stub S3/WebDAV implementations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PBKDF2-SHA256 key derivation (600k iterations) with AES-256-GCM
encryption for sync snapshots. Includes snapshot_hash for change
detection and encrypt_with_key/decrypt_with_key for local credential
storage. All 5 unit tests passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements SyncProvider for S3-compatible storage using reqwest
with manual AWS Signature V4 signing. Supports any S3-compatible
endpoint (AWS, MinIO, OSS, etc.). Added url crate dependency.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements SyncProvider for WebDAV servers using reqwest with
basic auth. Supports PROPFIND for connection testing and standard
HTTP PUT/GET/DELETE for object operations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Core sync orchestration: export_snapshot serializes local data
(connections, queries, AI providers) to encrypted snapshots.
import_snapshot clears local tables and re-imports from remote.
Supports configure, sync_now, force_push, force_pull, and
auto_sync_push with change detection via SHA-256 hash comparison.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add 8 sync commands: test_connection, configure, get_status,
sync_now, force_push, force_pull, disable, update_password.
Wrap local_db in Arc<Mutex> in AppState for SyncManager access.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add SyncConfig, SyncStatus, SyncResult types and syncApi namespace
with testConnection, configure, getStatus, syncNow, forcePush,
forcePull, disable, and updatePassword methods.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add SyncSettings panel with provider configuration (S3/WebDAV),
sync password setup, test connection, and sync status display.
Integrate as new "Sync" tab in SettingsDialog with Cloud icon.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The rename_all="camelCase" converted WebDAV to "webDAV" which
didn't match the frontend's "WebDAV". Use explicit #[serde(rename)]
to ensure exact string match: "S3" and "WebDAV".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add proper internationalization keys for all sync UI text in en/zh/ja
locales, replacing defaultValue fallbacks. Add sync_get_config backend
command to retrieve saved config so form fields are populated when
reopening the Sync settings tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ter it

Encrypt the sync password using the existing ai_master_key and store it
in sync_state table. Sync Now, Force Push, and Force Pull operations now
automatically retrieve the stored password instead of requiring manual
input each time. Password fields are hidden from the UI when sync is
already enabled.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add passwordStored field to SyncStatus so the frontend can detect when
sync was configured before the password persistence feature. In that
case, show a password input with a hint to re-enter the sync password
once, which gets stored encrypted for future automatic use.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The sync snapshot stores Connection objects which use the field name
"dbType", but create_connection expects ConnectionForm with "driver".
Add a JSON field mapping during import to fix the deserialization error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement automatic sync using two complementary mechanisms:

A) Periodic timer: SyncScheduler runs every 5 minutes, checks if sync
is enabled and local data has changed, then auto-pushes to remote.

B) Event-driven push: After any data mutation (create/update/delete
connection, saved query, or AI provider), notify_data_changed() is
called which triggers a debounced (3s delay) auto-sync push.

The scheduler starts on app launch and stops on exit. It only acts when
sync is enabled and the password has been stored.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add syncIntervalMinutes to SyncConfig (default: 5 min). Users can choose
from 1/3/5/10/30/60 minute intervals in the sync settings UI. The
scheduler reads the configured interval from the database on each cycle,
so changes take effect without restarting the app.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ctor

The scheduler was calling tokio::spawn directly, which panics outside
a Tokio runtime context. Switch to tauri::async_runtime::spawn which
properly uses Tauri's managed runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat: add config sync with S3/WebDAV support
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements comprehensive cross-device configuration synchronization for DbPaw, allowing users to synchronize database connections, saved queries, AI provider settings, and user preferences across multiple devices via S3 or WebDAV backends with end-to-end AES-256-GCM encryption, automatic periodic syncing, and manual sync controls.

Changes

Config Sync Feature

Layer / File(s) Summary
Design specification & implementation plan
docs/superpowers/plans/2026-06-02-config-sync.md, docs/superpowers/specs/2026-06-02-config-sync-design.md
Two reference documents defining the complete architecture, encryption strategy, snapshot format, conflict resolution via last-write-wins timestamps, backend module scaffolding, and end-to-end feature implementation tasks.
Provider abstraction & S3/WebDAV implementations
src-tauri/src/sync/provider.rs, src-tauri/src/sync/s3.rs, src-tauri/src/sync/webdav.rs
SyncProvider trait for object storage operations, S3 provider with AWS Signature Version 4 signing, and WebDAV provider with basic auth and PROPFIND discovery; includes provider routing and credential validation via build_provider.
Encryption engine with password derivation & hashing
src-tauri/src/sync/crypto.rs
AES-256-GCM encryption/decryption using PBKDF2 key derivation with random salts, random nonces, and SHA-256 snapshot hashing; includes round-trip and error-handling unit tests.
Database schema, encryption helpers, and state wrapper
src-tauri/Cargo.toml, src-tauri/migrations/017_sync_state.sql, src-tauri/src/db/local.rs, src-tauri/src/state.rs
New crate dependencies (sha2, hmac, pbkdf2, hex, url), sync_state migration table, LocalDb password encryption/decryption and sync state CRUD helpers, and AppState wrapped in Arc<Mutex<>> to share with the background scheduler.
Sync manager orchestration with snapshot export/import
src-tauri/src/sync/manager.rs
SyncManager implementing configure/enable, sync-now with pull-then-conditional-import/push logic, force-push/pull, password updates, local-change detection via hash comparison, and snapshot export/import supporting selective data synchronization (connections, saved queries, AI provider API keys).
Background sync scheduler with periodic intervals and change debouncing
src-tauri/src/sync/scheduler.rs
SyncScheduler background task loop reading DB-configured intervals, debounced change-notification handling, and watch-channel shutdown signaling.
Tauri command handlers and app wiring
src-tauri/src/commands/sync.rs, src-tauri/src/commands/mod.rs, src-tauri/src/lib.rs
Nine Tauri command handlers (test connection, configure, get status/config, sync now, force push/pull, disable, update password) with lifecycle integration to start the scheduler on app launch and stop on exit.
Sync scheduler notifications from existing data mutations
src-tauri/src/commands/ai.rs, src-tauri/src/commands/connection.rs, src-tauri/src/commands/storage.rs
Updates to AI provider, database connection, and saved query command handlers to call state.sync_scheduler.notify_data_changed() after mutations, enabling debounced auto-sync on local edits.
Frontend API types and Tauri invoke wrappers
src/services/api.ts
TypeScript types for SyncConfig, SyncStatus, and SyncResult, and api.system.sync.* methods invoking all sync-related Tauri commands.
SyncSettings React component and settings dialog integration
src/components/settings/SyncSettings.tsx, src/components/settings/SettingsDialog.tsx
New SyncSettings component with provider selection, provider-specific credential inputs, sync interval selector, password input with validation, connection testing, manual/forced sync actions, and status/result display; integrated into settings dialog as a new "Sync" tab.
Internationalization for English, Japanese, and Simplified Chinese
src/lib/i18n/locales/en.ts, src/lib/i18n/locales/ja.ts, src/lib/i18n/locales/zh.ts
Translation strings for sync configuration UI labels, password validation messages, connection test outcomes, sync status/interval display, and force push/pull action feedback.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A rabbit hops 'cross clouds so vast,
Each sync a leap of crypto-cast,
With S3, WebDAV to roam,
Your configs now find a new home!
Auto-push keeps secrets tight,
DbPaw's sync shines oh-so bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.37% 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 directly and accurately summarizes the main change: adding configuration synchronization functionality with S3 and WebDAV backend support.
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.

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

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

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Add configuration synchronization with S3 and WebDAV support

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Implements comprehensive configuration synchronization system with S3 and WebDAV support
• Adds end-to-end encryption using AES-256-GCM with PBKDF2-SHA256 key derivation for secure data
  protection
• Core SyncManager handles snapshot export/import with connections, saved queries, and AI
  providers
• Implements SyncProvider trait with S3 provider using AWS Signature V4 and WebDAV provider with
  HTTP basic auth
• Adds SyncScheduler for periodic auto-sync with configurable intervals and debounced push on
  local data changes
• Integrates sync notifications across connection, storage, and AI provider commands
• Extends AppState with SyncScheduler and manages lifecycle through app initialization and exit
• Adds database migration for sync_state table to persist sync configuration and state
• Provides Tauri command handlers for sync operations: test connection, configure, get status,
  manual sync, force push/pull, disable, and password updates
• Implements React UI component SyncSettings for sync configuration with provider selection and
  status display
• Adds TypeScript API types and service methods for frontend-backend communication
• Includes comprehensive translations in English, Japanese, and Chinese
• Adds required cryptographic dependencies: sha2, hmac, pbkdf2, hex, and url
Diagram
flowchart LR
  A["Local Data<br/>Connections, Queries, AI"] -->|"notify_data_changed"| B["SyncScheduler"]
  B -->|"periodic/debounced"| C["SyncManager"]
  C -->|"snapshot export/import"| D["Encryption<br/>AES-256-GCM"]
  D -->|"encrypted snapshot"| E["SyncProvider"]
  E -->|"S3 or WebDAV"| F["Remote Storage"]
  G["Tauri Commands"] -->|"configure, sync_now,<br/>force_push/pull"| C
  H["React UI<br/>SyncSettings"] -->|"user actions"| G
  C -->|"persist state"| I["sync_state Table"]

Loading

Grey Divider

File Changes

1. src-tauri/src/sync/manager.rs ✨ Enhancement +512/-0

Core sync manager with snapshot export/import logic

• Implements SyncManager struct with methods for configuring, enabling/disabling, and performing
 manual/automatic synchronization
• Handles snapshot export/import with connections, saved queries, and AI providers
• Manages sync state persistence (device ID, last sync hash, timestamps) in local database
• Supports force push/pull operations and password updates with re-encryption

src-tauri/src/sync/manager.rs


2. src-tauri/src/sync/s3.rs ✨ Enhancement +319/-0

S3 provider implementation with AWS Signature V4

• Implements S3Provider struct with AWS Signature V4 signing for S3 API requests
• Provides put_object, get_object, delete_object, and test_connection methods
• Handles endpoint, region, bucket, and path prefix configuration
• Uses reqwest client for HTTP operations without heavy AWS SDK dependency

src-tauri/src/sync/s3.rs


3. src-tauri/src/sync/crypto.rs ✨ Enhancement +168/-0

Cryptographic functions for sync data protection

• Implements encryption/decryption using AES-256-GCM with PBKDF2-SHA256 key derivation
• Provides snapshot_hash function for integrity verification using SHA-256
• Includes helper functions encrypt_with_key and decrypt_with_key for local storage
• Contains comprehensive unit tests for round-trip encryption and error cases

src-tauri/src/sync/crypto.rs


View more (22)
4. src-tauri/src/sync/provider.rs ✨ Enhancement +129/-0

Sync provider trait and configuration types

• Defines SyncProvider async trait with methods for connection testing and object operations
• Declares SyncConfig, SyncStatus, SyncResult, and SyncSnapshot data structures
• Implements build_provider factory function to instantiate S3 or WebDAV providers
• Validates required configuration fields for each provider type

src-tauri/src/sync/provider.rs


5. src-tauri/src/sync/webdav.rs ✨ Enhancement +133/-0

WebDAV provider implementation

• Implements WebdavProvider struct with WebDAV protocol support
• Provides put_object, get_object, delete_object, and test_connection methods
• Uses HTTP basic authentication and standard WebDAV methods (PROPFIND, PUT, GET, DELETE)
• Handles server URL normalization and object path construction

src-tauri/src/sync/webdav.rs


6. src-tauri/src/sync/scheduler.rs ✨ Enhancement +112/-0

Background sync scheduler with auto-sync timer

• Implements SyncScheduler for periodic auto-sync with configurable intervals
• Provides notify_data_changed method for debounced push on local data changes
• Uses tokio::select! for graceful shutdown handling
• Reads sync interval from database and checks sync enabled status before attempting sync

src-tauri/src/sync/scheduler.rs


7. src-tauri/src/sync/mod.rs ✨ Enhancement +6/-0

Sync module organization

• Module entry point exporting crypto, manager, provider, s3, scheduler, and webdav submodules

src-tauri/src/sync/mod.rs


8. src-tauri/src/commands/sync.rs ✨ Enhancement +66/-0

Tauri command handlers for sync operations

• Implements Tauri command handlers for sync operations: sync_test_connection, sync_configure,
 sync_get_status, sync_get_config
• Provides manual sync commands: sync_now, sync_force_push, sync_force_pull, sync_disable,
 sync_update_password
• Each command creates a SyncManager instance and delegates to appropriate manager method

src-tauri/src/commands/sync.rs


9. src-tauri/src/db/local.rs ✨ Enhancement +61/-0

Database sync state persistence layer

• Adds migration 017 to create sync_state table for storing sync configuration and state
• Implements get_sync_state, set_sync_state, and delete_sync_state methods for CRUD operations
• Adds encrypt_sync_password and decrypt_sync_password methods using AI master key
• Updates migration test to include new sync_state migration

src-tauri/src/db/local.rs


10. src-tauri/src/lib.rs ✨ Enhancement +19/-1

App lifecycle integration for sync scheduler

• Registers new sync command handlers in invoke handler
• Initializes and starts SyncScheduler after LocalDb initialization
• Stops scheduler on app exit via RunEvent::Exit handler
• Adds sync module to public exports

src-tauri/src/lib.rs


11. src-tauri/src/state.rs ✨ Enhancement +8/-2

AppState extended with sync scheduler

• Changes local_db from Mutex>> to Arc>>>
• Adds sync_scheduler field of type SyncScheduler to AppState
• Initializes SyncScheduler in AppState::new() with shared local_db reference

src-tauri/src/state.rs


12. src-tauri/src/commands/connection.rs ✨ Enhancement +12/-4

Connection commands trigger sync notifications

• Adds state.sync_scheduler.notify_data_changed() calls after connection create/update/delete
 operations
• Adds notification after importing connections from file
• Ensures sync scheduler is notified of data changes for debounced auto-sync

src-tauri/src/commands/connection.rs


13. src-tauri/src/commands/storage.rs ✨ Enhancement +16/-5

Storage commands trigger sync notifications

• Adds state.sync_scheduler.notify_data_changed() calls after saved query create/update/delete
 operations
• Properly drops local_db lock before calling notify to avoid deadlocks

src-tauri/src/commands/storage.rs


14. src-tauri/src/commands/ai.rs ✨ Enhancement +11/-3

AI provider commands trigger sync notifications

• Adds state.sync_scheduler.notify_data_changed() calls after AI provider create/update/delete
 operations
• Adds notification when setting default AI provider or clearing API keys
• Ensures sync scheduler is notified of AI provider data changes

src-tauri/src/commands/ai.rs


15. src-tauri/src/commands/mod.rs ✨ Enhancement +1/-0

Commands module exports sync submodule

• Adds pub mod sync to export new sync commands module

src-tauri/src/commands/mod.rs


16. src-tauri/Cargo.toml Dependencies +5/-0

Cargo dependencies for sync cryptography

• Adds dependencies: sha2 = "0.10", hmac = "0.12", pbkdf2 = "0.12", hex = "0.4", url = "2"
• These dependencies support cryptographic operations and URL parsing for sync functionality

src-tauri/Cargo.toml


17. src-tauri/migrations/017_sync_state.sql ⚙️ Configuration changes +5/-0

Database migration for sync state table

• Creates sync_state table with key (PRIMARY KEY), value, and updated_at columns
• Stores sync configuration, device ID, last sync hash, and sync status information

src-tauri/migrations/017_sync_state.sql


18. src/services/api.ts ✨ Enhancement +55/-1

Frontend API types and sync service methods

• Adds TypeScript types: SyncProviderType, SyncConfig, SyncStatus, SyncResult
• Implements api.sync namespace with methods: testConnection, configure, getStatus,
 getConfig, syncNow, forcePush, forcePull, disable, updatePassword
• Each method invokes corresponding Tauri command with proper parameter mapping

src/services/api.ts


19. src/components/settings/SyncSettings.tsx ✨ Enhancement +440/-0

React component for sync settings UI

• Implements SyncSettings React component for sync configuration UI
• Provides provider selection (S3/WebDAV) with dynamic form fields
• Includes sync password input, test connection, configure, and manual sync action buttons
• Displays sync status with device ID, last sync time, and sync interval

src/components/settings/SyncSettings.tsx


20. src/components/settings/SettingsDialog.tsx ✨ Enhancement +18/-1

Settings dialog integration with sync tab

• Adds "sync" to SettingsSection type union
• Imports SyncSettings component and Cloud icon from lucide-react
• Adds sync button to settings navigation sidebar
• Renders SyncSettings component when sync section is active

src/components/settings/SettingsDialog.tsx


21. src/lib/i18n/locales/en.ts 📝 Documentation +39/-5

English translations for sync feature

• Adds "sync" entry to settings sections menu
• Adds comprehensive sync object with 33 translation keys for sync UI labels, messages, and status
 text
• Includes keys for configuration, testing, password management, and sync operations

src/lib/i18n/locales/en.ts


22. src/lib/i18n/locales/ja.ts 📝 Documentation +37/-2

Japanese translations for sync feature

• Adds "同期" (sync) entry to settings sections menu
• Adds comprehensive sync object with 33 Japanese translation keys matching English structure
• Includes translations for configuration, testing, password management, and sync operations

src/lib/i18n/locales/ja.ts


23. src/lib/i18n/locales/zh.ts 📝 Documentation +32/-0

Chinese translations for sync feature

• Adds "同步" (sync) entry to settings sections menu
• Adds comprehensive sync object with 33 Chinese translation keys matching English structure
• Includes translations for configuration, testing, password management, and sync operations

src/lib/i18n/locales/zh.ts


24. docs/superpowers/specs/2026-06-02-config-sync-design.md 📝 Documentation +245/-0

Config sync design specification document

• Comprehensive design specification for config sync feature covering architecture, encryption,
 snapshot format, and conflict resolution
• Documents SyncProvider trait, S3 and WebDAV implementations, PBKDF2+AES-256-GCM encryption scheme
• Specifies database schema, Tauri commands, error prefixes, and edge case handling
• Includes auto-sync lifecycle, file structure, and new dependencies

docs/superpowers/specs/2026-06-02-config-sync-design.md


25. docs/superpowers/plans/2026-06-02-config-sync.md 📝 Documentation +2029/-0

Add comprehensive config sync implementation plan with encryption

• Comprehensive implementation plan for cross-device configuration synchronization with S3/WebDAV
 support and end-to-end AES-256-GCM encryption
• Detailed task breakdown covering backend (Rust) and frontend (TypeScript/React) components with
 step-by-step instructions
• Includes crypto engine with PBKDF2 key derivation, snapshot-based sync architecture, and
 SyncProvider trait abstraction
• Specifies database migration, Tauri command handlers, and React UI components for sync
 configuration and management

docs/superpowers/plans/2026-06-02-config-sync.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Jun 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (1)

Grey Divider


Action required

1. api.sync missing mocks 📘 Rule violation ☼ Reliability
Description
New api.sync methods were added to src/services/api.ts but there are no corresponding sync_*
handlers in src/services/mocks.ts, so Mock mode will throw Mock: Unknown command for these
calls. This breaks the compliance requirement that every API method must be mockable when
VITE_USE_MOCK=true.
Code

src/services/api.ts[R1776-1789]

Evidence
PR Compliance ID 3 requires that new API methods in src/services/api.ts have corresponding mock
implementations in src/services/mocks.ts. The PR adds a new api.sync section that invokes
sync_* commands, but invokeMock() in mocks.ts has no sync_* cases and falls through to the
default branch that throws an unknown-command error.

AGENTS.md: New API methods added to src/services/api.ts must have corresponding mock implementations
src/services/api.ts[1776-1789]
src/services/mocks.ts[1836-1847]
src/services/mocks.ts[2434-2437]

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

## Issue description
`src/services/api.ts` introduces new `api.sync.*` methods that call Tauri commands (`sync_test_connection`, `sync_configure`, `sync_get_status`, etc.). In Mock mode (`VITE_USE_MOCK=true`), these commands route through `invokeMock()` in `src/services/mocks.ts`, but there are no `case "sync_*"` handlers, so the default branch throws `Mock: Unknown command`.

## Issue Context
Compliance requires that every new API method added to `src/services/api.ts` has a corresponding mock implementation in `src/services/mocks.ts` to keep Mock mode complete.

## Fix Focus Areas
- src/services/mocks.ts[1836-2437]
- src/services/api.ts[1776-1789]

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


2. SigV4 scope order wrong 🐞 Bug ≡ Correctness
Description
S3Provider::sign_request builds credential_scope as {region}/{date}/s3/aws4_request instead of
{date}/{region}/s3/aws4_request, producing an invalid string-to-sign and signature. All S3 sync
operations will fail with signature mismatch errors.
Code

src-tauri/src/sync/s3.rs[R91-97]

Evidence
The code formats credential_scope with self.region before date, and then uses that scope both
in the string-to-sign and Authorization header; this guarantees signatures won’t match the server’s
expected scope ordering.

src-tauri/src/sync/s3.rs[86-108]

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

### Issue description
S3 SigV4 signing uses the wrong credential scope component ordering (`region/date`), which makes signatures invalid and breaks all S3 operations.

### Issue Context
In SigV4, credential scope must be `YYYYMMDD/region/service/aws4_request`.

### Fix Focus Areas
- src-tauri/src/sync/s3.rs[91-107]

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


3. Pull sync state not saved 🐞 Bug ≡ Correctness
Description
SyncManager::sync_now imports a remote snapshot from another device but does not update
last_synced_hash, so subsequent runs still consider the local DB “changed” and will keep
re-syncing unnecessarily. This also prevents the app from recognizing it is already in-sync after a
pull.
Code

src-tauri/src/sync/manager.rs[R187-198]

Evidence
The pull branch returns after setting only last_sync_result/last_sync_at, while the push branch
persists last_synced_hash; change detection explicitly depends on last_synced_hash.

src-tauri/src/sync/manager.rs[159-218]
src-tauri/src/sync/manager.rs[313-324]

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

### Issue description
After a successful pull/import in `sync_now`, the code does not persist `last_synced_hash`, so change detection stays stale.

### Issue Context
`has_local_changes()` compares the exported snapshot hash to `last_synced_hash`; if the pulled branch never writes it, auto-sync and future sync decisions are incorrect.

### Fix Focus Areas
- src-tauri/src/sync/manager.rs[169-217]
- src-tauri/src/sync/manager.rs[313-324]

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


View more (2)
4. Saved query links broken 🐞 Bug ≡ Correctness
Description
import_snapshot recreates connections with new autoincrement IDs but imports saved queries using
the snapshot’s connectionId, leaving saved queries pointing at non-existent connections. This
breaks saved-query to connection association after any pull/force-pull.
Code

src-tauri/src/sync/manager.rs[R444-495]

Evidence
The import code discards original connection IDs by calling create_connection, and then passes the
snapshot’s connectionId directly into create_saved_query; create_connection always inserts a
new row/ID.

src-tauri/src/sync/manager.rs[444-495]
src-tauri/src/db/local.rs[390-451]

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

### Issue description
During snapshot import, connections are deleted and recreated, which changes their DB IDs; saved queries are then imported with stale `connectionId` values.

### Issue Context
`LocalDb::create_connection` always inserts a new row and returns a new ID. Snapshot entries include the old IDs; without an old->new mapping, `saved_queries.connection_id` becomes orphaned.

### Fix Focus Areas
- src-tauri/src/sync/manager.rs[444-495]
- src-tauri/src/db/local.rs[390-451]

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


5. Credentials stored plaintext 🐞 Bug ⛨ Security
Description
SyncManager::configure stores the full serialized SyncConfig into sync_state, which includes
S3 secretAccessKey and WebDAV password in cleartext. Compromise of the local SQLite DB exposes
remote storage credentials.
Code

src-tauri/src/sync/manager.rs[R91-101]

Evidence
SyncConfig explicitly contains credential fields, and configure serializes and stores the config
JSON without applying any encryption (unlike sync_password_enc, which is encrypted).

src-tauri/src/sync/provider.rs[14-29]
src-tauri/src/sync/manager.rs[91-117]

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

### Issue description
Provider credentials (S3 secret access key, WebDAV password) are persisted unencrypted in `sync_state.sync_config`.

### Issue Context
`SyncConfig` contains sensitive fields (`secret_access_key`, `password`) and is saved verbatim as JSON.

### Fix Focus Areas
- src-tauri/src/sync/manager.rs[91-117]
- src-tauri/src/sync/provider.rs[14-29]
- src-tauri/src/db/local.rs[317-326]

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



Remediation recommended

6. Import not transactional 🐞 Bug ☼ Reliability
Description
import_snapshot deletes all existing connections/queries/AI providers and recreates them without a
DB transaction, so any mid-import failure can permanently wipe data and leave the database partially
restored. This is especially risky given multiple awaited operations across tables.
Code

src-tauri/src/sync/manager.rs[R444-508]

Evidence
The import implementation issues deletes for all existing entities and then performs inserts, with
many await points and no transaction boundary, so errors can leave data missing.

src-tauri/src/sync/manager.rs[438-508]

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

### Issue description
Snapshot import performs destructive deletes and inserts across several tables without a transaction; failures leave partial state.

### Issue Context
`import_snapshot` awaits multiple operations between deletes and inserts. Without an explicit transaction, SQLite commits each statement independently.

### Fix Focus Areas
- src-tauri/src/sync/manager.rs[438-509]

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


7. Debounce spawns redundant syncs 🐞 Bug ➹ Performance
Description
SyncScheduler::notify_data_changed spawns a new delayed task on every change and never cancels
previous tasks, so bursts of edits can trigger many redundant auto-sync attempts and overlap with
the periodic loop. This can cause unnecessary network traffic and repeated remote writes.
Code

src-tauri/src/sync/scheduler.rs[R52-62]

Evidence
The scheduler runs a periodic sync loop and additionally spawns a brand-new delayed task for each
change event, with no shared cancellation/deduplication mechanism.

src-tauri/src/sync/scheduler.rs[25-62]

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 current debounce implementation spawns one task per change event and cannot collapse a burst into a single sync.

### Issue Context
Multiple UI operations call `notify_data_changed()`; each call spawns a 3-second sleep then sync.

### Fix Focus Areas
- src-tauri/src/sync/scheduler.rs[25-62]

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


Grey Divider

Qodo Logo

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 813b9faf5c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src-tauri/src/sync/s3.rs
method, path, query, canonical_headers, signed_headers, payload_hash
);

let credential_scope = format!("{}/{}/s3/aws4_request", self.region, date);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Put the SigV4 credential scope in date-first order

For S3 sync, every signed request uses this credential scope, but AWS Signature V4 expects date/region/service/aws4_request, not region/date/service/aws4_request. With real S3-compatible services that verify SigV4, the Authorization header will not match and testConnection, put_object, and get_object all fail for otherwise valid S3 settings.

Useful? React with 👍 / 👎.

Comment on lines +485 to +488
let connection_id = q_val
.get("connectionId")
.or_else(|| q_val.get("connection_id"))
.and_then(|v| v.as_i64());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve saved-query connection references during import

When a snapshot is imported on another device, connections are recreated with fresh autoincrement ids because create_connection ignores the snapshot's id/uuid, but saved queries are then reinserted with the original numeric connectionId. If the destination ids differ, imported saved queries point at the wrong connection or no connection at all; the import needs to map snapshot connection identifiers to the newly created ids before creating queries.

Useful? React with 👍 / 👎.

Comment on lines +91 to +95
// Save config
let config_json = serde_json::to_string(config)
.map_err(|e| format!("[SYNC_CONFIG_ERROR] Serialize config: {e}"))?;

db.set_sync_state("sync_config", &config_json).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid storing provider credentials in plaintext config

When configuring S3 or WebDAV, SyncConfig contains secretAccessKey/WebDAV password, and serializing the whole struct here writes those secrets directly into sync_state and later returns them through sync_get_config. This bypasses the local encryption used for other sync secrets, so anyone with the local database can read credentials that grant access to the remote sync store.

Useful? React with 👍 / 👎.

Comment on lines +96 to +99
// Check if local data has changed
if !manager.has_local_changes().await? {
return Ok(());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pull remote changes during scheduled sync

The periodic scheduler exits whenever there are no local changes, so an idle device never downloads snapshots pushed by another device unless the user manually clicks sync or first makes a local edit. That breaks automatic cross-device synchronization in the common case where one machine changes settings and another is just waiting for updates; the scheduled path should call a pull-capable sync flow instead of gating entirely on local changes.

Useful? React with 👍 / 👎.

Comment thread src/services/api.ts
Comment on lines +1776 to +1789
sync: {
testConnection: (config: SyncConfig): Promise<void> =>
invoke("sync_test_connection", { config }),
configure: (config: SyncConfig, syncPassword: string): Promise<void> =>
invoke("sync_configure", { config, syncPassword }),
getStatus: (): Promise<SyncStatus> => invoke("sync_get_status"),
getConfig: (): Promise<SyncConfig | null> => invoke("sync_get_config"),
syncNow: (): Promise<SyncResult> => invoke("sync_now"),
forcePush: (): Promise<void> => invoke("sync_force_push"),
forcePull: (): Promise<void> => invoke("sync_force_pull"),
disable: (): Promise<void> => invoke("sync_disable"),
updatePassword: (oldPassword: string, newPassword: string): Promise<void> =>
invoke("sync_update_password", { oldPassword, newPassword }),
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. api.sync missing mocks 📘 Rule violation ☼ Reliability

New api.sync methods were added to src/services/api.ts but there are no corresponding sync_*
handlers in src/services/mocks.ts, so Mock mode will throw Mock: Unknown command for these
calls. This breaks the compliance requirement that every API method must be mockable when
VITE_USE_MOCK=true.
Agent Prompt
## Issue description
`src/services/api.ts` introduces new `api.sync.*` methods that call Tauri commands (`sync_test_connection`, `sync_configure`, `sync_get_status`, etc.). In Mock mode (`VITE_USE_MOCK=true`), these commands route through `invokeMock()` in `src/services/mocks.ts`, but there are no `case "sync_*"` handlers, so the default branch throws `Mock: Unknown command`.

## Issue Context
Compliance requires that every new API method added to `src/services/api.ts` has a corresponding mock implementation in `src/services/mocks.ts` to keep Mock mode complete.

## Fix Focus Areas
- src/services/mocks.ts[1836-2437]
- src/services/api.ts[1776-1789]

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

Comment thread src-tauri/src/sync/s3.rs
Comment on lines +91 to +97
let credential_scope = format!("{}/{}/s3/aws4_request", self.region, date);
let string_to_sign = format!(
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
datetime,
credential_scope,
hex::encode(sha256(canonical_request.as_bytes()))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Sigv4 scope order wrong 🐞 Bug ≡ Correctness

S3Provider::sign_request builds credential_scope as {region}/{date}/s3/aws4_request instead of
{date}/{region}/s3/aws4_request, producing an invalid string-to-sign and signature. All S3 sync
operations will fail with signature mismatch errors.
Agent Prompt
### Issue description
S3 SigV4 signing uses the wrong credential scope component ordering (`region/date`), which makes signatures invalid and breaks all S3 operations.

### Issue Context
In SigV4, credential scope must be `YYYYMMDD/region/service/aws4_request`.

### Fix Focus Areas
- src-tauri/src/sync/s3.rs[91-107]

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

Comment on lines +187 to +198
// Import remote data if it's from a different device
if remote_snapshot.device_id != local_device_id {
self.import_snapshot(&db, &remote_snapshot).await?;
db.set_sync_state("last_sync_result", "success").await?;
db.set_sync_state("last_sync_at", &now).await?;

return Ok(SyncResult {
action: "pulled".to_string(),
timestamp: now,
remote_device_id: Some(remote_snapshot.device_id),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Pull sync state not saved 🐞 Bug ≡ Correctness

SyncManager::sync_now imports a remote snapshot from another device but does not update
last_synced_hash, so subsequent runs still consider the local DB “changed” and will keep
re-syncing unnecessarily. This also prevents the app from recognizing it is already in-sync after a
pull.
Agent Prompt
### Issue description
After a successful pull/import in `sync_now`, the code does not persist `last_synced_hash`, so change detection stays stale.

### Issue Context
`has_local_changes()` compares the exported snapshot hash to `last_synced_hash`; if the pulled branch never writes it, auto-sync and future sync decisions are incorrect.

### Fix Focus Areas
- src-tauri/src/sync/manager.rs[169-217]
- src-tauri/src/sync/manager.rs[313-324]

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

Comment on lines +444 to +495
// Clear and re-import connections
let existing_connections = db.list_connections().await?;
for conn in &existing_connections {
db.delete_connection(conn.id).await?;
}
for conn_val in &snapshot.data.connections {
// Snapshot stores Connection objects (dbType), but create_connection
// expects ConnectionForm (driver). Map the field name.
let mut form_val = conn_val.clone();
if let Some(db_type) = form_val.get("dbType").cloned() {
form_val.as_object_mut().map(|m| {
m.insert("driver".to_string(), db_type);
m
});
}
let form: crate::models::ConnectionForm =
serde_json::from_value(form_val)
.map_err(|e| format!("[SYNC_MERGE_ERROR] Parse connection: {e}"))?;
db.create_connection(form).await?;
}

// Clear and re-import saved queries
let existing_queries = db.list_saved_queries().await?;
for q in &existing_queries {
db.delete_saved_query(q.id).await?;
}
for q_val in &snapshot.data.saved_queries {
let name = q_val
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let query = q_val
.get("query")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let description = q_val
.get("description")
.and_then(|v| v.as_str())
.map(String::from);
let connection_id = q_val
.get("connectionId")
.or_else(|| q_val.get("connection_id"))
.and_then(|v| v.as_i64());
let database = q_val
.get("database")
.and_then(|v| v.as_str())
.map(String::from);
db.create_saved_query(name, query, description, connection_id, database)
.await?;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Saved query links broken 🐞 Bug ≡ Correctness

import_snapshot recreates connections with new autoincrement IDs but imports saved queries using
the snapshot’s connectionId, leaving saved queries pointing at non-existent connections. This
breaks saved-query to connection association after any pull/force-pull.
Agent Prompt
### Issue description
During snapshot import, connections are deleted and recreated, which changes their DB IDs; saved queries are then imported with stale `connectionId` values.

### Issue Context
`LocalDb::create_connection` always inserts a new row and returns a new ID. Snapshot entries include the old IDs; without an old->new mapping, `saved_queries.connection_id` becomes orphaned.

### Fix Focus Areas
- src-tauri/src/sync/manager.rs[444-495]
- src-tauri/src/db/local.rs[390-451]

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

Comment on lines +91 to +101
// Save config
let config_json = serde_json::to_string(config)
.map_err(|e| format!("[SYNC_CONFIG_ERROR] Serialize config: {e}"))?;

db.set_sync_state("sync_config", &config_json).await?;
db.set_sync_state(
"provider_type",
&format!("{:?}", config.provider_type),
)
.await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

5. Credentials stored plaintext 🐞 Bug ⛨ Security

SyncManager::configure stores the full serialized SyncConfig into sync_state, which includes
S3 secretAccessKey and WebDAV password in cleartext. Compromise of the local SQLite DB exposes
remote storage credentials.
Agent Prompt
### Issue description
Provider credentials (S3 secret access key, WebDAV password) are persisted unencrypted in `sync_state.sync_config`.

### Issue Context
`SyncConfig` contains sensitive fields (`secret_access_key`, `password`) and is saved verbatim as JSON.

### Fix Focus Areas
- src-tauri/src/sync/manager.rs[91-117]
- src-tauri/src/sync/provider.rs[14-29]
- src-tauri/src/db/local.rs[317-326]

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

@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: 17

🧹 Nitpick comments (2)
src/components/settings/SettingsDialog.tsx (1)

58-58: ⚡ Quick win

Use the @/ alias for the new settings import.

Please keep this import consistent with the repo-wide TS import convention.

Suggested change
-import { SyncSettings } from "./SyncSettings";
+import { SyncSettings } from "`@/components/settings/SyncSettings`";

As per coding guidelines, src/**/*.{ts,tsx}: Use path alias @/ mapping to ./src/ for imports in TypeScript files.

🤖 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/SettingsDialog.tsx` at line 58, Update the import in
SettingsDialog.tsx to use the repo TypeScript path alias instead of a relative
path; replace the current import of SyncSettings (import { SyncSettings } from
"./SyncSettings";) with the aliased module import (use "`@/`..." rooted at src) so
the component imports follow the repo-wide convention.
src-tauri/src/sync/scheduler.rs (1)

15-112: 🏗️ Heavy lift

Add unit coverage for the scheduler contract.

The interval clamp, shutdown path, and change-trigger behavior are the core logic in this new module, but there are no source-level tests locking them down. As per coding guidelines, src-tauri/**/*.rs: Use Rust unit tests marked with #[test] in source files, run with cargo test --lib, and use #[ignore] for integration tests that should only run explicitly with specific database environment variables.

🤖 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-tauri/src/sync/scheduler.rs` around lines 15 - 112, Add source-level unit
tests to src-tauri/src/sync/scheduler.rs to cover the interval clamping,
shutdown path, and change-trigger behavior: create a #[cfg(test)] mod tests in
the same file and add tests that exercise SyncScheduler::get_sync_interval_secs
(verify clamping and default), SyncScheduler::start/stop (spawn the scheduler,
send stop via stop() and assert it exits/shuts down), and notify_data_changed
(ensure try_auto_sync is invoked after debounce) using tokio async tests
(#[tokio::test]) so the async runtime and tokio::time can be controlled (use
tokio::time::pause/advance for timing); mark only long/integration-like tests
with #[ignore] per guidelines and keep tests deterministic by injecting or
mocking the LocalDb/SyncManager behaviors referenced in try_auto_sync / manager
methods.
🤖 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 `@docs/superpowers/plans/2026-06-02-config-sync.md`:
- Around line 1065-1070: The code currently serializes the entire SyncConfig
(via serde_json::to_string into config_json) and persists it with
db.set_sync_state("sync_config", &config_json), which will persist provider
secrets in plaintext; instead, filter out or redact sensitive fields (e.g. S3
secret_key, WebDAV password) before serializing or replace them with non-secret
placeholders, and persist actual credentials separately using the intended
encrypted/secure storage mechanism (or call the existing credential store API)
so that db.set_sync_state("sync_config", ...) only receives a non-sensitive
representation of SyncConfig.
- Line 1320: export_snapshot() currently hardcodes settings to an empty object
and import_snapshot() skips applying settings/shortcuts, so user settings and
keyboard shortcuts never get synced; update export_snapshot() to read and
include the real settings/shortcuts (e.g., load from settings.json or the
settings store used elsewhere) instead of
serde_json::Value::Object(serde_json::Map::new()), and update import_snapshot()
to apply those settings/shortcuts during import (use the same application
logic/handlers that the app uses to persist settings and shortcuts), ensuring
the snapshot payload contains the settings and import_snapshot() processes the
settings and shortcuts entries rather than skipping them.
- Around line 1131-1142: The current code imports any snapshot from another
device without checking its timestamp; before calling import_snapshot(&db,
&remote_snapshot) (inside the branch that compares remote_snapshot.device_id !=
local_device_id) fetch the local snapshot's timestamp (e.g., via the existing
local snapshot lookup or a db method that returns the latest snapshot timestamp)
and compare timestamps per Last-Write-Wins: only proceed to import if
remote_snapshot.timestamp is strictly newer than the local timestamp; if it's
older or equal, skip import, set appropriate sync state (e.g.,
"last_sync_result" => "ignored" or "no-op") and return a SyncResult indicating
no pull occurred instead of overwriting newer local data.

In `@docs/superpowers/specs/2026-06-02-config-sync-design.md`:
- Around line 137-148: Remove the fast password verifier stored as the key
"sync_password_hash" in the sync_state table and any code paths that read/write
that key (references to sync_state and "sync_password_hash"); either drop the
key from the schema and associated logic, or if a verifier must be kept, replace
it with a salted, slow KDF output (e.g., PBKDF2/Argon2) and update all
read/write/verification code to use the KDF output and salt rather than a raw
hash so offline brute-force is not trivial.

In `@src-tauri/src/commands/sync.rs`:
- Around line 13-19: Validate SyncConfig.sync_interval_minutes in sync_configure
and reject values equal to 0 before calling SyncManager::configure: check
config.sync_interval_minutes == 0 and return an Err with a clear message (e.g.
"sync_interval_minutes must be >= 1") so the invalid value is not persisted;
reference sync_configure, SyncConfig.sync_interval_minutes,
SyncManager::configure, SyncScheduler::get_sync_interval_secs, and
sync_get_status when making the change.

In `@src-tauri/src/sync/manager.rs`:
- Around line 390-423: export_snapshot currently builds the snapshot from
LocalDb::list_connections(), which returns redacted connection records
(sentinel_password, api_key_secret, api_key_encoded nulled) so exported
snapshots miss required secrets; change export_snapshot to fetch connections
with their real secret fields (e.g., call a new or existing non-redacting method
such as LocalDb::list_connections_unredacted or
LocalDb::list_connections_with_secrets instead of LocalDb::list_connections()),
then keep the existing AI provider decrypt/re-encrypt logic
(db.decrypt_ai_api_key / LocalDb::has_encrypted_ai_api_key) but operate on the
unredacted connection structs so exported snapshots include the full
Redis/Elasticsearch/cloud config fields.
- Around line 445-495: When recreating connections, capture each snapshot
connection’s original ID (e.g., "id" or "connection_id") before you call
db.create_connection and store a mapping from original_id -> newly_created_id
(the value returned by db.create_connection); then, when importing saved queries
in the saved_queries loop, look up the snapshot query’s
connectionId/connection_id in that mapping and pass the mapped new local
connection id to db.create_saved_query (or None if no mapping found). Implement
this with a HashMap collected in the connections-create loop (update the block
that clones conn_val and calls db.create_connection) and consult that map in the
saved_queries import block to translate connection references.
- Around line 422-423: The snapshot currently hardcodes settings to an empty
object (settings: serde_json::Value::Object(...)) and never applies imported
settings; update the snapshot creation in the export path (e.g., where the
Snapshot struct or export_snapshot is constructed) to serialize and include the
live application settings instead of an empty map, and update the import path
(e.g., import_snapshot/apply_snapshot or wherever snapshots are deserialized) to
read the snapshot.settings Value and merge/apply it into the application's
settings store (call the existing SettingsManager/Settings API such as
set_settings/apply_from_json or deserialize into the Settings type) so user
preferences are persisted on import. Ensure appropriate error handling when
deserializing/merging the serde_json::Value.
- Around line 169-199: The current pull path unconditionally calls
import_snapshot(&db, &remote_snapshot) and returns, which can overwrite local
unsynced edits; before calling import_snapshot (in the branch where
remote_snapshot.device_id != local_device_id) detect whether the local device
has pending/unsynced changes (e.g., via an existing DB method or by computing
local snapshot/data and comparing hashes), and if unsynced changes exist avoid
the destructive import: either return a conflict result (e.g., an Err or a
SyncResult indicating "conflict") or trigger a merge flow instead of
import_snapshot; ensure you still update sync state only after a non-destructive
successful resolution and preserve local connections/queries/AI providers when
reporting the conflict.
- Around line 444-510: Validate and convert the entire snapshot into DB-ready
objects first (parse all connection entries into crate::models::ConnectionForm,
saved queries into the needed fields, and AiProviderForm) and return an error on
any parse failure before performing any deletes, then wrap the
clear-and-reimport sequence (the loops that call db.delete_connection /
db.create_connection, db.delete_saved_query / db.create_saved_query, and
db.delete_ai_provider / db.create_ai_provider) inside a single database
transaction (use your DB client's transaction API, e.g., db.transaction() or
similar) so that you perform deletes and inserts atomically and rollback on
error; ensure you locate and replace the current destructive loops in
import_snapshot (the code that calls list_connections, list_saved_queries,
list_ai_providers and then deletes and re-creates them) with the validated
pre-built forms and a single transactional commit/rollback.

In `@src-tauri/src/sync/provider.rs`:
- Around line 80-82: build_provider is currently matching on
config.provider_type which attempts to move ProviderType out of a shared
&SyncConfig; instead match on a reference or clone the enum. Update the match to
use the referenced value (e.g., match &config.provider_type and pattern-match
ProviderType::S3, ProviderType::Gcs, etc.) or explicitly clone the field
(config.provider_type.clone()) before matching so you don't move out of the
borrowed config; reference the build_provider function, the SyncConfig struct
and its provider_type field, and the ProviderType::S3 pattern when making the
change.

In `@src-tauri/src/sync/s3.rs`:
- Around line 63-107: Fix the SigV4 canonical request construction in s3.rs:
build credential_scope as "{date}/{region}/s3/aws4_request" (swap date and
region) instead of "{region}/{date}/s3/aws4_request"; ensure the host header
includes the port for custom endpoints by appending the url.port() when present
(so host header becomes "host:port" when url.port() is Some) instead of using
host_str() alone; and produce canonical and signed headers using lowercase
header names (make signed_headers use k.to_lowercase() and canonical_headers use
k.to_lowercase() as the header name) and ensure headers are sorted by lowercase
key before joining so SignedHeaders matches the canonical_headers order (refer
to variables/methods: credential_scope, signed_headers, canonical_headers, host,
url.port(), and derive_signing_key/signature construction).
- Around line 145-179: The connection test currently probes the bucket root via
a GET (building URL from self.endpoint and self.bucket) which requires
ListBucket; instead, test an object under the configured prefix using a
non-listing method. Modify the code that builds the test URL to include the
configured prefix/object (e.g., append self.prefix or a small probe object name
under the prefix) and change the request to a HEAD (update the sign_request call
to use "HEAD" and replace client.get(...) with client.head(...)); keep using the
empty payload hash and existing signing flow (sign_request, now_timestamps) and
fall back to the bucket-root behavior only if no prefix is configured. Ensure
headers are signed with "HEAD" and the final request uses the head response
status to determine success.

In `@src-tauri/src/sync/scheduler.rs`:
- Around line 9-13: Change the current per-event sleepers into a single
debounced timer that routes all change notifications through shared state so
each new event resets the same pending timer: create within SyncScheduler a
single background debounce task (spawned once in start) that receives
notifications (e.g., via a watch/Notify or an Arc<AtomicBool> + Notify) instead
of spawning a new sleeper per change, have that task await either the debounce
timeout or shutdown_rx (so it cancels on stop), and call try_auto_sync() only
when the timer elapses; also update any code that previously spawned sleepers
(the logic around handling change events and the call sites of try_auto_sync())
to signal the shared notifier/state rather than creating new timers so pending
syncs are cancelled when stop() signals shutdown_rx.

In `@src/components/settings/SyncSettings.tsx`:
- Around line 325-368: The UI hides both password inputs and the save button
when status.enabled && status.passwordStored, preventing persisting provider
changes or password rotation; update the conditional render logic in
SyncSettings (where status, syncPassword, confirmPassword, handleConfigure,
handleSavePassword, and handleTestConnection are used) to always render a save
button for saving provider/config changes even when password is stored/enabled
(e.g., show a "Save" or "Save changes" Button that invokes handleConfigure), and
keep a separate action (e.g., "Rotate password") that invokes handleSavePassword
only when the user wants to update the password; ensure the password inputs
remain hidden when passwordStored is true but provider save remains available.
- Around line 405-433: The force push/pull buttons (rendered with the Button
components calling handleForcePush and handleForcePull) perform destructive
overwrite operations without confirmation; add a confirmation step before
executing: update the onClick handlers for the force push and force pull buttons
to first show a modal or browser confirm dialog that clearly describes the
consequence and requires explicit user confirmation, and only call
handleForcePush or handleForcePull when the user confirms; ensure the
confirmation UI disables the confirm action while loading and preserves existing
loading/error handling.
- Around line 243-319: Hardcoded English strings in the SyncSettings JSX
(provider SelectItem labels, Input placeholders, the "Device ID" label, and the
"min" suffix used with syncInterval options) bypass i18n; replace those literals
with t(...) lookups and interpolations. Update each SelectItem (e.g., the
S3/WebDAV items), all Input placeholders (e.g., endpoint, region, bucket,
serverUrl, username, password, pathPrefix), the Label rendering "Device ID" and
the syncInterval SelectItem labels (e.g., "1 min", "3 min", etc.) to use
translation keys (for example settings.sync.provider.S3,
settings.sync.placeholder.endpoint, settings.sync.deviceId,
settings.sync.intervalLabel with a "{{minutes}} min" interpolation or separate
key for "min") so locales are applied; ensure you call t(...) where
SelectValue/SelectItem and Input.placeholder are defined (components:
SelectItem, Input, Label inside SyncSettings) and update locale files
accordingly.

---

Nitpick comments:
In `@src-tauri/src/sync/scheduler.rs`:
- Around line 15-112: Add source-level unit tests to
src-tauri/src/sync/scheduler.rs to cover the interval clamping, shutdown path,
and change-trigger behavior: create a #[cfg(test)] mod tests in the same file
and add tests that exercise SyncScheduler::get_sync_interval_secs (verify
clamping and default), SyncScheduler::start/stop (spawn the scheduler, send stop
via stop() and assert it exits/shuts down), and notify_data_changed (ensure
try_auto_sync is invoked after debounce) using tokio async tests
(#[tokio::test]) so the async runtime and tokio::time can be controlled (use
tokio::time::pause/advance for timing); mark only long/integration-like tests
with #[ignore] per guidelines and keep tests deterministic by injecting or
mocking the LocalDb/SyncManager behaviors referenced in try_auto_sync / manager
methods.

In `@src/components/settings/SettingsDialog.tsx`:
- Line 58: Update the import in SettingsDialog.tsx to use the repo TypeScript
path alias instead of a relative path; replace the current import of
SyncSettings (import { SyncSettings } from "./SyncSettings";) with the aliased
module import (use "`@/`..." rooted at src) so the component imports follow the
repo-wide convention.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88b432b6-2514-40a6-9111-d625797c7ace

📥 Commits

Reviewing files that changed from the base of the PR and between 6b53a5e and 813b9fa.

⛔ Files ignored due to path filters (1)
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • docs/superpowers/plans/2026-06-02-config-sync.md
  • docs/superpowers/specs/2026-06-02-config-sync-design.md
  • src-tauri/Cargo.toml
  • src-tauri/migrations/017_sync_state.sql
  • src-tauri/src/commands/ai.rs
  • src-tauri/src/commands/connection.rs
  • src-tauri/src/commands/mod.rs
  • src-tauri/src/commands/storage.rs
  • src-tauri/src/commands/sync.rs
  • src-tauri/src/db/local.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/state.rs
  • src-tauri/src/sync/crypto.rs
  • src-tauri/src/sync/manager.rs
  • src-tauri/src/sync/mod.rs
  • src-tauri/src/sync/provider.rs
  • src-tauri/src/sync/s3.rs
  • src-tauri/src/sync/scheduler.rs
  • src-tauri/src/sync/webdav.rs
  • src/components/settings/SettingsDialog.tsx
  • src/components/settings/SyncSettings.tsx
  • src/lib/i18n/locales/en.ts
  • src/lib/i18n/locales/ja.ts
  • src/lib/i18n/locales/zh.ts
  • src/services/api.ts

Comment on lines +1065 to +1070
// Save config (encrypt sensitive fields)
let config_json = serde_json::to_string(config)
.map_err(|e| format!("[SYNC_CONFIG_ERROR] Serialize config: {e}"))?;

// Store non-sensitive config fields
db.set_sync_state("sync_config", &config_json).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not persist provider secrets inside sync_config.

This step serializes the whole SyncConfig, so the S3 secret key / WebDAV password would be written into sync_state in plaintext. That conflicts with the design's “store credentials locally encrypted or outside sync_config” rule and turns the local SQLite file into a cloud-credential leak point.

🤖 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 `@docs/superpowers/plans/2026-06-02-config-sync.md` around lines 1065 - 1070,
The code currently serializes the entire SyncConfig (via serde_json::to_string
into config_json) and persists it with db.set_sync_state("sync_config",
&config_json), which will persist provider secrets in plaintext; instead, filter
out or redact sensitive fields (e.g. S3 secret_key, WebDAV password) before
serializing or replace them with non-secret placeholders, and persist actual
credentials separately using the intended encrypted/secure storage mechanism (or
call the existing credential store API) so that db.set_sync_state("sync_config",
...) only receives a non-sensitive representation of SyncConfig.

Comment on lines +1131 to +1142
// Import remote data if it's from a different device and newer
if remote_snapshot.device_id != local_device_id {
self.import_snapshot(&db, &remote_snapshot).await?;
db.set_sync_state("last_sync_result", "success").await?;
db.set_sync_state("last_sync_at", &now).await?;

return Ok(SyncResult {
action: "pulled".to_string(),
timestamp: now,
remote_device_id: Some(remote_snapshot.device_id),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the timestamp check before importing a remote snapshot.

This imports any snapshot from another device, even when it is older than the local state. The spec's Last-Write-Wins rule requires comparing timestamps first; otherwise sync_now() can overwrite newer local edits with stale remote data.

🤖 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 `@docs/superpowers/plans/2026-06-02-config-sync.md` around lines 1131 - 1142,
The current code imports any snapshot from another device without checking its
timestamp; before calling import_snapshot(&db, &remote_snapshot) (inside the
branch that compares remote_snapshot.device_id != local_device_id) fetch the
local snapshot's timestamp (e.g., via the existing local snapshot lookup or a db
method that returns the latest snapshot timestamp) and compare timestamps per
Last-Write-Wins: only proceed to import if remote_snapshot.timestamp is strictly
newer than the local timestamp; if it's older or equal, skip import, set
appropriate sync state (e.g., "last_sync_result" => "ignored" or "no-op") and
return a SyncResult indicating no pull occurred instead of overwriting newer
local data.

.cloned()
.unwrap_or_default(),
ai_providers: ai_providers_json,
settings: serde_json::Value::Object(serde_json::Map::new()), // TODO: read from settings.json if needed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This plan currently makes settings/shortcuts sync a no-op.

export_snapshot() hardcodes settings to {}, and import_snapshot() explicitly skips applying them. That means the implementation described here would not actually sync user settings or keyboard shortcuts, despite both being in scope for this PR.

Also applies to: 1375-1376

🤖 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 `@docs/superpowers/plans/2026-06-02-config-sync.md` at line 1320,
export_snapshot() currently hardcodes settings to an empty object and
import_snapshot() skips applying settings/shortcuts, so user settings and
keyboard shortcuts never get synced; update export_snapshot() to read and
include the real settings/shortcuts (e.g., load from settings.json or the
settings store used elsewhere) instead of
serde_json::Value::Object(serde_json::Map::new()), and update import_snapshot()
to apply those settings/shortcuts during import (use the same application
logic/handlers that the app uses to persist settings and shortcuts), ensuring
the snapshot payload contains the settings and import_snapshot() processes the
settings and shortcuts entries rather than skipping them.

Comment on lines +137 to +148
## Database: sync_state Table

```sql
CREATE TABLE IF NOT EXISTS sync_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
```

Keys: `device_id`, `sync_config` (JSON, provider params without passwords), `sync_enabled`, `last_synced_hash`, `last_sync_at`, `last_sync_result`, `sync_password_hash` (for verification without storing plaintext)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid storing a fast password verifier in sync_state.

A sync_password_hash in local SQLite creates an offline brute-force target if the DB is exposed, and it does not buy you much because decrypting the snapshot already verifies the password. Prefer dropping the verifier entirely, or store a salted slow KDF output instead of a raw hash.

🤖 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 `@docs/superpowers/specs/2026-06-02-config-sync-design.md` around lines 137 -
148, Remove the fast password verifier stored as the key "sync_password_hash" in
the sync_state table and any code paths that read/write that key (references to
sync_state and "sync_password_hash"); either drop the key from the schema and
associated logic, or if a verifier must be kept, replace it with a salted, slow
KDF output (e.g., PBKDF2/Argon2) and update all read/write/verification code to
use the KDF output and salt rather than a raw hash so offline brute-force is not
trivial.

Comment on lines +13 to +19
pub async fn sync_configure(
state: State<'_, AppState>,
config: SyncConfig,
sync_password: String,
) -> Result<(), String> {
let manager = SyncManager::new(state.local_db.clone());
manager.configure(&config, &sync_password).await

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject syncIntervalMinutes = 0 before calling configure().

SyncConfig.sync_interval_minutes can currently be 0. SyncManager::configure() persists that raw value and sync_get_status() will report 0, but SyncScheduler::get_sync_interval_secs() clamps it to 1 minute. That leaves the stored config/status out of sync with the actual runtime behavior and can silently increase sync frequency.

Suggested guard
 pub async fn sync_configure(
     state: State<'_, AppState>,
     config: SyncConfig,
     sync_password: String,
 ) -> Result<(), String> {
+    if matches!(config.sync_interval_minutes, Some(0)) {
+        return Err("[SYNC_CONFIG_ERROR] syncIntervalMinutes must be at least 1".to_string());
+    }
     let manager = SyncManager::new(state.local_db.clone());
     manager.configure(&config, &sync_password).await
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub async fn sync_configure(
state: State<'_, AppState>,
config: SyncConfig,
sync_password: String,
) -> Result<(), String> {
let manager = SyncManager::new(state.local_db.clone());
manager.configure(&config, &sync_password).await
pub async fn sync_configure(
state: State<'_, AppState>,
config: SyncConfig,
sync_password: String,
) -> Result<(), String> {
if matches!(config.sync_interval_minutes, Some(0)) {
return Err("[SYNC_CONFIG_ERROR] syncIntervalMinutes must be at least 1".to_string());
}
let manager = SyncManager::new(state.local_db.clone());
manager.configure(&config, &sync_password).await
}
🤖 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-tauri/src/commands/sync.rs` around lines 13 - 19, Validate
SyncConfig.sync_interval_minutes in sync_configure and reject values equal to 0
before calling SyncManager::configure: check config.sync_interval_minutes == 0
and return an Err with a clear message (e.g. "sync_interval_minutes must be >=
1") so the invalid value is not persisted; reference sync_configure,
SyncConfig.sync_interval_minutes, SyncManager::configure,
SyncScheduler::get_sync_interval_secs, and sync_get_status when making the
change.

Comment thread src-tauri/src/sync/s3.rs
Comment on lines +145 to +179
let url: url::Url = format!("{}/{}/", self.endpoint, self.bucket)
.parse()
.map_err(|e: url::ParseError| {
format!("[SYNC_CONNECTION_ERROR] Invalid URL: {e}")
})?;

let empty_payload_hash = hex::encode(sha256(b""));
let (date, datetime) = Self::now_timestamps();

let mut headers = vec![(
"Content-Type".to_string(),
"application/octet-stream".to_string(),
)];
self.sign_request("GET", &url, &mut headers, &empty_payload_hash, &date, &datetime);

let mut req = self.client.get(url.as_str());
for (k, v) in &headers {
req = req.header(k.as_str(), v.as_str());
}

let resp = req
.send()
.await
.map_err(|e| format!("[SYNC_CONNECTION_ERROR] {e}"))?;
let status = resp.status();
if status.is_success() {
Ok(())
} else {
let body = resp.text().await.unwrap_or_default();
Err(format!(
"[SYNC_CONNECTION_ERROR] S3 returned {}: {}",
status,
body.chars().take(200).collect::<String>()
))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bucket-root GET makes test_connection() stricter than the real sync operations.

This probes GET /{bucket}/, which typically needs ListBucket permission. A credential set scoped only to the configured prefix/object can successfully PUT/GET/DELETE the snapshot but will still fail this test, so valid least-privilege configs get rejected up front.

🤖 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-tauri/src/sync/s3.rs` around lines 145 - 179, The connection test
currently probes the bucket root via a GET (building URL from self.endpoint and
self.bucket) which requires ListBucket; instead, test an object under the
configured prefix using a non-listing method. Modify the code that builds the
test URL to include the configured prefix/object (e.g., append self.prefix or a
small probe object name under the prefix) and change the request to a HEAD
(update the sign_request call to use "HEAD" and replace client.get(...) with
client.head(...)); keep using the empty payload hash and existing signing flow
(sign_request, now_timestamps) and fall back to the bucket-root behavior only if
no prefix is configured. Ensure headers are signed with "HEAD" and the final
request uses the head response status to determine success.

Comment on lines +9 to +13
pub struct SyncScheduler {
local_db: Arc<Mutex<Option<Arc<crate::db::local::LocalDb>>>>,
shutdown_tx: watch::Sender<bool>,
shutdown_rx: watch::Receiver<bool>,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Coalesce change notifications into a single debounce window.

This delays each event, but it does not actually debounce them. A burst of writes will spawn several sleepers that can all reach try_auto_sync() together, and those pending tasks still run after stop() because they never observe the shutdown channel. Please route change events through shared state so each new event resets one pending timer instead of starting another sync attempt.

Also applies to: 54-61

🤖 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-tauri/src/sync/scheduler.rs` around lines 9 - 13, Change the current
per-event sleepers into a single debounced timer that routes all change
notifications through shared state so each new event resets the same pending
timer: create within SyncScheduler a single background debounce task (spawned
once in start) that receives notifications (e.g., via a watch/Notify or an
Arc<AtomicBool> + Notify) instead of spawning a new sleeper per change, have
that task await either the debounce timeout or shutdown_rx (so it cancels on
stop), and call try_auto_sync() only when the timer elapses; also update any
code that previously spawned sleepers (the logic around handling change events
and the call sites of try_auto_sync()) to signal the shared notifier/state
rather than creating new timers so pending syncs are cancelled when stop()
signals shutdown_rx.

Comment on lines +243 to +319
<SelectItem value="S3">S3 (AWS / MinIO / OSS)</SelectItem>
<SelectItem value="WebDAV">WebDAV</SelectItem>
</SelectContent>
</Select>

{providerType === "S3" ? (
<div className="space-y-2">
<Input
placeholder="Endpoint (e.g., https://s3.amazonaws.com)"
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
/>
<Input
placeholder="Region (e.g., us-east-1)"
value={region}
onChange={(e) => setRegion(e.target.value)}
/>
<Input
placeholder="Bucket"
value={bucket}
onChange={(e) => setBucket(e.target.value)}
/>
<Input
placeholder="Access Key ID"
value={accessKeyId}
onChange={(e) => setAccessKeyId(e.target.value)}
/>
<Input
placeholder="Secret Access Key"
type="password"
value={secretAccessKey}
onChange={(e) => setSecretAccessKey(e.target.value)}
/>
<Input
placeholder="Path Prefix (default: dbpaw/)"
value={pathPrefix}
onChange={(e) => setPathPrefix(e.target.value)}
/>
</div>
) : (
<div className="space-y-2">
<Input
placeholder="Server URL (e.g., https://dav.example.com/dbpaw/)"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
<Input
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<Input
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
)}

<Separator className="my-2" />

<Label className="text-base">{t("settings.sync.syncInterval")}</Label>
<Select
value={String(syncInterval)}
onValueChange={(v) => setSyncInterval(Number(v))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 min</SelectItem>
<SelectItem value="3">3 min</SelectItem>
<SelectItem value="5">5 min</SelectItem>
<SelectItem value="10">10 min</SelectItem>
<SelectItem value="30">30 min</SelectItem>
<SelectItem value="60">60 min</SelectItem>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Several new sync strings bypass i18n.

The provider option labels, input placeholders, Device ID, and min suffix are hardcoded in English, so the new sync page will still be partially untranslated in ja and zh even though locale files were added.

Also applies to: 331-346, 389-403

🤖 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/SyncSettings.tsx` around lines 243 - 319, Hardcoded
English strings in the SyncSettings JSX (provider SelectItem labels, Input
placeholders, the "Device ID" label, and the "min" suffix used with syncInterval
options) bypass i18n; replace those literals with t(...) lookups and
interpolations. Update each SelectItem (e.g., the S3/WebDAV items), all Input
placeholders (e.g., endpoint, region, bucket, serverUrl, username, password,
pathPrefix), the Label rendering "Device ID" and the syncInterval SelectItem
labels (e.g., "1 min", "3 min", etc.) to use translation keys (for example
settings.sync.provider.S3, settings.sync.placeholder.endpoint,
settings.sync.deviceId, settings.sync.intervalLabel with a "{{minutes}} min"
interpolation or separate key for "min") so locales are applied; ensure you call
t(...) where SelectValue/SelectItem and Input.placeholder are defined
(components: SelectItem, Input, Label inside SyncSettings) and update locale
files accordingly.

Comment on lines +325 to +368
{(!status?.enabled || !status?.passwordStored) && (
<>
<Label className="text-base">
{t("settings.sync.syncPassword")}
</Label>
<Input
placeholder="Sync password (min 6 chars)"
type="password"
value={syncPassword}
onChange={(e) => setSyncPassword(e.target.value)}
/>
{!status?.enabled && (
<Input
placeholder="Confirm password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
)}
{status?.enabled && !status?.passwordStored && (
<div className="text-xs text-muted-foreground">
{t("settings.sync.passwordNotStored")}
</div>
)}
</>
)}

<div className="flex gap-2 mt-2">
<Button
variant="outline"
onClick={handleTestConnection}
disabled={loading}
>
{t("settings.sync.testConnection")}
</Button>
{!status?.enabled ? (
<Button onClick={handleConfigure} disabled={loading}>
{t("settings.sync.saveAndEnable")}
</Button>
) : !status?.passwordStored ? (
<Button onClick={handleSavePassword} disabled={loading}>
{t("settings.sync.saveAndEnable")}
</Button>
) : null}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Enabled sync becomes impossible to edit from the UI.

When status.enabled && status.passwordStored is true, this branch hides both the password inputs and the only save button, so provider changes made in the form can never be persisted and users have no path to rotate the sync password without disabling sync first.

🤖 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/SyncSettings.tsx` around lines 325 - 368, The UI
hides both password inputs and the save button when status.enabled &&
status.passwordStored, preventing persisting provider changes or password
rotation; update the conditional render logic in SyncSettings (where status,
syncPassword, confirmPassword, handleConfigure, handleSavePassword, and
handleTestConnection are used) to always render a save button for saving
provider/config changes even when password is stored/enabled (e.g., show a
"Save" or "Save changes" Button that invokes handleConfigure), and keep a
separate action (e.g., "Rotate password") that invokes handleSavePassword only
when the user wants to update the password; ensure the password inputs remain
hidden when passwordStored is true but provider save remains available.

Comment on lines +405 to +433
{status.enabled && status.passwordStored && (
<div className="mt-2 flex gap-2">
<Button
size="sm"
variant="outline"
onClick={handleSyncNow}
disabled={loading}
>
<RefreshCw className="w-3.5 h-3.5 mr-1" />
{t("settings.sync.syncNow")}
</Button>
<Button
size="sm"
variant="outline"
onClick={handleForcePush}
disabled={loading}
>
<Upload className="w-3.5 h-3.5 mr-1" />
{t("settings.sync.forcePush")}
</Button>
<Button
size="sm"
variant="outline"
onClick={handleForcePull}
disabled={loading}
>
<Download className="w-3.5 h-3.5 mr-1" />
{t("settings.sync.forcePull")}
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add confirmation before force push/pull.

These buttons trigger overwrite-style operations immediately. A misclick here can replace either the remote snapshot or the local config with no confirmation step.

🤖 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/SyncSettings.tsx` around lines 405 - 433, The force
push/pull buttons (rendered with the Button components calling handleForcePush
and handleForcePull) perform destructive overwrite operations without
confirmation; add a confirmation step before executing: update the onClick
handlers for the force push and force pull buttons to first show a modal or
browser confirm dialog that clearly describes the consequence and requires
explicit user confirmation, and only call handleForcePush or handleForcePull
when the user confirms; ensure the confirmation UI disables the confirm action
while loading and preserves existing loading/error handling.

@codeErrorSleep

Copy link
Copy Markdown
Owner

@mrhuangyong 感谢感谢,我今晚review一下再合入master

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.

2 participants