From db791f04930190c19190ac80cdb531eb6320d6ee Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Sun, 13 Sep 2026 06:09:01 +0200 Subject: [PATCH] fix: enable a draft-only schedule the way deploying it will Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ViTUkt4czrvZdxdWwxqRAQ --- .../schedules/ScheduleEditorInner.svelte | 6 +++++- .../triggers/schedules/scheduleCfg.test.ts | 20 ++++++++++++++++++- .../triggers/schedules/scheduleCfg.ts | 10 ++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte index 59f3d2adcf..87865767d5 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte @@ -55,6 +55,7 @@ import { onUserInput } from '$lib/userDraftEditGate' import { isTemporaryPath, newItemPath, useItem, type ItemAdapter } from '$lib/itemStore.svelte' import { + draftOnlyScheduleCfg, newScheduleCfg, normalizeScheduleCfg, scheduleCfgOf, @@ -192,9 +193,12 @@ const s = await ScheduleService.getSchedule({ workspace, path, getDraft: true }) // Drafts are saved in the form's config shape, over the deployed fields. const { draft, draft_saved_at, no_deployed, ...deployedSchedule } = s as any + const loadedDraft = draft + ? normalizeScheduleCfg({ ...deployedSchedule, ...draft }) + : undefined return { deployed: no_deployed ? undefined : normalizeScheduleCfg(deployedSchedule), - draft: draft ? normalizeScheduleCfg({ ...deployedSchedule, ...draft }) : undefined, + draft: no_deployed && loadedDraft ? draftOnlyScheduleCfg(loadedDraft) : loadedDraft, draftSavedAt: draft_saved_at } } catch (err) { diff --git a/frontend/src/lib/components/triggers/schedules/scheduleCfg.test.ts b/frontend/src/lib/components/triggers/schedules/scheduleCfg.test.ts index 2c125e1293..36ce2cefaf 100644 --- a/frontend/src/lib/components/triggers/schedules/scheduleCfg.test.ts +++ b/frontend/src/lib/components/triggers/schedules/scheduleCfg.test.ts @@ -1,5 +1,10 @@ import { describe, it, expect } from 'vitest' -import { normalizeScheduleCfg, scheduleCfgOf, scheduleFormOf } from './scheduleCfg' +import { + draftOnlyScheduleCfg, + normalizeScheduleCfg, + scheduleCfgOf, + scheduleFormOf +} from './scheduleCfg' // A deployed schedule as the API returns it: server fields the form never shows, a short cron. const deployed = { @@ -37,4 +42,17 @@ describe('schedule config normalization', () => { }) expect(once).not.toHaveProperty('edited_by') }) + + // Deploying a draft-only schedule creates it, and a create always enables it. A draft that + // kept its stored state would show a disabled schedule the server has enabled, and later + // updates omit the field, so nothing would ever correct it. + it('records a draft-only schedule as enabled, however it was stored', () => { + expect( + draftOnlyScheduleCfg(normalizeScheduleCfg({ ...deployed, enabled: false })) + ).toMatchObject({ enabled: true }) + const { enabled: _dropped, ...withoutEnabled } = deployed + expect(draftOnlyScheduleCfg(normalizeScheduleCfg(withoutEnabled))).toMatchObject({ + enabled: true + }) + }) }) diff --git a/frontend/src/lib/components/triggers/schedules/scheduleCfg.ts b/frontend/src/lib/components/triggers/schedules/scheduleCfg.ts index b55950b993..6645ac7295 100644 --- a/frontend/src/lib/components/triggers/schedules/scheduleCfg.ts +++ b/frontend/src/lib/components/triggers/schedules/scheduleCfg.ts @@ -173,6 +173,16 @@ export function normalizeScheduleCfg(raw: ScheduleCfg): ScheduleCfg { return scheduleCfgOf(scheduleFormOf(raw)) } +/** + * A schedule that exists only as a draft, as deploying it will leave it: deploying one creates + * it, and a create always enables it (`writeScheduleCfg`), so the draft has to carry the state + * that create produces. Left as stored, a disabled draft would record `enabled: false` against + * a server that says otherwise, and later updates omit the field, so nothing corrects it. + */ +export function draftOnlyScheduleCfg(cfg: ScheduleCfg): ScheduleCfg { + return { ...cfg, enabled: true } +} + /** The handlers a new schedule starts with, from the workspace's defaults. */ async function workspaceDefaultHandlers(workspace: string): Promise { const [error, recovery, success] = (await Promise.all(