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]>
This commit is contained in:
1 parent
a68fcea13b
commit
d800987403
11 files changed
+1024
-3
No files matched your search
@@ -0,0 +1,94 @@
|
||||
import type {
|
||||
AgentCheckTrigger,
|
||||
ScheduleGroup,
|
||||
ScheduleRunStatus,
|
||||
ScheduleSpec,
|
||||
ScheduleTrigger,
|
||||
} from "@umt/contracts";
|
||||
import type { StatusTone } from "~/components/console/status-badge.tsx";
|
||||
|
||||
// 定时任务看板的标签映射(只在本页用,不放 lib/labels.ts)
|
||||
|
||||
export const GROUP_ORDER: ScheduleGroup[] = ["collect", "download", "organize", "maintain", "agent"];
|
||||
|
||||
export const GROUP_LABEL: Record<ScheduleGroup, string> = {
|
||||
collect: "采集",
|
||||
download: "下载",
|
||||
organize: "整理",
|
||||
maintain: "维护",
|
||||
agent: "Agent 巡检",
|
||||
};
|
||||
|
||||
export const RUN_STATUS: Record<ScheduleRunStatus, { label: string; tone: StatusTone }> = {
|
||||
ok: { label: "成功", tone: "success" },
|
||||
failed: { label: "失败", tone: "destructive" },
|
||||
skipped: { label: "已跳过", tone: "warning" },
|
||||
running: { label: "运行中", tone: "neutral" },
|
||||
};
|
||||
|
||||
export const TRIGGER_LABEL: Record<ScheduleTrigger, string> = {
|
||||
schedule: "定时",
|
||||
manual: "手动",
|
||||
skip: "跳过",
|
||||
};
|
||||
|
||||
export const CHECK_TRIGGER_LABEL: Record<AgentCheckTrigger, string> = {
|
||||
always: "每次都跑",
|
||||
open_decisions: "有待处理时",
|
||||
stalled_downloads: "有下载无进展时",
|
||||
subscriptions_active: "有追踪订阅时",
|
||||
};
|
||||
|
||||
export type IntervalUnit = "s" | "m" | "h";
|
||||
export const UNIT_MS: Record<IntervalUnit, number> = { s: 1000, m: 60_000, h: 3600_000 };
|
||||
export const UNIT_LABEL: Record<IntervalUnit, string> = { s: "秒", m: "分钟", h: "小时" };
|
||||
|
||||
/** 毫秒 → 最大整除单位 */
|
||||
export function splitInterval(ms: number): { value: number; unit: IntervalUnit } {
|
||||
if (ms % UNIT_MS.h === 0) return { value: ms / UNIT_MS.h, unit: "h" };
|
||||
if (ms % UNIT_MS.m === 0) return { value: ms / UNIT_MS.m, unit: "m" };
|
||||
return { value: Math.max(1, Math.round(ms / 1000)), unit: "s" };
|
||||
}
|
||||
|
||||
/** 调度的简短描述:"每 30 秒" / "cron 0 3 * * *" */
|
||||
export function describeSchedule(s: ScheduleSpec): string {
|
||||
if (s.type === "cron") return `cron ${s.cron}`;
|
||||
if (s.everyMs < 1000) return `每 ${s.everyMs} 毫秒`;
|
||||
const { value, unit } = splitInterval(s.everyMs);
|
||||
return `每 ${value} ${UNIT_LABEL[unit]}`;
|
||||
}
|
||||
|
||||
/** 毫秒耗时:"320 毫秒" / "2.4 秒" / "3 分 5 秒" */
|
||||
export function formatMs(ms: number | null | undefined): string {
|
||||
if (ms === null || ms === undefined) return "—";
|
||||
if (ms < 1000) return `${ms} 毫秒`;
|
||||
if (ms < 60_000) return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)} 秒`;
|
||||
const sec = Math.round(ms / 1000);
|
||||
if (sec < 3600) return `${Math.floor(sec / 60)} 分 ${sec % 60} 秒`;
|
||||
return `${Math.floor(sec / 3600)} 小时 ${Math.floor((sec % 3600) / 60)} 分`;
|
||||
}
|
||||
|
||||
/** 倒计时:"12 秒后" / "3 分 5 秒后" / "已到期" */
|
||||
export function countdown(iso: string, now: number): string {
|
||||
const diff = Math.round((new Date(iso).getTime() - now) / 1000);
|
||||
if (Number.isNaN(diff)) return "—";
|
||||
if (diff <= 0) return "即将运行";
|
||||
if (diff < 60) return `${diff} 秒后`;
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)} 分 ${diff % 60} 秒后`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)} 小时 ${Math.floor((diff % 3600) / 60)} 分后`;
|
||||
return `${Math.floor(diff / 86400)} 天 ${Math.floor((diff % 86400) / 3600)} 小时后`;
|
||||
}
|
||||
|
||||
export function formatClock(iso: string | null | undefined): string {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleString("zh-CN", {
|
||||
hour12: false,
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user