Fold the brain and health screens into Home
Home is the one overview now: the brain graph flat across the top, the flow switches, then the health sections. The /brain and /health routes and their sidebar entries are gone. - charts report their cursor and clicks, so hovering one filters the list beside it to that minute and a click pins it until Escape or Clear - chart values round to about three significant digits, the legend mounts under the plot so it can wrap without leaving the card, and axis ticks shorten past a thousand - the embedded brain leaves the wheel to the page rather than zooming - number fields no longer draw their up/down spinner (NOTEPAD) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Link } from "@tanstack/react-router"
|
||||
|
||||
import type { FlowRollup, HistoryPoint } from "@/client"
|
||||
import { shape } from "@/components/Flow/MessageSparkline"
|
||||
import { PANEL_SECTION } from "@/components/Flow/SidePanel"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { compact } from "@/lib/utils"
|
||||
import {
|
||||
ago,
|
||||
CARD,
|
||||
failuresQueryOptions,
|
||||
flowRollupsQueryOptions,
|
||||
summaryQueryOptions,
|
||||
} from "./queries"
|
||||
|
||||
function Tile({
|
||||
label,
|
||||
value,
|
||||
note,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
note?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={CARD}>
|
||||
<div className={PANEL_SECTION}>{label}</div>
|
||||
<div className="mt-1 text-2xl">{value}</div>
|
||||
{note ? (
|
||||
<div className="text-xs text-muted-foreground">{note}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** A flow's execution trend, drawn from the 60 slices the rollup carries. */
|
||||
function Spark({ counts }: { counts: number[] }) {
|
||||
const points: HistoryPoint[] = counts.map((value, index) => ({
|
||||
ts: index,
|
||||
value,
|
||||
}))
|
||||
if (points.every((point) => point.value === 0)) {
|
||||
return <span className="text-xs text-muted-foreground">nothing yet</span>
|
||||
}
|
||||
const { line } = shape(points)
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
className="h-6 w-24 overflow-visible"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d={line}
|
||||
fill="none"
|
||||
stroke="var(--chart-1)"
|
||||
strokeWidth="1.5"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* How the engine is doing, and how each flow has been doing for a day.
|
||||
*
|
||||
* The tiles are the standing state; the table below is the same day the charts
|
||||
* cover, one row per flow.
|
||||
*/
|
||||
export function HealthOverview() {
|
||||
const { data: summary } = useQuery(summaryQueryOptions())
|
||||
const { data: flows } = useQuery(flowRollupsQueryOptions())
|
||||
// Shares the list below's cache entry, for the one tile that dates them.
|
||||
const { data: failures } = useQuery(failuresQueryOptions())
|
||||
|
||||
const queue = (summary?.queue ?? {}) as Record<string, number>
|
||||
const degraded = summary?.status === "degraded"
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="grid gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className={PANEL_SECTION}>Health</h2>
|
||||
<Badge variant={degraded ? "destructive" : "secondary"}>
|
||||
{degraded ? "Degraded" : "Running normally"}
|
||||
</Badge>
|
||||
</div>
|
||||
{summary?.problems.length ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{summary.problems.join(" · ")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<Tile
|
||||
label="Nodes"
|
||||
value={String(summary?.nodes.total ?? 0)}
|
||||
note={
|
||||
summary?.nodes.error
|
||||
? `${summary.nodes.error} failed to load`
|
||||
: "all loaded"
|
||||
}
|
||||
/>
|
||||
<Tile
|
||||
label="Flows running"
|
||||
value={`${summary?.flows.running ?? 0}/${summary?.flows.total ?? 0}`}
|
||||
note={
|
||||
summary?.flows.quarantined
|
||||
? `${summary.flows.quarantined} quarantined`
|
||||
: summary?.flows.paused
|
||||
? `${summary.flows.paused} paused`
|
||||
: "none paused"
|
||||
}
|
||||
/>
|
||||
<Tile
|
||||
label="Failures (24h)"
|
||||
value={String(summary?.failures_24h ?? 0)}
|
||||
note={
|
||||
failures?.length
|
||||
? `latest ${ago(failures[0].ts)}`
|
||||
: "nothing recorded"
|
||||
}
|
||||
/>
|
||||
<Tile
|
||||
label="Queue in flight"
|
||||
value={String(queue.pending ?? 0)}
|
||||
note={`${queue.delayed ?? 0} waiting · ${queue.parked ?? 0} parked`}
|
||||
/>
|
||||
<Tile
|
||||
label="Loop lag"
|
||||
value={`${compact(summary?.loop_lag.ewma ?? 0)} ms`}
|
||||
note={`peak ${compact(summary?.loop_lag.max_60s ?? 0)} ms in the last minute`}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-3">
|
||||
<h2 className={PANEL_SECTION}>Flow activity (24h)</h2>
|
||||
<div className={`${CARD} overflow-x-auto`}>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-muted-foreground">
|
||||
<tr className="text-left">
|
||||
<th className="pb-2 font-medium">Flow</th>
|
||||
<th className="pb-2 font-medium">Executions</th>
|
||||
<th className="pb-2 font-medium">Errors</th>
|
||||
<th className="pb-2 font-medium">Avg</th>
|
||||
<th className="pb-2 font-medium">Lag</th>
|
||||
<th className="pb-2 font-medium">Trend</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(flows ?? []).map((row: FlowRollup) => (
|
||||
<tr key={row.flow} className="border-t border-border">
|
||||
<td className="py-2">
|
||||
<Link
|
||||
to="/flows/$flowName"
|
||||
params={{ flowName: row.flow }}
|
||||
className="font-mono hover:underline"
|
||||
>
|
||||
{row.flow || "—"}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-2">{row.executions}</td>
|
||||
<td className="py-2">
|
||||
{row.errors ? (
|
||||
<Badge variant="destructive">{row.errors} failed</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground">none</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2">{compact(row.avg_ms)} ms</td>
|
||||
<td className="py-2">{compact(row.avg_lag_ms)} ms</td>
|
||||
<td className="py-2">
|
||||
<Spark counts={row.spark} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{flows?.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
className="py-6 text-center text-muted-foreground"
|
||||
>
|
||||
No flow has run in the last day.
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user