import { useQueryClient } from "@tanstack/react-query"; import { SCHEDULE_CHANGED_EVENT, type ScheduledJob, type ScheduleGroup } from "@umt/contracts"; import { AlertTriangle, CalendarClock, CircleCheck, LoaderCircle, Plus, XCircle } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router"; import { useScheduleBoard } from "~/api/schedules.ts"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { StatTile } from "~/components/console/stat-tile.tsx"; import { Button } from "~/components/ui/button.tsx"; import { qk } from "~/lib/query-keys.ts"; import { useSse } from "~/lib/sse.ts"; import { AgentCheckDialog } from "./agent-check-dialog.tsx"; import { GROUP_LABEL, GROUP_ORDER } from "./labels.ts"; import { ScheduleCard } from "./schedule-card.tsx"; // 定时任务看板(W7-C),IA-3 起是任务页的"计划"页签。卡片"运行记录"跳到任务流按该定时任务筛选。 /** 每秒一跳的当前时间(倒计时用) */ export function useNow(ms = 1000) { const [now, setNow] = useState(() => Date.now()); useEffect(() => { const t = setInterval(() => setNow(Date.now()), ms); return () => clearInterval(t); }, [ms]); return now; } /** SSE:调度器每次开始/结束一次运行都会发 schedule.changed;1 秒节流 */ export function useLiveSchedules() { const qc = useQueryClient(); const timer = useRef | null>(null); useSse([SCHEDULE_CHANGED_EVENT], () => { if (timer.current) return; timer.current = setTimeout(() => { timer.current = null; qc.invalidateQueries({ queryKey: qk.all("schedules") }); }, 1000); }); useEffect( () => () => { if (timer.current) clearTimeout(timer.current); }, [], ); return useScheduleBoard(); } /** 任务流里某个定时任务的筛选地址 */ export const scheduleStreamPath = (id: string) => `/console/tasks?tab=all&kind=schedule&ref=${encodeURIComponent(id)}`; export function ScheduleBoard({ highlightJob }: { highlightJob?: string }) { const _qc = useQueryClient(); const navigate = useNavigate(); const board = useLiveSchedules(); const now = useNow(); const [editing, setEditing] = useState<{ job: ScheduledJob | null; key: number } | null>(null); const data = board.data; const items = data?.items ?? []; const byGroup = new Map(); for (const g of GROUP_ORDER) byGroup.set(g, []); for (const j of items) byGroup.get(j.group)?.push(j); const stats = data?.stats; const val = (n: number | undefined) => (board.isPending ? "…" : board.isError ? "—" : (n ?? 0)); return ( <>

周期任务与 Agent 巡检的开关、间隔和下次运行。

0 ? "destructive" : "neutral"} value={val(stats?.failed)} />
{data && !data.schedulerOnline ? (

调度器未在线,任务不会按时运行。

) : (
)} {board.isPending ? ( ) : board.isError ? ( board.refetch()} /> ) : items.length === 0 ? ( ) : (
{GROUP_ORDER.map((g) => { const jobs = byGroup.get(g) ?? []; return (

{GROUP_LABEL[g]} {jobs.length}

{jobs.length === 0 ? (

{g === "agent" ? "还没有巡检。" : "还没有定时任务。"}

) : ( jobs.map((j) => ( navigate(scheduleStreamPath(job.id))} onEdit={(job) => setEditing({ job, key: Date.now() })} /> )) )}
); })}
)} {editing ? ( !o && setEditing(null)} /> ) : null} ); }