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.