feat(web): status page and home regrouped around tasks, inbox and plan (FE-1)
Status page reads /system/health and history: level banner, process table, event-loop lag and SSE charts (monochrome recharts), database, engine, schedules, disks with thresholds and mount roots. Home now shows in-progress tasks, items needing attention, media overview, a plan strip with quick toggles, recent finished/failed tasks, cache and retention, source health with site account status, and a run-status summary. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d8b9bd9662
commit
4bb2583f6c
5 files changed
+1012
-182
No files matched your search
@@ -0,0 +1,104 @@
|
||||
import type { SystemHealthHistory } from "@umt/contracts";
|
||||
import { useMemo } from "react";
|
||||
import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
||||
import { EmptyState } from "~/components/console/empty-state.tsx";
|
||||
|
||||
// 心跳采样折线:无彩(前景色),多进程靠线型 + 图例区分,不靠颜色。
|
||||
|
||||
type Metric = "lagMaxMs" | "lagMs" | "sseConnections" | "runningRuns";
|
||||
|
||||
/** 同一色,不同线型;第 5 个起循环线型但图例仍带进程名 */
|
||||
const DASH = ["", "6 3", "2 3", "10 3 2 3"];
|
||||
const OPACITY = [0.9, 0.7, 0.55, 0.45];
|
||||
|
||||
function clock(iso: string, spanHours: number) {
|
||||
const d = new Date(iso);
|
||||
return spanHours > 24
|
||||
? d.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", hour12: false })
|
||||
: d.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false });
|
||||
}
|
||||
|
||||
export function seriesLabel(s: { processKind: string; instanceId: string }) {
|
||||
const pid = s.instanceId.split(":").at(-1);
|
||||
return `${s.processKind} · ${pid}`;
|
||||
}
|
||||
|
||||
export function HistoryChart({
|
||||
data,
|
||||
metric,
|
||||
unit,
|
||||
hours,
|
||||
height = 200,
|
||||
}: {
|
||||
data: SystemHealthHistory;
|
||||
metric: Metric;
|
||||
unit: string;
|
||||
hours: number;
|
||||
height?: number;
|
||||
}) {
|
||||
const series = data.series.filter((s) => s.points.some((p) => p[metric] !== null));
|
||||
const rows = useMemo(() => {
|
||||
const m = new Map<string, Record<string, number | string | null>>();
|
||||
for (const s of series)
|
||||
for (const p of s.points) {
|
||||
const row = m.get(p.at) ?? { at: p.at };
|
||||
row[s.instanceId] = p[metric];
|
||||
m.set(p.at, row);
|
||||
}
|
||||
return [...m.values()].sort((a, b) => String(a.at).localeCompare(String(b.at)));
|
||||
}, [series, metric]);
|
||||
|
||||
if (rows.length === 0) return <EmptyState plain title="这段时间没有采样" className="py-8" />;
|
||||
|
||||
return (
|
||||
<div className="text-muted-foreground" style={{ height }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={rows} margin={{ top: 8, right: 8, bottom: 0, left: -8 }}>
|
||||
<CartesianGrid stroke="var(--color-border)" strokeDasharray="2 4" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="at"
|
||||
tickFormatter={(v: string) => clock(v, hours)}
|
||||
tick={{ fontSize: 11, fill: "var(--color-muted-foreground)" }}
|
||||
stroke="var(--color-border)"
|
||||
minTickGap={40}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 11, fill: "var(--color-muted-foreground)" }}
|
||||
stroke="var(--color-border)"
|
||||
width={44}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
labelFormatter={(v) => new Date(String(v)).toLocaleString("zh-CN", { hour12: false })}
|
||||
formatter={(v) => [`${Math.round(Number(v) * 10) / 10} ${unit}`]}
|
||||
contentStyle={{
|
||||
background: "var(--color-popover)",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: 12,
|
||||
fontSize: 12,
|
||||
color: "var(--color-foreground)",
|
||||
}}
|
||||
cursor={{ stroke: "var(--color-muted-foreground)", strokeDasharray: "3 3" }}
|
||||
/>
|
||||
{series.length > 1 ? <Legend iconType="plainline" wrapperStyle={{ fontSize: 12 }} /> : null}
|
||||
{series.map((s, i) => (
|
||||
<Line
|
||||
key={s.instanceId}
|
||||
type="monotone"
|
||||
dataKey={s.instanceId}
|
||||
name={seriesLabel(s)}
|
||||
stroke="var(--color-foreground)"
|
||||
strokeOpacity={OPACITY[i % OPACITY.length]}
|
||||
strokeDasharray={DASH[i % DASH.length]}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 4, strokeWidth: 2, stroke: "var(--color-background)" }}
|
||||
connectNulls
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user