fix: atomic counters, plan limit re-check, and double-submit guard

Backend:
- Tree usage_count: use SQL-level UPDATE (Tree.usage_count + 1) instead
  of Python-level increment to prevent lost updates under concurrency
- Tag usage_count: same SQL-level atomic increment/decrement in both
  create_tree and update_tree (delete_tree already used this pattern)
- Plan tree limit: re-check count after db.flush() to close the TOCTOU
  window where two concurrent creates could both pass the pre-check

Frontend:
- TreeEditorPage: add isSaving early-return guard inside handleSaveDraft
  and handlePublish callbacks so Ctrl+S can't bypass the button disabled
  prop and fire duplicate save requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-09 17:27:09 -04:00
parent c724ad8062
commit 7e3b383a65
3 changed files with 46 additions and 11 deletions

View File

@@ -330,6 +330,7 @@ export function TreeEditorPage() {
}, [updateNode, selectNode])
const handleSaveDraft = useCallback(async () => {
if (isSaving) return
setSaving(true)
try {
// In Code Mode, run fresh validation on current markdown before saving
@@ -388,9 +389,10 @@ export function TreeEditorPage() {
} finally {
setSaving(false)
}
}, [isEditMode, id, editorMode, getTreeForSave, markSaved, navigate])
}, [isSaving, isEditMode, id, editorMode, getTreeForSave, markSaved, navigate])
const handlePublish = useCallback(async () => {
if (isSaving) return
setSaving(true)
try {
// In Code Mode, run fresh validation on current markdown before publishing
@@ -467,7 +469,7 @@ export function TreeEditorPage() {
} finally {
setSaving(false)
}
}, [isEditMode, id, editorMode, validate, getTreeForSave, markSaved, navigate])
}, [isSaving, isEditMode, id, editorMode, validate, getTreeForSave, markSaved, navigate])
// Keep handleSave for backward compatibility (Ctrl+S shortcut)
const handleSave = useCallback(async () => {