From c8618743c8d20891c802145983f77ea0c6f5b57e Mon Sep 17 00:00:00 2001 From: ARUNAVO RAY Date: Wed, 16 Sep 2026 07:39:22 +0530 Subject: [PATCH] fix(settings): show the schedule that actually runs on the automation card (#435) A fresh install started with scheduling enabled on a plain 24 hour interval counted from first login. The automation card only knows clock schedules, so it showed the 22:00 placeholder as if it were saved while the scheduler ran at whatever time the user first signed in. Toggling the card off and on happened to store the clock schedule and hide the mismatch (#427). The card also showed the browser timezone whenever UTC was stored, so a stored 22:00 UTC read as 22:00 local. New configs start with scheduling off unless SCHEDULE_ENABLED or an interval variable turns it on, with the same rule as the environment loader, and the built-in default is the 22:00 clock schedule the card shows. When a plain interval is stored (SCHEDULE_INTERVAL, GITEA_MIRROR_INTERVAL or an older version) the card says what runs and how it got there, and leaves the frequency and start time unset until the user picks one. The timezone chip shows the stored timezone and turns into a one-click switch to the browser timezone when they differ. --- src/components/config/AutomationSettings.tsx | 89 ++++++++++++++++---- src/lib/utils/config-defaults.ts | 14 ++- src/lib/utils/config-mapper.test.ts | 17 ++++ 3 files changed, 100 insertions(+), 20 deletions(-) diff --git a/src/components/config/AutomationSettings.tsx b/src/components/config/AutomationSettings.tsx index 4170888..6ea56f3 100644 --- a/src/components/config/AutomationSettings.tsx +++ b/src/components/config/AutomationSettings.tsx @@ -68,6 +68,24 @@ const retentionPeriods = [ { label: "3 months", value: 7776000 }, ]; +/** "every 8 hours", "every 30 minutes", "every 2 days" for a plain interval. */ +function formatIntervalSeconds(interval: number | string | undefined): string { + const seconds = typeof interval === "number" ? interval : Number(interval); + if (!Number.isFinite(seconds) || seconds <= 0) return "on a fixed interval"; + const units: [number, string][] = [ + [86400, "day"], + [3600, "hour"], + [60, "minute"], + ]; + for (const [size, name] of units) { + if (seconds % size === 0 || seconds >= size * 2) { + const count = Math.round(seconds / size); + return `every ${count === 1 ? name : `${count} ${name}s`}`; + } + } + return `every ${seconds} seconds`; +} + function getCleanupInterval(retentionSeconds: number): number { const days = retentionSeconds / 86400; if (days <= 1) return 21600; // 6 hours @@ -126,16 +144,22 @@ export function AutomationSettings({ ? Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" : "UTC"; - // Use saved timezone, but treat "UTC" as unset for users who never chose - // it: older versions stored UTC as a default without asking. Anyone truly - // in UTC gets the same result via their browser timezone. - const effectiveTimezone = - scheduleConfig.timezone && scheduleConfig.timezone !== "UTC" - ? scheduleConfig.timezone - : browserTimezone; + // The timezone the scheduler really uses. It used to be shown as the + // browser timezone whenever UTC was stored, which made the card promise a + // local 22:00 while the sync ran at 22:00 UTC (#427). The chip below offers + // a one-click switch to the browser timezone when the two differ. + const effectiveTimezone = scheduleConfig.timezone || browserTimezone; + const timezoneDiffers = effectiveTimezone !== browserTimezone; + + // The stored schedule is a plain interval (seconds or a duration such as + // 8h) rather than a clock schedule. That is what SCHEDULE_INTERVAL and + // GITEA_MIRROR_INTERVAL produce, and what older versions stored. The card + // only edits clock schedules, so say what actually runs instead of showing + // the 22:00 placeholder as if it were saved (#427). + const isIntervalSchedule = scheduleConfig.scheduleMode === "interval"; const nextScheduledRun = useMemo(() => { - if (!scheduleConfig.enabled) return null; + if (!scheduleConfig.enabled || isIntervalSchedule) return null; const startTime = scheduleConfig.startTime || "22:00"; const frequencyHours = scheduleConfig.clockFrequencyHours || 24; const cronExpression = buildClockCronExpression(startTime, frequencyHours); @@ -145,7 +169,16 @@ export function AutomationSettings({ } catch { return null; } - }, [scheduleConfig.enabled, scheduleConfig.startTime, scheduleConfig.clockFrequencyHours, effectiveTimezone]); + }, [scheduleConfig.enabled, isIntervalSchedule, scheduleConfig.startTime, scheduleConfig.clockFrequencyHours, effectiveTimezone]); + + const switchToBrowserTimezone = () => + onScheduleChange({ + ...scheduleConfig, + scheduleMode: "clock", + startTime: scheduleConfig.startTime || "22:00", + clockFrequencyHours: scheduleConfig.clockFrequencyHours || 24, + timezone: browserTimezone, + }); // Update nextRun for cleanup when settings change useEffect(() => { @@ -217,15 +250,39 @@ export function AutomationSettings({ - - {effectiveTimezone} - + timezoneDiffers ? ( + + ) : ( + + + {effectiveTimezone} + + ) } > Schedule + {isIntervalSchedule && ( +

+ + + Syncing {formatIntervalSeconds(scheduleConfig.interval)}, counted from the last run. + {" "}This interval comes from SCHEDULE_INTERVAL or GITEA_MIRROR_INTERVAL, which is applied again on every restart while it is set, or from an older version. + {" "}Pick a frequency or start time to switch to a clock schedule. + +

+ )} +