feat: Reorder stats cards cache dashboard

This commit is contained in:
Janez T
2026-04-03 22:34:06 +02:00
parent 61a856e84c
commit d2fee94678
2 changed files with 92 additions and 92 deletions

View File

@@ -254,35 +254,57 @@ export function DashboardShell() {
/> />
</section> </section>
{/* Map + Traffic trend */} {/* Traffic breakdown */}
<section className="space-y-4"> <section className="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Reporter Locations</CardTitle> <CardTitle>Traffic Breakdown</CardTitle>
<CardDescription> <CardDescription>Observed traffic composition across all reporting nodes</CardDescription>
Approximate locations from Cloudflare edge nodes, not device GPS.
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="space-y-5">
<ReporterMap locations={summary?.locationPoints ?? []} /> {comp && compTotal > 0 ? (
<>
<div className="flex h-6 overflow-hidden rounded-full">
<div className="bg-primary" style={{ width: `${(comp.human / compTotal) * 100}%` }} title={`Messages: ${comp.human}`} />
<div className="bg-amber-400" style={{ width: `${(comp.acks / compTotal) * 100}%` }} title={`Acks: ${comp.acks}`} />
<div className="bg-secondary" style={{ width: `${(comp.overhead / compTotal) * 100}%` }} title={`Overhead: ${comp.overhead}`} />
</div>
<div className="grid grid-cols-3 gap-3 text-center">
<div>
<div className="mx-auto mb-1 h-2.5 w-2.5 rounded-full bg-primary" />
<div className="text-lg font-semibold tabular-nums">{comp.human.toLocaleString()}</div>
<div className="text-xs text-muted-foreground">Messages</div>
</div>
<div>
<div className="mx-auto mb-1 h-2.5 w-2.5 rounded-full bg-amber-400" />
<div className="text-lg font-semibold tabular-nums">{comp.acks.toLocaleString()}</div>
<div className="text-xs text-muted-foreground">Acks</div>
</div>
<div>
<div className="mx-auto mb-1 h-2.5 w-2.5 rounded-full bg-secondary" />
<div className="text-lg font-semibold tabular-nums">{comp.overhead.toLocaleString()}</div>
<div className="text-xs text-muted-foreground">Overhead</div>
</div>
</div>
</>
) : (
<EmptyState label="No traffic data yet." />
)}
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Observations Over Time</CardTitle> <CardTitle>Messages vs Protocol Overhead</CardTitle>
<CardDescription> <CardDescription>
Per-node average observations per {summary?.filter.bucket ?? "time"} bucket. Human messages (text + group text) compared to protocol overhead (acks, advertisements, routing, control) over time.
Normalizing by reporter count removes the bias of more nodes = higher numbers.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading && !summary ? ( {summary?.compositionOverTime.length ? (
<EmptyState label="Loading..." /> <CompositionChart points={summary.compositionOverTime} />
) : summary?.chartPoints.length ? (
<TrafficChart points={summary.chartPoints} maxValue={maxTrend} />
) : ( ) : (
<EmptyState label="No data for this window." /> <EmptyState label="No composition data yet." />
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -340,91 +362,67 @@ export function DashboardShell() {
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle>Path Routing Modes</CardTitle>
<CardDescription>
Path hash byte length determines routing precision.
Longer hashes allow more specific multi-hop paths.
</CardDescription>
</CardHeader>
<CardContent>
{activePathModes.length ? (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
{activePathModes.map((entry) => {
const pct = totalPackets > 0 ? ((entry.total / totalPackets) * 100).toFixed(1) : "0";
return (
<div key={entry.key} className="rounded-xl border border-border/50 bg-secondary/30 p-4 text-center">
<div className="mx-auto mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-sm font-bold text-primary">
{PATH_MODE_ICONS[entry.key] ?? "?"}
</div>
<div className="text-lg font-semibold tabular-nums">{entry.total.toLocaleString()}</div>
<div className="mt-0.5 text-xs text-muted-foreground">{entry.label}</div>
<div className="mt-1 text-xs text-muted-foreground">{pct}%</div>
</div>
);
})}
</div>
) : (
<EmptyState label="No path mode samples." />
)}
</CardContent>
</Card>
</section>
{/* Messages vs Overhead over time */}
<section className="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Messages vs Protocol Overhead</CardTitle> <CardTitle>Path Routing Modes</CardTitle>
<CardDescription> <CardDescription>
Human messages (text + group text) compared to protocol overhead (acks, advertisements, routing, control) over time. Path hash byte length determines routing precision.
Longer hashes allow more specific multi-hop paths.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{summary?.compositionOverTime.length ? ( {activePathModes.length ? (
<CompositionChart points={summary.compositionOverTime} /> <div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
{activePathModes.map((entry) => {
const pct = totalPackets > 0 ? ((entry.total / totalPackets) * 100).toFixed(1) : "0";
return (
<div key={entry.key} className="rounded-xl border border-border/50 bg-secondary/30 p-4 text-center">
<div className="mx-auto mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-sm font-bold text-primary">
{PATH_MODE_ICONS[entry.key] ?? "?"}
</div>
<div className="text-lg font-semibold tabular-nums">{entry.total.toLocaleString()}</div>
<div className="mt-0.5 text-xs text-muted-foreground">{entry.label}</div>
<div className="mt-1 text-xs text-muted-foreground">{pct}%</div>
</div>
);
})}
</div>
) : ( ) : (
<EmptyState label="No composition data yet." /> <EmptyState label="No path mode samples." />
)} )}
</CardContent> </CardContent>
</Card> </Card>
</section>
{/* Map + Traffic trend */}
<section className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Reporter Locations</CardTitle>
<CardDescription>
Approximate locations from Cloudflare edge nodes, not device GPS.
</CardDescription>
</CardHeader>
<CardContent>
<ReporterMap locations={summary?.locationPoints ?? []} />
</CardContent>
</Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Traffic Breakdown</CardTitle> <CardTitle>Observations Over Time</CardTitle>
<CardDescription>Observed traffic composition across all reporting nodes</CardDescription> <CardDescription>
Per-node average observations per {summary?.filter.bucket ?? "time"} bucket.
Normalizing by reporter count removes the bias of more nodes = higher numbers.
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-5"> <CardContent>
{comp && compTotal > 0 ? ( {isLoading && !summary ? (
<> <EmptyState label="Loading..." />
{/* stacked bar */} ) : summary?.chartPoints.length ? (
<div className="flex h-6 overflow-hidden rounded-full"> <TrafficChart points={summary.chartPoints} maxValue={maxTrend} />
<div className="bg-primary" style={{ width: `${(comp.human / compTotal) * 100}%` }} title={`Messages: ${comp.human}`} />
<div className="bg-amber-400" style={{ width: `${(comp.acks / compTotal) * 100}%` }} title={`Acks: ${comp.acks}`} />
<div className="bg-secondary" style={{ width: `${(comp.overhead / compTotal) * 100}%` }} title={`Overhead: ${comp.overhead}`} />
</div>
<div className="grid grid-cols-3 gap-3 text-center">
<div>
<div className="mx-auto mb-1 h-2.5 w-2.5 rounded-full bg-primary" />
<div className="text-lg font-semibold tabular-nums">{comp.human.toLocaleString()}</div>
<div className="text-xs text-muted-foreground">Messages</div>
</div>
<div>
<div className="mx-auto mb-1 h-2.5 w-2.5 rounded-full bg-amber-400" />
<div className="text-lg font-semibold tabular-nums">{comp.acks.toLocaleString()}</div>
<div className="text-xs text-muted-foreground">Acks</div>
</div>
<div>
<div className="mx-auto mb-1 h-2.5 w-2.5 rounded-full bg-secondary" />
<div className="text-lg font-semibold tabular-nums">{comp.overhead.toLocaleString()}</div>
<div className="text-xs text-muted-foreground">Overhead</div>
</div>
</div>
</>
) : ( ) : (
<EmptyState label="No traffic data yet." /> <EmptyState label="No data for this window." />
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -500,8 +498,8 @@ function CompositionChart({ points }: { points: CompositionPoint[] }) {
<div className="relative select-none"> <div className="relative select-none">
<svg <svg
viewBox={`${-pad.left} ${-pad.top} ${innerW + pad.left + pad.right} ${innerH + pad.top + pad.bottom}`} viewBox={`${-pad.left} ${-pad.top} ${innerW + pad.left + pad.right} ${innerH + pad.top + pad.bottom}`}
className="h-auto w-full" className="h-auto max-h-[280px] w-full"
preserveAspectRatio="none" preserveAspectRatio="xMidYMid meet"
onMouseLeave={() => setHover(null)} onMouseLeave={() => setHover(null)}
> >
<defs> <defs>
@@ -626,8 +624,8 @@ function TrafficChart({ points, maxValue: _rawMax }: { points: ChartPoint[]; max
<div className="relative select-none"> <div className="relative select-none">
<svg <svg
viewBox={`${-CHART_PAD.left} ${-CHART_PAD.top} ${innerW + CHART_PAD.left + CHART_PAD.right} ${CHART_H}`} viewBox={`${-CHART_PAD.left} ${-CHART_PAD.top} ${innerW + CHART_PAD.left + CHART_PAD.right} ${CHART_H}`}
className="h-auto w-full" className="h-auto max-h-[280px] w-full"
preserveAspectRatio="none" preserveAspectRatio="xMidYMid meet"
onMouseLeave={() => setHover(null)} onMouseLeave={() => setHover(null)}
> >
<defs> <defs>

View File

@@ -105,7 +105,9 @@ async function handleDashboard(request: Request, env: Env): Promise<Response> {
} }
const url = new URL(request.url); const url = new URL(request.url);
const summary = await loadDashboardSummary(env, url.searchParams.get("window")); const windowParam = url.searchParams.get("window");
const summary = await loadDashboardSummary(env, windowParam);
const cacheTtl = windowParam === "7d" || windowParam === "30d" ? 86400 : 60;
return Response.json( return Response.json(
{ {
generatedAt: new Date().toISOString(), generatedAt: new Date().toISOString(),
@@ -114,8 +116,8 @@ async function handleDashboard(request: Request, env: Env): Promise<Response> {
{ {
headers: { headers: {
...jsonHeaders, ...jsonHeaders,
"cache-control": "public, max-age=60, s-maxage=60", "cache-control": `public, max-age=${cacheTtl}, s-maxage=${cacheTtl}`,
"cdn-cache-control": "max-age=60", "cdn-cache-control": `max-age=${cacheTtl}`,
}, },
}, },
); );