From b0d7224b58e3c6b5ec77d0d14445ff8ced886c2e Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Mon, 7 Sep 2026 20:15:54 +0200 Subject: [PATCH] fix(schedules): re-read the editor's identity after each settle, and say when a toggle is refused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The identity check was made before `settleDraftAfterWrite`, which awaits a request of its own: a drawer moved on during that one still had the previous schedule's completion read its form and close it. It is re-read after the settle now, on both write paths. And a toggle refused because a write is already in flight returned nothing, so the switch — already flipped by the click — was left reading disabled on a schedule that is still running. `TriggerModeToggle` resyncs from a `false` return, which is what a refusal is. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 --- .../schedules/ScheduleEditorInner.svelte | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte index fa8f237947..6f4b3f828e 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte @@ -746,14 +746,17 @@ ) // Read after the settle, not before: that awaits a request of its own with the // form still editable, and an edit made in the meantime is one the host must - // not remount over — which this is the only thing that can tell it. + // not remount over — which this is the only thing that can tell it. That + // request is also long enough for the drawer to have moved on, so the + // identity is re-read rather than carried over from before it. + const stillHereNow = initialPath === previousPath && wsId === previousWs onUpdate?.( scheduleCfg.path, previousPath, previousWs, - stillHere && !draftValuesEqual(getScheduleCfg(), scheduleCfg) + stillHereNow && !draftValuesEqual(getScheduleCfg(), scheduleCfg) ) - if (stillHere) drawer?.closeDrawer() + if (stillHereNow) drawer?.closeDrawer() } } finally { deploymentLoading = false @@ -851,14 +854,16 @@ } } - async function handleToggleEnabled(nEnabled: boolean) { + async function handleToggleEnabled(nEnabled: boolean): Promise { const previousEnabled = enabled const path = initialPath const ws = wsId // A write this cell is already settling — this editor's or another's over the // same schedule. Before anything is mutated, so a refused toggle leaves the // switch where it was. - if (isDraftSaving('trigger_schedule', path, { workspace: ws })) return + // `false`, not a bare return: the switch has already flipped itself, and that is + // what tells it to snap back (see TriggerModeToggle). + if (isDraftSaving('trigger_schedule', path, { workspace: ws })) return false // The baseline of the schedule being toggled, read before the request: this // editor is reused for whatever the drawer opens next, and `initialConfig` is // then that one's. @@ -887,7 +892,7 @@ const stillHere = initialPath === path && wsId === ws if (!ok) { if (stillHere) enabled = previousEnabled - return + return false } sendUserToast(`${nEnabled ? 'enabled' : 'disabled'} schedule ${path}`) if (stillHere) { @@ -913,8 +918,15 @@ ) // This request carried the enabled flag alone, so anything else the form has // diverged into is the user's — read after the settle, which awaits a request - // of its own with the form still editable. - onUpdate?.(path, path, ws, stillHere && !draftValuesEqual(getScheduleCfg(), initialConfig)) + // of its own with the form still editable, and re-read because the drawer can + // have moved on during that one too. + const stillHereNow = initialPath === path && wsId === ws + onUpdate?.( + path, + path, + ws, + stillHereNow && !draftValuesEqual(getScheduleCfg(), initialConfig) + ) } } finally { closeSettleWindow?.()