diff --git a/src/pages/ethereum/consensus/overview/IndexPage.tsx b/src/pages/ethereum/consensus/overview/IndexPage.tsx index f3823d5f9..9426109d4 100644 --- a/src/pages/ethereum/consensus/overview/IndexPage.tsx +++ b/src/pages/ethereum/consensus/overview/IndexPage.tsx @@ -59,6 +59,8 @@ import { formatTooltipDate, buildTooltipHtml, formatBand, + dailyWindowStartDate, + trimDailyRecords, buildBlobCountChartConfig, buildAttestationParticipationChartConfig, buildHeadVoteCorrectnessChartConfig, @@ -92,6 +94,10 @@ export function IndexPage(): JSX.Element { return now - config.days * 24 * 60 * 60; }, [config.days]); + // Daily endpoints have no server-side range filter on their primary key, so the + // selected window is applied to daily records client-side via this cutoff date. + const dailyStartDate = useMemo(() => dailyWindowStartDate(config.days), [config.days]); + // --- Queries --- const blobHourlyQuery = useQuery({ @@ -235,63 +241,80 @@ export function IndexPage(): JSX.Element { const blobRecords = useMemo( () => isDaily - ? [...(blobDailyQuery.data?.fct_blob_count_daily ?? [])].reverse() + ? trimDailyRecords([...(blobDailyQuery.data?.fct_blob_count_daily ?? [])].reverse(), dailyStartDate) : blobHourlyQuery.data?.fct_blob_count_hourly, - [isDaily, blobDailyQuery.data, blobHourlyQuery.data] + [isDaily, blobDailyQuery.data, blobHourlyQuery.data, dailyStartDate] ); const attnRecords = useMemo( () => isDaily - ? [...(attnDailyQuery.data?.fct_attestation_participation_rate_daily ?? [])].reverse() + ? trimDailyRecords( + [...(attnDailyQuery.data?.fct_attestation_participation_rate_daily ?? [])].reverse(), + dailyStartDate + ) : attnHourlyQuery.data?.fct_attestation_participation_rate_hourly, - [isDaily, attnDailyQuery.data, attnHourlyQuery.data] + [isDaily, attnDailyQuery.data, attnHourlyQuery.data, dailyStartDate] ); const hvRecords = useMemo( () => isDaily - ? [...(hvDailyQuery.data?.fct_head_vote_correctness_rate_daily ?? [])].reverse() + ? trimDailyRecords( + [...(hvDailyQuery.data?.fct_head_vote_correctness_rate_daily ?? [])].reverse(), + dailyStartDate + ) : hvHourlyQuery.data?.fct_head_vote_correctness_rate_hourly, - [isDaily, hvDailyQuery.data, hvHourlyQuery.data] + [isDaily, hvDailyQuery.data, hvHourlyQuery.data, dailyStartDate] ); const reorgRecords = useMemo( () => - isDaily ? [...(reorgDailyQuery.data?.fct_reorg_daily ?? [])].reverse() : reorgHourlyQuery.data?.fct_reorg_hourly, - [isDaily, reorgDailyQuery.data, reorgHourlyQuery.data] + isDaily + ? trimDailyRecords([...(reorgDailyQuery.data?.fct_reorg_daily ?? [])].reverse(), dailyStartDate) + : reorgHourlyQuery.data?.fct_reorg_hourly, + [isDaily, reorgDailyQuery.data, reorgHourlyQuery.data, dailyStartDate] ); const missedSlotRecords = useMemo( () => isDaily - ? [...(missedSlotDailyQuery.data?.fct_missed_slot_rate_daily ?? [])].reverse() + ? trimDailyRecords([...(missedSlotDailyQuery.data?.fct_missed_slot_rate_daily ?? [])].reverse(), dailyStartDate) : missedSlotHourlyQuery.data?.fct_missed_slot_rate_hourly, - [isDaily, missedSlotDailyQuery.data, missedSlotHourlyQuery.data] + [isDaily, missedSlotDailyQuery.data, missedSlotHourlyQuery.data, dailyStartDate] ); const proposalStatusRecords = useMemo( () => isDaily - ? [...(proposalStatusDailyQuery.data?.fct_block_proposal_status_daily ?? [])].reverse() + ? trimDailyRecords( + [...(proposalStatusDailyQuery.data?.fct_block_proposal_status_daily ?? [])].reverse(), + dailyStartDate + ) : proposalStatusHourlyQuery.data?.fct_block_proposal_status_hourly, - [isDaily, proposalStatusDailyQuery.data, proposalStatusHourlyQuery.data] + [isDaily, proposalStatusDailyQuery.data, proposalStatusHourlyQuery.data, dailyStartDate] ); const inclusionDelayRecords = useMemo( () => isDaily - ? [...(inclusionDelayDailyQuery.data?.fct_attestation_inclusion_delay_daily ?? [])].reverse() + ? trimDailyRecords( + [...(inclusionDelayDailyQuery.data?.fct_attestation_inclusion_delay_daily ?? [])].reverse(), + dailyStartDate + ) : inclusionDelayHourlyQuery.data?.fct_attestation_inclusion_delay_hourly, - [isDaily, inclusionDelayDailyQuery.data, inclusionDelayHourlyQuery.data] + [isDaily, inclusionDelayDailyQuery.data, inclusionDelayHourlyQuery.data, dailyStartDate] ); const proposerRewardRecords = useMemo( () => isDaily - ? [...(proposerRewardDailyQuery.data?.fct_proposer_reward_daily ?? [])].reverse() + ? trimDailyRecords( + [...(proposerRewardDailyQuery.data?.fct_proposer_reward_daily ?? [])].reverse(), + dailyStartDate + ) : proposerRewardHourlyQuery.data?.fct_proposer_reward_hourly, - [isDaily, proposerRewardDailyQuery.data, proposerRewardHourlyQuery.data] + [isDaily, proposerRewardDailyQuery.data, proposerRewardHourlyQuery.data, dailyStartDate] ); // --- Unified time keys from all datasets --- diff --git a/src/pages/ethereum/consensus/overview/overview.utils.test.ts b/src/pages/ethereum/consensus/overview/overview.utils.test.ts new file mode 100644 index 000000000..01960fe9c --- /dev/null +++ b/src/pages/ethereum/consensus/overview/overview.utils.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import { dailyWindowStartDate, trimDailyRecords } from './overview.utils'; + +describe('dailyWindowStartDate', () => { + // 2026-07-13T12:00:00Z + const now = Date.UTC(2026, 6, 13, 12, 0, 0); + + it('returns undefined for an unbounded ("all") window', () => { + expect(dailyWindowStartDate(null, now)).toBeUndefined(); + }); + + it('spans exactly 30 calendar days including today', () => { + expect(dailyWindowStartDate(30, now)).toBe('2026-06-14'); + }); + + it('spans exactly 180 calendar days including today', () => { + expect(dailyWindowStartDate(180, now)).toBe('2026-01-15'); + }); + + it('spans exactly 365 calendar days including today', () => { + expect(dailyWindowStartDate(365, now)).toBe('2025-07-14'); + }); +}); + +describe('trimDailyRecords', () => { + const records = [ + { day_start_date: '2025-01-01', value: 1 }, + { day_start_date: '2026-01-14', value: 2 }, + { day_start_date: '2026-07-13', value: 3 }, + ]; + + it('returns an empty array for nullish records', () => { + expect(trimDailyRecords(undefined, '2026-01-01')).toEqual([]); + }); + + it('returns records unchanged when startDate is undefined (the "all" window)', () => { + expect(trimDailyRecords(records, undefined)).toBe(records); + }); + + it('keeps only records on or after the cutoff (inclusive)', () => { + expect(trimDailyRecords(records, '2026-01-14')).toEqual([ + { day_start_date: '2026-01-14', value: 2 }, + { day_start_date: '2026-07-13', value: 3 }, + ]); + }); + + it('treats a missing day_start_date as excluded', () => { + const withMissing = [{ value: 9 }, ...records]; + expect(trimDailyRecords(withMissing, '2025-01-01')).toEqual(records); + }); +}); diff --git a/src/pages/ethereum/consensus/overview/overview.utils.ts b/src/pages/ethereum/consensus/overview/overview.utils.ts index 65c8ebd3c..de3a6604f 100644 --- a/src/pages/ethereum/consensus/overview/overview.utils.ts +++ b/src/pages/ethereum/consensus/overview/overview.utils.ts @@ -39,6 +39,30 @@ export function formatBand(lower: number | undefined, upper: number | undefined) return `${l.toFixed(2)} – ${u.toFixed(2)}`; } +/** + * Computes the inclusive start date (YYYY-MM-DD, UTC) for a daily window of `days` calendar days. + * The window spans exactly `days` buckets ending on the current UTC day (today and the `days - 1` + * preceding days). Returns undefined for an unbounded ("all") window. + */ +export function dailyWindowStartDate(days: number | null, now: number = Date.now()): string | undefined { + if (days === null) return undefined; + return new Date(now - (days - 1) * 24 * 60 * 60 * 1000).toISOString().slice(0, 10); +} + +/** + * Trims daily records to those on or after `startDate` (inclusive, string comparison on YYYY-MM-DD). + * The daily API endpoints have no range filter on their primary key, so the window is applied client-side. + * A nullish `startDate` (the "all" window) returns the records unchanged. + */ +export function trimDailyRecords( + records: T[] | undefined, + startDate: string | undefined +): T[] { + if (!records) return []; + if (!startDate) return records; + return records.filter(r => (r.day_start_date ?? '') >= startDate); +} + /** @deprecated Use formatBand instead */ export const formatBlobBand = formatBand;