From 1b4e686b2f5de3138df0c3b6ed627ea0fe0a2f7d Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 9 Oct 2026 14:03:54 +0200 Subject: [PATCH] fix: load schedules once per page load, poll critical alerts every 30s (#11639) * fix: load schedules once per page load and poll critical alerts every 30s Co-Authored-By: Claude Opus 5.5 * fix: list schedules read-only when the user lookup fails Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .../components/schedules/SchedulesList.svelte | 30 +++++++++++-------- .../sidebar/CriticalAlertModal.svelte | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/frontend/src/lib/components/schedules/SchedulesList.svelte b/frontend/src/lib/components/schedules/SchedulesList.svelte index c2b81434a7..1556c2770a 100644 --- a/frontend/src/lib/components/schedules/SchedulesList.svelte +++ b/frontend/src/lib/components/schedules/SchedulesList.svelte @@ -56,7 +56,7 @@ import JobPreview from '$lib/components/jobs/JobPreview.svelte' import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' - import { untrack } from 'svelte' + import { onDestroy, untrack } from 'svelte' import { page } from '$app/stores' import { useHostedPage } from '$lib/components/hostedPage' import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' @@ -98,7 +98,15 @@ deployUiSettings = settings.deploy_ui ?? ALL_DEPLOYABLE } getDeployUiSettings() + // Bumped by every load and on destroy: a load that resumes from an await under an older + // value was superseded or outlived the page, and must not fetch or write anything more. + let loadGeneration = 0 + onDestroy(() => { + loadGeneration++ + }) + async function loadSchedules(): Promise { + const generation = ++loadGeneration const currentFilters = filters.val // Build API parameters from filters. @@ -139,6 +147,7 @@ const result = (await ScheduleService.listSchedules(apiParams)).map((x) => { return { canWrite: canWrite(x.path, x.extra_perms!, operatingUser.current), ...x } }) + if (generation !== loadGeneration) return // Extract unique values for autocomplete allPaths = Array.from(new Set(result.map((x) => x.path))).sort() @@ -152,23 +161,27 @@ // after the schedule core data has been loaded, load all the job stats // TODO: we could potentially not reload the job stats on every call to loadSchedules, but for now it's // simpler to always call it. Update if performance becomes an issue. - loadSchedulesWithJobStats() + loadSchedulesWithJobStats(generation) } - // Reload schedules when filters change + // One effect for every reason to reload (filters, workspace, user), so that mounting + // issues a single load rather than one per reason. Waits for the user lookup to settle, + // not for a user: a failed lookup still lists, read-only. $effect(() => { filters.val - if ($operatingWorkspace) { + operatingUser.current + if ($operatingWorkspace && operatingUser.resolved($operatingWorkspace)) { untrack(() => loadSchedules()) } }) - async function loadSchedulesWithJobStats(): Promise { + async function loadSchedulesWithJobStats(generation: number): Promise { loadingSchedulesWithJobStats = true let schedulesWithJobsByPath = new Map() let schedulesWithJobsList = await ScheduleService.listSchedulesWithJobs({ workspace: $operatingWorkspace! }) + if (generation !== loadGeneration) return schedulesWithJobsList.map((x) => { schedulesWithJobsByPath[x.path] = x }) @@ -222,13 +235,6 @@ } } - $effect(() => { - if ($operatingWorkspace && operatingUser.current) { - untrack(() => { - loadSchedules() - }) - } - }) let scheduleEditor: ScheduleEditor | undefined = $state() // Deep link: # opens that schedule's edit drawer. Tracks the last diff --git a/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte b/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte index 4a41c65a31..6b6092f6a2 100644 --- a/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte +++ b/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte @@ -71,7 +71,7 @@ await updateHasUnacknowledgedCriticalAlerts(false) checkForNewAlertsInterval = setInterval(() => { updateHasUnacknowledgedCriticalAlerts(true) - }, 15000) + }, 30000) }) onDestroy(() => {