@@ -10,10 +10,15 @@ import { ScatterAndLineChart } from '@/components/Charts/ScatterAndLine';
1010import { useThemeColors } from '@/hooks/useThemeColors' ;
1111import { formatSlot , getExecutionClientColor } from '@/utils' ;
1212import { ClientLogo } from '@/components/Ethereum/ClientLogo' ;
13- import type { FctEngineNewPayloadWinrateHourly , IntEngineNewPayloadFastestExecutionByNodeClass } from '@/api/types.gen' ;
13+ import type {
14+ FctEngineNewPayloadWinrateHourly ,
15+ FctEngineNewPayloadWinrateDaily ,
16+ IntEngineNewPayloadFastestExecutionByNodeClass ,
17+ } from '@/api/types.gen' ;
1418import type { EngineTimingsData } from '../../hooks/useEngineTimingsData' ;
15- import { PER_SLOT_CHART_RANGES , type TimeRange } from '../../IndexPage.types' ;
19+ import { PER_SLOT_CHART_RANGES , HOURLY_CHART_RANGES , type TimeRange } from '../../IndexPage.types' ;
1620import { ClientVersionBreakdown } from '../ClientVersionBreakdown' ;
21+ import { RangeUnavailableNote } from '../RangeUnavailableNote' ;
1722
1823export interface NewPayloadTabProps {
1924 data : EngineTimingsData ;
@@ -30,12 +35,16 @@ export function NewPayloadTab({ data, timeRange }: NewPayloadTabProps): JSX.Elem
3035 newPayloadByElClient,
3136 newPayloadByElClientHourly,
3237 winrateHourly,
38+ winrateDaily,
3339 winratePerSlot,
3440 } = data ;
3541
3642 // Check if we should show per-slot charts (only for short time ranges)
3743 const showPerSlotCharts = PER_SLOT_CHART_RANGES . includes ( timeRange ) ;
3844
45+ // Long ranges (90d+) only fetch daily winrate data - hourly-based sections are unavailable
46+ const showHourlyCharts = HOURLY_CHART_RANGES . includes ( timeRange ) ;
47+
3948 // Filter to VALID status only for duration-based charts
4049 const validPayloadByElClient = newPayloadByElClient . filter ( r => r . status ?. toUpperCase ( ) === 'VALID' ) ;
4150
@@ -454,10 +463,12 @@ export function NewPayloadTab({ data, timeRange }: NewPayloadTabProps): JSX.Elem
454463 { /* Execution Winrate — fastest client per slot */ }
455464 < WinrateSection
456465 hourlyRecords = { winrateHourly }
466+ dailyRecords = { winrateDaily }
457467 perSlotRecords = { computedWinratePerSlot }
458468 allClients = { [ ...new Set ( [ ...hourlyClientList , ...elClientList ] ) ] }
459469 timeRange = { timeRange }
460470 showPerSlot = { showPerSlotCharts }
471+ showDaily = { ! showHourlyCharts }
461472 />
462473
463474 { /* Client Version Breakdown — VALID status only */ }
@@ -467,14 +478,18 @@ export function NewPayloadTab({ data, timeRange }: NewPayloadTabProps): JSX.Elem
467478 anchorId = "client-duration"
468479 modalSize = "full"
469480 >
470- { ( ) => (
471- < ClientVersionBreakdown
472- data = { validPayloadByElClient }
473- hourlyData = { newPayloadByElClientHourly }
474- hideRange
475- noCard
476- />
477- ) }
481+ { ( ) =>
482+ showHourlyCharts ? (
483+ < ClientVersionBreakdown
484+ data = { validPayloadByElClient }
485+ hourlyData = { newPayloadByElClientHourly }
486+ hideRange
487+ noCard
488+ />
489+ ) : (
490+ < RangeUnavailableNote />
491+ )
492+ }
478493 </ PopoutCard >
479494
480495 { /* Per-slot charts — short time ranges only */ }
@@ -570,7 +585,7 @@ export function NewPayloadTab({ data, timeRange }: NewPayloadTabProps): JSX.Elem
570585 ) }
571586
572587 { /* Hourly charts — longer time ranges */ }
573- { ! showPerSlotCharts && (
588+ { ! showPerSlotCharts && showHourlyCharts && (
574589 < div className = "grid grid-cols-1 gap-6 xl:grid-cols-2" >
575590 { hourlyTrendSeries . length > 0 && hourlyTrendSeries . some ( s => s . data . length > 0 ) && (
576591 < PopoutCard
@@ -826,16 +841,20 @@ function buildStandings(totalsByImpl: Map<string, number>, allClients: string[])
826841/** Winrate chart + standings for engine_newPayload fastest client per slot */
827842function WinrateSection ( {
828843 hourlyRecords,
844+ dailyRecords,
829845 perSlotRecords,
830846 allClients,
831847 timeRange,
832848 showPerSlot,
849+ showDaily,
833850} : {
834851 hourlyRecords : FctEngineNewPayloadWinrateHourly [ ] ;
852+ dailyRecords : FctEngineNewPayloadWinrateDaily [ ] ;
835853 perSlotRecords : IntEngineNewPayloadFastestExecutionByNodeClass [ ] ;
836854 allClients : string [ ] ;
837855 timeRange : TimeRange ;
838856 showPerSlot : boolean ;
857+ showDaily : boolean ;
839858} ) : JSX . Element | null {
840859 const { chartConfig, standings, tooltipLabels } = useMemo ( ( ) => {
841860 if ( showPerSlot ) {
@@ -928,7 +947,77 @@ function WinrateSection({
928947 } ;
929948 }
930949
931- // Hourly mode (24h / 7d)
950+ // Daily mode (90d+): one bar per day, win_count normalized by that day's total
951+ // Ties award full credit to each tied client, so totals may exceed slot counts
952+ if ( showDaily ) {
953+ if ( ! dailyRecords . length && ! allClients . length )
954+ return { chartConfig : null , standings : [ ] , tooltipLabels : [ ] as string [ ] } ;
955+
956+ const byDayAndImpl = new Map < string , Map < string , number > > ( ) ;
957+ const allImpls = new Set < string > ( ) ;
958+
959+ for ( const r of dailyRecords ) {
960+ const day = r . day_start_date ?? '' ;
961+ const impl = normalizeClient ( r . meta_execution_implementation ?? '' ) ;
962+ const count = r . win_count ?? 0 ;
963+ if ( ! impl || ! day ) continue ;
964+
965+ allImpls . add ( impl ) ;
966+ if ( ! byDayAndImpl . has ( day ) ) byDayAndImpl . set ( day , new Map ( ) ) ;
967+ const dayMap = byDayAndImpl . get ( day ) ! ;
968+ dayMap . set ( impl , ( dayMap . get ( impl ) ?? 0 ) + count ) ;
969+ }
970+
971+ // YYYY-MM-DD strings sort chronologically
972+ const sortedDays = [ ...byDayAndImpl . keys ( ) ] . sort ( ) ;
973+ if ( ! sortedDays . length ) return { chartConfig : null , standings : [ ] } ;
974+
975+ const totalsByDay = new Map < string , number > ( ) ;
976+ for ( const day of sortedDays ) {
977+ const implMap = byDayAndImpl . get ( day ) ! ;
978+ let total = 0 ;
979+ for ( const count of implMap . values ( ) ) total += count ;
980+ totalsByDay . set ( day , total ) ;
981+ }
982+
983+ const labels = sortedDays . map ( day => {
984+ const date = new Date ( day + 'T00:00:00Z' ) ;
985+ return date . toLocaleDateString ( [ ] , { month : 'short' , day : 'numeric' , timeZone : 'UTC' } ) ;
986+ } ) ;
987+
988+ const tooltipLabels = sortedDays . map ( day => {
989+ const date = new Date ( day + 'T00:00:00Z' ) ;
990+ return date . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' , year : 'numeric' , timeZone : 'UTC' } ) ;
991+ } ) ;
992+
993+ const sortedImpls = [ ...allImpls ] . sort ( ) ;
994+ const series : SeriesData [ ] = sortedImpls . map ( ( impl , i ) => ( {
995+ name : impl ,
996+ data : sortedDays . map ( day => {
997+ const count = byDayAndImpl . get ( day ) ?. get ( impl ) ?? 0 ;
998+ const total = totalsByDay . get ( day ) ?? 0 ;
999+ return total > 0 ? Number ( ( ( count / total ) * 100 ) . toFixed ( 2 ) ) : null ;
1000+ } ) ,
1001+ color : getExecutionClientColor ( impl , i ) ,
1002+ seriesType : 'bar' as const ,
1003+ stack : 'winrate' ,
1004+ } ) ) ;
1005+
1006+ const totalsByImpl = new Map < string , number > ( ) ;
1007+ for ( const r of dailyRecords ) {
1008+ const impl = normalizeClient ( r . meta_execution_implementation ?? '' ) ;
1009+ if ( ! impl ) continue ;
1010+ totalsByImpl . set ( impl , ( totalsByImpl . get ( impl ) ?? 0 ) + ( r . win_count ?? 0 ) ) ;
1011+ }
1012+
1013+ return {
1014+ chartConfig : { labels, series } ,
1015+ standings : buildStandings ( totalsByImpl , allClients ) ,
1016+ tooltipLabels,
1017+ } ;
1018+ }
1019+
1020+ // Hourly mode (24h / 7d / 31d)
9321021 if ( ! hourlyRecords . length && ! allClients . length )
9331022 return { chartConfig : null , standings : [ ] , tooltipLabels : [ ] as string [ ] } ;
9341023
@@ -1014,7 +1103,7 @@ function WinrateSection({
10141103 standings : buildStandings ( totalsByImpl , allClients ) ,
10151104 tooltipLabels,
10161105 } ;
1017- } , [ hourlyRecords , perSlotRecords , allClients , timeRange , showPerSlot ] ) ;
1106+ } , [ hourlyRecords , dailyRecords , perSlotRecords , allClients , timeRange , showPerSlot , showDaily ] ) ;
10181107
10191108 // Custom tooltip formatter that shows full timestamps instead of abbreviated axis labels
10201109 const winrateTooltipFormatter = useMemo ( ( ) => {
0 commit comments