diff --git a/autogpt_platform/backend/backend/executor/scheduler.py b/autogpt_platform/backend/backend/executor/scheduler.py index e476b3ceed..77ed652886 100644 --- a/autogpt_platform/backend/backend/executor/scheduler.py +++ b/autogpt_platform/backend/backend/executor/scheduler.py @@ -191,15 +191,22 @@ class GraphExecutionJobInfo(GraphExecutionJobArgs): id: str name: str next_run_time: str + timezone: str = Field(default="UTC", description="Timezone used for scheduling") @staticmethod def from_db( job_args: GraphExecutionJobArgs, job_obj: JobObj ) -> "GraphExecutionJobInfo": + # Extract timezone from the trigger if it's a CronTrigger + timezone_str = "UTC" + if hasattr(job_obj.trigger, "timezone"): + timezone_str = str(job_obj.trigger.timezone) + return GraphExecutionJobInfo( id=job_obj.id, name=job_obj.name, next_run_time=job_obj.next_run_time.isoformat(), + timezone=timezone_str, **job_args.model_dump(), ) @@ -395,6 +402,7 @@ class Scheduler(AppService): input_data: BlockInput, input_credentials: dict[str, CredentialsMetaInput], name: Optional[str] = None, + user_timezone: str | None = None, ) -> GraphExecutionJobInfo: # Validate the graph before scheduling to prevent runtime failures # We don't need the return value, just want the validation to run @@ -408,7 +416,18 @@ class Scheduler(AppService): ) ) - logger.info(f"Scheduling job for user {user_id} in UTC (cron: {cron})") + # Use provided timezone or default to UTC + # Note: Timezone should be passed from the client to avoid database lookups + if not user_timezone: + user_timezone = "UTC" + logger.warning( + f"No timezone provided for user {user_id}, using UTC for scheduling. " + f"Client should pass user's timezone for correct scheduling." + ) + + logger.info( + f"Scheduling job for user {user_id} with timezone {user_timezone} (cron: {cron})" + ) job_args = GraphExecutionJobArgs( user_id=user_id, @@ -422,12 +441,12 @@ class Scheduler(AppService): execute_graph, kwargs=job_args.model_dump(), name=name, - trigger=CronTrigger.from_crontab(cron, timezone="UTC"), + trigger=CronTrigger.from_crontab(cron, timezone=user_timezone), jobstore=Jobstores.EXECUTION.value, replace_existing=True, ) logger.info( - f"Added job {job.id} with cron schedule '{cron}' in UTC, input data: {input_data}" + f"Added job {job.id} with cron schedule '{cron}' in timezone {user_timezone}, input data: {input_data}" ) return GraphExecutionJobInfo.from_db(job_args, job) diff --git a/autogpt_platform/backend/backend/server/routers/v1.py b/autogpt_platform/backend/backend/server/routers/v1.py index c5c7d818e3..084b0f3cb2 100644 --- a/autogpt_platform/backend/backend/server/routers/v1.py +++ b/autogpt_platform/backend/backend/server/routers/v1.py @@ -90,7 +90,6 @@ from backend.util.cloud_storage import get_cloud_storage_handler from backend.util.exceptions import GraphValidationError, NotFoundError from backend.util.settings import Settings from backend.util.timezone_utils import ( - convert_cron_to_utc, convert_utc_time_to_user_timezone, get_user_timezone_or_utc, ) @@ -945,6 +944,10 @@ class ScheduleCreationRequest(pydantic.BaseModel): cron: str inputs: dict[str, Any] credentials: dict[str, CredentialsMetaInput] = pydantic.Field(default_factory=dict) + timezone: Optional[str] = pydantic.Field( + default=None, + description="User's timezone for scheduling (e.g., 'America/New_York'). If not provided, will use user's saved timezone or UTC.", + ) @v1_router.post( @@ -969,26 +972,22 @@ async def create_graph_execution_schedule( detail=f"Graph #{graph_id} v{schedule_params.graph_version} not found.", ) - user = await get_user_by_id(user_id) - user_timezone = get_user_timezone_or_utc(user.timezone if user else None) - - # Convert cron expression from user timezone to UTC - try: - utc_cron = convert_cron_to_utc(schedule_params.cron, user_timezone) - except ValueError as e: - raise HTTPException( - status_code=400, - detail=f"Invalid cron expression for timezone {user_timezone}: {e}", - ) + # Use timezone from request if provided, otherwise fetch from user profile + if schedule_params.timezone: + user_timezone = schedule_params.timezone + else: + user = await get_user_by_id(user_id) + user_timezone = get_user_timezone_or_utc(user.timezone if user else None) result = await get_scheduler_client().add_execution_schedule( user_id=user_id, graph_id=graph_id, graph_version=graph.version, name=schedule_params.name, - cron=utc_cron, # Send UTC cron to scheduler + cron=schedule_params.cron, input_data=schedule_params.inputs, input_credentials=schedule_params.credentials, + user_timezone=user_timezone, ) # Convert the next_run_time back to user timezone for display @@ -1010,24 +1009,11 @@ async def list_graph_execution_schedules( user_id: Annotated[str, Security(get_user_id)], graph_id: str = Path(), ) -> list[scheduler.GraphExecutionJobInfo]: - schedules = await get_scheduler_client().get_execution_schedules( + return await get_scheduler_client().get_execution_schedules( user_id=user_id, graph_id=graph_id, ) - # Get user timezone for conversion - user = await get_user_by_id(user_id) - user_timezone = get_user_timezone_or_utc(user.timezone if user else None) - - # Convert next_run_time to user timezone for display - for schedule in schedules: - if schedule.next_run_time: - schedule.next_run_time = convert_utc_time_to_user_timezone( - schedule.next_run_time, user_timezone - ) - - return schedules - @v1_router.get( path="/schedules", @@ -1038,20 +1024,7 @@ async def list_graph_execution_schedules( async def list_all_graphs_execution_schedules( user_id: Annotated[str, Security(get_user_id)], ) -> list[scheduler.GraphExecutionJobInfo]: - schedules = await get_scheduler_client().get_execution_schedules(user_id=user_id) - - # Get user timezone for conversion - user = await get_user_by_id(user_id) - user_timezone = get_user_timezone_or_utc(user.timezone if user else None) - - # Convert UTC next_run_time to user timezone for display - for schedule in schedules: - if schedule.next_run_time: - schedule.next_run_time = convert_utc_time_to_user_timezone( - schedule.next_run_time, user_timezone - ) - - return schedules + return await get_scheduler_client().get_execution_schedules(user_id=user_id) @v1_router.delete( diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentModal/useAgentRunModal.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentModal/useAgentRunModal.ts index 4fc6e1c926..daaa920814 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentModal/useAgentRunModal.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentModal/useAgentRunModal.ts @@ -15,6 +15,7 @@ import { usePostV2SetupTrigger } from "@/app/api/__generated__/endpoints/presets import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta"; import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo"; import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset"; +import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth"; export type RunVariant = | "manual" @@ -48,6 +49,13 @@ export function useAgentRunModal( agent.recommended_schedule_cron || "0 9 * * 1", ); + // Get user timezone for scheduling + const { data: userTimezone } = useGetV1GetUserTimezone({ + query: { + select: (res) => (res.status === 200 ? res.data.timezone : undefined), + }, + }); + // Determine the default run type based on agent capabilities const defaultRunType: RunVariant = agent.has_external_trigger ? "automatic-trigger" @@ -307,6 +315,8 @@ export function useAgentRunModal( inputs: inputValues, graph_version: agent.graph_version, credentials: inputCredentials, + timezone: + userTimezone && userTimezone !== "not-set" ? userTimezone : undefined, }, }); }, [ @@ -319,6 +329,7 @@ export function useAgentRunModal( notifyMissingRequirements, createScheduleMutation, toast, + userTimezone, ]); function handleShowSchedule() { diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/ScheduleDetails/ScheduleDetails.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/ScheduleDetails/ScheduleDetails.tsx index 9cd20aa6c2..7040b2f958 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/ScheduleDetails/ScheduleDetails.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/ScheduleDetails/ScheduleDetails.tsx @@ -98,7 +98,7 @@ export function ScheduleDetails({ run={undefined} scheduleRecurrence={ schedule - ? `${humanizeCronExpression(schedule.cron || "", userTzRes)} · ${getTimezoneDisplayName(userTzRes || "UTC")}` + ? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}` : undefined } /> @@ -161,10 +161,12 @@ export function ScheduleDetails({ Recurrence

- {humanizeCronExpression(schedule.cron, userTzRes)} + {humanizeCronExpression(schedule.cron)} {" • "} - {getTimezoneDisplayName(userTzRes || "UTC")} + {getTimezoneDisplayName( + schedule.timezone || userTzRes || "UTC", + )}

@@ -187,7 +189,9 @@ export function ScheduleDetails({ )}{" "} •{" "} - {getTimezoneDisplayName(userTzRes || "UTC")} + {getTimezoneDisplayName( + schedule.timezone || userTzRes || "UTC", + )}

diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx index fbf057f42f..dddc870823 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx @@ -59,7 +59,7 @@ export function AgentScheduleDetailsView({ }, { label: "Schedule", - value: humanizeCronExpression(schedule.cron, userTimezone), + value: humanizeCronExpression(schedule.cron), }, { label: "Next run", diff --git a/autogpt_platform/frontend/src/app/(platform)/monitoring/page.tsx b/autogpt_platform/frontend/src/app/(platform)/monitoring/page.tsx index 434103bf34..f02d146c5a 100644 --- a/autogpt_platform/frontend/src/app/(platform)/monitoring/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/monitoring/page.tsx @@ -1,12 +1,12 @@ "use client"; import React, { useCallback, useEffect, useState } from "react"; +import { GraphExecutionMeta, LibraryAgent } from "@/lib/autogpt-server-api"; +import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo"; import { - GraphExecutionMeta, - Schedule, - LibraryAgent, - ScheduleID, -} from "@/lib/autogpt-server-api"; + useGetV1ListExecutionSchedulesForAUser, + useDeleteV1DeleteExecutionSchedule, +} from "@/app/api/__generated__/endpoints/schedules/schedules"; import { Card } from "@/components/ui/card"; import { @@ -22,26 +22,29 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context"; const Monitor = () => { const [flows, setFlows] = useState([]); const [executions, setExecutions] = useState([]); - const [schedules, setSchedules] = useState([]); const [selectedFlow, setSelectedFlow] = useState(null); const [selectedRun, setSelectedRun] = useState( null, ); - const [sortColumn, setSortColumn] = useState("id"); + const [sortColumn, setSortColumn] = + useState("id"); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const api = useBackendAPI(); - const fetchSchedules = useCallback(async () => { - setSchedules(await api.listAllGraphsExecutionSchedules()); - }, [api]); + // Use generated API hooks for schedules + const { data: schedulesResponse, refetch: refetchSchedules } = + useGetV1ListExecutionSchedulesForAUser(); + const deleteScheduleMutation = useDeleteV1DeleteExecutionSchedule(); + + const schedules = + schedulesResponse?.status === 200 ? schedulesResponse.data : []; const removeSchedule = useCallback( - async (scheduleId: ScheduleID) => { - const removedSchedule = - await api.deleteGraphExecutionSchedule(scheduleId); - setSchedules(schedules.filter((s) => s.id !== removedSchedule.id)); + async (scheduleId: string) => { + await deleteScheduleMutation.mutateAsync({ scheduleId }); + refetchSchedules(); }, - [schedules, api], + [deleteScheduleMutation, refetchSchedules], ); const fetchAgents = useCallback(() => { @@ -57,10 +60,6 @@ const Monitor = () => { fetchAgents(); }, [fetchAgents]); - useEffect(() => { - fetchSchedules(); - }, [fetchSchedules]); - useEffect(() => { const intervalId = setInterval(() => fetchAgents(), 5000); return () => clearInterval(intervalId); @@ -70,7 +69,7 @@ const Monitor = () => { const column2 = "md:col-span-3 lg:col-span-2 xl:col-span-3"; const column3 = "col-span-full xl:col-span-4 xxl:col-span-5"; - const handleSort = (column: keyof Schedule) => { + const handleSort = (column: keyof GraphExecutionJobInfo) => { if (sortColumn === column) { setSortDirection(sortDirection === "asc" ? "desc" : "asc"); } else { diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index f860d76e79..b4d3ed9597 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -5369,7 +5369,13 @@ }, "id": { "type": "string", "title": "Id" }, "name": { "type": "string", "title": "Name" }, - "next_run_time": { "type": "string", "title": "Next Run Time" } + "next_run_time": { "type": "string", "title": "Next Run Time" }, + "timezone": { + "type": "string", + "title": "Timezone", + "description": "Timezone used for scheduling", + "default": "UTC" + } }, "type": "object", "required": [ @@ -7051,6 +7057,11 @@ }, "type": "object", "title": "Credentials" + }, + "timezone": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Timezone", + "description": "User's timezone for scheduling (e.g., 'America/New_York'). If not provided, will use user's saved timezone or UTC." } }, "type": "object", diff --git a/autogpt_platform/frontend/src/components/monitor/scheduleTable.tsx b/autogpt_platform/frontend/src/components/monitor/scheduleTable.tsx index de50dfe3e4..153417a665 100644 --- a/autogpt_platform/frontend/src/components/monitor/scheduleTable.tsx +++ b/autogpt_platform/frontend/src/components/monitor/scheduleTable.tsx @@ -1,4 +1,5 @@ -import { LibraryAgent, Schedule, ScheduleID } from "@/lib/autogpt-server-api"; +import { LibraryAgent } from "@/lib/autogpt-server-api"; +import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { @@ -39,12 +40,12 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; interface SchedulesTableProps { - schedules: Schedule[]; + schedules: GraphExecutionJobInfo[]; agents: LibraryAgent[]; - onRemoveSchedule: (scheduleId: ScheduleID, enabled: boolean) => void; - sortColumn: keyof Schedule; + onRemoveSchedule: (scheduleId: string, enabled: boolean) => void; + sortColumn: keyof GraphExecutionJobInfo; sortDirection: "asc" | "desc"; - onSort: (column: keyof Schedule) => void; + onSort: (column: keyof GraphExecutionJobInfo) => void; } export const SchedulesTable = ({ @@ -84,7 +85,7 @@ export const SchedulesTable = ({ return String(bValue).localeCompare(String(aValue)); }); - const handleToggleSchedule = (scheduleId: ScheduleID, enabled: boolean) => { + const handleToggleSchedule = (scheduleId: string, enabled: boolean) => { onRemoveSchedule(scheduleId, enabled); if (!enabled) { toast({ @@ -257,12 +258,14 @@ export const SchedulesTable = ({ - {humanizeCronExpression(schedule.cron, userTimezone)} + {humanizeCronExpression(schedule.cron)} - {userTimezone && getTimezoneAbbreviation(userTimezone)} + {schedule.timezone + ? getTimezoneAbbreviation(schedule.timezone) + : userTimezone && getTimezoneAbbreviation(userTimezone)} diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts index 1b06121d2f..eadd84d818 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts @@ -797,6 +797,7 @@ export type Schedule = { input_data: Record; input_credentials: Record; next_run_time: Date; + timezone: string; }; export type ScheduleID = Brand; diff --git a/autogpt_platform/frontend/src/lib/cron-expression-utils.ts b/autogpt_platform/frontend/src/lib/cron-expression-utils.ts index afe290bb7e..c75dbd5e68 100644 --- a/autogpt_platform/frontend/src/lib/cron-expression-utils.ts +++ b/autogpt_platform/frontend/src/lib/cron-expression-utils.ts @@ -79,10 +79,7 @@ export function makeCronExpression(params: CronExpressionParams): string { return ""; } -export function humanizeCronExpression( - cronExpression: string, - userTimezone?: string, -): string { +export function humanizeCronExpression(cronExpression: string): string { const parts = cronExpression.trim().split(/\s+/); if (parts.length !== 5) { throw new Error("Invalid cron expression format."); @@ -138,7 +135,7 @@ export function humanizeCronExpression( !minute.includes("/") && !hour.includes("/") ) { - return `Every day at ${formatTime(hour, minute, userTimezone)}`; + return `Every day at ${formatTime(hour, minute)}`; } // Handle weekly (e.g., 30 14 * * 1,3,5) @@ -150,7 +147,7 @@ export function humanizeCronExpression( !hour.includes("/") ) { const days = getDayNames(dayOfWeek); - return `Every ${days} at ${formatTime(hour, minute, userTimezone)}`; + return `Every ${days} at ${formatTime(hour, minute)}`; } // Handle monthly (e.g., 30 14 1,15 * *) @@ -163,7 +160,7 @@ export function humanizeCronExpression( ) { const days = dayOfMonth.split(",").map(Number); const dayList = days.join(", "); - return `On day ${dayList} of every month at ${formatTime(hour, minute, userTimezone)}`; + return `On day ${dayList} of every month at ${formatTime(hour, minute)}`; } // Handle yearly (e.g., 30 14 1 1,6,12 *) @@ -175,7 +172,7 @@ export function humanizeCronExpression( !hour.includes("/") ) { const months = getMonthNames(month); - return `Every year on the 1st day of ${months} at ${formatTime(hour, minute, userTimezone)}`; + return `Every year on the 1st day of ${months} at ${formatTime(hour, minute)}`; } // Handle custom minute intervals with other fields as * (e.g., every N minutes) @@ -211,41 +208,15 @@ export function humanizeCronExpression( !hour.includes("/") ) { const interval = dayOfMonth.substring(2); - return `Every ${interval} days at ${formatTime(hour, minute, userTimezone)}`; + return `Every ${interval} days at ${formatTime(hour, minute)}`; } return `Cron Expression: ${cronExpression}`; } -function formatTime( - hour: string, - minute: string, - userTimezone?: string, -): string { - // Convert from UTC cron time to user timezone for display consistency with next_run_time - if (userTimezone && userTimezone !== "UTC" && userTimezone !== "not-set") { - try { - // Create a date in UTC with the cron hour/minute (cron expressions are stored in UTC) - const utcDate = new Date(); - utcDate.setUTCHours(parseInt(hour), parseInt(minute), 0, 0); - - // Format in user's timezone to match next_run_time display - const formatter = new Intl.DateTimeFormat("en-US", { - hour: "2-digit", - minute: "2-digit", - hour12: false, - timeZone: userTimezone, - }); - - return formatter.format(utcDate); - } catch { - // Fallback to original formatting if timezone conversion fails - const formattedHour = padZero(hour); - const formattedMinute = padZero(minute); - return `${formattedHour}:${formattedMinute}`; - } - } - +function formatTime(hour: string, minute: string): string { + // Cron expressions are now stored in the schedule's timezone (not UTC) + // So we just format the time as-is without conversion const formattedHour = padZero(hour); const formattedMinute = padZero(minute); return `${formattedHour}:${formattedMinute}`;