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
This commit is contained in:
Classic298 2026-09-30 17:43:41 +02:00 • committed by GitHub
parent 32e532b459
commit 909a2075d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 26 additions and 2 deletions

View file

@ -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,

View file

@ -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

View file

@ -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.