@@ -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,16 +321,21 @@ export function OverviewPanel({
294321 [ filteredSuites ] ,
295322 ) ;
296323
297- // Auto-select latest bucket
298- const activeBucketId =
299- selectedBucketId ?? ( 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 ;
300327
301328 // ---------------------------------------------------------------------------
302329 // Section D: Suite Table — severity-sorted, filtered, searchable
303330 // ---------------------------------------------------------------------------
304331 const tableSuites = useMemo ( ( ) => {
305332 let list = [ ...filteredSuites ] ;
306333
334+ // Filter by selected timeline bucket
335+ if ( activeBucket ) {
336+ list = list . filter ( ( e ) => activeBucket . suiteIds . has ( e . suite . _id ) ) ;
337+ }
338+
307339 // Search filter
308340 if ( suiteSearch ) {
309341 const q = suiteSearch . toLowerCase ( ) ;
@@ -333,14 +365,18 @@ export function OverviewPanel({
333365 } ) ;
334366
335367 return list ;
336- } , [ filteredSuites , suiteSearch , failuresOnly ] ) ;
368+ } , [ filteredSuites , suiteSearch , failuresOnly , activeBucket ] ) ;
337369
338- // Failure feed entries
370+ // Failure feed entries (also filtered by active bucket)
339371 const failureEntries = useMemo ( ( ) => {
340- 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 (
341377 ( e ) => e . latestRun ?. result === "failed" || ! e . latestRun ,
342378 ) ;
343- } , [ filteredSuites ] ) ;
379+ } , [ filteredSuites , activeBucket ] ) ;
344380
345381 // Auto-collapse failure feed when no failures
346382 const hasFailures = failureEntries . length > 0 ;
@@ -534,6 +570,24 @@ export function OverviewPanel({
534570 { timeline . length > 0 && (
535571 < div className = "rounded-xl border bg-card p-3" >
536572 < div className = "flex items-center gap-2 overflow-x-auto pb-1" >
573+ { /* "All" chip to clear filter */ }
574+ < button
575+ onClick = { ( ) => setSelectedBucketId ( null ) }
576+ className = { cn (
577+ "flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[48px]" ,
578+ activeBucketId === null
579+ ? "bg-accent ring-2 ring-primary/30 shadow-sm"
580+ : "hover:bg-accent/50" ,
581+ ) }
582+ >
583+ < span className = "text-xs font-medium" > All</ span >
584+ < span className = "text-[10px] text-muted-foreground" >
585+ { timeline . reduce ( ( n , b ) => n + b . runs . length , 0 ) } runs
586+ </ span >
587+ </ button >
588+
589+ < div className = "w-px h-6 bg-border shrink-0" />
590+
537591 { timeline . map ( ( bucket ) => {
538592 const isActive = bucket . id === activeBucketId ;
539593 const chipColor =
@@ -545,12 +599,31 @@ export function OverviewPanel({
545599 ? "bg-emerald-500"
546600 : "bg-muted-foreground" ;
547601
602+ const chipLabel = bucket . commitSha
603+ ? bucket . commitSha . slice ( 0 , 7 )
604+ : "manual" ;
605+
606+ const totalRuns = bucket . runs . length ;
607+ const summaryParts : string [ ] = [ ] ;
608+ if ( bucket . passedCount > 0 ) summaryParts . push ( `${ bucket . passedCount } ✓` ) ;
609+ if ( bucket . failedCount > 0 ) summaryParts . push ( `${ bucket . failedCount } ✗` ) ;
610+ const summaryText = summaryParts . length > 0
611+ ? summaryParts . join ( " " )
612+ : `${ totalRuns } run${ totalRuns !== 1 ? "s" : "" } ` ;
613+
614+ const tooltipParts = [
615+ bucket . branch ? `${ bucket . branch } @ ${ chipLabel } ` : chipLabel ,
616+ `${ bucket . passedCount } passed, ${ bucket . failedCount } failed of ${ totalRuns } ` ,
617+ new Date ( bucket . timestamp ) . toLocaleString ( ) ,
618+ ] ;
619+
548620 return (
549621 < button
550622 key = { bucket . id }
551- onClick = { ( ) => setSelectedBucketId ( bucket . id ) }
623+ onClick = { ( ) => setSelectedBucketId ( bucket . id === activeBucketId ? null : bucket . id ) }
624+ title = { tooltipParts . join ( "\n" ) }
552625 className = { cn (
553- "flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[60px ]" ,
626+ "flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[68px ]" ,
554627 isActive
555628 ? "bg-accent ring-2 ring-primary/30 shadow-sm"
556629 : "hover:bg-accent/50" ,
@@ -560,10 +633,13 @@ export function OverviewPanel({
560633 < div
561634 className = { cn ( "h-2.5 w-2.5 rounded-full" , chipColor ) }
562635 />
563- < span className = "text-xs font-medium" > { bucket . label } </ span >
636+ < span className = "text-xs font-mono font- medium" > { chipLabel } </ span >
564637 </ div >
565638 < span className = "text-[10px] text-muted-foreground" >
566- { bucket . date }
639+ { formatRelativeTime ( bucket . timestamp ) }
640+ </ span >
641+ < span className = "text-[10px] text-muted-foreground" >
642+ { summaryText }
567643 </ span >
568644 </ button >
569645 ) ;
@@ -679,6 +755,17 @@ export function OverviewPanel({
679755 < div className = "rounded-xl border bg-card" >
680756 { /* Table toolbar */ }
681757 < div className = "flex items-center gap-2 px-4 py-2.5 border-b flex-wrap" >
758+ { activeBucket && (
759+ < button
760+ onClick = { ( ) => setSelectedBucketId ( null ) }
761+ 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"
762+ >
763+ < span className = "font-mono" >
764+ { activeBucket . commitSha ? activeBucket . commitSha . slice ( 0 , 7 ) : "manual" }
765+ </ span >
766+ < span > ×</ span >
767+ </ button >
768+ ) }
682769 < button
683770 onClick = { ( ) => setFailuresOnly ( ! failuresOnly ) }
684771 className = { cn (
0 commit comments