diff --git a/dashboard/src/v2/SchedulerPage.tsx b/dashboard/src/v2/SchedulerPage.tsx index 676140cb48..7048d64f4d 100644 --- a/dashboard/src/v2/SchedulerPage.tsx +++ b/dashboard/src/v2/SchedulerPage.tsx @@ -1,5 +1,5 @@ import type { FunctionComponent, JSX } from "preact"; -import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; import { CalendarDays, BellRing, @@ -24,15 +24,6 @@ import { PageHeader } from "./components/layout/PageHeader.js"; import { AvantgardeSelect } from "./components/ui/AvantgardeSelect.js"; import { Button } from "./components/ui/Button.js"; import { useProjectData } from "./context/project-data.js"; -import { - translateDashboardMessage, - translateDashboardPlural, - type DashboardLocale, - type DashboardMessageVariables, - type DashboardTextMessageKey, -} from "./i18n/locales.js"; -import { schedulerMessages } from "./i18n/messages/scheduler.js"; -import { useDashboardI18n } from "./i18n/context.js"; import { subscribeToDashboardRealtime } from "../lib/realtime/dashboard-realtime-client.js"; import { fetchSprints } from "./lib/project-api.js"; import { fetchNodeFlows } from "./lib/node-flow-api.js"; @@ -41,7 +32,6 @@ import { createSchedulerEntry, deleteSchedulerEntry, fetchProjectSchedule, - formatSchedulerDateValueInTimeZone, updateSchedulerEntry, } from "./lib/scheduler-api.js"; import type { @@ -71,6 +61,7 @@ const SCHEDULER_COMPACT_FIELD_CLASS = `${SCHEDULER_FIELD_CLASS} min-h-[40px]`; const TARGET_OPTIONS: Array<{ value: ScheduleTargetType; + label: string; icon: typeof Zap; tone: string; activeClassName: string; @@ -78,6 +69,7 @@ const TARGET_OPTIONS: Array<{ }> = [ { value: "sprint", + label: "Sprint", icon: Zap, tone: "text-ember-500", activeClassName: "border-ember-500/35 bg-ember-500/10 shadow-[0_12px_34px_rgba(255,184,0,0.13)]", @@ -85,6 +77,7 @@ const TARGET_OPTIONS: Array<{ }, { value: "quicksprint", + label: "Quicksprint", icon: RefreshCw, tone: "text-sky-500", activeClassName: "border-sky-500/35 bg-sky-500/10 shadow-[0_12px_34px_rgba(14,165,233,0.13)]", @@ -92,6 +85,7 @@ const TARGET_OPTIONS: Array<{ }, { value: "chat", + label: "Chat message", icon: MessageCircle, tone: "text-violet-500", activeClassName: "border-violet-500/35 bg-violet-500/10 shadow-[0_12px_34px_rgba(139,92,246,0.13)]", @@ -99,6 +93,7 @@ const TARGET_OPTIONS: Array<{ }, { value: "memory_remediation", + label: "Memory", icon: Brain, tone: "text-signal-500", activeClassName: "border-signal-500/35 bg-signal-500/10 shadow-[0_12px_34px_rgba(0,224,160,0.13)]", @@ -106,6 +101,7 @@ const TARGET_OPTIONS: Array<{ }, { value: "node_flow", + label: "Node flow", icon: Workflow, tone: "text-rose-500", activeClassName: "border-rose-500/35 bg-rose-500/10 shadow-[0_12px_34px_rgba(244,63,94,0.13)]", @@ -113,6 +109,7 @@ const TARGET_OPTIONS: Array<{ }, { value: "agent_wakeup", + label: "Agent wakeup", icon: BellRing, tone: "text-fuchsia-500", activeClassName: "border-fuchsia-500/35 bg-fuchsia-500/10 shadow-[0_12px_34px_rgba(217,70,239,0.13)]", @@ -120,6 +117,7 @@ const TARGET_OPTIONS: Array<{ }, { value: "task", + label: "Task", icon: ListTodo, tone: "text-cyan-500", activeClassName: "border-cyan-500/35 bg-cyan-500/10 shadow-[0_12px_34px_rgba(6,182,212,0.13)]", @@ -155,57 +153,17 @@ const startOfWeek = (date: Date): Date => { return addDays(next, -offset); }; -type SchedulerTextKey = DashboardTextMessageKey; - -const schedulerText = ( - locale: DashboardLocale, - key: SchedulerTextKey, - variables?: DashboardMessageVariables, -): string => translateDashboardMessage(schedulerMessages, locale, key, variables); - -export const formatSchedulerDayLabel = (date: Date, locale: DashboardLocale): string => ( - formatSchedulerDateValueInTimeZone(date, locale, { weekday: "short", month: "short", day: "numeric" }) -); - -export const formatSchedulerTimeLabel = ( - value: string | Date, - locale: DashboardLocale, - timeZone?: string, -): string => formatSchedulerDateValueInTimeZone( - typeof value === "string" ? new Date(value) : value, - locale, - { hour: "2-digit", minute: "2-digit" }, - timeZone, +const formatDayLabel = (date: Date): string => ( + date.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }) ); -const formatSchedulerDateTime = ( - value: string | Date, - locale: DashboardLocale, - timeZone?: string, -): string => formatSchedulerDateValueInTimeZone( - typeof value === "string" ? new Date(value) : value, - locale, - { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }, - timeZone, +const formatTimeLabel = (iso: string): string => ( + new Date(iso).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }) ); -const targetLabel = (targetType: ScheduleTargetType, locale: DashboardLocale): string => { - const key = { - sprint: "targetSprint", - quicksprint: "targetQuicksprint", - chat: "targetChat", - memory_remediation: "targetMemory", - node_flow: "targetNodeFlow", - agent_wakeup: "targetAgentWakeup", - task: "targetTask", - } as const; - return schedulerText(locale, key[targetType] ?? "targetFallback"); +const targetLabel = (targetType: ScheduleTargetType): string => { + const option = targetOptionByType.get(targetType); + return option?.label || "Schedule"; }; function isOperatorTargetType(targetType: ScheduleTargetType): targetType is OperatorScheduleTargetType { @@ -216,101 +174,49 @@ const isOperatorEditableTarget = (targetType: ScheduleTargetType): targetType is isOperatorTargetType(targetType) ); -const unsupportedEditReason = (targetType: ScheduleTargetType, locale: DashboardLocale): string => ( - schedulerText(locale, "unsupportedEdit", { target: targetLabel(targetType, locale) }) +const unsupportedEditReason = (targetType: ScheduleTargetType): string => ( + `${targetLabel(targetType)} schedules are created by the secured MCP scheduler tool and cannot be edited in the dashboard form. Pause, resume, or delete remains available.` ); -const scheduleStatusLabel = ( - status: SchedulerEntryRecord["status"] | SchedulerOccurrence["status"] | undefined, - locale: DashboardLocale, -): string => { - const key = { - scheduled: "statusScheduled", - paused: "statusPaused", - completed: "statusCompleted", - failed: "statusFailed", - cancelled: "statusCancelled", - } as const; - return schedulerText(locale, key[status ?? "scheduled"]); -}; - -export const formatSchedulerSprintStatus = ( - status: SprintRecord["status"], - locale: DashboardLocale, -): string => { - const key = { - running: "sprintStatusRunning", - paused: "sprintStatusPaused", - completed: "sprintStatusCompleted", - failed: "sprintStatusFailed", - cancelled: "sprintStatusCancelled", - idle: "sprintStatusIdle", - } as const; - return schedulerText(locale, key[status]); -}; - -const schedulerViewLabel = (view: SchedulerView, locale: DashboardLocale): string => ( - schedulerText(locale, view === "calendar" ? "calendar" : "dayView") +const scheduleStatusLabel = (status: SchedulerEntryRecord["status"] | SchedulerOccurrence["status"] | undefined): string => ( + status ? status.replaceAll("_", " ") : "scheduled" ); -const recurrenceFrequencyLabel = ( - frequency: ScheduleRecurrenceRule["frequency"], - interval: number, - locale: DashboardLocale, -): string => { +const schedulerViewLabel = (view: SchedulerView): string => view === "calendar" ? "Calendar" : "24 Hours"; + +const recurrenceFrequencyLabel = (frequency: ScheduleRecurrenceRule["frequency"], interval: number): string => { if (frequency === "minutely") { - return schedulerText(locale, interval === 1 ? "frequencyMinute" : "frequencyMinutes", { count: interval }); + return interval === 1 ? "minute" : `${interval} minutes`; } if (interval === 1) { - const key = { - hourly: "frequencyHourly", - daily: "frequencyDaily", - weekly: "frequencyWeekly", - monthly: "frequencyMonthly", - none: "recurrenceOneTime", - } as const; - return schedulerText(locale, key[frequency]); + return frequency; } - const key = { - hourly: "frequencyHours", - daily: "frequencyDays", - weekly: "frequencyWeeks", - monthly: "frequencyMonths", - none: "recurrenceOneTime", - } as const; - return schedulerText(locale, key[frequency], { count: interval }); + return `${interval} ${frequency}`; }; -export const summarizeSchedulerRecurrence = ( - recurrence: ScheduleRecurrenceRule, - locale: DashboardLocale, - timeZone?: string, -): string => { +const recurrenceSummary = (recurrence: ScheduleRecurrenceRule): string => { if (recurrence.frequency === "none") { - return schedulerText(locale, "recurrenceOneTime"); + return "One time"; } - const every = recurrenceFrequencyLabel(recurrence.frequency, recurrence.interval, locale); + const every = recurrenceFrequencyLabel(recurrence.frequency, recurrence.interval); if (recurrence.endMode === "after_count" && recurrence.count) { - return schedulerText(locale, "recurrenceEveryRuns", { frequency: every, count: recurrence.count }); + return `Every ${every}, ${recurrence.count} runs`; } if (recurrence.endMode === "on_date" && recurrence.until) { - return schedulerText(locale, "recurrenceEveryUntil", { - frequency: every, - until: formatSchedulerDateTime(recurrence.until, locale, timeZone), - }); + return `Every ${every} until ${new Date(recurrence.until).toLocaleString()}`; } - return schedulerText(locale, "recurrenceEvery", { frequency: every }); + return `Every ${every}`; }; -const scheduleAnchorOffsetLabel = (offsetMinutes: number | undefined, locale: DashboardLocale): string => { +const scheduleAnchorOffsetLabel = (offsetMinutes?: number): string => { const offset = Math.max(0, Math.floor(Number(offsetMinutes ?? 0))); if (offset === 0) { return ""; } if (offset === 1) { - return schedulerText(locale, "offsetOneMinute"); + return " + 1 minute"; } - return schedulerText(locale, "offsetManyMinutes", { count: offset }); + return ` + ${offset} minutes`; }; const sprintDisplayName = (sprints: SprintRecord[], sprintId: string): string => ( @@ -341,33 +247,26 @@ const isNodeFlowJsonObject = (value: unknown): value is NodeFlowJsonObject => ( && Object.values(value as Record).every(isNodeFlowJsonValue) ); -const parseNodeFlowInputJson = (rawInput: string, locale: DashboardLocale): { input?: NodeFlowJsonObject; error?: string } => { +const parseNodeFlowInputJson = (rawInput: string): { input?: NodeFlowJsonObject; error?: string } => { if (!rawInput.trim()) { return {}; } try { const parsed: unknown = JSON.parse(rawInput); if (!isNodeFlowJsonObject(parsed)) { - return { error: schedulerText(locale, "validationNodeFlowObject") }; + return { error: "Node flow input must be a JSON object." }; } return { input: parsed }; } catch { - return { error: schedulerText(locale, "validationNodeFlowJson") }; + return { error: "Node flow input must be valid JSON." }; } }; -const scheduleTimingSummary = (entry: SchedulerEntryRecord, sprints: SprintRecord[], locale: DashboardLocale): string => { +const scheduleTimingSummary = (entry: SchedulerEntryRecord, sprints: SprintRecord[]): string => { if (entry.scheduleAnchor?.mode === "after_sprint_end") { - return schedulerText(locale, "anchorAfterSprint", { - sprint: sprintDisplayName(sprints, entry.scheduleAnchor.sourceSprintId), - offset: scheduleAnchorOffsetLabel(entry.scheduleAnchor.offsetMinutes, locale), - }); + return `After ${sprintDisplayName(sprints, entry.scheduleAnchor.sourceSprintId)} ends${scheduleAnchorOffsetLabel(entry.scheduleAnchor.offsetMinutes)}`; } - return schedulerText(locale, "nextRunAt", { - date: entry.nextRunAt - ? `${formatSchedulerDateTime(entry.nextRunAt, locale, entry.timezone)} (${entry.timezone})` - : schedulerText(locale, "none"), - }); + return `Next run: ${entry.nextRunAt ? new Date(entry.nextRunAt).toLocaleString() : "none"}`; }; const scheduleTargetSummary = ( @@ -375,45 +274,30 @@ const scheduleTargetSummary = ( sprints: SprintRecord[], templates: QuicksprintTemplateRecord[], nodeFlows: NodeFlowRecord[], - locale: DashboardLocale, ): string => { if (entry.targetType === "sprint") { - return entry.sprintTarget - ? schedulerText(locale, "sprintSummary", { name: sprintDisplayName(sprints, entry.sprintTarget.sprintId) }) - : schedulerText(locale, "targetSprint"); + return entry.sprintTarget ? `Sprint: ${sprintDisplayName(sprints, entry.sprintTarget.sprintId)}` : "Sprint"; } if (entry.targetType === "quicksprint") { const templateId = entry.quicksprintTarget?.templateId; - const templateName = templateId - ? templates.find((template) => template.id === templateId)?.name || templateId - : schedulerText(locale, "templateFallback"); - return schedulerText(locale, "quicksprintSummary", { name: templateName }); + const templateName = templateId ? templates.find((template) => template.id === templateId)?.name || templateId : "template"; + return `Quicksprint: ${templateName}`; } if (entry.targetType === "chat") { - return entry.chatTarget?.threadId - ? schedulerText(locale, "chatThreadSummary", { threadId: entry.chatTarget.threadId }) - : schedulerText(locale, "projectChatMessage"); + return entry.chatTarget?.threadId ? `Chat thread: ${entry.chatTarget.threadId}` : "Project chat message"; } if (entry.targetType === "memory_remediation") { - return schedulerText(locale, "remediationSummary", { - mode: schedulerText(locale, entry.memoryRemediationTarget?.mode === "ai" ? "aiReview" : "deterministicCleanup"), - }); + return `Memory remediation: ${entry.memoryRemediationTarget?.mode === "ai" ? "AI review" : "deterministic cleanup"}`; } if (entry.targetType === "node_flow") { const flowId = entry.nodeFlowTarget?.flowId; - return flowId - ? schedulerText(locale, "nodeFlowSummary", { name: nodeFlowDisplayName(nodeFlows, flowId) }) - : schedulerText(locale, "targetNodeFlow"); + return flowId ? `Node flow: ${nodeFlowDisplayName(nodeFlows, flowId)}` : "Node flow"; } if (entry.targetType === "agent_wakeup") { - return entry.agentWakeupTarget?.threadId - ? schedulerText(locale, "agentWakeupSummary", { threadId: entry.agentWakeupTarget.threadId }) - : schedulerText(locale, "agentWakeupMessage"); + return entry.agentWakeupTarget?.threadId ? `Agent wakeup: ${entry.agentWakeupTarget.threadId}` : "Agent wakeup message"; } const provider = entry.taskTarget?.provider ? ` · ${entry.taskTarget.provider}` : ""; - return entry.taskTarget?.taskId - ? schedulerText(locale, "taskRerunSummary", { taskId: entry.taskTarget.taskId, provider }) - : schedulerText(locale, "taskRerun"); + return entry.taskTarget?.taskId ? `Task rerun: ${entry.taskTarget.taskId}${provider}` : "Task rerun"; }; const ProjectPlaceholder: FunctionComponent = () => ( @@ -421,27 +305,15 @@ const ProjectPlaceholder: FunctionComponent = () => (
- - -); - -const LocalizedProjectPlaceholderCopy: FunctionComponent = () => { - const { translate } = useDashboardI18n(); - return ( - <> -

{translate(schedulerMessages, "selectProjectTitle")}

+

Select a project to schedule work.

- {translate(schedulerMessages, "selectProjectDescription")} + Scheduler entries are project-scoped so sprints, quicksprints, node flows, and chat messages run against the right workspace.

- - ); -}; + +); export const SchedulerPage: FunctionComponent = () => { const { selectedProject } = useProjectData(); - const { locale, translate, translatePlural, formatNumber, formatTime } = useDashboardI18n(); - const refreshSequence = useRef(0); - const submitInFlight = useRef(false); const [view, setView] = useState("calendar"); const [selectedDate, setSelectedDate] = useState(() => startOfDay(new Date())); const [schedule, setSchedule] = useState(null); @@ -475,7 +347,6 @@ export const SchedulerPage: FunctionComponent = () => { }; }, []); const [loading, setLoading] = useState(false); - const [submitting, setSubmitting] = useState(false); const [feedback, setFeedback] = useState({ tone: "idle", message: null }); const [editingEntry, setEditingEntry] = useState(null); @@ -502,7 +373,6 @@ export const SchedulerPage: FunctionComponent = () => { const [endMode, setEndMode] = useState("never"); const [count, setCount] = useState(6); const [until, setUntil] = useState(() => toDateInputValue(addDays(new Date(), 30))); - const selectedTimezone = editingEntry?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; const range = useMemo(() => { const from = startOfWeek(selectedDate); @@ -515,7 +385,6 @@ export const SchedulerPage: FunctionComponent = () => { if (!selectedProject) { return; } - const requestId = ++refreshSequence.current; setLoading(true); try { const [nextSchedule, sprintResponse, quicksprintTemplates, nodeFlowResponse] = await Promise.all([ @@ -524,9 +393,6 @@ export const SchedulerPage: FunctionComponent = () => { fetchQuicksprintTemplates(selectedProject.id), fetchNodeFlows(selectedProject.id, signal), ]); - if (signal?.aborted || requestId !== refreshSequence.current) { - return; - } setSchedule(nextSchedule); setSprints(sprintResponse.sprints); setTemplates(quicksprintTemplates); @@ -540,15 +406,15 @@ export const SchedulerPage: FunctionComponent = () => { : nodeFlowResponse.flows[0]?.id || "" )); } catch (error) { - if (!signal?.aborted && requestId === refreshSequence.current) { - setFeedback({ tone: "error", message: error instanceof Error ? error.message : translate(schedulerMessages, "failedLoad") }); + if (!signal?.aborted) { + setFeedback({ tone: "error", message: error instanceof Error ? error.message : "Failed to load scheduler." }); } } finally { - if (!signal?.aborted && requestId === refreshSequence.current) { + if (!signal?.aborted) { setLoading(false); } } - }, [range.from, range.to, selectedProject, translate]); + }, [range.from, range.to, selectedProject]); useEffect(() => { const controller = new AbortController(); @@ -617,19 +483,9 @@ export const SchedulerPage: FunctionComponent = () => { }; }, [schedule?.entries, schedule?.occurrences]); - const scheduleRangeStatus = loading && !schedule - ? translate(schedulerMessages, "loadingSchedule") - : loading - ? translate(schedulerMessages, "updatingSchedule", { - count: formatNumber(schedulerStats.visibleCount), - from: formatSchedulerDayLabel(range.from, locale), - to: formatSchedulerDayLabel(range.to, locale), - }) - : translate(schedulerMessages, "visibleScheduleRange", { - count: formatNumber(schedulerStats.visibleCount), - from: formatSchedulerDayLabel(range.from, locale), - to: formatSchedulerDayLabel(range.to, locale), - }); + const scheduleRangeStatus = loading && schedule + ? `Updating schedule. Showing cached ${schedulerStats.visibleCount} visible occurrences from ${formatDayLabel(range.from)} to ${formatDayLabel(range.to)}.` + : `${schedulerStats.visibleCount} visible occurrences · ${formatDayLabel(range.from)} to ${formatDayLabel(range.to)}`; const focusSchedulerView = (nextView: SchedulerView) => { setView(nextView); @@ -659,7 +515,7 @@ export const SchedulerPage: FunctionComponent = () => { const startEdit = (entry: SchedulerEntryRecord) => { if (!isOperatorEditableTarget(entry.targetType)) { - setFeedback({ tone: "error", message: unsupportedEditReason(entry.targetType, locale) }); + setFeedback({ tone: "error", message: unsupportedEditReason(entry.targetType) }); return; } setEditingEntry(entry); @@ -705,7 +561,7 @@ export const SchedulerPage: FunctionComponent = () => { } }; - const cancelEdit = (clearFeedback = true) => { + const cancelEdit = () => { setEditingEntry(null); setEntryTitle(""); setTargetType("sprint"); @@ -728,9 +584,7 @@ export const SchedulerPage: FunctionComponent = () => { setEndMode("never"); setCount(6); setUntil(toDateInputValue(addDays(new Date(), 30))); - if (clearFeedback) { - setFeedback({ tone: "idle", message: null }); - } + setFeedback({ tone: "idle", message: null }); }; const editOccurrence = (occurrence: SchedulerOccurrence) => { @@ -738,56 +592,26 @@ export const SchedulerPage: FunctionComponent = () => { if (entry) { startEdit(entry); } else { - setFeedback({ tone: "error", message: translate(schedulerMessages, "parentEntryNotFound") }); + setFeedback({ tone: "error", message: "Parent schedule entry not found." }); } }; const submitSchedule = async () => { - if (!selectedProject || submitInFlight.current) { + if (!selectedProject) { return; } setFeedback({ tone: "idle", message: null }); if (!isOperatorEditableTarget(targetType)) { - setFeedback({ tone: "error", message: unsupportedEditReason(targetType, locale) }); - return; - } - - const scheduledDate = isAnchoredTiming ? null : new Date(scheduledFor); - if (scheduledDate && !Number.isFinite(scheduledDate.getTime())) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationDateTime") }); + setFeedback({ tone: "error", message: unsupportedEditReason(targetType) }); return; } - - let recurrenceUntil: Date | null = null; - if (repeatEnabled && !isAnchoredTiming) { - if (!Number.isInteger(interval) || interval < 1) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationInterval") }); - return; - } - if (endMode === "after_count" && (!Number.isInteger(count) || count < 1)) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationCount") }); - return; - } - if (endMode === "on_date") { - recurrenceUntil = new Date(until); - if (!Number.isFinite(recurrenceUntil.getTime())) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationEndDate") }); - return; - } - if (scheduledDate && recurrenceUntil.getTime() <= scheduledDate.getTime()) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationEndAfterStart") }); - return; - } - } - } - const recurrence: Partial = repeatEnabled && !isAnchoredTiming ? { frequency, interval, endMode, count: endMode === "after_count" ? count : null, - until: recurrenceUntil?.toISOString() ?? null, + until: endMode === "on_date" ? new Date(until).toISOString() : null, } : { frequency: "none", interval: 1, endMode: "never" }; @@ -813,36 +637,32 @@ export const SchedulerPage: FunctionComponent = () => { if (targetType === "sprint") { if (!selectedSprintId) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationChooseSprint") }); + setFeedback({ tone: "error", message: "Choose a sprint that is not completed." }); return; } const sprint = sprints.find((item) => item.id === selectedSprintId); if (!sprint || (sprint.status === "completed" && (!editingEntry || editingEntry.sprintTarget?.sprintId !== selectedSprintId))) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationChooseSprint") }); + setFeedback({ tone: "error", message: "Choose a sprint that is not completed." }); return; } } else if (targetType === "quicksprint") { if (!selectedTemplateId) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationChooseTemplate") }); - return; - } - if (!Number.isInteger(taskCount) || taskCount < 1 || taskCount > 50) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationTaskCount") }); + setFeedback({ tone: "error", message: "Choose a quicksprint template." }); return; } } else if (targetType === "chat") { if (!chatMessage.trim()) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationChatMessage") }); + setFeedback({ tone: "error", message: "Write the chat message to schedule." }); return; } } else if (targetType === "node_flow") { if (!selectedNodeFlowId) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationChooseNodeFlow") }); + setFeedback({ tone: "error", message: "Choose a node flow." }); return; } } - const parsedNodeFlowInput = targetType === "node_flow" ? parseNodeFlowInputJson(nodeFlowInputJson, locale) : {}; + const parsedNodeFlowInput = targetType === "node_flow" ? parseNodeFlowInputJson(nodeFlowInputJson) : {}; if (parsedNodeFlowInput.error) { setFeedback({ tone: "error", message: parsedNodeFlowInput.error }); return; @@ -851,16 +671,16 @@ export const SchedulerPage: FunctionComponent = () => { let scheduleAnchor: ScheduleAnchor | undefined; if (isAnchoredTiming) { if (!anchorSourceSprintId) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationChooseAnchorSprint") }); + setFeedback({ tone: "error", message: "Choose the sprint this schedule should wait for." }); return; } const offsetMinutes = Math.floor(Number(anchorOffsetMinutes || 0)); if (!Number.isFinite(offsetMinutes) || offsetMinutes < 0) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationOffset") }); + setFeedback({ tone: "error", message: "Offset minutes must be zero or greater." }); return; } if (targetType === "sprint" && selectedSprintId === anchorSourceSprintId) { - setFeedback({ tone: "error", message: translate(schedulerMessages, "validationOwnSprint") }); + setFeedback({ tone: "error", message: "A sprint cannot be scheduled after its own completion." }); return; } scheduleAnchor = { @@ -873,13 +693,13 @@ export const SchedulerPage: FunctionComponent = () => { const input: SchedulerFormInput = { title: finalTitle, targetType, - timezone: selectedTimezone, + timezone: editingEntry ? editingEntry.timezone : (Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"), recurrence, }; if (scheduleAnchor) { input.scheduleAnchor = scheduleAnchor; } else { - input.scheduledFor = scheduledDate!.toISOString(); + input.scheduledFor = new Date(scheduledFor).toISOString(); if (editingEntry?.scheduleAnchor) { input.scheduleAnchor = null; } @@ -909,29 +729,24 @@ export const SchedulerPage: FunctionComponent = () => { }; } - submitInFlight.current = true; - setSubmitting(true); try { if (editingEntry) { await updateSchedulerEntry(editingEntry.id, input); - cancelEdit(false); - setFeedback({ tone: "success", message: translate(schedulerMessages, "scheduleUpdated") }); + setFeedback({ tone: "success", message: "Schedule entry updated." }); + cancelEdit(); } else { const createInput: CreateSchedulerEntryInput = { ...input, scheduleAnchor: input.scheduleAnchor ?? undefined, }; await createSchedulerEntry(selectedProject.id, createInput); - setFeedback({ tone: "success", message: translate(schedulerMessages, "scheduleCreated") }); + setFeedback({ tone: "success", message: "Schedule entry created." }); setChatMessage(""); setEntryTitle(""); } await refresh(); } catch (error) { - setFeedback({ tone: "error", message: error instanceof Error ? error.message : translate(schedulerMessages, "failedSave") }); - } finally { - submitInFlight.current = false; - setSubmitting(false); + setFeedback({ tone: "error", message: error instanceof Error ? error.message : "Failed to save schedule entry." }); } }; @@ -940,38 +755,35 @@ export const SchedulerPage: FunctionComponent = () => { await updateSchedulerEntry(entryId, { status: paused ? "scheduled" : "paused" }); await refresh(); } catch (error) { - setFeedback({ tone: "error", message: error instanceof Error ? error.message : translate(schedulerMessages, "failedUpdate") }); + setFeedback({ tone: "error", message: error instanceof Error ? error.message : "Failed to update schedule entry." }); } }; const removeEntry = async (entryId: string) => { - if (!window.confirm(translate(schedulerMessages, "deleteConfirmation"))) { - return; - } try { await deleteSchedulerEntry(entryId); await refresh(); } catch (error) { - setFeedback({ tone: "error", message: error instanceof Error ? error.message : translate(schedulerMessages, "failedDelete") }); + setFeedback({ tone: "error", message: error instanceof Error ? error.message : "Failed to delete schedule entry." }); } }; if (!selectedProject) { return ( - + ); } return ( - +
{(["calendar", "day"] as SchedulerView[]).map((item) => ( @@ -1015,7 +827,7 @@ export const SchedulerPage: FunctionComponent = () => { view === item ? "bg-signal-500 text-white dark:text-void-900 shadow-[0_2px_8px_rgba(0,224,160,0.2)]" : "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white" }`} > - {schedulerViewLabel(item, locale)} + {schedulerViewLabel(item)} ))}
@@ -1025,13 +837,13 @@ export const SchedulerPage: FunctionComponent = () => {
{[ - { id: "active", label: translate(schedulerMessages, "activeEntries"), value: formatNumber(schedulerStats.activeCount), detail: translate(schedulerMessages, "readyToFire"), icon: Play, tone: "text-signal-500" }, - { id: "repeating", label: translate(schedulerMessages, "repeating"), value: formatNumber(schedulerStats.repeatingCount), detail: translate(schedulerMessages, "recurrenceRules"), icon: Repeat, tone: "text-signal-500" }, + { id: "active", label: "Active entries", value: schedulerStats.activeCount, detail: "ready to fire", icon: Play, tone: "text-signal-500" }, + { id: "repeating", label: "Repeating", value: schedulerStats.repeatingCount, detail: "recurrence rules", icon: Repeat, tone: "text-signal-500" }, { id: "next-run", - label: translate(schedulerMessages, "nextRun"), - value: schedulerStats.nextOccurrence ? formatSchedulerTimeLabel(schedulerStats.nextOccurrence.startsAt, locale) : translate(schedulerMessages, "none"), - detail: schedulerStats.nextOccurrence ? schedulerStats.nextOccurrence.title : translate(schedulerMessages, "noUpcomingWork"), + label: "Next run", + value: schedulerStats.nextOccurrence ? formatTimeLabel(schedulerStats.nextOccurrence.startsAt) : "None", + detail: schedulerStats.nextOccurrence ? schedulerStats.nextOccurrence.title : "no upcoming work", icon: Clock3, tone: "text-ember-500", }, @@ -1057,10 +869,10 @@ export const SchedulerPage: FunctionComponent = () => {

- {translate(schedulerMessages, editingEntry ? "editEntry" : "addEntry")} + {editingEntry ? "Edit entry" : "Add entry"}

- {translate(schedulerMessages, editingEntry ? "editEntryDescription" : "addEntryDescription")} + {editingEntry ? "Modify the title, work, time, and recurrence." : "Choose the work, time, and recurrence."}

@@ -1068,9 +880,9 @@ export const SchedulerPage: FunctionComponent = () => {
-
+
- {translate(schedulerMessages, "selectedScheduleTarget", { target: targetLabel(targetType, locale) })} + Selected schedule target: {targetLabel(targetType)}. {FORM_TARGET_OPTIONS.map((option) => ( ))}
{targetType === "sprint" && (