Skip to content

fix: harden AI/WebDAV config sync to prevent data loss#168

Open
AmintaCCCP wants to merge 8 commits into
mainfrom
fix/168-ai-config-sync-failure
Open

fix: harden AI/WebDAV config sync to prevent data loss#168
AmintaCCCP wants to merge 8 commits into
mainfrom
fix/168-ai-config-sync-failure

Conversation

@AmintaCCCP
Copy link
Copy Markdown
Owner

@AmintaCCCP AmintaCCCP commented May 28, 2026

Summary

  • encrypt() 调用加 try-catch: 单个配置加密失败不再导致整个事务回滚,而是回退到已有的加密 key
  • 全部跳过安全守卫: 如果所有配置都被跳过(缺少 API Key),事务回滚而非提交空数据库,返回 422 错误
  • 响应包含跳过详情: 响应新增 skipped/errors 字段,前端可诊断哪些配置未同步
  • fetchWithRetry: syncAIConfigs/syncWebDAVConfigs 改用 3 次重试,与 repo/release 同步一致
  • 预检警告: 空 apiKey/password 的配置在同步前记录 console.warn
  • 新错误码: SYNC_AI_CONFIGS_ALL_SKIPPEDSYNC_WEBDAV_CONFIGS_ALL_SKIPPED

Root Cause

PR #167 修复了 AI 代理请求时 configId 找不到的问题,但配置同步本身有多个脆弱点:

  1. encrypt() 无 try-catch — 单个配置加密失败导致 DELETE + INSERT 事务回滚,返回 500
  2. 全部跳过无守卫 — 所有配置被跳过时,DELETE 清空了数据库但返回 200
  3. syncAIConfigsfetchWithTimeout 而非 fetchWithRetry — 瞬时网络错误导致永久失败

Test plan

  • 创建一个有 apiKey 的 AI 配置 → 触发同步 → 验证后端数据库中有正确的加密数据
  • 创建一个空 apiKey 的 AI 配置 → 触发同步 → 验证该配置被跳过但不报 500
  • 模拟网络超时 → 验证自动重试 3 次
  • 无后端模式下正常使用不受影响

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Bulk AI/WebDAV sync now reports per-item outcomes with explicit skip reasons; requests that skip all items return HTTP 422 with skipped/error details.
    • Encryption failures for credentials fall back to previously stored encrypted values; masked or empty credentials resolve from existing values or are skipped with clear reasons.
  • Updates

    • Bulk sync requests use retries and surface per-item skipped details.
    • Settings sync runs in parallel and shows aggregated success/failure counts.
    • Local credentials are preserved when backend credentials are missing or decryption failed.

Review Change Stack

…lures

- Wrap encrypt() calls in try-catch so a single config encryption failure
  doesn't abort the entire bulk sync transaction
- Add safety guard: if ALL configs are skipped (missing keys), roll back
  the transaction instead of committing an empty database
- Track and return skipped config details in the response so the frontend
  can surface partial failures
- Switch syncAIConfigs/syncWebDAVConfigs from fetchWithTimeout to
  fetchWithRetry (3 retries) matching the pattern used by repo/release sync
- Add pre-sync warnings for configs with empty apiKey/password
- Add new error codes: SYNC_AI_CONFIGS_ALL_SKIPPED,
  SYNC_WEBDAV_CONFIGS_ALL_SKIPPED

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Server bulk sync handlers accumulate per-item inserted/skipped outcomes, try per-item secret encryption with fallback to stored encrypted values, and return { synced, skipped, errors }; on an all-skipped outcome handlers surface an error (HTTP 422). Client sync uses retryable requests and parses partial-failure responses; frontend runs syncs in parallel; autosync preserves local secrets when backend decrypt fails.

Changes

Bulk Config Sync Per-Item Tracking and Resilience

Layer / File(s) Summary
Error codes for all-skipped sync
src/utils/backendErrors.ts
Adds SYNC_AI_CONFIGS_ALL_SKIPPED and SYNC_WEBDAV_CONFIGS_ALL_SKIPPED messages (zh/en) used by server handlers.
AI bulk sync per-item outcome tracking
server/src/routes/configs.ts
Adds syncResult (inserted + skipped list); wraps apiKey encryption in try/catch with fallback to stored encrypted key, records encrypt_failed or masked/missing reasons, increments inserted count on success, throws ALL_CONFIGS_SKIPPED when nothing inserted to force rollback, and returns { synced, skipped, errors } (all-skipped now surfaces as an error/422).
WebDAV bulk sync per-item outcome tracking
server/src/routes/configs.ts
Same structure as AI sync for password: syncResult accumulation, per-item encryption try/catch with fallback, skip reasons for encrypt/masked/missing cases, increment inserted count on success, all-skip guard, and response { synced, skipped, errors } (all-skipped surfaces as an error/422).
Client retry strategy and response parsing
src/services/backendAdapter.ts
syncAIConfigs/syncWebDAVConfigs use fetchWithRetry; pre-warn on empty credentials; parse response JSON to detect skipped and per-item reasons and throw on partial failures while suppressing unrelated JSON parse errors.
Frontend parallel sync orchestration
src/components/settings/BackendPanel.tsx
handleSyncToBackend runs multiple syncs concurrently via Promise.allSettled, aggregates fulfillment/rejection counts, logs failures, and shows an error toast when any sync fails or a success when all succeed. Also expands settings payload sent to backend.
Auto-sync merge to preserve local secrets
src/services/autoSync.ts
When applying backend AI/WebDAV configs, merges backend payload with local configs to keep local apiKey/password when backend reports decrypt_failed or lacks the secret, and computes _lastHash from the raw backend payload.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Frontend Sync Orchestrator
  participant Adapter as BackendAdapter (sync methods)
  participant Retry as fetchWithRetry
  participant Server as Bulk Sync Handler (/api/configs/*/bulk)
  participant Encrypt as ServerEncryption
  participant Parser as Response Parser

  Client->>Adapter: Trigger bulk syncs (concurrent)
  Adapter->>Retry: PUT /configs/*/bulk (retry)
  Retry->>Server: Send configs payload
  Server->>Encrypt: Try encrypt per-item secret
  Encrypt-->>Server: Return encrypted secret or throw
  Server->>Server: Fallback to stored encrypted secret or mark skipped
  Server-->>Retry: Respond { synced, skipped, errors } or throw ALL_CONFIGS_SKIPPED
  Retry-->>Adapter: Return response
  Adapter->>Parser: Parse JSON response
  Parser-->>Adapter: Log skipped items / throw on partial failures (per method)
  Adapter-->>Client: Resolve or reject result
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰 I hop through logs by moonlit code,
Counting inserts on the syncer road.
When secrets balk and cannot hide,
I fetch old keys or gently skip aside.
Retries hum softly — then all is stowed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: harden AI/WebDAV config sync to prevent data loss' directly and clearly summarizes the main change: hardening the config sync flow to prevent data loss through better error handling, transaction guards, and retry logic.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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
  • Commit unit tests in branch fix/168-ai-config-sync-failure

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@server/src/routes/configs.ts`:
- Around line 195-199: When errMsg === 'ALL_CONFIGS_SKIPPED' the handler only
returns a numeric count so the frontend loses per-config rejection reasons;
update the 422 response to include the detailed skipped items (use the
already-populated syncResult.skipped variable) along with the existing
count/message so callers get per-config reasons—apply the same change to the
other identical branch (the second 422 block around the 407-411 area). Ensure
you reference errMsg, req.body?.configs, syncResult.skipped and the
res.status(...).json(...) call when making the change.

In `@src/services/backendAdapter.ts`:
- Around line 350-363: The current bulk AI config PUT call (the method in
backendAdapter that uses fetchWithRetry for `/configs/ai/bulk`) treats any
HTTP-ok response as success even when the parsed body reports data.skipped > 0,
which causes callers (autoSync and the settings panel) to clear pending changes
incorrectly; change the behavior so that after parsing res.json() you do not
silently log and return on partial skips — either throw a translated error via
throwTranslatedError (including the skipped count and data.errors) or return the
parsed result object (synced/skipped/errors) to callers so they can surface a
partial-sync state; apply the same fix to the other identical block around the
second occurrence (the similar fetchWithRetry/parse block).
🪄 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: b0cf4aaa-1f5d-478c-a2d9-d0236768c305

📥 Commits

Reviewing files that changed from the base of the PR and between 2889594 and 446e201.

📒 Files selected for processing (3)
  • server/src/routes/configs.ts
  • src/services/backendAdapter.ts
  • src/utils/backendErrors.ts

Comment thread server/src/routes/configs.ts
Comment thread src/services/backendAdapter.ts Outdated
… and throw on partial sync

- Move syncResult declaration before try block so catch handler can
  access skip details in the ALL_CONFIGS_SKIPPED 422 response
- Frontend sync methods now throw on partial failure (skipped > 0)
  instead of silently logging, so autoSync keeps _hasPendingLocalChanges
  and won't overwrite local data with incomplete backend state

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Problem 1: Backend returned 422 on ALL_CONFIGS_SKIPPED, causing frontend
to throw and set _hasPendingLocalChanges=true, which blocked all subsequent
pulls from backend and prevented other syncs (settings, repos) from completing.

Problem 2: Backend decrypt_failed status caused empty apiKey values to
overwrite valid local data, and subsequent push would skip all configs.

Problem 3: handleSyncToBackend used sequential await, so one failure
(like AI config sync) would abort the entire sync operation.

Solutions:
1. Backend returns 200 (not 422) for ALL_CONFIGS_SKIPPED, since the
   transaction rollback preserves existing data correctly.
2. Frontend logs warnings instead of throwing on partial skip.
3. autoSync merges decrypt_failed configs with local values to preserve
   valid apiKey data instead of overwriting with empty strings.
4. handleSyncToBackend uses Promise.allSettled to isolate failures.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AmintaCCCP
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@src/services/autoSync.ts`:
- Around line 167-169: The code stores _lastHash.ai = quickHash(mergedConfigs)
but change detection compares _lastHash.ai against the raw backend payload hash,
causing changed.ai to stay true; make the hash basis consistent by assigning
_lastHash.ai to the same value used in the comparison (i.e., use the backend
payload hash variable instead of quickHash(mergedConfigs), or else change the
comparison to use quickHash(mergedConfigs)). Update the assignment near
state.setAIConfigs(mergedConfigs) so _lastHash.ai is set to the exact hash
computed from the backend payload (the same symbol used where changed.ai is
evaluated), ensuring consistent use of quickHash(...) on the same input across
both assignment and comparison.
🪄 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: 3be8756e-4cd4-434a-8528-679abdb07138

📥 Commits

Reviewing files that changed from the base of the PR and between 9452946 and 036fa3a.

📒 Files selected for processing (4)
  • server/src/routes/configs.ts
  • src/components/settings/BackendPanel.tsx
  • src/services/autoSync.ts
  • src/services/backendAdapter.ts

Comment thread src/services/autoSync.ts
CodeRabbit identified that storing quickHash(mergedConfigs) in _lastHash.ai
would cause a mismatch with the raw backend payload hash used in change
detection, triggering infinite update loops on every poll cycle.

Now storing hashes.ai (raw backend hash) to maintain consistency with the
change detection comparison logic.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/services/autoSync.ts (1)

167-168: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep AI hash basis consistent with change detection.

Line 168 stores a hash of mergedConfigs, but Line 95-Line 99 compares against hash of raw backend payload. This can cause perpetual changed.ai = true and repeated store updates/log spam.

💡 Minimal fix
-      _lastHash.ai = quickHash(mergedConfigs);
+      _lastHash.ai = hashes.ai;
🤖 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/services/autoSync.ts` around lines 167 - 168, The AI hash is being
computed from mergedConfigs while change detection compares the hash of the raw
backend payload, causing mismatches and perpetual changed.ai=true; make the
basis consistent by computing and storing the hash from the same object used for
comparison—either change the comparison to use mergedConfigs or change the
assignment _lastHash.ai = quickHash(mergedConfigs) to use the raw backend
payload variable (the same ai payload used in the earlier hash check), ensuring
you use the same identifier (e.g., aiPayload or fetchedAIConfig) and the same
quickHash(...) call in both places.
🤖 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.

Duplicate comments:
In `@src/services/autoSync.ts`:
- Around line 167-168: The AI hash is being computed from mergedConfigs while
change detection compares the hash of the raw backend payload, causing
mismatches and perpetual changed.ai=true; make the basis consistent by computing
and storing the hash from the same object used for comparison—either change the
comparison to use mergedConfigs or change the assignment _lastHash.ai =
quickHash(mergedConfigs) to use the raw backend payload variable (the same ai
payload used in the earlier hash check), ensuring you use the same identifier
(e.g., aiPayload or fetchedAIConfig) and the same quickHash(...) call in both
places.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99754e34-1d83-41dd-a72e-059f695a626a

📥 Commits

Reviewing files that changed from the base of the PR and between 2889594 and 036fa3a.

📒 Files selected for processing (5)
  • server/src/routes/configs.ts
  • src/components/settings/BackendPanel.tsx
  • src/services/autoSync.ts
  • src/services/backendAdapter.ts
  • src/utils/backendErrors.ts

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

@AmintaCCCP
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/services/backendAdapter.ts (1)

350-358: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

AI partial skips are still treated as success by callers.

When data.skipped > 0, this path only logs and returns fulfilled. Downstream orchestrators (src/services/autoSync.ts:233-297, src/components/settings/BackendPanel.tsx:93-119) treat fulfilled as success, so AI pending state can be cleared (and UI can report success) while configs were skipped. Consider matching syncWebDAVConfigs behavior (reject on partial skip) or returning structured sync results to callers and handling partial state explicitly.

Proposed minimal fix
-    // Parse response to detect partial failures (some configs skipped due to missing keys).
-    // Log warnings but do not throw — the backend has preserved existing data via rollback,
-    // and throwing would block subsequent syncs of other data types (repos, settings).
+    // Parse response and reject on partial failure so callers can surface partial-sync state.
     try {
       const data = await res.json() as { synced?: number; skipped?: number; errors?: Array<{ id: string; name: string; reason: string }> };
       if (data.skipped && data.skipped > 0) {
         const reasons = data.errors?.map(e => `${e.name}: ${e.reason}`).join('; ') ?? '';
-        console.warn(`[sync] ${data.skipped} AI config(s) skipped${reasons ? ` (${reasons})` : ''}`);
+        throw new Error(`Sync AI configs partial failure: ${data.skipped} skipped${reasons ? ` (${reasons})` : ''}`);
       }
-    } catch {
-      // Ignore parse errors (empty body, etc.)
+    } catch (err) {
+      if (err instanceof Error && err.message.startsWith('Sync AI configs partial failure')) throw err;
+      // Ignore parse errors (empty body, etc.)
     }
🤖 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/services/backendAdapter.ts` around lines 350 - 358, The current handler
that parses res.json() treats partial skips (data.skipped > 0) as success by
only logging a warning; update the behavior to mirror syncWebDAVConfigs by
failing the promise when partial skips occur (i.e., throw or return a rejected
Promise with a clear Error containing count and reasons built from data.errors)
so callers in autoSync/BackendPanel see a rejection instead of a fulfilled
result; specifically modify the branch that checks data.skipped (the code
reading const data = await res.json() and the subsequent if (data.skipped &&
data.skipped > 0) block) to construct a descriptive error message (include
`${data.skipped}` and the joined reasons) and throw it rather than only
console.warn, or alternatively return a structured result object indicating
partialSkip=true so callers can handle it explicitly.
🤖 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 `@server/src/routes/configs.ts`:
- Around line 195-199: The branch that checks errMsg === 'ALL_CONFIGS_SKIPPED'
should return a non-2xx response so downstream mapping can emit the proper
event: replace the res.json(...) call with a 422 response and include the error
code in the body (e.g. res.status(422).json({ code: 'ALL_CONFIGS_SKIPPED',
synced: 0, skipped: syncResult.skipped.length, errors: syncResult.skipped })).
Make the same change in the other identical branch that handles WebDAV sync (the
branch currently returning 200 for ALL_CONFIGS_SKIPPED) so both
SYNC_AI_CONFIGS_ALL_SKIPPED / SYNC_WEBDAV_CONFIGS_ALL_SKIPPED can be emitted.

---

Duplicate comments:
In `@src/services/backendAdapter.ts`:
- Around line 350-358: The current handler that parses res.json() treats partial
skips (data.skipped > 0) as success by only logging a warning; update the
behavior to mirror syncWebDAVConfigs by failing the promise when partial skips
occur (i.e., throw or return a rejected Promise with a clear Error containing
count and reasons built from data.errors) so callers in autoSync/BackendPanel
see a rejection instead of a fulfilled result; specifically modify the branch
that checks data.skipped (the code reading const data = await res.json() and the
subsequent if (data.skipped && data.skipped > 0) block) to construct a
descriptive error message (include `${data.skipped}` and the joined reasons) and
throw it rather than only console.warn, or alternatively return a structured
result object indicating partialSkip=true so callers can handle it explicitly.
🪄 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: 9f824ae0-6f06-4660-91e7-3069ec50f3e4

📥 Commits

Reviewing files that changed from the base of the PR and between 2889594 and 299de58.

📒 Files selected for processing (5)
  • server/src/routes/configs.ts
  • src/components/settings/BackendPanel.tsx
  • src/services/autoSync.ts
  • src/services/backendAdapter.ts
  • src/utils/backendErrors.ts

Comment thread server/src/routes/configs.ts Outdated
Backend now returns HTTP 422 (not 200) when all configs are skipped,
so the error codes SYNC_AI_CONFIGS_ALL_SKIPPED and
SYNC_WEBDAV_CONFIGS_ALL_SKIPPED reach the frontend error handling.

Promise.allSettled keeps the rest of the sync run from being blocked.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

AmintaCCCP and others added 2 commits May 28, 2026 20:03
Align syncAIConfigs behavior with syncWebDAVConfigs - both now throw
errors when configs are skipped, ensuring callers (autoSync, BackendPanel)
properly track pending changes instead of clearing them on partial success.

This addresses CodeRabbit's consistency concern about partial skip handling.
1. WebDAV decrypt_failed protection: autoSync now merges decrypt_failed
   WebDAV configs with local password values, same as AI configs.
   Prevents backend decryption failures from overwriting valid local data.

2. Align syncAIConfigs/syncWebDAVConfigs: added pre-sync validation
   warnings to syncAIConfigs for empty apiKey configs, matching the
   pattern already in syncWebDAVConfigs.

3. BackendPanel handleSyncToBackend: now passes all 7 settings fields
   (activeAIConfig, activeWebDAVConfig, categoryOrder, customCategories,
   assetFilters, collapsedSidebarCategoryCount) instead of only
   hiddenDefaultCategoryIds. This prevents category data loss when
   manually syncing to backend — the root cause of the user's
   "分类数据丢失" complaint in Issue #166 comments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AmintaCCCP
Copy link
Copy Markdown
Owner Author

@coderabbitai review the latest commit (a913d1c) which fixes remaining issues: WebDAV decrypt_failed protection, syncAIConfigs pre-sync warnings alignment, and BackendPanel settings completeness.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the diff for commit a913d1c
gh api repos/AmintaCCCP/GithubStarsManager/commits/a913d1c --jq '.files[] | "=== \(.filename) ===\n\(.patch // "(no patch)")'

Length of output: 609


🏁 Script executed:

#!/bin/bash
gh api repos/AmintaCCCP/GithubStarsManager/commits/a913d1c > /tmp/commit.json
jq -r '.files[] | "=== " + .filename + " ===\n" + (.patch // "(no patch)")' /tmp/commit.json

Length of output: 3156


🏁 Script executed:

#!/bin/bash
echo "=== syncSettings definition ==="
rg -n "syncSettings" --type ts -A 5 -B 2

echo ""
echo "=== settings route/endpoint ==="
rg -n "syncSettings\|settings.*bulk\|/configs/settings" --type ts -A 8 -B 2

echo ""
echo "=== store fields for new keys ==="
rg -n "activeAIConfig\|activeWebDAVConfig\|categoryOrder\|customCategories\|assetFilters\|collapsedSidebarCategoryCount" --type ts -B 1 -A 1 | head -80

Length of output: 2396


🏁 Script executed:

#!/bin/bash
echo "=== Full syncSettings call in autoSync.ts ==="
rg -n "syncSettings" src/services/autoSync.ts -A 15

echo ""
echo "=== Full syncSettings call in BackendPanel.tsx ==="
rg -n "syncSettings" src/components/settings/BackendPanel.tsx -A 15

echo ""
echo "=== WebDAV passwordStatus type definition ==="
rg -n "passwordStatus\|decrypt_failed" --type ts -B 1 -A 2 | head -60

echo ""
echo "=== AI apiKeyStatus type definition ==="
rg -n "apiKeyStatus\|decrypt_failed" --type ts -B 1 -A 2 | head -40

Length of output: 2197


🏁 Script executed:

#!/bin/bash
echo "=== WebDAV config type ==="
rg -n "WebDAVConfig\|WebdavConfig" --type ts -A 10 | head -60

echo ""
echo "=== AIConfig type ==="
rg -n "interface AIConfig\|type AIConfig" --type ts -A 10 | head -40

echo ""
echo "=== passwordStatus field ==="
rg -n "passwordStatus" --type ts -B 2 -A 2 | head -40

echo ""
echo "=== apiKeyStatus field ==="
rg -n "apiKeyStatus" --type ts -B 2 -A 2 | head -40

Length of output: 5481


🏁 Script executed:

#!/bin/bash
echo "=== SecretStatus type ==="
rg -n "SecretStatus" --type ts -A 5 -B 1

echo ""
echo "=== fetchWithRetry vs fetchWithTimeout in backendAdapter ==="
rg -n "fetchWithRetry\|fetchWithTimeout" src/services/backendAdapter.ts -B 1 -A 2 | head -60

echo ""
echo "=== syncSettings uses fetchWithTimeout still? ==="
rg -n "fetchWithTimeout\|fetchWithRetry" src/services/backendAdapter.ts | head -30

Length of output: 2496


🏁 Script executed:

#!/bin/bash
echo "=== All fetch method usages in backendAdapter.ts ==="
grep -n "fetchWith\|this\.fetch" src/services/backendAdapter.ts | head -50

echo ""
echo "=== syncSettings full method ==="
sed -n '420,445p' src/services/backendAdapter.ts

echo ""
echo "=== syncAIConfigs full method ==="
grep -n "syncAIConfigs\|syncWebDAVConfigs" src/services/backendAdapter.ts -A 30 | head -80

Length of output: 7304



@AmintaCCCP Reviewed commit a913d1c. Here's the breakdown:


✅ WebDAV decrypt_failed protection (autoSync.ts)

The new merge logic correctly mirrors the existing AI-config pattern. SecretStatus = 'ok' | 'empty' | 'decrypt_failed' confirms 'ok' as const is valid. The guard condition bc.passwordStatus === 'decrypt_failed' || !bc.password covers both explicit decryption failures and missing values, and the raw backend hash is still used for change-detection consistency. Looks solid.


syncAIConfigs pre-sync warning alignment (backendAdapter.ts)

The pre-sync for…of warning loop for empty apiKey is now consistent with the equivalent loop in syncWebDAVConfigs. Both methods also throw for skipped > 0 from the response — this is good symmetric behaviour.


✅ BackendPanel settings completeness (BackendPanel.tsx)

The most impactful fix in this commit. Previously BackendPanel only sent hiddenDefaultCategoryIds to syncSettings, while autoSync.ts was already syncing all 7 fields. The gap is now closed:

Field autoSync.ts (before) BackendPanel.tsx (before) BackendPanel.tsx (after)
activeAIConfig
activeWebDAVConfig
hiddenDefaultCategoryIds
categoryOrder
customCategories
assetFilters
collapsedSidebarCategoryCount

⚠️ Minor nit — inconsistent indentation in BackendPanel.tsx

The syncSettings(...) arguments use 12-space indent while the sibling calls use 8-space:

// Sibling calls (8-space indent):
        backend.syncAIConfigs(aiConfigs),
        backend.syncWebDAVConfigs(webdavConfigs),
        backend.syncSettings({
            activeAIConfig,      // ← 12-space, should be 10-space to match
            ...
          }),

Not blocking, but worth a cleanup pass.


💡 Follow-up observation (out of scope for this commit)

syncSettings (line 426) still uses fetchWithTimeout while the bulk config sync methods now use fetchWithRetry. If settings sync is equally critical to reliability, it could be worth upgrading in a follow-up. Would you like me to open an issue for that?


Overall: The three stated goals are all correctly implemented. The commit is ready to merge from a functional standpoint.

@AmintaCCCP
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/services/backendAdapter.ts (1)

357-367: ⚡ Quick win

Extract shared partial-sync response handling into one helper.

These two blocks are functionally identical; centralizing parse/throw logic will reduce drift risk and keep AI/WebDAV behavior consistent.

Refactor sketch
+  private async throwIfPartialSync(
+    res: Response,
+    label: 'AI configs' | 'WebDAV configs'
+  ): Promise<void> {
+    try {
+      const data = await res.json() as {
+        skipped?: number;
+        errors?: Array<{ id: string; name: string; reason: string }>;
+      };
+      if (data.skipped && data.skipped > 0) {
+        const reasons = data.errors?.map(e => `${e.name}: ${e.reason}`).join('; ') ?? '';
+        throw new Error(`Sync ${label} partial failure: ${data.skipped} skipped${reasons ? ` (${reasons})` : ''}`);
+      }
+    } catch (err) {
+      if (err instanceof Error && err.message.startsWith(`Sync ${label} partial failure`)) throw err;
+    }
+  }
...
-    try {
-      const data = await res.json() as { synced?: number; skipped?: number; errors?: Array<{ id: string; name: string; reason: string }> };
-      if (data.skipped && data.skipped > 0) {
-        const reasons = data.errors?.map(e => `${e.name}: ${e.reason}`).join('; ') ?? '';
-        throw new Error(`Sync AI configs partial failure: ${data.skipped} skipped${reasons ? ` (${reasons})` : ''}`);
-      }
-    } catch (err) {
-      if (err instanceof Error && err.message.startsWith('Sync AI configs partial failure')) throw err;
-    }
+    await this.throwIfPartialSync(res, 'AI configs');
...
-    try {
-      const data = await res.json() as { synced?: number; skipped?: number; errors?: Array<{ id: string; name: string; reason: string }> };
-      if (data.skipped && data.skipped > 0) {
-        const reasons = data.errors?.map(e => `${e.name}: ${e.reason}`).join('; ') ?? '';
-        throw new Error(`Sync WebDAV configs partial failure: ${data.skipped} skipped${reasons ? ` (${reasons})` : ''}`);
-      }
-    } catch (err) {
-      if (err instanceof Error && err.message.startsWith('Sync WebDAV configs partial failure')) throw err;
-    }
+    await this.throwIfPartialSync(res, 'WebDAV configs');

Also applies to: 397-407

🤖 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/services/backendAdapter.ts` around lines 357 - 367, Extract the
duplicated try/catch into a single async helper (e.g.,
parseAndThrowPartialSyncResponse or handlePartialSyncResponse) that accepts the
Response (res), calls await res.json(), checks data.skipped>0, constructs the
reasons string from data.errors, and throws an Error whose message starts with
"Sync AI configs partial failure" so existing instanceof/message checks keep
working; ensure the helper catches JSON parse errors and only re-throws errors
that start with that message. Replace both duplicated blocks (the current one
using res and the similar block later) with a call to this helper to centralize
behavior.
🤖 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 `@server/src/routes/configs.ts`:
- Around line 195-202: The 422 response branch that checks errMsg ===
'ALL_CONFIGS_SKIPPED' currently returns a top-level error string that suggests
missing API keys; change that top-level "error" message to a neutral phrase
(e.g., "No AI configs were synced") so it doesn't misattribute causes—leave
synced/skipped/errors (syncResult.skipped and syncResult.skipped array) intact
so per-config failure details remain in errors; apply the same neutralization to
the similar 412-419 branch and ensure backendErrors translation entries in
backendErrors.ts stay aligned (do not remove per-config details that are already
appended from data.error).

---

Nitpick comments:
In `@src/services/backendAdapter.ts`:
- Around line 357-367: Extract the duplicated try/catch into a single async
helper (e.g., parseAndThrowPartialSyncResponse or handlePartialSyncResponse)
that accepts the Response (res), calls await res.json(), checks data.skipped>0,
constructs the reasons string from data.errors, and throws an Error whose
message starts with "Sync AI configs partial failure" so existing
instanceof/message checks keep working; ensure the helper catches JSON parse
errors and only re-throws errors that start with that message. Replace both
duplicated blocks (the current one using res and the similar block later) with a
call to this helper to centralize behavior.
🪄 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: c8e661bc-c14d-4619-b17b-f58f29ab686c

📥 Commits

Reviewing files that changed from the base of the PR and between 2889594 and a913d1c.

📒 Files selected for processing (5)
  • server/src/routes/configs.ts
  • src/components/settings/BackendPanel.tsx
  • src/services/autoSync.ts
  • src/services/backendAdapter.ts
  • src/utils/backendErrors.ts

Comment thread server/src/routes/configs.ts
CodeRabbit nitpick: the 422 error messages were too specific ('due to
missing API keys'/'due to missing passwords'), which could misdiagnose
the real failure. The per-config reasons are in the errors field, so
the top-level message should be neutral and point users to that field.

Aligned backend error messages, frontend error translations, and the
error code naming convention.
@AmintaCCCP
Copy link
Copy Markdown
Owner Author

@coderabbitai review the latest commit (e20b22f)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the diff for commit e20b22f
gh api repos/AmintaCCCP/GithubStarsManager/commits/e20b22f > /tmp/commit_e20b22f.json
jq -r '.files[] | "=== " + .filename + " ===\n" + (.patch // "(no patch)")' /tmp/commit_e20b22f.json

echo ""
echo "=== Commit message ==="
jq -r '.commit.message' /tmp/commit_e20b22f.json

Length of output: 402


🏁 Script executed:

#!/bin/bash
# List commits on the PR branch
gh api "repos/AmintaCCCP/GithubStarsManager/pulls/168/commits" | jq -r '.[] | .sha + " " + .commit.message' | head -20

echo ""
echo "=== Try with full SHA lookup for prefix e20b22f ==="
gh api "repos/AmintaCCCP/GithubStarsManager/git/refs/heads/fix/168-ai-config-sync-failure" | jq -r '.object.sha'

Length of output: 1613


🏁 Script executed:

#!/bin/bash
# Try to fetch the commit directly with the short SHA
gh api "repos/AmintaCCCP/GithubStarsManager/commits/e20b22f" 2>&1| head -5 || true

# Check the PR's head SHA
gh api "repos/AmintaCCCP/GithubStarsManager/pulls/168" | jq -r '.head.sha, .head.ref, .head.repo.full_name'

echo ""
echo "=== Commits on the PR branch (via compare) ==="
BASE_SHA=$(gh api "repos/AmintaCCCP/GithubStarsManager/pulls/168" | jq -r '.base.sha')
HEAD_SHA=$(gh api "repos/AmintaCCCP/GithubStarsManager/pulls/168" | jq -r '.head.sha')
echo "Base: $BASE_SHA"
echo "Head: $HEAD_SHA"

# Try fetching all commits via the compare endpoint
gh api "repos/AmintaCCCP/GithubStarsManager/compare/${BASE_SHA}...${HEAD_SHA}" | jq -r '.commits[] | .sha + " " + .commit.message[:80]'

Length of output: 2033


🏁 Script executed:

#!/bin/bash
# Fetch the diff using the full SHA
gh api "repos/AmintaCCCP/GithubStarsManager/commits/e20b22f45dc09a1b87aa6123786af63a798ad157" > /tmp/commit_full.json
jq -r '.commit.message' /tmp/commit_full.json
echo ""
echo "=== Files changed ==="
jq -r '.files[] | "=== " + .filename + " ===\n" + (.patch // "(no patch)")' /tmp/commit_full.json

echo ""
echo "=== Stats ==="
jq -r '.files[] | .filename + ": +" + (.additions|tostring) + " -" + (.deletions|tostring)' /tmp/commit_full.json

Length of output: 586


🏁 Script executed:

#!/bin/bash
# Read current state of backendErrors.ts
cat src/utils/backendErrors.ts

echo ""
echo "=== 422 / ALL_CONFIGS_SKIPPED in configs.ts ==="
rg -n "ALL_CONFIGS_SKIPPED\|422\|neutral\|error.*message\|SKIPPED" server/src/routes/configs.ts -A 3 -B 2 | head -80

echo ""
echo "=== Error messages around skipped ==="
rg -n "SKIPPED\|skipped\|all.*config" server/src/routes/configs.ts -A 2 -B 2 | head -60

echo ""
echo "=== backendErrors.ts content ==="
cat src/utils/backendErrors.ts 2>/dev/null || fd "backendErrors" --exec cat {}

Length of output: 10719



@AmintaCCCP Reviewed commit e20b22f ("fix: use neutral error messages in 422 all-skipped responses").


SYNC_AI_CONFIGS_ALL_SKIPPED & SYNC_WEBDAV_CONFIGS_ALL_SKIPPED (backendErrors.ts)

The updated messages are consistent with the rest of the ERROR_MESSAGES map and correctly address the nitpick:

Code EN ZH
SYNC_AI_CONFIGS_ALL_SKIPPED Failed to sync AI configs: all configs skipped, see errors for details 同步 AI 配置失败:所有配置均被跳过,请查看错误详情
SYNC_WEBDAV_CONFIGS_ALL_SKIPPED Failed to sync WebDAV configs: all configs skipped, see errors for details 同步 WebDAV 配置失败:所有配置均被跳过,请查看错误详情

Observations:

  • ✅ Tone is consistent with sibling entries (SYNC_AI_CONFIGS_FAILED, SYNC_WEBDAV_CONFIGS_FAILED).
  • ✅ The see errors for details directive usefully points the user to the errors field in the 422 response body.
  • ✅ ZH translation is accurate and natural.
  • ✅ No other files are touched — this is a clean, single-concern change.

Overall: This commit cleanly closes the last open nit from the previous review. The PR is now in good shape to merge. 🐇

@AmintaCCCP
Copy link
Copy Markdown
Owner Author

@coderabbitai ignore the nitpick about extracting shared partial-sync response handling — that's a pre-existing pattern not introduced by this PR. All previous actionable comments (422 response details, hash basis consistency, partial skip throw, neutral error messages) have been resolved in commits b083f9a, df23785, a913d1c, and e20b22f.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Tip

For best results, initiate chat on the files or code changes.

@AmintaCCCP Acknowledged — the partial-sync response handling duplication is a pre-existing pattern and I'll treat it as out of scope for this PR going forward.

With all actionable items resolved across commits b083f9a, df23785, a913d1c, and e20b22f, the PR is clean and ready to merge. 🐇


✏️ Learnings added
Learnt from: AmintaCCCP
URL: https://github.com/AmintaCCCP/GithubStarsManager/pull/168

Timestamp: 2026-05-28T13:10:07.966Z
Learning: In the AmintaCCCP/GithubStarsManager repository, the duplicated partial-sync response handling pattern (shared logic between AI and WebDAV bulk sync routes in server/src/routes/configs.ts) is a pre-existing pattern and should NOT be flagged as a refactoring nitpick in PRs that don't specifically introduce it.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

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