Files
rootandClaude Opus 5.5 d800987403 feat(web): scheduled jobs board (/console/schedules)
- cards grouped by collect / download / organize / maintain / agent checks: switch,
  in-place interval or cron edit, last result and duration, next-run countdown,
  failure reason, run now, skip next, reset to default, history drawer
- top stats (enabled / failed / running / total) and a scheduler-offline notice;
  refreshed by the schedule.changed SSE event with a 30s fallback
- agent check cards show goal template, trigger, budget and allowlist size; create /
  edit dialog (read-only capabilities, budget, trigger, interval, report to inbox)
- nav entry in the system group, route and smoke route

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 02:33:20 +08:00

87 lines
2.5 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type {
AgentCheckCreate,
ScheduleBoard,
ScheduledJob,
ScheduledJobRun,
ScheduleRunNowResult,
ScheduleUpdate,
} from "@umt/contracts";
import { apiData } from "~/lib/api.ts";
import { qk } from "~/lib/query-keys.ts";
// 定时任务看板(W7-C):GET /schedules 一次取全部卡片;写操作成功后整块失效(SSE schedule.changed 也会触发)。
const AREA = "schedules";
export function useScheduleBoard() {
return useQuery({
queryKey: qk.list(AREA),
queryFn: ({ signal }) => apiData<ScheduleBoard>("/schedules", { signal }),
// SSE 是主路径,30 秒兜底
refetchInterval: 30_000,
});
}
export function useScheduleRuns(id: string | null, enabled = true) {
return useQuery({
queryKey: qk.sub(AREA, "runs", id ?? ""),
queryFn: ({ signal }) =>
apiData<{ items: ScheduledJobRun[] }>("/schedules/{id}/runs", {
pathParams: { id: id ?? "" },
params: { limit: 50 },
signal,
}),
enabled: !!id && enabled,
});
}
function useInvalidate() {
const qc = useQueryClient();
return () => qc.invalidateQueries({ queryKey: qk.all(AREA) });
}
export function useUpdateSchedule() {
const invalidate = useInvalidate();
return useMutation({
mutationFn: ({ id, body }: { id: string; body: ScheduleUpdate }) =>
apiData<ScheduledJob>("/schedules/{id}", { method: "PATCH", pathParams: { id }, body }),
onSettled: invalidate,
});
}
export function useRunScheduleNow() {
const invalidate = useInvalidate();
return useMutation({
mutationFn: ({ id }: { id: string }) =>
apiData<ScheduleRunNowResult>("/schedules/{id}/run-now", { method: "POST", pathParams: { id } }),
onSettled: invalidate,
});
}
export function useSkipSchedule() {
const invalidate = useInvalidate();
return useMutation({
mutationFn: ({ id }: { id: string }) =>
apiData<ScheduledJob>("/schedules/{id}/skip", { method: "POST", pathParams: { id } }),
onSettled: invalidate,
});
}
export function useCreateAgentCheck() {
const invalidate = useInvalidate();
return useMutation({
mutationFn: (body: AgentCheckCreate) => apiData<ScheduledJob>("/schedules", { method: "POST", body }),
onSettled: invalidate,
});
}
export function useDeleteSchedule() {
const invalidate = useInvalidate();
return useMutation({
mutationFn: ({ id }: { id: string }) =>
apiData<{ ok: true }>("/schedules/{id}", { method: "DELETE", pathParams: { id } }),
onSettled: invalidate,
});
}