SurfSense/surfsense_web/lib/automations/schedule-builder.ts
DESKTOP-RTLN3BA\$punk d013617bf6 feat(automations): added UI and improved mentions
- Added support for @-mentions in agent tasks, allowing users to reference documents, folders, and connectors directly in their queries.
- Updated `run_agent_task` to resolve mentions and include them in the context passed to the agent.
- Introduced new parameters in `AgentTaskActionParams` for handling mentioned document and connector IDs.
- Refactored the automation edit and new components to utilize the new `AutomationBuilderForm` for a more streamlined user experience.
- Removed deprecated JSON forms to simplify the automation creation process.
2026-05-28 21:26:32 -07:00

132 lines
4.4 KiB
TypeScript

/**
* Bidirectional bridge between a friendly schedule model and the 5-field cron
* expression the backend ``schedule`` trigger expects (see
* ``app/automations/triggers/schedule/params.py``).
*
* The form builder never asks users to type cron. They pick a frequency + time
* (+ days), which ``toCron`` compiles. On edit we ``fromCron`` an existing
* expression back into the model; anything we don't recognize returns ``null``
* so the caller can fall back to a raw-cron escape hatch instead of silently
* losing the user's schedule.
*
* The recognized patterns are intentionally the same family that
* ``describe-cron.ts`` humanizes, keeping the picker and the label in sync.
*/
export type ScheduleFrequency = "hourly" | "daily" | "weekdays" | "weekly" | "monthly";
export interface ScheduleModel {
frequency: ScheduleFrequency;
/** 0-23. Ignored for ``hourly``. */
hour: number;
/** 0-59. */
minute: number;
/** 0 (Sun) - 6 (Sat). Used by ``weekly``. */
daysOfWeek: number[];
/** 1-31. Used by ``monthly``. */
dayOfMonth: number;
}
/** Sunday-first, matching cron's 0-6 day-of-week numbering. */
export const WEEKDAY_OPTIONS: ReadonlyArray<{ value: number; short: string; long: string }> = [
{ value: 1, short: "Mon", long: "Monday" },
{ value: 2, short: "Tue", long: "Tuesday" },
{ value: 3, short: "Wed", long: "Wednesday" },
{ value: 4, short: "Thu", long: "Thursday" },
{ value: 5, short: "Fri", long: "Friday" },
{ value: 6, short: "Sat", long: "Saturday" },
{ value: 0, short: "Sun", long: "Sunday" },
];
export const FREQUENCY_OPTIONS: ReadonlyArray<{ value: ScheduleFrequency; label: string }> = [
{ value: "hourly", label: "Every hour" },
{ value: "daily", label: "Every day" },
{ value: "weekdays", label: "Every weekday (Mon\u2013Fri)" },
{ value: "weekly", label: "Specific days of the week" },
{ value: "monthly", label: "Once a month" },
];
export const DEFAULT_SCHEDULE: ScheduleModel = {
frequency: "weekdays",
hour: 9,
minute: 0,
daysOfWeek: [1],
dayOfMonth: 1,
};
function isInt(value: string): boolean {
return /^\d+$/.test(value);
}
function clamp(value: number, min: number, max: number): number {
if (Number.isNaN(value)) return min;
return Math.min(max, Math.max(min, value));
}
/** Compile a schedule model into a 5-field cron expression. */
export function toCron(model: ScheduleModel): string {
const minute = clamp(model.minute, 0, 59);
const hour = clamp(model.hour, 0, 23);
switch (model.frequency) {
case "hourly":
return `${minute} * * * *`;
case "daily":
return `${minute} ${hour} * * *`;
case "weekdays":
return `${minute} ${hour} * * 1-5`;
case "weekly": {
const days = [...new Set(model.daysOfWeek)].sort((a, b) => a - b);
// Guard against an empty selection producing an invalid cron.
const dow = days.length > 0 ? days.join(",") : "1";
return `${minute} ${hour} * * ${dow}`;
}
case "monthly":
return `${minute} ${hour} ${clamp(model.dayOfMonth, 1, 31)} * *`;
}
}
/**
* Parse a 5-field cron expression back into a schedule model. Returns ``null``
* for anything outside the recognized pattern family so callers can fall back
* to the raw-cron field.
*/
export function fromCron(cron: string): ScheduleModel | null {
const parts = cron.trim().split(/\s+/);
if (parts.length !== 5) return null;
const [minute, hour, dom, month, dow] = parts;
// Hourly: "M * * * *"
if (month === "*" && dom === "*" && dow === "*" && hour === "*" && isInt(minute)) {
return { ...DEFAULT_SCHEDULE, frequency: "hourly", minute: Number(minute) };
}
// Everything below requires concrete minute + hour.
if (!isInt(minute) || !isInt(hour)) return null;
const base = { hour: Number(hour), minute: Number(minute) };
// Daily: "M H * * *"
if (month === "*" && dom === "*" && dow === "*") {
return { ...DEFAULT_SCHEDULE, ...base, frequency: "daily" };
}
// Weekdays: "M H * * 1-5"
if (month === "*" && dom === "*" && dow === "1-5") {
return { ...DEFAULT_SCHEDULE, ...base, frequency: "weekdays" };
}
// Weekly: "M H * * 1,3,5"
if (month === "*" && dom === "*" && /^[0-6](,[0-6])*$/.test(dow)) {
const daysOfWeek = [...new Set(dow.split(",").map(Number))].sort((a, b) => a - b);
return { ...DEFAULT_SCHEDULE, ...base, frequency: "weekly", daysOfWeek };
}
// Monthly: "M H D * *"
if (month === "*" && dow === "*" && isInt(dom)) {
return { ...DEFAULT_SCHEDULE, ...base, frequency: "monthly", dayOfMonth: Number(dom) };
}
return null;
}