import { useMemo, useState } from 'react'; import type { CreateScheduledTaskInput, DashboardTask, DashboardTaskAction, DashboardTaskContextMode, DashboardTaskScheduleType, UpdateScheduledTaskInput, } from './api'; import { EmptyState } from './EmptyState'; import { localeTags, type Locale, type Messages } from './i18n'; import { redactSecretsForPreview } from './redaction'; import { statusLabel } from './dashboardHelpers'; import { TaskActionButtons, type TaskActionKey } from './TaskActionButtons'; import './TaskPanel.css'; export type { TaskActionKey } from './TaskActionButtons'; type TaskGroupKey = 'watchers' | 'scheduled' | 'paused' | 'completed'; type TaskFilterKey = TaskGroupKey | 'all'; type TaskResultTone = 'ok' | 'fail' | 'none'; export interface RoomOption { jid: string; name: string; folder: string; } export interface TaskPanelProps { tasks: DashboardTask[]; rooms: RoomOption[]; locale: Locale; onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void; onTaskCreate: (input: CreateScheduledTaskInput) => void; onTaskUpdate: (task: DashboardTask, input: UpdateScheduledTaskInput) => void; taskActionKey: TaskActionKey | null; t: Messages; } interface TaskGroup { key: TaskGroupKey; tasks: DashboardTask[]; } interface TaskSummary { completed: number; nextTask: DashboardTask | null; paused: number; scheduled: number; total: number; watchers: number; } interface TaskCreateFormProps { rooms: RoomOption[]; onTaskCreate: (input: CreateScheduledTaskInput) => void; taskActionKey: TaskActionKey | null; t: Messages; } interface TaskGroupSectionProps { group: TaskGroup; locale: Locale; onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void; onTaskUpdate: (task: DashboardTask, input: UpdateScheduledTaskInput) => void; taskActionKey: TaskActionKey | null; t: Messages; } interface TaskCardProps { groupKey: TaskGroupKey; locale: Locale; onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void; onTaskUpdate: (task: DashboardTask, input: UpdateScheduledTaskInput) => void; task: DashboardTask; taskActionKey: TaskActionKey | null; t: Messages; } interface TaskDateProps { locale: Locale; t: Messages; task: DashboardTask; } interface TaskEditFormProps { onTaskUpdate: (task: DashboardTask, input: UpdateScheduledTaskInput) => void; task: DashboardTask; taskActionKey: TaskActionKey | null; t: Messages; } const TASK_TIME_FORMATTERS: Record = { en: new Intl.DateTimeFormat(localeTags.en, { hour: '2-digit', minute: '2-digit', hour12: false, }), ja: new Intl.DateTimeFormat(localeTags.ja, { hour: '2-digit', minute: '2-digit', hour12: false, }), ko: new Intl.DateTimeFormat(localeTags.ko, { hour: '2-digit', minute: '2-digit', hour12: false, }), zh: new Intl.DateTimeFormat(localeTags.zh, { hour: '2-digit', minute: '2-digit', hour12: false, }), }; const EN_TASK_DATE_TIME_FORMATTER = new Intl.DateTimeFormat(localeTags.en, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false, }); const RELATIVE_TIME_FORMATTERS: Record = { en: new Intl.RelativeTimeFormat(localeTags.en, { numeric: 'auto', style: 'short', }), ja: new Intl.RelativeTimeFormat(localeTags.ja, { numeric: 'auto', style: 'short', }), ko: new Intl.RelativeTimeFormat(localeTags.ko, { numeric: 'auto', style: 'short', }), zh: new Intl.RelativeTimeFormat(localeTags.zh, { numeric: 'auto', style: 'short', }), }; function formatTaskDate( value: string | null | undefined, locale: Locale, ): string { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const time = TASK_TIME_FORMATTERS[locale].format(date); if (locale === 'ko') return `${date.getMonth() + 1}월 ${date.getDate()}일 ${time}`; if (locale === 'ja' || locale === 'zh') return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`; return EN_TASK_DATE_TIME_FORMATTER.format(date); } function formatRelativeDate( value: string | null | undefined, locale: Locale, t: Messages, ): string { if (!value) return t.tasks.noTime; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const diffMs = date.getTime() - Date.now(); const absMs = Math.abs(diffMs); if (absMs < 45_000) return t.tasks.now; const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [ ['day', 86_400_000], ['hour', 3_600_000], ['minute', 60_000], ]; const [unit, unitMs] = units.find(([, threshold]) => absMs >= threshold) ?? units.at(-1)!; return RELATIVE_TIME_FORMATTERS[locale].format( Math.round(diffMs / unitMs), unit, ); } function safePreview( value: string | null | undefined, fallback: string, ): string { const cleaned = redactSecretsForPreview(value ?? '') .replace(/<\/?internal[^>]*>/gi, '') .replace(/\s+/g, ' ') .trim(); if (!cleaned) return fallback; return cleaned.length > 120 ? `${cleaned.slice(0, 120)}...` : cleaned; } function taskGroupKey(task: DashboardTask): TaskGroupKey { if (task.status === 'completed') return 'completed'; if (task.status === 'paused') return 'paused'; if (task.isWatcher) return 'watchers'; return 'scheduled'; } function taskResultTone(task: DashboardTask): TaskResultTone { if (!task.lastResult) return 'none'; const normalized = task.lastResult.toLowerCase(); if ( normalized.includes('fail') || normalized.includes('error') || normalized.includes('timeout') || normalized.includes('cancel') || normalized.includes('reject') ) { return 'fail'; } return 'ok'; } function taskDisplayName(task: DashboardTask, t: Messages): string { if (task.isWatcher) return t.tasks.ciWatch; if (task.scheduleType) return task.scheduleType; return task.id; } function taskHeadline(task: DashboardTask, t: Messages): string { if (task.isWatcher && task.ciProvider) { return `${t.tasks.ciWatch} · ${task.ciProvider}`; } return safePreview(task.promptPreview, taskDisplayName(task, t)); } function taskScheduleText(task: DashboardTask, t: Messages): string { const scheduleType = task.scheduleType === 'cron' || task.scheduleType === 'interval' || task.scheduleType === 'once' ? t.tasks.scheduleTypes[task.scheduleType] : task.scheduleType; return `${scheduleType} · ${task.scheduleValue || '-'}`; } function isTaskScheduleType( value: FormDataEntryValue | null, ): value is DashboardTaskScheduleType { return value === 'cron' || value === 'interval' || value === 'once'; } function isTaskContextMode( value: FormDataEntryValue | null, ): value is DashboardTaskContextMode { return value === 'group' || value === 'isolated'; } function readRequiredText(form: FormData, name: string): string | null { const value = form.get(name); if (typeof value !== 'string') return null; const trimmed = value.trim(); return trimmed ? trimmed : null; } function readTaskForm( form: FormData, includeRoom: true, ): CreateScheduledTaskInput | null; function readTaskForm( form: FormData, includeRoom: false, ): UpdateScheduledTaskInput | null; function readTaskForm( form: FormData, includeRoom: boolean, ): CreateScheduledTaskInput | UpdateScheduledTaskInput | null { const prompt = readRequiredText(form, 'prompt'); const scheduleValue = readRequiredText(form, 'scheduleValue'); const scheduleTypeValue = form.get('scheduleType'); if (!scheduleValue || !isTaskScheduleType(scheduleTypeValue)) { return null; } const scheduleType = scheduleTypeValue; if (!includeRoom) { return prompt ? { prompt, scheduleType, scheduleValue } : { scheduleType, scheduleValue }; } if (!prompt) { return null; } const roomJid = readRequiredText(form, 'roomJid'); const contextMode = form.get('contextMode'); if (!roomJid || !isTaskContextMode(contextMode)) return null; return { contextMode, prompt, roomJid, scheduleType, scheduleValue, }; } function buildTaskGroups(tasks: DashboardTask[]): TaskGroup[] { const groups: Record = { watchers: [], scheduled: [], paused: [], completed: [], }; for (const task of tasks) { groups[taskGroupKey(task)].push(task); } for (const groupTasks of Object.values(groups)) { groupTasks.sort((a, b) => (a.nextRun ?? a.lastRun ?? a.createdAt).localeCompare( b.nextRun ?? b.lastRun ?? b.createdAt, ), ); } return [ { key: 'watchers', tasks: groups.watchers }, { key: 'scheduled', tasks: groups.scheduled }, { key: 'paused', tasks: groups.paused }, { key: 'completed', tasks: groups.completed }, ]; } function buildTaskSummary(tasks: DashboardTask[]): TaskSummary { const summary: TaskSummary = { completed: 0, nextTask: null, paused: 0, scheduled: 0, total: tasks.length, watchers: 0, }; for (const task of tasks) { const group = taskGroupKey(task); if (group === 'watchers') summary.watchers += 1; if (group === 'scheduled') summary.scheduled += 1; if (group === 'paused') summary.paused += 1; if (group === 'completed') summary.completed += 1; if (task.status !== 'active' || !task.nextRun) continue; const nextTime = new Date(task.nextRun).getTime(); if (Number.isNaN(nextTime)) continue; const currentTime = summary.nextTask?.nextRun ? new Date(summary.nextTask.nextRun).getTime() : Number.POSITIVE_INFINITY; if (nextTime < currentTime) { summary.nextTask = task; } } return summary; } function TaskSummaryBar({ locale, summary, t, }: { locale: Locale; summary: TaskSummary; t: Messages; }) { const nextTask = summary.nextTask; return (
{t.tasks.next} {nextTask ? formatRelativeDate(nextTask.nextRun, locale, t) : '-'} {nextTask ? taskHeadline(nextTask, t) : summary.total > 0 ? `${summary.total} ${t.tasks.count}` : t.tasks.empty}
); } function TaskFilterTabs({ activeFilter, counts, onSelect, t, }: { activeFilter: TaskFilterKey; counts: Record; onSelect: (filter: TaskFilterKey) => void; t: Messages; }) { const tabs: Array<{ key: TaskFilterKey; label: string }> = [ { key: 'all', label: t.tasks.filterAll }, { key: 'scheduled', label: t.tasks.groups.scheduled }, { key: 'watchers', label: t.tasks.groups.watchers }, { key: 'paused', label: t.tasks.groups.paused }, { key: 'completed', label: t.tasks.groups.completed }, ]; return (
{tabs.map((tab) => ( ))}
); } function TaskCreateForm({ rooms, onTaskCreate, taskActionKey, t, }: TaskCreateFormProps) { return (
{t.tasks.createTitle}
{ event.preventDefault(); const form = new FormData(event.currentTarget); const input = readTaskForm(form, true); if (!input) return; onTaskCreate(input); event.currentTarget.reset(); }} >

{t.tasks.scheduleValueHint}