Home: one sparkline style with a left fade, SI values, a full-width activity table, roomier panels

The trend curve was two components: a rich one in the node panel and on edges,
and a bare line in the flow table. It is one `Sparkline` now, taking the colour
token, the height and whether the live dot and the readout show. The flow table
draws its rollup in the chart ramp with no dot — the rollups are polled, so the
right edge is the last completed slice rather than this instant — and keeps its
"nothing yet" state, as the panel keeps its three distinct silences.

All of them fade out to the left, through an SVG mask over the curve and its
area. The dot sits outside the mask: the newest reading is the one thing that
must stay solid.

`si` replaces `compact` and the ad-hoc "k" the uPlot axis carried. It prefixes
k/M/G and m/µ, but only outside 0.01–1000, where the plain number is already
the shortest thing to read and a written unit ("0.4 ms") stays honest. The
sparkline readout asks for four digits, so two neighbouring readings never
collapse into one string. Exact counts and anything the user acts on — a
payload, a form field, the edge inspector's value — are left unrounded.
`src/lib/utils.check.ts` asserts the rounding cases.

The activity table now spans its card: the flow name anchors the left, the four
numbers read down their own centre, and the trend takes the slack on the right.
Panel rhythm steps up one notch, gap-5 to gap-6 outside and gap-2 to gap-3
within a section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
This commit is contained in:
2026-08-17 11:27:54 +02:00
co-authored by Claude Opus 5
parent 5483de13c0
commit 78c605bd37
10 changed files with 320 additions and 188 deletions
@@ -7,7 +7,7 @@ import { UplotChart } from "@/components/Common/UplotChart"
import { useEngineEvents } from "@/components/Flow/liveStore"
import { PANEL_SECTION } from "@/components/Flow/SidePanel"
import { Button } from "@/components/ui/button"
import { cn, compact } from "@/lib/utils"
import { cn, si } from "@/lib/utils"
import {
ago,
auditQueryOptions,
@@ -293,7 +293,7 @@ export function HealthActivity() {
{run.status}
</span>
<span className="w-16 text-right text-muted-foreground">
{compact(run.duration_ms)} ms
{si(run.duration_ms)} ms
</span>
<span className="w-16 text-right text-xs text-muted-foreground">
{ago(run.started_at)}
@@ -2,10 +2,10 @@ 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 { Sparkline } from "@/components/Common/Sparkline"
import { PANEL_SECTION } from "@/components/Flow/SidePanel"
import { Badge } from "@/components/ui/badge"
import { compact } from "@/lib/utils"
import { si } from "@/lib/utils"
import {
ago,
CARD,
@@ -34,7 +34,13 @@ function Tile({
)
}
/** A flow's execution trend, drawn from the 60 slices the rollup carries. */
/**
* A flow's execution trend, drawn from the 60 slices the rollup carries.
*
* The same curve the node panel and the edge popover draw, in the chart ramp
* this page's other graphs use. No live dot: the rollups are polled, so the
* right edge is the last completed slice rather than this instant.
*/
function Spark({ counts }: { counts: number[] }) {
const points: HistoryPoint[] = counts.map((value, index) => ({
ts: index,
@@ -43,22 +49,14 @@ function Spark({ counts }: { counts: number[] }) {
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>
<Sparkline
points={points}
color="var(--chart-1)"
height="h-6"
dot={false}
readout={false}
/>
)
}
@@ -129,8 +127,8 @@ export function HealthOverview() {
/>
<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`}
value={`${si(summary?.loop_lag.ewma ?? 0)} ms`}
note={`peak ${si(summary?.loop_lag.max_60s ?? 0)} ms in the last minute`}
/>
</div>
</section>
@@ -138,21 +136,27 @@ export function HealthOverview() {
<section className="grid gap-3">
<h2 className={PANEL_SECTION}>Flow activity (24h)</h2>
<div className={`${CARD} overflow-x-auto`}>
{/* The name anchors the left, the numbers read down their own
centre, and the trend takes whatever width is left over. */}
<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>
<th className="pb-2 text-left font-medium">Flow</th>
<th className="px-3 pb-2 text-center font-medium">
Executions
</th>
<th className="px-3 pb-2 text-center font-medium">Errors</th>
<th className="px-3 pb-2 text-center font-medium">Avg</th>
<th className="px-3 pb-2 text-center font-medium">Lag</th>
<th className="w-full min-w-32 pb-2 pl-3 text-right font-medium">
Trend
</th>
</tr>
</thead>
<tbody>
{(flows ?? []).map((row: FlowRollup) => (
<tr key={row.flow} className="border-t border-border">
<td className="py-2">
<td className="py-2 pr-3">
<Link
to="/flows/$flowName"
params={{ flowName: row.flow }}
@@ -161,17 +165,21 @@ export function HealthOverview() {
{row.flow || "—"}
</Link>
</td>
<td className="py-2">{row.executions}</td>
<td className="py-2">
<td className="px-3 py-2 text-center">{row.executions}</td>
<td className="px-3 py-2 text-center">
{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">
<td className="whitespace-nowrap px-3 py-2 text-center">
{si(row.avg_ms)} ms
</td>
<td className="whitespace-nowrap px-3 py-2 text-center">
{si(row.avg_lag_ms)} ms
</td>
<td className="py-2 pl-3 text-right">
<Spark counts={row.spark} />
</td>
</tr>