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
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ export function ProbeFlow({ probe }: ProbeFlowProps): JSX.Element {
const isSuccess = probe.result === 'success';
const isFailure = probe.result === 'failure';

// Calculate total data requested
// Calculate total data requested: one cell per blob in each column
const columnsCount = probe.column_indices?.length ?? 0;
const cellsCount = columnsCount;
const blobCount = probe.blob_submitters?.length ?? 0;
const cellsCount = columnsCount * blobCount;
const cellSizeKiB = 2;
const totalSizeKiB = cellsCount * cellSizeKiB;
const totalSizeMiB = totalSizeKiB / 1024;
const sizeDisplay = totalSizeMiB >= 1 ? `${totalSizeMiB.toFixed(1)} MiB` : `${totalSizeKiB} KiB`;
const sizeDisplay =
blobCount === 0 ? '---' : totalSizeMiB >= 1 ? `${totalSizeMiB.toFixed(1)} MiB` : `${totalSizeKiB} KiB`;

// Get country codes for flags
const clientCountryCode = probe.meta_client_geo_country_code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ export function RecentActivityTable({ epochs }: RecentActivityTableProps): JSX.E
},
{
header: 'Attestations Attested',
accessor: row => (
<span className="text-success">{(row.totalAttestations - row.missedAttestations).toLocaleString()}</span>
),
accessor: row => <span className="text-success">{row.totalAttestations.toLocaleString()}</span>,
},
{
header: 'Attestations Missed',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface EntityEpochData {
epoch: number;
/** Epoch start timestamp (Unix seconds) */
epochStartDateTime: number;
/** Total attestations in this epoch */
/** Attested (non-missed) attestations in this epoch */
totalAttestations: number;
/** Missed attestations in this epoch */
missedAttestations: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,56 +279,29 @@ export function ForksTimeline({
);
}
})()
: blobIndex > 0
? (() => {
const prevBlob = blobItems[blobIndex - 1];
const epochsSincePrev = blobItem.epoch - prevBlob.epoch;
const daysSincePrev = Math.floor((epochsSincePrev * 32 * 12) / 86400);
: (() => {
const epochsSinceActivation = currentEpoch - blobItem.epoch;
const daysSinceActivation = Math.floor((epochsSinceActivation * 32 * 12) / 86400);

if (daysSincePrev < 60) {
return (
<p className="text-xs text-muted">Activated {daysSincePrev} days ago</p>
);
} else if (daysSincePrev < 730) {
const months = Math.floor(daysSincePrev / 30);
return <p className="text-xs text-muted">Activated {months} months ago</p>;
} else {
const years = Math.floor(daysSincePrev / 365);
const remainingMonths = Math.floor((daysSincePrev % 365) / 30);
return (
<p className="text-xs text-muted">
Activated {years}y {remainingMonths > 0 ? `${remainingMonths}m` : ''} ago
</p>
);
}
})()
: (() => {
const epochsSinceActivation = currentEpoch - blobItem.epoch;
const daysSinceActivation = Math.floor(
(epochsSinceActivation * 32 * 12) / 86400
if (daysSinceActivation < 1) {
return <p className="text-xs text-muted">Activated recently</p>;
} else if (daysSinceActivation < 60) {
return (
<p className="text-xs text-muted">Activated {daysSinceActivation} days ago</p>
);

if (daysSinceActivation < 1) {
return <p className="text-xs text-muted">Activated recently</p>;
} else if (daysSinceActivation < 60) {
return (
<p className="text-xs text-muted">
Activated {daysSinceActivation} days ago
</p>
);
} else if (daysSinceActivation < 730) {
const months = Math.floor(daysSinceActivation / 30);
return <p className="text-xs text-muted">Activated {months} months ago</p>;
} else {
const years = Math.floor(daysSinceActivation / 365);
const remainingMonths = Math.floor((daysSinceActivation % 365) / 30);
return (
<p className="text-xs text-muted">
Activated {years}y {remainingMonths > 0 ? `${remainingMonths}m` : ''} ago
</p>
);
}
})()}
} else if (daysSinceActivation < 730) {
const months = Math.floor(daysSinceActivation / 30);
return <p className="text-xs text-muted">Activated {months} months ago</p>;
} else {
const years = Math.floor(daysSinceActivation / 365);
const remainingMonths = Math.floor((daysSinceActivation % 365) / 30);
return (
<p className="text-xs text-muted">
Activated {years}y {remainingMonths > 0 ? `${remainingMonths}m` : ''} ago
</p>
);
}
})()}
</div>
</div>
</div>
Expand Down
7 changes: 5 additions & 2 deletions src/pages/ethereum/validators/IndexPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import clsx from 'clsx';
import { MagnifyingGlassIcon, ArrowDownTrayIcon } from '@heroicons/react/24/outline';
import { Container } from '@/components/Layout/Container';
import { Header } from '@/components/Layout/Header';
import { useNetwork } from '@/hooks/useNetwork';
import { useValidatorsData, useValidatorResolver, useValidatorStatus } from './hooks';
import {
ValidatorInput,
Expand Down Expand Up @@ -57,6 +58,7 @@ function tagsFromUrl(indices?: string): string[] {
export function IndexPage(): JSX.Element {
const navigate = useNavigate();
const search = useSearch({ from: '/ethereum/validators/' }) as ValidatorsSearch;
const { currentNetwork } = useNetwork();

// Initialize state from URL params or defaults
const defaultRange = getDefaultTimeRange();
Expand Down Expand Up @@ -176,13 +178,14 @@ export function IndexPage(): JSX.Element {

// Handle download report
const handleDownloadReport = useCallback(() => {
if (!summary) return;
if (!summary || !currentNetwork) return;

const markdown = generateValidatorReport({
validators,
summary,
thresholds,
timeRange,
network: currentNetwork.name,
url: window.location.href,
blockProposals,
validatorMap,
Expand All @@ -195,7 +198,7 @@ export function IndexPage(): JSX.Element {
a.download = `validator-report-${new Date().toISOString().split('T')[0]}.md`;
a.click();
URL.revokeObjectURL(url);
}, [validators, summary, thresholds, timeRange, blockProposals, validatorMap]);
}, [validators, summary, thresholds, timeRange, currentNetwork, blockProposals, validatorMap]);

return (
<Container>
Expand Down
55 changes: 55 additions & 0 deletions src/pages/ethereum/validators/utils/generateReport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest';
import type { AggregateSummary } from '../hooks';
import type { ThresholdValues } from '../components';
import { generateValidatorReport } from './generateReport';

const summary: AggregateSummary = {
totalValidators: 0,
attestation: {
totalDuties: 0,
attestedCount: 0,
missedCount: 0,
headCorrectCount: 0,
targetCorrectCount: 0,
sourceCorrectCount: 0,
inclusionRate: 0,
headCorrectRate: 0,
targetCorrectRate: 0,
sourceCorrectRate: 0,
},
syncCommittee: { totalSlots: 0, participatedCount: 0, missedCount: 0, participationRate: 0, validatorsWithDuties: 0 },
blockProposal: {
totalProposals: 0,
canonicalCount: 0,
missedCount: 0,
orphanedCount: 0,
proposalRate: 0,
validatorsWithDuties: 0,
},
balance: { minBalance: null, maxBalance: null, avgEndBalance: null },
};

const thresholds: ThresholdValues = {
inclusionRate: 95,
correctnessRate: 98,
syncParticipation: 95,
minBalance: 31.95,
inclusionDelay: 1.5,
blockProductionRate: 95,
};

describe('generateValidatorReport', () => {
it('should report the network it was generated for', () => {
const report = generateValidatorReport({
validators: [],
summary,
thresholds,
timeRange: { from: 1760918400, to: 1761523200 },
network: 'hoodi',
url: 'https://lab.ethpandaops.io/ethereum/validators?network=hoodi',
});

expect(report).toContain('- **Network**: hoodi');
expect(report).not.toContain('mainnet');
});
});
4 changes: 3 additions & 1 deletion src/pages/ethereum/validators/utils/generateReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ReportParams {
summary: AggregateSummary;
thresholds: ThresholdValues;
timeRange: { from: number; to: number };
network: string;
url: string;
blockProposals?: BlockProposalDataPoint[];
validatorMap?: Map<number, string>;
Expand Down Expand Up @@ -50,6 +51,7 @@ export function generateValidatorReport({
summary,
thresholds,
timeRange,
network,
url,
blockProposals = [],
validatorMap,
Expand All @@ -69,7 +71,7 @@ export function generateValidatorReport({
sections.push('## Configuration');
sections.push('');
sections.push(`- **Generated**: ${generatedAt}`);
sections.push(`- **Network**: mainnet`);
sections.push(`- **Network**: ${network}`);
sections.push(`- **Validators Analyzed**: ${summary.totalValidators}`);
sections.push(`- **Time Range**: ${fromDate} to ${toDate}`);
sections.push('');
Expand Down
Loading