From cdf4c936307ace10ded91b5f58a940db8a6f1913 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:19:59 +0100 Subject: [PATCH] Make stats endpoint respect selected time window instead of hardcoded last hour P99 latency and active count now use the same from/to parameters as the timeseries sparklines, so all stat cards are consistent with the user's selected time range. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/controller/SearchController.java | 7 +++++-- .../app/search/ClickHouseSearchEngine.java | 6 +++--- .../server/core/search/ExecutionStats.java | 2 +- .../server/core/search/SearchEngine.java | 4 +++- .../server/core/search/SearchService.java | 4 ++-- ui/src/api/openapi.json | 20 +++++++++++++++++++ ui/src/api/queries/executions.ts | 15 +++++++++++--- ui/src/api/schema.d.ts | 6 ++++++ ui/src/pages/executions/ExecutionExplorer.tsx | 11 +++++----- 9 files changed, 57 insertions(+), 18 deletions(-) diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/SearchController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/SearchController.java index 922e443f..fd984d9c 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/SearchController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/SearchController.java @@ -70,8 +70,11 @@ public class SearchController { @GetMapping("/stats") @Operation(summary = "Aggregate execution stats (P99 latency, active count)") - public ResponseEntity stats() { - return ResponseEntity.ok(searchService.stats()); + public ResponseEntity stats( + @RequestParam Instant from, + @RequestParam(required = false) Instant to) { + Instant end = to != null ? to : Instant.now(); + return ResponseEntity.ok(searchService.stats(from, end)); } @GetMapping("/stats/timeseries") 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 67744177..ba449897 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 @@ -88,11 +88,11 @@ public class ClickHouseSearchEngine implements SearchEngine { } @Override - public ExecutionStats stats() { + public ExecutionStats stats(Instant from, Instant to) { Long p99 = jdbcTemplate.queryForObject( "SELECT quantile(0.99)(duration_ms) FROM route_executions " + - "WHERE start_time >= now() - INTERVAL 1 HOUR", - Long.class); + "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); 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 4094bf03..939d9f42 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 @@ -3,7 +3,7 @@ package com.cameleer3.server.core.search; /** * Aggregate execution statistics. * - * @param p99LatencyMs 99th percentile duration in milliseconds (last hour) + * @param p99LatencyMs 99th percentile duration in milliseconds within the requested time window * @param activeCount number of currently running executions */ public record ExecutionStats(long p99LatencyMs, long activeCount) {} diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchEngine.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchEngine.java index 3d5ee450..e88d385c 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchEngine.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchEngine.java @@ -28,9 +28,11 @@ public interface SearchEngine { /** * Compute aggregate stats: P99 latency and count of currently running executions. * + * @param from start of the time window + * @param to end of the time window * @return execution stats */ - ExecutionStats stats(); + ExecutionStats stats(java.time.Instant from, java.time.Instant to); /** * Compute bucketed time-series stats over a time window. diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchService.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchService.java index 2673983f..6ec5df48 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchService.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/SearchService.java @@ -32,8 +32,8 @@ public class SearchService { /** * Compute aggregate execution stats (P99 latency, active count). */ - public ExecutionStats stats() { - return engine.stats(); + public ExecutionStats stats(java.time.Instant from, java.time.Instant to) { + return engine.stats(from, to); } /** diff --git a/ui/src/api/openapi.json b/ui/src/api/openapi.json index 422a2de5..c647ae67 100644 --- a/ui/src/api/openapi.json +++ b/ui/src/api/openapi.json @@ -664,6 +664,26 @@ ], "summary": "Aggregate execution stats (P99 latency, active count)", "operationId": "stats", + "parameters": [ + { + "name": "from", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + } + ], "responses": { "200": { "description": "OK", diff --git a/ui/src/api/queries/executions.ts b/ui/src/api/queries/executions.ts index 974d7510..d54aa5af 100644 --- a/ui/src/api/queries/executions.ts +++ b/ui/src/api/queries/executions.ts @@ -2,14 +2,23 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../client'; import type { SearchRequest } from '../schema'; -export function useExecutionStats() { +export function useExecutionStats(timeFrom: string | undefined, timeTo: string | undefined) { return useQuery({ - queryKey: ['executions', 'stats'], + queryKey: ['executions', 'stats', timeFrom, timeTo], queryFn: async () => { - const { data, error } = await api.GET('/search/stats'); + const { data, error } = await api.GET('/search/stats', { + params: { + query: { + from: timeFrom!, + to: timeTo || undefined, + }, + }, + }); if (error) throw new Error('Failed to load stats'); return data!; }, + enabled: !!timeFrom, + placeholderData: (prev) => prev, refetchInterval: 10_000, }); } diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts index a3077833..8703b1a0 100644 --- a/ui/src/api/schema.d.ts +++ b/ui/src/api/schema.d.ts @@ -97,6 +97,12 @@ export interface paths { }; '/search/stats': { get: { + parameters: { + query: { + from: string; + to?: string; + }; + }; responses: { 200: { content: { diff --git a/ui/src/pages/executions/ExecutionExplorer.tsx b/ui/src/pages/executions/ExecutionExplorer.tsx index f5ccf80a..4662f387 100644 --- a/ui/src/pages/executions/ExecutionExplorer.tsx +++ b/ui/src/pages/executions/ExecutionExplorer.tsx @@ -10,11 +10,10 @@ export function ExecutionExplorer() { const { toSearchRequest, offset, limit, setOffset, live, toggleLive } = useExecutionSearch(); const searchRequest = toSearchRequest(); const { data, isLoading, isFetching } = useSearchExecutions(searchRequest, live); - const { data: stats } = useExecutionStats(); - const { data: timeseries } = useStatsTimeseries( - searchRequest.timeFrom ?? undefined, - searchRequest.timeTo ?? undefined, - ); + const timeFrom = searchRequest.timeFrom ?? undefined; + const timeTo = searchRequest.timeTo ?? undefined; + const { data: stats } = useExecutionStats(timeFrom, timeTo); + const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo); const sparkTotal = timeseries?.buckets.map((b) => b.totalCount) ?? []; const sparkFailed = timeseries?.buckets.map((b) => b.failedCount) ?? []; @@ -53,7 +52,7 @@ export function ExecutionExplorer() { - +