diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java index b7fc07b2..8c03b7e2 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java @@ -14,6 +14,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.sql.Timestamp; @@ -44,7 +45,9 @@ public class RouteCatalogController { @Operation(summary = "Get route catalog", description = "Returns all applications with their routes, agents, and health status") @ApiResponse(responseCode = "200", description = "Catalog returned") - public ResponseEntity> getCatalog() { + public ResponseEntity> getCatalog( + @RequestParam(required = false) String from, + @RequestParam(required = false) String to) { List allAgents = registryService.findAll(); // Group agents by application name @@ -63,9 +66,10 @@ public class RouteCatalogController { routesByApp.put(entry.getKey(), routes); } - // Query route-level stats for the last 24 hours + // Time range for exchange counts — use provided range or default to last 24h Instant now = Instant.now(); - Instant from24h = now.minus(24, ChronoUnit.HOURS); + Instant rangeFrom = from != null ? Instant.parse(from) : now.minus(24, ChronoUnit.HOURS); + Instant rangeTo = to != null ? Instant.parse(to) : now; Instant from1m = now.minus(1, ChronoUnit.MINUTES); // Route exchange counts from continuous aggregate @@ -82,7 +86,7 @@ public class RouteCatalogController { Timestamp ts = rs.getTimestamp("last_seen"); if (ts != null) routeLastSeen.put(key, ts.toInstant()); }, - Timestamp.from(from24h), Timestamp.from(now)); + Timestamp.from(rangeFrom), Timestamp.from(rangeTo)); } catch (Exception e) { // Continuous aggregate may not exist yet } diff --git a/ui/src/api/queries/catalog.ts b/ui/src/api/queries/catalog.ts index 2b9184da..db040b3e 100644 --- a/ui/src/api/queries/catalog.ts +++ b/ui/src/api/queries/catalog.ts @@ -3,13 +3,17 @@ import { config } from '../../config'; import { useAuthStore } from '../../auth/auth-store'; import { useRefreshInterval } from './use-refresh-interval'; -export function useRouteCatalog() { +export function useRouteCatalog(from?: string, to?: string) { const refetchInterval = useRefreshInterval(15_000); return useQuery({ - queryKey: ['routes', 'catalog'], + queryKey: ['routes', 'catalog', from, to], queryFn: async () => { const token = useAuthStore.getState().accessToken; - const res = await fetch(`${config.apiBaseUrl}/routes/catalog`, { + const params = new URLSearchParams(); + if (from) params.set('from', from); + if (to) params.set('to', to); + const qs = params.toString(); + const res = await fetch(`${config.apiBaseUrl}/routes/catalog${qs ? `?${qs}` : ''}`, { headers: { Authorization: `Bearer ${token}`, 'X-Cameleer-Protocol-Version': '1', @@ -18,6 +22,7 @@ export function useRouteCatalog() { if (!res.ok) throw new Error('Failed to load route catalog'); return res.json(); }, + placeholderData: (prev) => prev, refetchInterval, }); } diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 0ee06a13..982bd31a 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -1,5 +1,5 @@ import { Outlet, useNavigate, useLocation } from 'react-router'; -import { AppShell, Sidebar, TopBar, CommandPalette, CommandPaletteProvider, GlobalFilterProvider, ToastProvider, BreadcrumbProvider, useCommandPalette } from '@cameleer/design-system'; +import { AppShell, Sidebar, TopBar, CommandPalette, CommandPaletteProvider, GlobalFilterProvider, ToastProvider, BreadcrumbProvider, useCommandPalette, useGlobalFilters } from '@cameleer/design-system'; import type { SidebarApp, SearchResult } from '@cameleer/design-system'; import { useRouteCatalog } from '../api/queries/catalog'; import { useAgents } from '../api/queries/agents'; @@ -89,7 +89,8 @@ function useDebouncedValue(value: T, delayMs: number): T { function LayoutContent() { const navigate = useNavigate(); const location = useLocation(); - const { data: catalog } = useRouteCatalog(); + const { timeRange } = useGlobalFilters(); + const { data: catalog } = useRouteCatalog(timeRange.start.toISOString(), timeRange.end.toISOString()); const { data: agents } = useAgents(); const { username, logout } = useAuthStore(); const { open: paletteOpen, setOpen: setPaletteOpen } = useCommandPalette();