Files
cameleer-server/ui/src/pages/executions/ExecutionExplorer.tsx
hsiegeln 3641dffecc
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 48s
CI / deploy (push) Successful in 37s
Add comparison stats: failure rate %, vs-yesterday change, today total
Stats endpoint now returns current + previous period (24h shift) values
plus today's total count. UI shows:
- Total Matches: "of 12.3K today"
- Avg Duration: arrow + % vs yesterday
- Failure Rate: percentage of errors vs total, arrow + % vs yesterday
- P99 Latency: arrow + % vs yesterday
- In-Flight: unchanged (running executions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 09:29:14 +01:00

97 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useSearchExecutions, useExecutionStats, useStatsTimeseries } from '../../api/queries/executions';
import { useExecutionSearch } from './use-execution-search';
import { StatCard } from '../../components/shared/StatCard';
import { Pagination } from '../../components/shared/Pagination';
import { SearchFilters } from './SearchFilters';
import { ResultsTable } from './ResultsTable';
import styles from './ExecutionExplorer.module.css';
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 ExecutionExplorer() {
const { toSearchRequest, offset, limit, setOffset, live, toggleLive } = useExecutionSearch();
const searchRequest = toSearchRequest();
const { data, isLoading, isFetching } = useSearchExecutions(searchRequest, live);
const timeFrom = searchRequest.timeFrom ?? undefined;
const timeTo = searchRequest.timeTo ?? undefined;
const { data: stats } = useExecutionStats(timeFrom, timeTo);
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo);
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 ?? [];
// Failure rate as percentage
const failureRate = stats && stats.totalCount > 0
? (stats.failedCount / stats.totalCount) * 100 : 0;
const prevFailureRate = stats && stats.prevTotalCount > 0
? (stats.prevFailedCount / stats.prevTotalCount) * 100 : 0;
// Comparison vs yesterday
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);
return (
<>
{/* Page Header */}
<div className={`${styles.pageHeader} animate-in`}>
<div>
<h1>Route Explorer</h1>
<div className={styles.subtitle}>Search and analyze route executions</div>
</div>
<button className={`${styles.liveToggle} ${live ? styles.liveOn : styles.liveOff}`} onClick={toggleLive}>
<span className={styles.liveDot} />
{live ? 'LIVE' : 'PAUSED'}
</button>
</div>
{/* Stats Bar */}
<div className={styles.statsBar}>
<StatCard label="Total Matches" value={total.toLocaleString()} accent="amber" change={stats ? `of ${formatCompact(stats.totalToday)} today` : 'from current search'} sparkData={sparkTotal} />
<StatCard label="Avg Duration" value={stats ? `${stats.avgDurationMs.toLocaleString()}ms` : '--'} accent="cyan" change={avgChange?.text} changeDirection={avgChange?.direction} sparkData={sparkAvgDuration} />
<StatCard label="Failure Rate" value={stats ? `${failureRate.toFixed(1)}%` : '--'} accent="rose" change={failRateChange?.text} changeDirection={failRateChange?.direction} sparkData={sparkFailed} />
<StatCard label="P99 Latency" value={stats ? `${stats.p99LatencyMs.toLocaleString()}ms` : '--'} accent="green" change={p99Change?.text} changeDirection={p99Change?.direction} sparkData={sparkP99} />
<StatCard label="In-Flight" value={stats ? stats.activeCount.toLocaleString() : '--'} accent="blue" change="running executions" sparkData={sparkActive} />
</div>
{/* Filters */}
<SearchFilters />
{/* Results Header */}
<div className={`${styles.resultsHeader} animate-in delay-4`}>
<span className={styles.resultsCount}>
Showing <strong>{showFrom}{showTo}</strong> of <strong>{total.toLocaleString()}</strong> results
{isFetching && !isLoading && ' · updating...'}
</span>
</div>
{/* Results Table */}
<ResultsTable results={results} loading={isLoading} />
{/* Pagination */}
<Pagination total={total} offset={offset} limit={limit} onChange={setOffset} />
</>
);
}