Fix sync: consistent lastEdited timestamp + persistent error + retry button

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 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-05-28 14:59:29 -04:00
parent 1a0ade21a5
commit 81a05bc145
+34 -8
View File
@@ -130,7 +130,9 @@ function loadProjectIndex() {
} }
function saveProjectToStorage(project) { 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)); window.localStorage.setItem(projectKey(updated.id), JSON.stringify(updated));
const index = loadProjectIndex(); const index = loadProjectIndex();
const existing = index.findIndex((e) => e.id === updated.id); const existing = index.findIndex((e) => e.id === updated.id);
@@ -146,6 +148,7 @@ function saveProjectToStorage(project) {
index.push(summary); index.push(summary);
} }
window.localStorage.setItem(INDEX_KEY, JSON.stringify(index)); window.localStorage.setItem(INDEX_KEY, JSON.stringify(index));
return updated; // caller can use this exact object for the server PUT
} }
function deleteProjectFromStorage(id) { function deleteProjectFromStorage(id) {
@@ -592,21 +595,25 @@ const App = () => {
}, [project?.selectedChunkId]); }, [project?.selectedChunkId]);
// Autosave // Autosave
useEffect(() => { useEffect(() => {
if (!project) return; if (!project) return;
if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current); if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current);
saveTimerRef.current = window.setTimeout(async () => { saveTimerRef.current = window.setTimeout(async () => {
// 1. Always save locally first // Stamp lastEdited once so localStorage and server receive the same value
saveProjectToStorage(project); const toSave = { ...project, lastEdited: Date.now() };
// 1. Save locally
saveProjectToStorage(toSave);
setProjectIndex(loadProjectIndex()); setProjectIndex(loadProjectIndex());
setSaveStatus('Saved'); setSaveStatus('Saved');
window.setTimeout(() => setSaveStatus(''), 1400); 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'); setSyncStatus('syncing');
const result = await saveRemoteProject(project); const result = await saveRemoteProject(toSave);
setSyncStatus(result.ok ? 'synced' : 'error'); 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); }, 1000);
return () => { return () => {
if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current); if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current);
@@ -1445,7 +1452,26 @@ const restoreRemoteProject = async (id) => {
)} )}
{syncStatus === 'syncing' && <div className="text-xs text-slate-400">Syncing</div>} {syncStatus === 'syncing' && <div className="text-xs text-slate-400">Syncing</div>}
{syncStatus === 'synced' && <div className="text-xs text-emerald-400">Synced </div>} {syncStatus === 'synced' && <div className="text-xs text-emerald-400">Synced </div>}
{syncStatus === 'error' && <div className="text-xs text-amber-400">Sync failed (saved locally)</div>} {syncStatus === 'error' && (
<div className="flex items-center gap-2">
<span className="text-xs text-rose-400"> Sync failed</span>
<button
type="button"
onClick={async () => {
if (!project) return;
setSyncStatus('syncing');
const toSave = { ...project, lastEdited: Date.now() };
saveProjectToStorage(toSave);
const result = await saveRemoteProject(toSave);
setSyncStatus(result.ok ? 'synced' : 'error');
if (result.ok) window.setTimeout(() => setSyncStatus(''), 2500);
}}
className="text-xs text-slate-300 underline hover:text-white"
>
Retry
</button>
</div>
)}
</div> </div>
</div> </div>
); );