From d9a33b0672136c4bd1e2b062f801f6c465e4ab00 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 21 Jul 2026 12:01:57 +0100 Subject: [PATCH] fix(web): stop autosave loop by ignoring volatile server-echo fields --- web/app/workflows/[id]/page.tsx | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/web/app/workflows/[id]/page.tsx b/web/app/workflows/[id]/page.tsx index 32f851e..e3231c7 100644 --- a/web/app/workflows/[id]/page.tsx +++ b/web/app/workflows/[id]/page.tsx @@ -35,6 +35,17 @@ function AdhocBadge() { ); } +// Stable snapshot of only the fields the editor controls. Excludes volatile +// server-echo fields (e.g. updated_at) that would otherwise change on every +// save and cause autosave to loop forever. +function snapshotOf(w: Workflow): string { + return JSON.stringify({ + name: w.name, + target_server_ids: w.target_server_ids, + steps: w.steps, + }); +} + function timeAgo(date: Date): string { const s = Math.floor((Date.now() - date.getTime()) / 1000); if (s < 5) return "just now"; @@ -80,7 +91,7 @@ export default function WorkflowBuilder() { useEffect(() => { if (loaded && !wf) { setWf(loaded); - savedSnapshotRef.current = JSON.stringify(loaded); + savedSnapshotRef.current = snapshotOf(loaded); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [loaded]); @@ -115,7 +126,7 @@ export default function WorkflowBuilder() { // Must stay above the early return below so hook order is stable. useEffect(() => { if (!wf || savedSnapshotRef.current === null) return; - if (JSON.stringify(wf) === savedSnapshotRef.current) return; + if (snapshotOf(wf) === savedSnapshotRef.current) return; const t = setTimeout(() => { save(); }, 800); @@ -147,7 +158,7 @@ export default function WorkflowBuilder() { if (savingRef.current) return; const current = wfRef.current; if (!current) return; - const snapshot = JSON.stringify(current); + const snapshot = snapshotOf(current); if (snapshot === savedSnapshotRef.current) return; savingRef.current = true; setSaving(true); @@ -158,10 +169,10 @@ export default function WorkflowBuilder() { setError("Save failed: server returned an unexpected response."); return; } - if (JSON.stringify(wfRef.current) === snapshot) { + if (wfRef.current && snapshotOf(wfRef.current) === snapshot) { // Nothing changed while the request was in flight: adopt the // server echo as the new saved baseline. - savedSnapshotRef.current = JSON.stringify(updated); + savedSnapshotRef.current = snapshotOf(updated); setWf(updated); } else { // The user edited again mid-flight. Keep their newer state and @@ -177,7 +188,7 @@ export default function WorkflowBuilder() { setSaving(false); // If edits arrived during the save (or a concurrent save was // skipped), persist them on the next tick. - if (wfRef.current && JSON.stringify(wfRef.current) !== savedSnapshotRef.current) { + if (wfRef.current && snapshotOf(wfRef.current) !== savedSnapshotRef.current) { setTimeout(() => save(), 0); } } @@ -638,7 +649,7 @@ export default function WorkflowBuilder() { onSaved={(w) => { // The modal already persisted w; sync the snapshot so // autosave doesn't fire a redundant follow-up save. - savedSnapshotRef.current = JSON.stringify(w); + savedSnapshotRef.current = snapshotOf(w); setWf(w); }} onClose={() => setEditWorkflowOpen(false)}