diff --git a/apps/server/src/client/lib/api.ts b/apps/server/src/client/lib/api.ts index 2ed15c0..a8dfd9e 100644 --- a/apps/server/src/client/lib/api.ts +++ b/apps/server/src/client/lib/api.ts @@ -12,6 +12,10 @@ export type EventsParams = { event?: string repo?: string entityKey?: string + from?: number + to?: number + /** Relative window in seconds; the caller derives `from` at fetch time. */ + windowSeconds?: number } export type AgentWorkItem = { @@ -164,6 +168,8 @@ export const api = { if (params.event) qs.set("event", params.event) if (params.repo) qs.set("repo", params.repo) if (params.entityKey) qs.set("entityKey", params.entityKey) + if (params.from != null) qs.set("from", String(params.from)) + if (params.to != null) qs.set("to", String(params.to)) const res = await fetch(`/api/events?${qs.toString()}`) if (!res.ok) throw new Error(`Failed to fetch events: ${res.status}`) diff --git a/apps/server/src/client/lib/queries.ts b/apps/server/src/client/lib/queries.ts index c74c342..9ef78cd 100644 --- a/apps/server/src/client/lib/queries.ts +++ b/apps/server/src/client/lib/queries.ts @@ -18,9 +18,16 @@ export function useSession() { } export function useEvents(params: EventsParams = {}, options: { enabled?: boolean } = {}) { + const { windowSeconds, ...rest } = params return useQuery({ queryKey: ["events", params], - queryFn: () => api.getEvents(params), + // Derive `from` at fetch time so each auto-refetch keeps a window relative + // to the current moment rather than the timestamp captured at render. + queryFn: () => + api.getEvents({ + ...rest, + from: windowSeconds ? Math.floor(Date.now() / 1000) - windowSeconds : undefined, + }), enabled: options.enabled ?? true, placeholderData: keepPreviousData, refetchInterval: 10_000, diff --git a/apps/server/src/client/pages/events.tsx b/apps/server/src/client/pages/events.tsx index 7fcd3bc..1a81c9c 100644 --- a/apps/server/src/client/pages/events.tsx +++ b/apps/server/src/client/pages/events.tsx @@ -21,12 +21,25 @@ import { import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" const STATUS_OPTIONS = ["all", "pending", "admitted", "settled", "completed", "failed", "skipped", "d:boot"] as const const PAGE_SIZES = [10, 25, 50] as const +const TIME_RANGES = [ + { value: "all", label: "All time", seconds: 0 }, + { value: "1h", label: "Last hour", seconds: 60 * 60 }, + { value: "6h", label: "Last 6 hours", seconds: 6 * 60 * 60 }, + { value: "24h", label: "Last 24 hours", seconds: 24 * 60 * 60 }, + { value: "7d", label: "Last 7 days", seconds: 7 * 24 * 60 * 60 }, +] as const + +type TimeRangeValue = (typeof TIME_RANGES)[number]["value"] + +const isTimeRangeValue = (value: string): value is TimeRangeValue => TIME_RANGES.some((r) => r.value === value) + export default function EventsPage() { const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() @@ -41,12 +54,18 @@ export default function EventsPage() { const limit = Number(searchParams.get("limit")) || 25 const statusFilter = searchParams.get("status") ?? "all" const repoFilter = searchParams.get("repo") ?? "" + const timeRangeParam = searchParams.get("timeRange") ?? "all" + const timeRange: TimeRangeValue = isTimeRangeValue(timeRangeParam) ? timeRangeParam : "all" + const windowSeconds = TIME_RANGES.find((r) => r.value === timeRange)?.seconds || undefined const { data, isLoading, isError, dataUpdatedAt, isFetching, refetch } = useEvents({ page, limit, status: statusFilter !== "all" ? statusFilter : undefined, repo: repoFilter || undefined, + // Pass the window size, not an absolute cutoff: `from` is derived at fetch + // time so each auto-refetch keeps the window relative to the current moment. + windowSeconds, }) // Sync input with URL param when navigating @@ -69,6 +88,7 @@ export default function EventsPage() { const setPage = (p: number) => updateParams({ page: String(p) }) const setLimit = (l: number) => updateParams({ limit: String(l), page: "1" }) const setStatus = (s: string) => updateParams({ status: s === "all" ? null : s, page: "1" }) + const setTimeRange = (r: string) => updateParams({ timeRange: r === "all" ? null : r, page: "1" }) const applyRepoFilter = () => { updateParams({ repo: repoInput || null, page: "1" }) @@ -96,7 +116,7 @@ export default function EventsPage() { } const pagination = data?.pagination - const hasActiveFilters = statusFilter !== "all" || !!repoFilter + const hasActiveFilters = statusFilter !== "all" || !!repoFilter || timeRange !== "all" return (