Summary
PR #267 removed the always-visible "Save draft" button, making the debounced autosave the only save path. But saveNow() treats an optimistic-lock 409 exactly like a transient network blip:
// src/iceberg/static/js/tags.js:180-184
const res = await fetch(form.action, { method: 'POST', ... });
if (!res.ok) return false; // <-- 409 indistinguishable from a network error
The server correctly raises 409 on a stale version (src/iceberg/web/reports.py:321-323), but the editor just leaves the header chip on "Unsaved changes" and every subsequent autosave re-posts the same stale version, 409ing forever with no error state and no recovery affordance.
Failure scenario
Notebook access is deliberately role-wide (#65), so two writers in one report is a supported case (also: the same user in two tabs). Writer A saves. Writer B's every keystroke thereafter is never persisted; B sees only a small amber "Unsaved changes" and eventually navigates away — all of B's work is lost, with no conflict message, no reload prompt, nothing. Before this PR, clicking "Save draft" did a full-form POST and the app-level 409 handler (main.py:186-189) surfaced the conflict; that escape hatch is now gone.
Suggested fix
Inspect res.status === 409 in saveNow() and drive the save-state chip to a distinct conflict state, ideally with a reload/merge affordance. Add a test exercising the 409 surfacing.
Found in Fable review of PR #232 → HEAD.
Summary
PR #267 removed the always-visible "Save draft" button, making the debounced autosave the only save path. But
saveNow()treats an optimistic-lock 409 exactly like a transient network blip:The server correctly raises 409 on a stale
version(src/iceberg/web/reports.py:321-323), but the editor just leaves the header chip on "Unsaved changes" and every subsequent autosave re-posts the same staleversion, 409ing forever with no error state and no recovery affordance.Failure scenario
Notebook access is deliberately role-wide (#65), so two writers in one report is a supported case (also: the same user in two tabs). Writer A saves. Writer B's every keystroke thereafter is never persisted; B sees only a small amber "Unsaved changes" and eventually navigates away — all of B's work is lost, with no conflict message, no reload prompt, nothing. Before this PR, clicking "Save draft" did a full-form POST and the app-level 409 handler (
main.py:186-189) surfaced the conflict; that escape hatch is now gone.Suggested fix
Inspect
res.status === 409insaveNow()and drive the save-state chip to a distinct conflict state, ideally with a reload/merge affordance. Add a test exercising the 409 surfacing.Found in Fable review of PR #232 → HEAD.