From 81a05bc145d6f847bc555b381dfe9670225665dc Mon Sep 17 00:00:00 2001 From: nmemmert Date: Thu, 28 May 2026 14:59:29 -0400 Subject: [PATCH] Fix sync: consistent lastEdited timestamp + persistent error + retry button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs fixed: 1. lastEdited mismatch: saveProjectToStorage stamped Date.now() in localStorage but saveRemoteProject sent the React state's stale lastEdited — server and localStorage had different timestamps, so the "Pull latest" stale detection on a second device never fired. Fix: stamp lastEdited once before both calls so they're identical. 2. saveProjectToStorage now returns the stamped object so callers don't have to re-compute the timestamp. 3. Sync errors were auto-clearing after 2.5s making failures invisible. Now the "⚠ Sync failed" badge stays visible until the next successful sync, and a Retry button lets you force a re-push without making an edit. Co-Authored-By: Claude Sonnet 4.6 --- src/App.jsx | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 0f0db53..074c5ab 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -130,7 +130,9 @@ function loadProjectIndex() { } function saveProjectToStorage(project) { - const updated = { ...project, lastEdited: Date.now() }; + // Honour an already-stamped lastEdited (e.g. from autosave) so localStorage + // and the server always receive the identical timestamp. + const updated = { ...project, lastEdited: project.lastEdited ?? Date.now() }; window.localStorage.setItem(projectKey(updated.id), JSON.stringify(updated)); const index = loadProjectIndex(); const existing = index.findIndex((e) => e.id === updated.id); @@ -146,6 +148,7 @@ function saveProjectToStorage(project) { index.push(summary); } window.localStorage.setItem(INDEX_KEY, JSON.stringify(index)); + return updated; // caller can use this exact object for the server PUT } function deleteProjectFromStorage(id) { @@ -592,21 +595,25 @@ const App = () => { }, [project?.selectedChunkId]); // Autosave -useEffect(() => { + useEffect(() => { if (!project) return; if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current); saveTimerRef.current = window.setTimeout(async () => { - // 1. Always save locally first - saveProjectToStorage(project); + // Stamp lastEdited once so localStorage and server receive the same value + const toSave = { ...project, lastEdited: Date.now() }; + + // 1. Save locally + saveProjectToStorage(toSave); setProjectIndex(loadProjectIndex()); setSaveStatus('Saved'); window.setTimeout(() => setSaveStatus(''), 1400); - // 2. Then sync to server (non-blocking — failures are silent) + // 2. Sync to server with the same timestamped object setSyncStatus('syncing'); - const result = await saveRemoteProject(project); + const result = await saveRemoteProject(toSave); setSyncStatus(result.ok ? 'synced' : 'error'); - window.setTimeout(() => setSyncStatus(''), 2500); + // On success clear after 2.5s; on error keep visible until next successful sync + if (result.ok) window.setTimeout(() => setSyncStatus(''), 2500); }, 1000); return () => { if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current); @@ -1445,7 +1452,26 @@ const restoreRemoteProject = async (id) => { )} {syncStatus === 'syncing' &&
Syncing…
} {syncStatus === 'synced' &&
Synced ✓
} - {syncStatus === 'error' &&
Sync failed (saved locally)
} + {syncStatus === 'error' && ( +
+ ⚠ Sync failed + +
+ )} );