diff --git a/mcpjam-inspector/client/src/components/evals/overview-panel.tsx b/mcpjam-inspector/client/src/components/evals/overview-panel.tsx index 437c36cc6e..309830d5b0 100644 --- a/mcpjam-inspector/client/src/components/evals/overview-panel.tsx +++ b/mcpjam-inspector/client/src/components/evals/overview-panel.tsx @@ -54,13 +54,6 @@ function formatRelativeTime(timestamp?: number): string { return new Date(timestamp).toLocaleDateString(); } -function formatShortDate(timestamp: number): string { - return new Date(timestamp).toLocaleDateString(undefined, { - month: "short", - day: "numeric", - }); -} - /** Tiny inline sparkline rendered as CSS bars. */ function Sparkline({ data, @@ -91,11 +84,14 @@ function Sparkline({ interface RunBucket { id: string; - label: string; - date: string; + commitSha: string | null; + branch: string | null; timestamp: number; result: "passed" | "failed" | "mixed" | "running" | "pending"; runs: EvalSuiteRun[]; + suiteIds: Set; + passedCount: number; + failedCount: number; } function buildRunTimeline( @@ -108,23 +104,48 @@ function buildRunTimeline( // Sort by creation time const sorted = [...allRuns].sort((a, b) => a.createdAt - b.createdAt); - // Group runs that happened within 60s of each other (same batch) - const buckets: EvalSuiteRun[][] = []; - let currentBucket: EvalSuiteRun[] = [sorted[0]]; + // Group runs by commit SHA when available, else by 60s time proximity + const commitGroups = new Map(); + const manualRuns: EvalSuiteRun[] = []; - for (let i = 1; i < sorted.length; i++) { - const prev = currentBucket[currentBucket.length - 1]; - if (sorted[i].createdAt - prev.createdAt < 60_000) { - currentBucket.push(sorted[i]); + for (const run of sorted) { + const sha = run.ciMetadata?.commitSha; + if (sha) { + const group = commitGroups.get(sha) ?? []; + group.push(run); + commitGroups.set(sha, group); } else { - buckets.push(currentBucket); - currentBucket = [sorted[i]]; + manualRuns.push(run); } } - buckets.push(currentBucket); - // Dedupe by taking the latest N buckets - const recentBuckets = buckets.slice(-maxBuckets); + // Time-bucket manual runs (no commit SHA) + const manualBuckets: EvalSuiteRun[][] = []; + if (manualRuns.length > 0) { + let currentBucket: EvalSuiteRun[] = [manualRuns[0]]; + for (let i = 1; i < manualRuns.length; i++) { + const prev = currentBucket[currentBucket.length - 1]; + if (manualRuns[i].createdAt - prev.createdAt < 60_000) { + currentBucket.push(manualRuns[i]); + } else { + manualBuckets.push(currentBucket); + currentBucket = [manualRuns[i]]; + } + } + manualBuckets.push(currentBucket); + } + + // Merge commit groups + manual buckets, sort by latest timestamp + const allBucketRuns: EvalSuiteRun[][] = [ + ...Array.from(commitGroups.values()), + ...manualBuckets, + ].sort( + (a, b) => + Math.max(...a.map((r) => r.createdAt)) - + Math.max(...b.map((r) => r.createdAt)), + ); + + const recentBuckets = allBucketRuns.slice(-maxBuckets); return recentBuckets.map((runs, idx) => { const hasFailure = runs.some((r) => r.result === "failed"); @@ -142,17 +163,23 @@ function buildRunTimeline( : "mixed"; const timestamp = Math.max(...runs.map((r) => r.createdAt)); - // Use runNumber from first run if available - const runNum = runs[0]?.runNumber; - const label = runNum ? `#${runNum}` : `#${idx + 1}`; + const commitSha = runs[0]?.ciMetadata?.commitSha ?? null; + const branch = runs[0]?.ciMetadata?.branch ?? null; + const suiteIds = new Set(runs.map((r) => r.suiteId)); + + const passedCount = runs.filter((r) => r.result === "passed").length; + const failedCount = runs.filter((r) => r.result === "failed").length; return { - id: `bucket-${idx}`, - label, - date: formatShortDate(timestamp), + id: commitSha ?? `manual-${idx}`, + commitSha, + branch, timestamp, result, runs, + suiteIds, + passedCount, + failedCount, }; }); } @@ -294,10 +321,9 @@ export function OverviewPanel({ [filteredSuites], ); - // Auto-select latest bucket - const activeBucketId = - selectedBucketId ?? - (timeline.length > 0 ? timeline[timeline.length - 1].id : null); + // null = show all suites (no filter) + const activeBucketId = selectedBucketId; + const activeBucket = timeline.find((b) => b.id === activeBucketId) ?? null; // --------------------------------------------------------------------------- // Section D: Suite Table — severity-sorted, filtered, searchable @@ -305,6 +331,11 @@ export function OverviewPanel({ const tableSuites = useMemo(() => { let list = [...filteredSuites]; + // Filter by selected timeline bucket + if (activeBucket) { + list = list.filter((e) => activeBucket.suiteIds.has(e.suite._id)); + } + // Search filter if (suiteSearch) { const q = suiteSearch.toLowerCase(); @@ -334,14 +365,18 @@ export function OverviewPanel({ }); return list; - }, [filteredSuites, suiteSearch, failuresOnly]); + }, [filteredSuites, suiteSearch, failuresOnly, activeBucket]); - // Failure feed entries + // Failure feed entries (also filtered by active bucket) const failureEntries = useMemo(() => { - return filteredSuites.filter( + let list = filteredSuites; + if (activeBucket) { + list = list.filter((e) => activeBucket.suiteIds.has(e.suite._id)); + } + return list.filter( (e) => e.latestRun?.result === "failed" || !e.latestRun, ); - }, [filteredSuites]); + }, [filteredSuites, activeBucket]); // Auto-collapse failure feed when no failures const hasFailures = failureEntries.length > 0; @@ -536,6 +571,24 @@ export function OverviewPanel({ {timeline.length > 0 && (
+ {/* "All" chip to clear filter */} + + +
+ {timeline.map((bucket) => { const isActive = bucket.id === activeBucketId; const chipColor = @@ -547,12 +600,31 @@ export function OverviewPanel({ ? "bg-emerald-500" : "bg-muted-foreground"; + const chipLabel = bucket.commitSha + ? bucket.commitSha.slice(0, 7) + : "manual"; + + const totalRuns = bucket.runs.length; + const summaryParts: string[] = []; + if (bucket.passedCount > 0) summaryParts.push(`${bucket.passedCount}✓`); + if (bucket.failedCount > 0) summaryParts.push(`${bucket.failedCount}✗`); + const summaryText = summaryParts.length > 0 + ? summaryParts.join(" ") + : `${totalRuns} run${totalRuns !== 1 ? "s" : ""}`; + + const tooltipParts = [ + bucket.branch ? `${bucket.branch} @ ${chipLabel}` : chipLabel, + `${bucket.passedCount} passed, ${bucket.failedCount} failed of ${totalRuns}`, + new Date(bucket.timestamp).toLocaleString(), + ]; + return ( ); @@ -688,6 +763,17 @@ export function OverviewPanel({
{/* Table toolbar */}
+ {activeBucket && ( + + )}