From 393d19e3f4a26146b8939bdf9c14e1300f81a92f Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:51:43 +0100 Subject: [PATCH] Move failed count and avg duration from page-derived to backend stats All stat card values now come from the /search/stats endpoint which queries the full time window, not just the current page of results. Consolidated into a single ClickHouse query for efficiency. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/search/ClickHouseSearchEngine.java | 22 ++++++++++--------- .../server/core/search/ExecutionStats.java | 10 +++++---- ui/src/api/openapi.json | 8 +++++++ ui/src/api/schema.d.ts | 2 ++ ui/src/pages/executions/ExecutionExplorer.tsx | 10 ++------- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java index 4bebd8ad..fe89222e 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/ClickHouseSearchEngine.java @@ -89,16 +89,18 @@ public class ClickHouseSearchEngine implements SearchEngine { @Override public ExecutionStats stats(Instant from, Instant to) { - Long p99 = jdbcTemplate.queryForObject( - "SELECT toInt64(quantile(0.99)(duration_ms)) FROM route_executions " + - "WHERE start_time >= ? AND start_time <= ?", - Long.class, Timestamp.from(from), Timestamp.from(to)); - Long active = jdbcTemplate.queryForObject( - "SELECT count() FROM route_executions WHERE status = 'RUNNING'", - Long.class); - return new ExecutionStats( - p99 != null ? p99 : 0L, - active != null ? active : 0L); + return jdbcTemplate.queryForObject( + "SELECT countIf(status = 'FAILED') AS failed_count, " + + "toInt64(avg(duration_ms)) AS avg_duration_ms, " + + "toInt64(quantile(0.99)(duration_ms)) AS p99_duration_ms, " + + "countIf(status = 'RUNNING') AS active_count " + + "FROM route_executions WHERE start_time >= ? AND start_time <= ?", + (rs, rowNum) -> new ExecutionStats( + rs.getLong("failed_count"), + rs.getLong("avg_duration_ms"), + rs.getLong("p99_duration_ms"), + rs.getLong("active_count")), + Timestamp.from(from), Timestamp.from(to)); } @Override diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java index 939d9f42..b4fac6b5 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionStats.java @@ -1,9 +1,11 @@ package com.cameleer3.server.core.search; /** - * Aggregate execution statistics. + * Aggregate execution statistics within a time window. * - * @param p99LatencyMs 99th percentile duration in milliseconds within the requested time window - * @param activeCount number of currently running executions + * @param failedCount number of failed executions + * @param avgDurationMs average duration in milliseconds + * @param p99LatencyMs 99th percentile duration in milliseconds + * @param activeCount number of currently running executions */ -public record ExecutionStats(long p99LatencyMs, long activeCount) {} +public record ExecutionStats(long failedCount, long avgDurationMs, long p99LatencyMs, long activeCount) {} diff --git a/ui/src/api/openapi.json b/ui/src/api/openapi.json index c647ae67..1dab1a6e 100644 --- a/ui/src/api/openapi.json +++ b/ui/src/api/openapi.json @@ -1049,6 +1049,14 @@ "ExecutionStats": { "type": "object", "properties": { + "failedCount": { + "type": "integer", + "format": "int64" + }, + "avgDurationMs": { + "type": "integer", + "format": "int64" + }, "p99LatencyMs": { "type": "integer", "format": "int64" diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts index 8703b1a0..5d52548a 100644 --- a/ui/src/api/schema.d.ts +++ b/ui/src/api/schema.d.ts @@ -217,6 +217,8 @@ export interface ProcessorNode { export type ProcessorSnapshot = Record; export interface ExecutionStats { + failedCount: number; + avgDurationMs: number; p99LatencyMs: number; activeCount: number; } diff --git a/ui/src/pages/executions/ExecutionExplorer.tsx b/ui/src/pages/executions/ExecutionExplorer.tsx index 1b04d707..8ad12ec0 100644 --- a/ui/src/pages/executions/ExecutionExplorer.tsx +++ b/ui/src/pages/executions/ExecutionExplorer.tsx @@ -24,12 +24,6 @@ export function ExecutionExplorer() { const total = data?.total ?? 0; const results = data?.data ?? []; - // Derive stats from current search results - const failedCount = results.filter((r) => r.status === 'FAILED').length; - const avgDuration = results.length > 0 - ? Math.round(results.reduce((sum, r) => sum + r.durationMs, 0) / results.length) - : 0; - const showFrom = total > 0 ? offset + 1 : 0; const showTo = Math.min(offset + limit, total); @@ -50,8 +44,8 @@ export function ExecutionExplorer() { {/* Stats Bar */}
- - + +