Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 39 additions & 16 deletions src/pages/ethereum/consensus/overview/IndexPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import {
formatTooltipDate,
buildTooltipHtml,
formatBand,
dailyWindowStartDate,
trimDailyRecords,
buildBlobCountChartConfig,
buildAttestationParticipationChartConfig,
buildHeadVoteCorrectnessChartConfig,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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 ---
Expand Down
51 changes: 51 additions & 0 deletions src/pages/ethereum/consensus/overview/overview.utils.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
24 changes: 24 additions & 0 deletions src/pages/ethereum/consensus/overview/overview.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends { day_start_date?: string }>(
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;

Expand Down
Loading