@@ -54,13 +54,6 @@ function formatRelativeTime(timestamp?: number): string {
5454 return new Date ( timestamp ) . toLocaleDateString ( ) ;
5555}
5656
57- function formatShortDate ( timestamp : number ) : string {
58- return new Date ( timestamp ) . toLocaleDateString ( undefined , {
59- month : "short" ,
60- day : "numeric" ,
61- } ) ;
62- }
63-
6457/** Tiny inline sparkline rendered as CSS bars. */
6558function Sparkline ( {
6659 data,
@@ -91,11 +84,14 @@ function Sparkline({
9184
9285interface RunBucket {
9386 id : string ;
94- label : string ;
95- date : string ;
87+ commitSha : string | null ;
88+ branch : string | null ;
9689 timestamp : number ;
9790 result : "passed" | "failed" | "mixed" | "running" | "pending" ;
9891 runs : EvalSuiteRun [ ] ;
92+ suiteIds : Set < string > ;
93+ passedCount : number ;
94+ failedCount : number ;
9995}
10096
10197function buildRunTimeline (
@@ -108,23 +104,48 @@ function buildRunTimeline(
108104 // Sort by creation time
109105 const sorted = [ ...allRuns ] . sort ( ( a , b ) => a . createdAt - b . createdAt ) ;
110106
111- // Group runs that happened within 60s of each other (same batch)
112- const buckets : EvalSuiteRun [ ] [ ] = [ ] ;
113- let currentBucket : EvalSuiteRun [ ] = [ sorted [ 0 ] ] ;
107+ // Group runs by commit SHA when available, else by 60s time proximity
108+ const commitGroups = new Map < string , EvalSuiteRun [ ] > ( ) ;
109+ const manualRuns : EvalSuiteRun [ ] = [ ] ;
114110
115- for ( let i = 1 ; i < sorted . length ; i ++ ) {
116- const prev = currentBucket [ currentBucket . length - 1 ] ;
117- if ( sorted [ i ] . createdAt - prev . createdAt < 60_000 ) {
118- currentBucket . push ( sorted [ i ] ) ;
111+ for ( const run of sorted ) {
112+ const sha = run . ciMetadata ?. commitSha ;
113+ if ( sha ) {
114+ const group = commitGroups . get ( sha ) ?? [ ] ;
115+ group . push ( run ) ;
116+ commitGroups . set ( sha , group ) ;
119117 } else {
120- buckets . push ( currentBucket ) ;
121- currentBucket = [ sorted [ i ] ] ;
118+ manualRuns . push ( run ) ;
122119 }
123120 }
124- buckets . push ( currentBucket ) ;
125121
126- // Dedupe by taking the latest N buckets
127- const recentBuckets = buckets . slice ( - maxBuckets ) ;
122+ // Time-bucket manual runs (no commit SHA)
123+ const manualBuckets : EvalSuiteRun [ ] [ ] = [ ] ;
124+ if ( manualRuns . length > 0 ) {
125+ let currentBucket : EvalSuiteRun [ ] = [ manualRuns [ 0 ] ] ;
126+ for ( let i = 1 ; i < manualRuns . length ; i ++ ) {
127+ const prev = currentBucket [ currentBucket . length - 1 ] ;
128+ if ( manualRuns [ i ] . createdAt - prev . createdAt < 60_000 ) {
129+ currentBucket . push ( manualRuns [ i ] ) ;
130+ } else {
131+ manualBuckets . push ( currentBucket ) ;
132+ currentBucket = [ manualRuns [ i ] ] ;
133+ }
134+ }
135+ manualBuckets . push ( currentBucket ) ;
136+ }
137+
138+ // Merge commit groups + manual buckets, sort by latest timestamp
139+ const allBucketRuns : EvalSuiteRun [ ] [ ] = [
140+ ...Array . from ( commitGroups . values ( ) ) ,
141+ ...manualBuckets ,
142+ ] . sort (
143+ ( a , b ) =>
144+ Math . max ( ...a . map ( ( r ) => r . createdAt ) ) -
145+ Math . max ( ...b . map ( ( r ) => r . createdAt ) ) ,
146+ ) ;
147+
148+ const recentBuckets = allBucketRuns . slice ( - maxBuckets ) ;
128149
129150 return recentBuckets . map ( ( runs , idx ) => {
130151 const hasFailure = runs . some ( ( r ) => r . result === "failed" ) ;
@@ -142,17 +163,23 @@ function buildRunTimeline(
142163 : "mixed" ;
143164
144165 const timestamp = Math . max ( ...runs . map ( ( r ) => r . createdAt ) ) ;
145- // Use runNumber from first run if available
146- const runNum = runs [ 0 ] ?. runNumber ;
147- const label = runNum ? `#${ runNum } ` : `#${ idx + 1 } ` ;
166+ const commitSha = runs [ 0 ] ?. ciMetadata ?. commitSha ?? null ;
167+ const branch = runs [ 0 ] ?. ciMetadata ?. branch ?? null ;
168+ const suiteIds = new Set ( runs . map ( ( r ) => r . suiteId ) ) ;
169+
170+ const passedCount = runs . filter ( ( r ) => r . result === "passed" ) . length ;
171+ const failedCount = runs . filter ( ( r ) => r . result === "failed" ) . length ;
148172
149173 return {
150- id : `bucket -${ idx } `,
151- label ,
152- date : formatShortDate ( timestamp ) ,
174+ id : commitSha ?? `manual -${ idx } `,
175+ commitSha ,
176+ branch ,
153177 timestamp,
154178 result,
155179 runs,
180+ suiteIds,
181+ passedCount,
182+ failedCount,
156183 } ;
157184 } ) ;
158185}
@@ -294,17 +321,21 @@ export function OverviewPanel({
294321 [ filteredSuites ] ,
295322 ) ;
296323
297- // Auto-select latest bucket
298- const activeBucketId =
299- selectedBucketId ??
300- ( timeline . length > 0 ? timeline [ timeline . length - 1 ] . id : null ) ;
324+ // null = show all suites (no filter)
325+ const activeBucketId = selectedBucketId ;
326+ const activeBucket = timeline . find ( ( b ) => b . id === activeBucketId ) ?? null ;
301327
302328 // ---------------------------------------------------------------------------
303329 // Section D: Suite Table — severity-sorted, filtered, searchable
304330 // ---------------------------------------------------------------------------
305331 const tableSuites = useMemo ( ( ) => {
306332 let list = [ ...filteredSuites ] ;
307333
334+ // Filter by selected timeline bucket
335+ if ( activeBucket ) {
336+ list = list . filter ( ( e ) => activeBucket . suiteIds . has ( e . suite . _id ) ) ;
337+ }
338+
308339 // Search filter
309340 if ( suiteSearch ) {
310341 const q = suiteSearch . toLowerCase ( ) ;
@@ -334,14 +365,18 @@ export function OverviewPanel({
334365 } ) ;
335366
336367 return list ;
337- } , [ filteredSuites , suiteSearch , failuresOnly ] ) ;
368+ } , [ filteredSuites , suiteSearch , failuresOnly , activeBucket ] ) ;
338369
339- // Failure feed entries
370+ // Failure feed entries (also filtered by active bucket)
340371 const failureEntries = useMemo ( ( ) => {
341- return filteredSuites . filter (
372+ let list = filteredSuites ;
373+ if ( activeBucket ) {
374+ list = list . filter ( ( e ) => activeBucket . suiteIds . has ( e . suite . _id ) ) ;
375+ }
376+ return list . filter (
342377 ( e ) => e . latestRun ?. result === "failed" || ! e . latestRun ,
343378 ) ;
344- } , [ filteredSuites ] ) ;
379+ } , [ filteredSuites , activeBucket ] ) ;
345380
346381 // Auto-collapse failure feed when no failures
347382 const hasFailures = failureEntries . length > 0 ;
@@ -536,6 +571,24 @@ export function OverviewPanel({
536571 { timeline . length > 0 && (
537572 < div className = "rounded-xl border bg-card p-3" >
538573 < div className = "flex items-center gap-2 overflow-x-auto pb-1" >
574+ { /* "All" chip to clear filter */ }
575+ < button
576+ onClick = { ( ) => setSelectedBucketId ( null ) }
577+ className = { cn (
578+ "flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[48px]" ,
579+ activeBucketId === null
580+ ? "bg-accent ring-2 ring-primary/30 shadow-sm"
581+ : "hover:bg-accent/50" ,
582+ ) }
583+ >
584+ < span className = "text-xs font-medium" > All</ span >
585+ < span className = "text-[10px] text-muted-foreground" >
586+ { timeline . reduce ( ( n , b ) => n + b . runs . length , 0 ) } runs
587+ </ span >
588+ </ button >
589+
590+ < div className = "w-px h-6 bg-border shrink-0" />
591+
539592 { timeline . map ( ( bucket ) => {
540593 const isActive = bucket . id === activeBucketId ;
541594 const chipColor =
@@ -547,12 +600,31 @@ export function OverviewPanel({
547600 ? "bg-emerald-500"
548601 : "bg-muted-foreground" ;
549602
603+ const chipLabel = bucket . commitSha
604+ ? bucket . commitSha . slice ( 0 , 7 )
605+ : "manual" ;
606+
607+ const totalRuns = bucket . runs . length ;
608+ const summaryParts : string [ ] = [ ] ;
609+ if ( bucket . passedCount > 0 ) summaryParts . push ( `${ bucket . passedCount } ✓` ) ;
610+ if ( bucket . failedCount > 0 ) summaryParts . push ( `${ bucket . failedCount } ✗` ) ;
611+ const summaryText = summaryParts . length > 0
612+ ? summaryParts . join ( " " )
613+ : `${ totalRuns } run${ totalRuns !== 1 ? "s" : "" } ` ;
614+
615+ const tooltipParts = [
616+ bucket . branch ? `${ bucket . branch } @ ${ chipLabel } ` : chipLabel ,
617+ `${ bucket . passedCount } passed, ${ bucket . failedCount } failed of ${ totalRuns } ` ,
618+ new Date ( bucket . timestamp ) . toLocaleString ( ) ,
619+ ] ;
620+
550621 return (
551622 < button
552623 key = { bucket . id }
553- onClick = { ( ) => setSelectedBucketId ( bucket . id ) }
624+ onClick = { ( ) => setSelectedBucketId ( bucket . id === activeBucketId ? null : bucket . id ) }
625+ title = { tooltipParts . join ( "\n" ) }
554626 className = { cn (
555- "flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[60px ]" ,
627+ "flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[68px ]" ,
556628 isActive
557629 ? "bg-accent ring-2 ring-primary/30 shadow-sm"
558630 : "hover:bg-accent/50" ,
@@ -562,10 +634,13 @@ export function OverviewPanel({
562634 < div
563635 className = { cn ( "h-2.5 w-2.5 rounded-full" , chipColor ) }
564636 />
565- < span className = "text-xs font-medium" > { bucket . label } </ span >
637+ < span className = "text-xs font-mono font- medium" > { chipLabel } </ span >
566638 </ div >
567639 < span className = "text-[10px] text-muted-foreground" >
568- { bucket . date }
640+ { formatRelativeTime ( bucket . timestamp ) }
641+ </ span >
642+ < span className = "text-[10px] text-muted-foreground" >
643+ { summaryText }
569644 </ span >
570645 </ button >
571646 ) ;
@@ -688,6 +763,17 @@ export function OverviewPanel({
688763 < div className = "rounded-xl border bg-card" >
689764 { /* Table toolbar */ }
690765 < div className = "flex items-center gap-2 px-4 py-2.5 border-b flex-wrap" >
766+ { activeBucket && (
767+ < button
768+ onClick = { ( ) => setSelectedBucketId ( null ) }
769+ className = "text-xs px-2.5 py-1 rounded-full border bg-primary/10 text-primary border-primary/30 hover:bg-primary/20 transition-colors flex items-center gap-1"
770+ >
771+ < span className = "font-mono" >
772+ { activeBucket . commitSha ? activeBucket . commitSha . slice ( 0 , 7 ) : "manual" }
773+ </ span >
774+ < span > ×</ span >
775+ </ button >
776+ ) }
691777 < button
692778 onClick = { ( ) => setFailuresOnly ( ! failuresOnly ) }
693779 className = { cn (
0 commit comments