Add stat card sparkline graphs with timeseries backend endpoint
All checks were successful
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 45s
CI / deploy (push) Successful in 23s

New /search/stats/timeseries endpoint returns bucketed counts/metrics
over a time window using ClickHouse toStartOfInterval(). Frontend
Sparkline component renders SVG polyline + gradient fill on each
stat card, driven by a useStatsTimeseries query hook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-13 18:20:08 +01:00
parent cccd3f07be
commit 9e6e1b350a
10 changed files with 217 additions and 7 deletions

View File

@@ -1,4 +1,4 @@
import { useSearchExecutions, useExecutionStats } from '../../api/queries/executions';
import { useSearchExecutions, useExecutionStats, useStatsTimeseries } from '../../api/queries/executions';
import { useExecutionSearch } from './use-execution-search';
import { StatCard } from '../../components/shared/StatCard';
import { Pagination } from '../../components/shared/Pagination';
@@ -11,6 +11,16 @@ export function ExecutionExplorer() {
const searchRequest = toSearchRequest();
const { data, isLoading, isFetching } = useSearchExecutions(searchRequest);
const { data: stats } = useExecutionStats();
const { data: timeseries } = useStatsTimeseries(
searchRequest.timeFrom ?? undefined,
searchRequest.timeTo ?? undefined,
);
const sparkTotal = timeseries?.buckets.map((b) => b.totalCount) ?? [];
const sparkFailed = timeseries?.buckets.map((b) => b.failedCount) ?? [];
const sparkAvgDuration = timeseries?.buckets.map((b) => b.avgDurationMs) ?? [];
const sparkP99 = timeseries?.buckets.map((b) => b.p99DurationMs) ?? [];
const sparkActive = timeseries?.buckets.map((b) => b.activeCount) ?? [];
const total = data?.total ?? 0;
const results = data?.data ?? [];
@@ -40,11 +50,11 @@ export function ExecutionExplorer() {
{/* Stats Bar */}
<div className={styles.statsBar}>
<StatCard label="Total Matches" value={total.toLocaleString()} accent="amber" change={`from current search`} />
<StatCard label="Avg Duration" value={`${avgDuration}ms`} accent="cyan" />
<StatCard label="Failed (page)" value={failedCount.toString()} accent="rose" />
<StatCard label="P99 Latency" value={stats ? `${stats.p99LatencyMs}ms` : '--'} accent="green" change="last hour" />
<StatCard label="Active Now" value={stats ? stats.activeCount.toString() : '--'} accent="blue" change="running executions" />
<StatCard label="Total Matches" value={total.toLocaleString()} accent="amber" change={`from current search`} sparkData={sparkTotal} />
<StatCard label="Avg Duration" value={`${avgDuration}ms`} accent="cyan" sparkData={sparkAvgDuration} />
<StatCard label="Failed (page)" value={failedCount.toString()} accent="rose" sparkData={sparkFailed} />
<StatCard label="P99 Latency" value={stats ? `${stats.p99LatencyMs}ms` : '--'} accent="green" change="last hour" sparkData={sparkP99} />
<StatCard label="Active Now" value={stats ? stats.activeCount.toString() : '--'} accent="blue" change="running executions" sparkData={sparkActive} />
</div>
{/* Filters */}