import { useState, useMemo, useCallback } from 'react'; import { useParams, NavLink } from 'react-router'; import { useAgents } from '../../api/queries/agents'; import { useSearchExecutions, useExecutionStats, useStatsTimeseries } from '../../api/queries/executions'; import { StatCard } from '../../components/shared/StatCard'; import { ResultsTable } from '../executions/ResultsTable'; import { Pagination } from '../../components/shared/Pagination'; import { FilterChip } from '../../components/shared/FilterChip'; import type { SearchRequest } from '../../api/types'; import styles from './AppScopedView.module.css'; function todayMidnight(): string { const d = new Date(); d.setHours(0, 0, 0, 0); const pad = (n: number) => n.toString().padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T00:00`; } function formatCompact(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return n.toLocaleString(); } function pctChange(current: number, previous: number): { text: string; direction: 'up' | 'down' | 'neutral' } { if (previous === 0) return { text: 'no prior data', direction: 'neutral' }; const pct = ((current - previous) / previous) * 100; if (Math.abs(pct) < 0.5) return { text: '~0% vs yesterday', direction: 'neutral' }; const arrow = pct > 0 ? '\u2191' : '\u2193'; return { text: `${arrow} ${Math.abs(pct).toFixed(1)}% vs yesterday`, direction: pct > 0 ? 'up' : 'down' }; } export function AppScopedView() { const { group } = useParams<{ group: string }>(); const { data: agents } = useAgents(); const [selectedRoute, setSelectedRoute] = useState(null); const [status, setStatus] = useState(['COMPLETED', 'FAILED']); const [live, setLive] = useState(true); const [offset, setOffset] = useState(0); const limit = 25; // Find agents belonging to this group const groupAgents = useMemo(() => { if (!agents || !group) return []; return agents.filter((a) => (a.group ?? 'default') === group); }, [agents, group]); const liveCount = groupAgents.filter((a) => a.status === 'LIVE').length; const staleCount = groupAgents.filter((a) => a.status === 'STALE').length; const deadCount = groupAgents.filter((a) => a.status === 'DEAD').length; // Collect unique routes from agents const routeIds = useMemo(() => { const set = new Set(); for (const a of groupAgents) { if (a.routeIds) for (const rid of a.routeIds) set.add(rid); } return Array.from(set).sort(); }, [groupAgents]); // Build search request scoped to this group const timeFrom = todayMidnight(); const timeFromIso = new Date(timeFrom).toISOString(); const searchRequest: SearchRequest = useMemo(() => ({ group: group || undefined, routeId: selectedRoute || undefined, status: status.length > 0 && status.length < 3 ? status.join(',') : undefined, timeFrom: timeFromIso, offset, limit, sortField: 'startTime', sortDir: 'desc', }), [group, selectedRoute, status, timeFromIso, offset, limit]); const { data, isLoading, isFetching } = useSearchExecutions(searchRequest, live); const { data: stats } = useExecutionStats(timeFromIso, undefined, selectedRoute || undefined, group); const { data: timeseries } = useStatsTimeseries(timeFromIso, undefined, selectedRoute || undefined, group); const sparkTotal = timeseries?.buckets.map((b) => b.totalCount) ?? []; const sparkFailed = timeseries?.buckets.map((b) => b.failedCount) ?? []; const sparkAvgDuration = timeseries?.buckets.map((b) => b.avgDurationMs) ?? []; const sparkP99 = timeseries?.buckets.map((b) => b.p99DurationMs) ?? []; const sparkActive = timeseries?.buckets.map((b) => b.activeCount) ?? []; const total = data?.total ?? 0; const results = data?.data ?? []; const failureRate = stats && stats.totalCount > 0 ? (stats.failedCount / stats.totalCount) * 100 : 0; const prevFailureRate = stats && stats.prevTotalCount > 0 ? (stats.prevFailedCount / stats.prevTotalCount) * 100 : 0; const avgChange = stats ? pctChange(stats.avgDurationMs, stats.prevAvgDurationMs) : null; const failRateChange = stats ? pctChange(failureRate, prevFailureRate) : null; const p99Change = stats ? pctChange(stats.p99LatencyMs, stats.prevP99LatencyMs) : null; const showFrom = total > 0 ? offset + 1 : 0; const showTo = Math.min(offset + limit, total); const toggleRoute = useCallback((rid: string) => { setSelectedRoute((prev) => prev === rid ? null : rid); setOffset(0); }, []); if (!group) { return
Missing group parameter
; } return ( <> {/* Breadcrumb */} {/* App Header */}
{group}
{liveCount > 0 && {liveCount} live} {staleCount > 0 && {staleCount} stale} {deadCount > 0 && {deadCount} dead} {groupAgents.length === 0 && no agents}
{/* Stats Bar */}
{/* Route Chips + Status Filters */}
setStatus((s) => s.includes('COMPLETED') ? s.filter((x) => x !== 'COMPLETED') : [...s, 'COMPLETED'])} /> setStatus((s) => s.includes('FAILED') ? s.filter((x) => x !== 'FAILED') : [...s, 'FAILED'])} /> setStatus((s) => s.includes('RUNNING') ? s.filter((x) => x !== 'RUNNING') : [...s, 'RUNNING'])} />
{/* Route Chips */} {routeIds.length > 0 && (
{routeIds.map((rid) => ( ))}
)} {/* Results Header */}
Showing {showFrom}–{showTo} of {total.toLocaleString()} results {isFetching && !isLoading && ' · updating...'}
{/* Results Table */} {/* Pagination */} ); }