From 92ed153bc02c664b583ff0aad835c5d0482c90e5 Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sat, 11 Jul 2026 18:32:15 +0100 Subject: [PATCH] fix(sync): surface attachment/folder restore failures (audit 2.3) applyRemoteSnapshot swallowed attachment + folder restore errors in silent catch blocks. Count them (attFailed/folderFailed) and warn in a toast after the sync summary. These don't abort the push (the entry synced, only its attachment/folder didn't) unlike a failed entry import. Co-Authored-By: Claude Opus 4.8 --- js/app.sync.js | 20 +++++++++++++++----- js/tests/merge.test.js | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/js/app.sync.js b/js/app.sync.js index a935d2b..6dda880 100644 --- a/js/app.sync.js +++ b/js/app.sync.js @@ -273,8 +273,8 @@ async function buildSyncSnapshot() { } async function applyRemoteSnapshot(remote) { - if (!remote || !Array.isArray(remote.entries)) return { added:0, updated:0, deleted:0, failed:0 }; - let added = 0, updated = 0, deleted = 0, failed = 0; + if (!remote || !Array.isArray(remote.entries)) return { added:0, updated:0, deleted:0, failed:0, attFailed:0, folderFailed:0 }; + let added = 0, updated = 0, deleted = 0, failed = 0, attFailed = 0, folderFailed = 0; // Ensure state.entries reflects the live DB before we read updated_at // for the resurrection arbitration below — a restore-then-sync must @@ -356,7 +356,7 @@ async function applyRemoteSnapshot(remote) { name: f.name, color: f.color || '', icon: f.icon || '', }), }); - } catch (_) {} + } catch (_) { folderFailed++; } } await loadFolders(); } @@ -410,7 +410,7 @@ async function applyRemoteSnapshot(remote) { size_bytes: a.size_bytes || bytes.length, }), }); - } catch (_) {} + } catch (_) { attFailed++; } } } } catch (_) { failed++; } @@ -432,7 +432,7 @@ async function applyRemoteSnapshot(remote) { } catch (_) { failed++; } } } - return { added, updated, deleted, failed }; + return { added, updated, deleted, failed, attFailed, folderFailed }; } // Update the inline sync-status label next to the button. Empty string @@ -623,5 +623,15 @@ async function runSyncNow(_attempt) { 'pushed ' + pushedCount + ' ' + (pushedCount === 1 ? 'entry' : 'entries'); toast('Sync complete — ' + summary); + + // Surface additive-restore gaps that used to be swallowed silently + // (§2.3). These don't risk overwriting the remote — the entry synced, + // only its attachment/folder didn't come down — so they warn, not abort. + if (merged.attFailed || merged.folderFailed) { + const parts = []; + if (merged.attFailed) parts.push(merged.attFailed + ' attachment(s)'); + if (merged.folderFailed) parts.push(merged.folderFailed + ' folder(s)'); + toast(parts.join(' · ') + ' failed to sync — retry sync', 'warning'); + } } diff --git a/js/tests/merge.test.js b/js/tests/merge.test.js index a5b37c6..9e98270 100644 --- a/js/tests/merge.test.js +++ b/js/tests/merge.test.js @@ -268,7 +268,7 @@ test('merge: empty/invalid remote snapshot is a no-op', async () => { const { T, db } = await freshMerge(); db.seedEntry({ uuid: 'u1' }); const res = await T.applyRemoteSnapshot(null); - assert.deepEqual({ ...res }, { added: 0, updated: 0, deleted: 0, failed: 0 }); + assert.deepEqual({ ...res }, { added: 0, updated: 0, deleted: 0, failed: 0, attFailed: 0, folderFailed: 0 }); assert.equal(db.entries.length, 1); });