From 61a4817460cac6b9bb0757cfdf916efef588ce0d Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:41:39 +0300 Subject: [PATCH] fix: tour -- close the save-bypass at its root, not by enumerating callers Round 3's review found the enumeration approach (patchStore()'s blocked list) was still incomplete, and worse, live rather than dormant: keyboard shortcuts (Delete/Backspace/S) bypass the tour's click-blocking backdrop entirely, since CSS pointer-events does nothing to a window-level keydown listener. timeline.js's shortcut handler calls removeSelectedScenes(), removeAudioTrack(), removeSelectedOverlay(), and splitScene() -- none of them in the blocked list -- and every one ends in scheduleSave(), whose setTimeout calls the closure-scoped commit() directly, reaching a real, unmocked PUT /funpack/api/projects/. tour-s1 is pre-selected from the moment the tour boots, so pressing Delete at any point immediately triggers this -- no click needed, no specific step required. The blocked-list pattern (three rounds of fixes: parsed.scenes crash, syncFromPreview, saveClipToMediaBin) was closing this one caller at a time and kept finding another. Fixed at the root instead: every mutating path in store.js funnels through the single closure-scoped commit() function before it can reach the network, so that's the one place a tour-mode guard covers every current AND future caller by construction, without needing tour.js to know the name of anything that might call it. Verified live: dispatched a real Delete keydown during the tour with tour-s1 selected (the default boot state) -- the scene still gets removed locally (harmless, it's fake demo data), but zero requests to /api/projects appear in the network log across the full debounce window after the fix, versus a real PUT before it. Confirmed the guard is a no-op outside tour mode (window.__FUNPACK_TOUR__ is false there) and the full 16-step tour sequence still completes with zero thrown errors. Co-Authored-By: Claude Sonnet 5 --- app/legacy/store.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/legacy/store.js b/app/legacy/store.js index 234f95bb..f02cb2f6 100644 --- a/app/legacy/store.js +++ b/app/legacy/store.js @@ -969,6 +969,17 @@ } async function commit(opts = {}) { + // Every mutating path in this module funnels through here before it can + // reach a real network write -- scheduleSave()'s own setTimeout calls + // this closure-scoped function directly, so patching Store.commit (or + // any individual caller) from outside cannot stop it. The tour's own + // welcome copy promises "nothing here touches your real files," and + // enumerating every caller in tour.js's blocklist misses ones reachable + // outside a click (keyboard shortcuts bypass the tour's click-blocking + // backdrop entirely, since CSS pointer-events does nothing to keydown + // listeners) -- so the guard belongs here, once, covering every path by + // construction instead of by an ever-growing enumeration. + if (window.__FUNPACK_TOUR__) return; if (!state.project) return; if (_commitPromise) { _commitQueued = true; return _commitPromise; } const skipPreviewRefresh = !!opts.skipPreviewRefresh;