fix(schedules): take the deployed enabled state from the deployed config alone

`loadScheduleCfg` applies the draft overlay as well as the deployed config, so
recording the accepted `enabled` there let a draft's own value pass for one the
server had taken: saving then settled the form back to it, and a schedule the
user had switched on came out off, clean, with nothing said. It is read where
the deployed config is the one on the form, and reset for a schedule that has
none yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
This commit is contained in:
Diego Imbert
2026-09-06 15:47:24 +02:00
co-authored by Claude Opus 5
parent 45c53a37da
commit 2583569150
3 changed files with 16 additions and 9 deletions
@@ -32,8 +32,7 @@
disableChatOffset?: boolean
/** A version was restored, at this path and in the workspace the history was
* pointed at — which `WsSpecificVersions` can make a linked one rather than the
* acting workspace. Both reported rather than left for the caller to read off
* its own live state, which the restore's await has already outlived. */
* acting workspace. */
onRestored?: (workspace: string, path: string) => void
/** Fires after Save has written, with the path it wrote to — which is not the
* one it was opened on when the user renamed it — and the workspace and path
@@ -198,8 +198,11 @@
draftOnly = noDeployed
if (!defaultCfg) {
// Form holds DEPLOYED here; capture it as `initialConfig` so the
// dirty check / banner fires whenever a saved draft exists.
// dirty check / banner fires whenever a saved draft exists. This is also
// the only point where the deployed `enabled` is on the form — the draft
// overlay below can carry one of its own, which the server has not taken.
initialConfig = structuredClone($state.snapshot(getScheduleCfg()))
deployedEnabled = enabled
}
if (draftOverlay) await loadScheduleCfg(draftOverlay)
await draftSync.maybeRestore()
@@ -353,6 +356,8 @@
runnable = undefined
edit = false
draftOnly = false
// Nothing deployed yet, so nothing accepted.
deployedEnabled = false
// No deployed baseline for a brand-new schedule. The editor instance
// is reused across open() calls, so clear any baseline left by a prior
// openEdit — otherwise the "unsaved changes" banner / dirty check would
@@ -549,7 +554,6 @@
initialCronVersion = cronVersion
isLatestCron = cronVersion == 'v2'
enabled = cfg.enabled
deployedEnabled = cfg.enabled
schedule = cfg.schedule
initialSchedule = schedule
timezone = cfg.timezone
@@ -572,8 +572,12 @@
const tabHosts: Record<string, PreviewTabHost | undefined> = {}
let reloadHandle: ReturnType<typeof setTimeout> | undefined
// Base-stripped list-page paths (e.g. `/schedules`) a chat round touched since
// the last flush — see toolReloadEffect for how tools map to pages.
/** A list page in the workspace it was touched in — see `queuePageReload`. */
function pageReloadKey(workspace: string, page: string): string {
return `${workspace}\u0000${page}`
}
// Pages touched since the last flush, by whatever touched them: a chat round
// (see toolReloadEffect), a hosted editor's write, a draft landing after one.
let pendingPages = new Set<string>()
// The same round's mutations, kept whole rather than folded into one verdict:
// a hosted entity editor is only affected by a mutation to its own item, in
@@ -590,7 +594,7 @@
const workspace = getEffectiveWorkspaceId(s)
// The queue names the workspace each page was touched in; this session only
// cares about the ones touched in its own.
const prefix = `${workspace}\u0000`
const prefix = pageReloadKey(workspace ?? '', '')
const mine = new Set(
[...pages].filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length))
)
@@ -668,7 +672,7 @@
// frame its scroll and everything else it holds.
function queuePageReload(workspace: string | undefined, page: string) {
if (!workspace) return
pendingPages.add(`${workspace}\u0000${page}`)
pendingPages.add(pageReloadKey(workspace, page))
clearTimeout(reloadHandle)
reloadHandle = setTimeout(flushReload, 500)
}
@@ -695,7 +699,7 @@
setToolCompletionListener((name, args, workspace) => {
const { pages, entity, path } = toolReloadEffect(name, args)
if (pages.length === 0) return
for (const p of pages) pendingPages.add(`${workspace}\u0000${p}`)
for (const p of pages) pendingPages.add(pageReloadKey(workspace, p))
// A write reaches a hosted editor through the draft cell it holds, but an
// editor still loading holds none yet and the seed no-opped past it. The
// miss is recorded when it happens — asking now would be too late, since