From 7be88e1428f93f96d83f56e5ae5eff363dfea7bc Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Sat, 19 Sep 2026 15:09:19 +0000 Subject: [PATCH 1/2] add time range filter to events page Adds a time-range Select to the events filter bar with presets (last hour, 6 hours, 24 hours, 7 days, all time), synced to the URL via ?timeRange=. The backend GET /api/events now accepts from/to unix-second query params and filters webhook_events.created_at accordingly. Fixes #200 --- apps/server/src/client/lib/api.ts | 4 + apps/server/src/client/pages/events.tsx | 46 +++++++++- .../__tests__/events-time-filter.test.ts | 89 +++++++++++++++++++ apps/server/src/routes/events/index.ts | 10 ++- 4 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 apps/server/src/routes/__tests__/events-time-filter.test.ts diff --git a/apps/server/src/client/lib/api.ts b/apps/server/src/client/lib/api.ts index 2ed15c0..7edcd9c 100644 --- a/apps/server/src/client/lib/api.ts +++ b/apps/server/src/client/lib/api.ts @@ -12,6 +12,8 @@ export type EventsParams = { event?: string repo?: string entityKey?: string + from?: number + to?: number } export type AgentWorkItem = { @@ -164,6 +166,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/pages/events.tsx b/apps/server/src/client/pages/events.tsx index 7fcd3bc..3a0aae5 100644 --- a/apps/server/src/client/pages/events.tsx +++ b/apps/server/src/client/pages/events.tsx @@ -1,5 +1,5 @@ import { CaretLeft, CaretRight, Funnel, ListBullets, MagnifyingGlass, Trash, X } from "@phosphor-icons/react" -import { useEffect, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { useNavigate, useSearchParams } from "react-router-dom" import { toast } from "sonner" import { entityGitHubUrl, formatTimeAgo, repoGitHubUrl } from "@/client/lib/format" @@ -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,22 @@ 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" + + // Recompute the window boundary each render so "last hour" stays relative. + const from = useMemo(() => { + const range = TIME_RANGES.find((r) => r.value === timeRange) + if (!range || range.seconds === 0) return undefined + return Math.floor(Date.now() / 1000) - range.seconds + }, [timeRange]) const { data, isLoading, isError, dataUpdatedAt, isFetching, refetch } = useEvents({ page, limit, status: statusFilter !== "all" ? statusFilter : undefined, repo: repoFilter || undefined, + from, }) // Sync input with URL param when navigating @@ -69,6 +92,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 +120,7 @@ export default function EventsPage() { } const pagination = data?.pagination - const hasActiveFilters = statusFilter !== "all" || !!repoFilter + const hasActiveFilters = statusFilter !== "all" || !!repoFilter || timeRange !== "all" return (
@@ -185,13 +209,27 @@ export default function EventsPage() { )}
+ {hasActiveFilters && (