Skip to content

Commit d1346bd

Browse files
authored
Fix tasks disappearing on Dashboard click (empty API guard) (#5815)
## Summary - **Root cause**: Rust backend's `get_action_items` Firestore query silently swallowed errors (`break` instead of `return Err`), returning `Ok(vec![])`. The handler's HTTP 500 fix (commit 35a8d4d) was bypassed because it only caught `Err` results. - When user clicked Dashboard, `refreshTasksIfNeeded()` and `reconcileWithAPIIfNeeded()` ran reconciliation against the empty response, hard-deleting all synced tasks from SQLite. - Same `break` pattern existed in `get_memories_filtered`. ## Changes 1. **Rust backend** (`firestore.rs`): Changed `break` to `return Err(...)` on Firestore query failures in `get_action_items` and `get_memories_filtered`, so errors propagate to handlers and result in HTTP 500 2. **Swift** (`TasksStore.swift`): Added `!response.items.isEmpty` guard to `refreshTasksIfNeeded()` and `allApiIds.isEmpty` guard to `reconcileWithAPIIfNeeded()` — matches existing guard in `forceReconcileOnLoad()` 3. **Swift** (`ActionItemStorage.swift`): Added empty-set guards to `hardDeleteAbsentTasks()` and `markAbsentTasksAsStaged()` as last line of defense ## Test plan - [x] Swift app builds clean - [x] Rust backend compiles clean - [x] Verified affected user (Salman, UID `6HbrDL1WZ4PYqwa6O7N4uWT0nPj1`) has 17 incomplete tasks in Firestore and macOS FCM token - [ ] After release, verify Salman's tasks persist across Dashboard navigation 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents d712eda + 76efa33 commit d1346bd

4 files changed

Lines changed: 26 additions & 6 deletions

File tree

desktop/Backend-Rust/src/services/firestore.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,8 +1072,8 @@ impl FirestoreService {
10721072

10731073
if !response.status().is_success() {
10741074
let error_text = response.text().await?;
1075-
tracing::error!("Firestore query error: {}", error_text);
1076-
break;
1075+
tracing::error!("Firestore query error for memories: {}", error_text);
1076+
return Err(format!("Firestore query error: {}", error_text).into());
10771077
}
10781078

10791079
let results: Vec<Value> = response.json().await?;
@@ -1997,8 +1997,8 @@ impl FirestoreService {
19971997

19981998
if !response.status().is_success() {
19991999
let error_text = response.text().await?;
2000-
tracing::error!("Firestore query error: {}", error_text);
2001-
break;
2000+
tracing::error!("Firestore query error for action_items: {}", error_text);
2001+
return Err(format!("Firestore query error: {}", error_text).into());
20022002
}
20032003

20042004
let results: Vec<Value> = response.json().await?;

desktop/CHANGELOG.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"unreleased": [
33
"Reduced auth error log spam with exponential backoff across all polling operations",
44
"Fixed Tasks page showing misleading \"No Matching Tasks\" when user has no tasks",
5-
"API keys are now served securely from the backend instead of bundled in the app"
5+
"API keys are now served securely from the backend instead of bundled in the app",
6+
"Fixed tasks disappearing when switching to Dashboard (missing safety guard on empty API responses)"
67
],
78
"releases": [
89
{

desktop/Desktop/Sources/Rewind/Core/ActionItemStorage.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,12 @@ actor ActionItemStorage {
610610
/// This cleans up tasks that were moved to staged_tasks or deleted on the backend
611611
/// but still linger in local SQLite, preventing phantom entries in the task list.
612612
func markAbsentTasksAsStaged(apiIds: Set<String>) async throws {
613+
// Safety guard: never wipe all tasks if the API set is empty (backend error)
614+
guard !apiIds.isEmpty else {
615+
log("ActionItemStorage: markAbsentTasksAsStaged skipped — empty API set")
616+
return
617+
}
618+
613619
let db = try await ensureInitialized()
614620

615621
let deleted = try await db.write { database -> Int in
@@ -640,6 +646,12 @@ actor ActionItemStorage {
640646
/// deleting locally-created tasks that haven't been pushed yet.
641647
/// Returns the number of records deleted.
642648
func hardDeleteAbsentTasks(apiIds: Set<String>) async throws -> Int {
649+
// Safety guard: never wipe all tasks if the API set is empty (backend error)
650+
guard !apiIds.isEmpty else {
651+
log("ActionItemStorage: hardDeleteAbsentTasks skipped — empty API set")
652+
return 0
653+
}
654+
643655
let db = try await ensureInitialized()
644656

645657
let deleted = try await db.write { database -> Int in

desktop/Desktop/Sources/Stores/TasksStore.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,8 @@ class TasksStore: ObservableObject {
203203

204204
// Reconcile: if we got the full set, hard-delete local tasks absent from API
205205
// (completed/deleted on mobile). Safe: only deletes synced records.
206-
if response.items.count < reloadLimit {
206+
// Safety guard: skip if API returned zero tasks (possible backend error / empty 200).
207+
if response.items.count < reloadLimit, !response.items.isEmpty {
207208
let apiIds = Set(response.items.map { $0.id })
208209
let reconciled = try await ActionItemStorage.shared.hardDeleteAbsentTasks(apiIds: apiIds)
209210
if reconciled > 0 {
@@ -341,6 +342,12 @@ class TasksStore: ObservableObject {
341342
if response.items.count < batchSize { break }
342343
}
343344

345+
// Safety guard: skip if API returned zero tasks (possible backend error / empty 200).
346+
if allApiIds.isEmpty {
347+
log("TasksStore: Periodic reconciliation skipped — API returned zero task IDs (possible backend error)")
348+
return
349+
}
350+
344351
let deleted = try await ActionItemStorage.shared.hardDeleteAbsentTasks(apiIds: allApiIds)
345352
lastReconciliationDate = Date()
346353

0 commit comments

Comments
 (0)