Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/server/src/client/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export type EventsParams = {
entityKey?: string
from?: number
to?: number
sortBy?: string
sortDir?: "asc" | "desc"
/** Relative window in seconds; the caller derives `from` at fetch time. */
windowSeconds?: number
}
Expand Down Expand Up @@ -135,6 +137,7 @@ export type EventStats = {
stuck: number
skipped: number
last24h: number
previous24h: number
maintenance: {
cron: string
scheduledAt: string
Expand Down Expand Up @@ -170,6 +173,8 @@ export const api = {
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))
if (params.sortBy) qs.set("sortBy", params.sortBy)
if (params.sortDir) qs.set("sortDir", params.sortDir)

const res = await fetch(`/api/events?${qs.toString()}`)
if (!res.ok) throw new Error(`Failed to fetch events: ${res.status}`)
Expand Down
16 changes: 1 addition & 15 deletions apps/server/src/client/pages/container-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
useWorkspaceHealth,
} from "@/client/lib/queries"
import { GitHubLink } from "@/components/github-link"
import { StatusDot } from "@/components/run-status"
import { StatusBadge } from "@/components/status-badge"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import {
Expand Down Expand Up @@ -69,21 +70,6 @@ import {
// Status helpers
// ---------------------------------------------------------------------------

function StatusDot({ status }: { status: string }) {
const styles: Record<string, string> = {
working: "bg-yellow-500 animate-pulse",
busy: "bg-yellow-500 animate-pulse",
idle: "bg-green-500",
blocked: "bg-red-500",
failed: "bg-destructive",
interrupted: "bg-destructive",
cleanup_pending: "bg-destructive",
sync_unavailable: "bg-amber-500",
historical: "bg-muted-foreground/50",
}
return <span className={`inline-block size-2 rounded-full ${styles[status] ?? "bg-gray-400"}`} />
}

function modelLabel(model: string): string {
if (model.includes("claude-opus-4.8")) return "Claude Opus 4.8"
if (model.includes("grok-4.3")) return "Grok 4.3"
Expand Down
34 changes: 32 additions & 2 deletions apps/server/src/client/pages/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { ArrowRight, CheckCircle, CircleDashed, Clock, Hourglass, Warning } from "@phosphor-icons/react"
import {
ArrowDown,
ArrowRight,
ArrowUp,
CheckCircle,
CircleDashed,
Clock,
Hourglass,
Warning,
} from "@phosphor-icons/react"
import { Link, useNavigate } from "react-router-dom"
import { formatTimeAgo, repoGitHubUrl } from "@/client/lib/format"
import { useEventStats, useEvents } from "@/client/lib/queries"
Expand All @@ -10,6 +19,26 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Skeleton } from "@/components/ui/skeleton"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"

function TrendBadge({ current, previous }: { current: number; previous: number }) {
if (previous === 0 && current === 0) return null
const delta = current - previous
if (delta === 0) return null
const pct = previous > 0 ? Math.round((Math.abs(delta) / previous) * 100) : null
const rising = delta > 0
return (
<span
className={`inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium ${
rising
? "bg-amber-100 text-amber-700 dark:bg-amber-950/50 dark:text-amber-300"
: "bg-green-100 text-green-700 dark:bg-green-950/50 dark:text-green-300"
}`}
>
{rising ? <ArrowUp className="size-2.5" weight="bold" /> : <ArrowDown className="size-2.5" weight="bold" />}
{pct !== null ? `${pct}%` : `+${Math.abs(delta)}`}
</span>
)
}

function StatsCards() {
const { data: stats, isLoading, isError, dataUpdatedAt, isFetching, refetch } = useEventStats()

Expand Down Expand Up @@ -118,8 +147,9 @@ function StatsCards() {
<span>
<strong className="font-medium text-foreground">{stats?.skipped ?? 0}</strong> skipped
</span>
<span>
<span className="inline-flex items-center gap-1">
<strong className="font-medium text-foreground">{stats?.last24h ?? 0}</strong> events in the last 24h
<TrendBadge current={stats?.last24h ?? 0} previous={stats?.previous24h ?? 0} />
</span>
<span>
<strong className="font-medium text-foreground">{stats?.total ?? 0}</strong> total retained events
Expand Down
97 changes: 51 additions & 46 deletions apps/server/src/client/pages/events.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { CaretLeft, CaretRight, Funnel, ListBullets, MagnifyingGlass, Trash, X } from "@phosphor-icons/react"
import { Funnel, ListBullets, MagnifyingGlass, Trash, X } from "@phosphor-icons/react"
import { useEffect, useState } from "react"
import { useNavigate, useSearchParams } from "react-router-dom"
import { toast } from "sonner"
import { entityGitHubUrl, formatTimeAgo, repoGitHubUrl } from "@/client/lib/format"
import { useClearEvents, useEventStats, useEvents, useEventsGrouped } from "@/client/lib/queries"
import { GitHubLink } from "@/components/github-link"
import { LastUpdated } from "@/components/last-updated"
import { PageSizeSelector, PaginationFooter } from "@/components/pagination"
import { SortableTableHead, type SortDir, toggleSort } from "@/components/sortable-table-head"
import { StatusBadge } from "@/components/status-badge"
import {
AlertDialog,
Expand All @@ -26,7 +28,6 @@ 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 },
Expand Down Expand Up @@ -57,12 +58,16 @@ export default function EventsPage() {
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 sortBy = searchParams.get("sortBy") ?? "time"
const sortDir = (searchParams.get("sortDir") as SortDir) ?? "desc"

const { data, isLoading, isError, dataUpdatedAt, isFetching, refetch } = useEvents({
page,
limit,
status: statusFilter !== "all" ? statusFilter : undefined,
repo: repoFilter || undefined,
sortBy: sortBy !== "time" ? sortBy : undefined,
sortDir: sortDir !== "desc" ? sortDir : 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,
Expand All @@ -89,6 +94,10 @@ export default function EventsPage() {
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 handleSort = (column: string) => {
const next = toggleSort({ sortBy, sortDir }, column, "desc")
updateParams({ sortBy: next.sortBy, sortDir: next.sortDir, page: "1" })
}

const applyRepoFilter = () => {
// No-op when the committed value is unchanged so a plain focus/blur doesn't
Expand Down Expand Up @@ -124,7 +133,7 @@ export default function EventsPage() {
const hasActiveFilters = statusFilter !== "all" || !!repoFilter || timeRange !== "all"

return (
<div className="space-y-4">
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
Expand Down Expand Up @@ -239,14 +248,7 @@ export default function EventsPage() {
Clear filters
</Button>
)}
<div className="ml-auto flex items-center gap-1.5 text-xs text-muted-foreground">
<span>Per page:</span>
{PAGE_SIZES.map((s) => (
<Button key={s} variant={limit === s ? "secondary" : "ghost"} size="xs" onClick={() => setLimit(s)}>
{s}
</Button>
))}
</div>
<PageSizeSelector current={limit} onChange={setLimit} className="ml-auto" />
</div>

{/* Grouped view */}
Expand Down Expand Up @@ -353,12 +355,37 @@ export default function EventsPage() {
<Table>
<TableHeader>
<TableRow>
<TableHead>Event</TableHead>
<SortableTableHead
column="event"
label="Event"
sortBy={sortBy}
sortDir={sortDir}
onSort={handleSort}
/>
<TableHead>Entity</TableHead>
<TableHead>Repo</TableHead>
<TableHead>Sender</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Time</TableHead>
<SortableTableHead column="repo" label="Repo" sortBy={sortBy} sortDir={sortDir} onSort={handleSort} />
<SortableTableHead
column="sender"
label="Sender"
sortBy={sortBy}
sortDir={sortDir}
onSort={handleSort}
/>
<SortableTableHead
column="status"
label="Status"
sortBy={sortBy}
sortDir={sortDir}
onSort={handleSort}
/>
<SortableTableHead
column="time"
label="Time"
sortBy={sortBy}
sortDir={sortDir}
onSort={handleSort}
className="text-right"
/>
</TableRow>
</TableHeader>
<TableBody>
Expand Down Expand Up @@ -406,36 +433,14 @@ export default function EventsPage() {
</Card>

{/* Pagination */}
{pagination && pagination.totalPages > 1 && (
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">
Showing {(pagination.page - 1) * pagination.limit + 1}–
{Math.min(pagination.page * pagination.limit, pagination.total)} of {pagination.total}
</span>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="xs"
disabled={pagination.page <= 1}
onClick={() => setPage(pagination.page - 1)}
>
<CaretLeft className="size-3" />
Prev
</Button>
<span className="px-2 text-xs tabular-nums text-muted-foreground">
{pagination.page} / {pagination.totalPages}
</span>
<Button
variant="outline"
size="xs"
disabled={pagination.page >= pagination.totalPages}
onClick={() => setPage(pagination.page + 1)}
>
Next
<CaretRight className="size-3" />
</Button>
</div>
</div>
{pagination && (
<PaginationFooter
page={pagination.page}
limit={pagination.limit}
total={pagination.total}
totalPages={pagination.totalPages}
onPageChange={setPage}
/>
)}
</div>
)
Expand Down
Loading
Loading