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 <noreply@anthropic.com>

* fix: list schedules read-only when the user lookup fails

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-10-09 14:03:54 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent fb22e5ce36
commit 1b4e686b2f
2 changed files with 19 additions and 13 deletions
@@ -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<void> {
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<void> {
async function loadSchedulesWithJobStats(generation: number): Promise<void> {
loadingSchedulesWithJobStats = true
let schedulesWithJobsByPath = new Map<string, ScheduleW>()
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: #<path> opens that schedule's edit drawer. Tracks the last
@@ -71,7 +71,7 @@
await updateHasUnacknowledgedCriticalAlerts(false)
checkForNewAlertsInterval = setInterval(() => {
updateHasUnacknowledgedCriticalAlerts(true)
}, 15000)
}, 30000)
})
onDestroy(() => {