From 909a2075d32d15aa1328e611cd6b6b58b6aa0e14 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:43:41 +0200 Subject: [PATCH] fix: Scheduled Tasks calendar shows extra runs for automations with a run count (#31604) An automation whose schedule ends after a fixed number of runs (COUNT in its RRULE) showed extra future runs in the Scheduled Tasks calendar after each run. With COUNT=3, the calendar kept showing three upcoming runs after the first and second run, although only the remaining ones execute. The calendar now counts runs from the schedule's own start date, the same way automations are actually run, so it shows exactly the runs that are still going to happen. The 5000-entry display limit now only counts entries inside the visible range, so very frequent schedules that started shortly before it no longer show up short or empty. Fixes #31600 --- backend/open_webui/routers/calendar.py | 12 +++++++++++- backend/open_webui/utils/calendar.py | 4 +++- backend/open_webui/utils/recurrence.py | 12 ++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/calendar.py b/backend/open_webui/routers/calendar.py index 86d9824803..acd434451d 100644 --- a/backend/open_webui/routers/calendar.py +++ b/backend/open_webui/routers/calendar.py @@ -26,6 +26,7 @@ from open_webui.models.users import UserModel from open_webui.utils.access_control import filter_allowed_access_grants, has_permission from open_webui.utils.auth import get_verified_user from open_webui.utils.calendar import expand_recurring_event +from open_webui.utils.recurrence import schedule_start_ns log = logging.getLogger(__name__) @@ -206,13 +207,22 @@ async def get_events( if not rrule_str: continue + start_at = auto.next_run_at or 0 + upper_rrule = rrule_str.upper() + if 'COUNT=' in upper_rrule and 'DTSTART' in upper_rrule: + # COUNT runs from DTSTART, anchoring on the next run would restart it + try: + start_at = schedule_start_ns(rrule_str, user.timezone) + except ValueError: + pass + virtual = { 'id': f'auto_{auto.id}', 'calendar_id': SCHEDULED_TASKS_CALENDAR_ID, 'user_id': user.id, 'title': auto.name, 'description': auto.data.get('prompt', '') if auto.data else '', - 'start_at': auto.next_run_at or 0, + 'start_at': start_at, 'end_at': None, 'all_day': False, 'rrule': rrule_str, diff --git a/backend/open_webui/utils/calendar.py b/backend/open_webui/utils/calendar.py index 063cbc8e3c..b7815da628 100644 --- a/backend/open_webui/utils/calendar.py +++ b/backend/open_webui/utils/calendar.py @@ -60,7 +60,7 @@ def expand_recurring_event( instances = [] previous_start = None - for occurrence_start in rule.xafter(scan_start, count=max_instances, inc=True): + for occurrence_start in rule.xafter(scan_start, inc=True): if occurrence_start >= range_end or occurrence_start == previous_start: break previous_start = occurrence_start @@ -76,6 +76,8 @@ def expand_recurring_event( 'instance_id': f'{event_dict["id"]}_{instance_start_ns}', } instances.append(instance) + if len(instances) >= max_instances: + break return instances diff --git a/backend/open_webui/utils/recurrence.py b/backend/open_webui/utils/recurrence.py index c4817eca07..717953cc6b 100644 --- a/backend/open_webui/utils/recurrence.py +++ b/backend/open_webui/utils/recurrence.py @@ -148,6 +148,18 @@ async def next_run_ns(s: str, tz: str = None) -> Optional[int]: return int(dt.timestamp() * 1_000_000_000) +def schedule_start_ns(s: str, tz: str = None) -> int: + """DTSTART the scheduler anchors the rule to, as epoch nanoseconds.""" + zi = _resolve_tz(tz) + now = datetime.now(zi).replace(tzinfo=None) if zi else datetime.now() + parsed = _parse_rule(s, now) + rule = parsed._rrule[0] if isinstance(parsed, rruleset) else parsed + dt = rule._dtstart + if zi: + dt = dt.replace(tzinfo=zi) + return int(dt.timestamp() * 1_000_000_000) + + async def next_n_runs_ns(s: str, n: int = 5, tz: str = None) -> list[int]: """Compute next N occurrences for UI preview.