fix(frontend/library): fix schedule display issues for recurring schedules (#11362)

Fixes two related bugs in the agent scheduling UI that caused confusion
for users setting up recurring schedules:

1. **"on day nan of every month" display bug**: When scheduling an agent
to repeat every N days (e.g., "every 2 days"), the schedule info panel
incorrectly displayed "on day nan of every month" instead of the correct
"Every N days at HH:MM" format.

2. **Confusing time picker for hourly intervals**: When setting up a
schedule with "every N hours", the UI displayed a time picker labeled
"at 9 o'clock" which was confusing because the time setting is ignored
for hourly intervals. Users were unclear about what this setting meant
or if it had any effect.

### Changes 🏗️

**Fixed `humanizeCronExpression` function**
(`autogpt_platform/frontend/src/lib/cron-expression-utils.ts`):
- Reordered cron expression parsing logic to handle day intervals
(`*/N`) before monthly checks
- Added `!dayOfMonth.startsWith("*/")` guard to monthly and yearly
checks to prevent misinterpreting day intervals as monthly day lists
- This ensures expressions like `0 9 */2 * *` (every 2 days at 9:00) are
correctly displayed as "Every 2 days at 09:00" instead of "on day nan of
every month"

**Updated `CronScheduler` component**
(`autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/ScheduleAgentModal/components/CronScheduler/CronScheduler.tsx`):
- Hide `TimeAt` component for custom intervals with unit "hours" (time
is ignored for hourly intervals)
- Pass context-aware label to `TimeAt`: "Starting at" for custom day
intervals, "At" for other frequencies
- This clarifies that the time setting is the starting time for day
intervals and removes confusion for hourly intervals

**Enhanced `TimeAt` component**
(`autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/ScheduleAgentModal/components/CronScheduler/TimeAt.tsx`):
- Added optional `label` prop (defaults to "At") to allow context-aware
labeling
- Component now displays "Starting at" when used with custom day
intervals for better clarity

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
- [x] Schedule an agent with "Custom" frequency, "Every 2 days" interval
- verify it displays as "Every 2 days at HH:MM" in the schedule info
panel (not "on day nan of every month")
- [x] Schedule an agent with "Monthly" frequency - verify it displays
correctly (e.g., "On day 1, 15 of every month at HH:MM")


<img width="845" height="388" alt="image"
src="https://github.com/user-attachments/assets/02ed0b73-bf5e-48fd-a7b0-6f4d4687eb13"
/>
<img width="839" height="374" alt="image"
src="https://github.com/user-attachments/assets/be62eee2-3fdd-4b20-aecf-669c3c6c6fb2"
/>
This commit is contained in:
Bently
2025-11-13 08:13:21 +00:00
committed by GitHub
parent 536e2a5ec8
commit 32bb6705d1
3 changed files with 45 additions and 29 deletions

View File

@@ -259,9 +259,18 @@ export function CronScheduler({
<YearlyPicker values={selectedMonths} onChange={setSelectedMonths} />
)}
{frequency !== "hourly" && (
<TimeAt value={selectedTime} onChange={setSelectedTime} />
)}
{frequency !== "hourly" &&
!(frequency === "custom" && customInterval.unit === "hours") && (
<TimeAt
value={selectedTime}
onChange={setSelectedTime}
label={
frequency === "custom" && customInterval.unit === "days"
? "Starting at"
: "At"
}
/>
)}
</div>
);
}

View File

@@ -6,9 +6,11 @@ import { Select } from "@/components/atoms/Select/Select";
export function TimeAt({
value,
onChange,
label = "At",
}: {
value: string;
onChange: (v: string) => void;
label?: string;
}) {
const [hour12, setHour12] = useState<string>("9");
const [minute, setMinute] = useState<string>("00");
@@ -54,7 +56,7 @@ export function TimeAt({
<div className="flex items-end gap-1">
<div className="relative">
<label className="mb-0 block text-sm font-medium text-zinc-700">
At
{label}
</label>
<div className="flex items-center gap-2">
<Select

View File

@@ -150,31 +150,6 @@ export function humanizeCronExpression(cronExpression: string): string {
return `Every ${days} at ${formatTime(hour, minute)}`;
}
// Handle monthly (e.g., 30 14 1,15 * *)
if (
dayOfMonth !== "*" &&
month === "*" &&
dayOfWeek === "*" &&
!minute.includes("/") &&
!hour.includes("/")
) {
const days = dayOfMonth.split(",").map(Number);
const dayList = days.join(", ");
return `On day ${dayList} of every month at ${formatTime(hour, minute)}`;
}
// Handle yearly (e.g., 30 14 1 1,6,12 *)
if (
dayOfMonth !== "*" &&
month !== "*" &&
dayOfWeek === "*" &&
!minute.includes("/") &&
!hour.includes("/")
) {
const months = getMonthNames(month);
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)
if (
minute.includes("/") &&
@@ -200,6 +175,7 @@ export function humanizeCronExpression(cronExpression: string): string {
}
// Handle specific days with custom intervals (e.g., every N days)
// This must come BEFORE the monthly check to avoid misinterpreting */N as monthly days
if (
dayOfMonth.startsWith("*/") &&
month === "*" &&
@@ -211,6 +187,35 @@ export function humanizeCronExpression(cronExpression: string): string {
return `Every ${interval} days at ${formatTime(hour, minute)}`;
}
// Handle monthly (e.g., 30 14 1,15 * *)
// Check that dayOfMonth doesn't start with */ to avoid matching day intervals
if (
dayOfMonth !== "*" &&
!dayOfMonth.startsWith("*/") &&
month === "*" &&
dayOfWeek === "*" &&
!minute.includes("/") &&
!hour.includes("/")
) {
const days = dayOfMonth.split(",").map(Number);
const dayList = days.join(", ");
return `On day ${dayList} of every month at ${formatTime(hour, minute)}`;
}
// Handle yearly (e.g., 30 14 1 1,6,12 *)
// Check that dayOfMonth doesn't start with */ to avoid matching day intervals
if (
dayOfMonth !== "*" &&
!dayOfMonth.startsWith("*/") &&
month !== "*" &&
dayOfWeek === "*" &&
!minute.includes("/") &&
!hour.includes("/")
) {
const months = getMonthNames(month);
return `Every year on the 1st day of ${months} at ${formatTime(hour, minute)}`;
}
return `Cron Expression: ${cronExpression}`;
}