diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08553f3..9d52e8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,7 +165,7 @@ jobs: /internal-explorer/bundles/0x1 /api/internal-explorer/blocks \ /tips /tips/block/0x1 /tips/bundles/0x1 /api/tips/blocks \ /benchmark /benchmark/run/latest /benchmark/run-comparison/1 \ - /benchmark/load-tests/sepolia /scaling /scaling/guardrails; do + /benchmark/load-tests/sepolia /scaling /scaling/da /scaling/guardrails; do code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:3000${route}") if [ "${code}" != "404" ]; then echo "FAIL: ${route} returned ${code}, expected 404" diff --git a/app/scaling/[area]/page.tsx b/app/scaling/[area]/page.tsx new file mode 100644 index 0000000..1e7afbc --- /dev/null +++ b/app/scaling/[area]/page.tsx @@ -0,0 +1,35 @@ +import { notFound } from 'next/navigation'; + +import { EmptyState } from '../../components/ui/EmptyState'; +import { Text } from '../../components/ui/Text'; +import { AREAS, CeilingCard } from '../components/CeilingCards'; +import { SectionTable } from '../components/SectionTable'; +import { Header, loadSnapshot, Unavailable } from '../loadSnapshot'; + +export const dynamic = 'force-dynamic'; + +export default async function ScalingAreaPage({ params }: { params: Promise<{ area: string }> }) { + const { area } = await params; + const entry = AREAS.find(([a]) => a === area); + if (!entry) notFound(); + const loaded = await loadSnapshot(); + if (loaded.kind !== 'ok') return ; + + const { snapshot } = loaded; + const section = snapshot.sections.find((s) => s.id === area); + return ( +
+
+
+ Ceiling · Mgas/s + +
+ {section ? ( + + ) : ( + + )} + +
+ ); +} diff --git a/app/scaling/components/CeilingCards.tsx b/app/scaling/components/CeilingCards.tsx index 8188204..15c1cdc 100644 --- a/app/scaling/components/CeilingCards.tsx +++ b/app/scaling/components/CeilingCards.tsx @@ -6,7 +6,7 @@ import { deriveBinding, type Ceiling, type CeilingInput } from '../snapshot.mjs' import { Info } from './Info'; import { Tag } from './Tag'; -const AREAS: [Ceiling['area'], string][] = [['da', 'DA'], ['execution', 'Execution'], ['state', 'State'], ['networking', 'Networking']]; +export const AREAS: [Ceiling['area'], string][] = [['da', 'DA'], ['execution', 'Execution'], ['state', 'State'], ['networking', 'Networking']]; function input({ name, value, unit, status, asOf }: CeilingInput): string { const shown = value === null ? status : `${value} ${unit}`.trim(); @@ -14,33 +14,35 @@ function input({ name, value, unit, status, asOf }: CeilingInput): string { } export function CeilingCards({ ceilings }: { ceilings: Ceiling[] }) { - const binding = deriveBinding(ceilings); return (
Ceilings by bottleneck · Mgas/s
- {AREAS.map(([area, name]) => { - const c = ceilings.find((x) => x.area === area)!; - return ( - - - {name} - {c.help} - - - {c.value === null ? '—' : c.value.toLocaleString('en-US', { maximumFractionDigits: 2 })} - - {c === binding ? Binding : null} - - {c.formula} - {c.reason ?

{c.reason}

: null} -
    - {c.inputs.map((i) =>
  • {input(i)}
  • )} -
-
- ); - })} + {AREAS.map(([area]) => )}
); } + +export function CeilingCard({ area, ceilings }: { area: Ceiling['area']; ceilings: Ceiling[] }) { + const name = AREAS.find(([a]) => a === area)![1]; + const c = ceilings.find((x) => x.area === area)!; + return ( + + + {name} + {c.help} + + + {c.value === null ? '—' : c.value.toLocaleString('en-US', { maximumFractionDigits: 2 })} + + {c === deriveBinding(ceilings) ? Binding : null} + + {c.formula} + {c.reason ?

{c.reason}

: null} + +
+ ); +} diff --git a/app/scaling/fixtures/synthetic.v2.json b/app/scaling/fixtures/synthetic.v2.json index d38a9e9..2229a46 100644 --- a/app/scaling/fixtures/synthetic.v2.json +++ b/app/scaling/fixtures/synthetic.v2.json @@ -180,6 +180,32 @@ ], "gaps": ["Confirmed drain"] }, + { + "id": "execution", + "title": "Execution", + "summary": "Synthetic block-building observations.", + "metrics": [ + { + "label": "Block build time p90", + "status": "partial", + "value": 310, + "unit": "ms", + "window": "week", + "note": "Invented; sampled from one synthetic sequencer.", + "source": { "label": "Synthetic dashboard", "url": "https://example.com/synthetic/build-time" } + } + ], + "gaps": [] + }, + { + "id": "state", + "title": "State", + "summary": "Synthetic state-size observations.", + "metrics": [ + { "label": "Live state growth", "status": "unavailable", "value": null, "unit": "GB/day", "window": "week", "note": "Synthetic query timed out." } + ], + "gaps": ["Live state growth"] + }, { "id": "outcomes", "title": "User outcomes", diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 60cf2c9..2fd3346 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url'; import { renderToStaticMarkup } from 'react-dom/server'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; +import ScalingAreaPage from './[area]/page'; import fixture from './fixtures/synthetic.v2.json'; import ScalingGuardrailsPage from './guardrails/page'; import ScalingPage from './page'; @@ -10,10 +11,40 @@ import ScalingPage from './page'; beforeEach(() => vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url)))); afterEach(() => vi.unstubAllEnvs()); -it('collapses each detail section by default with its title and row count', async () => { +const area = async (id: string) => renderToStaticMarkup(await ScalingAreaPage({ params: Promise.resolve({ area: id }) })); + +it('links the first screen to five detail pages instead of listing metric rows', async () => { const html = renderToStaticMarkup(await ScalingPage()); - expect(html).toMatch(/
]*>]*>Data availability<\/h2>]*> · 2 rows<\/span>/); - expect(html).not.toContain('
{ + for (const section of fixture.sections) { + const html = section.id === 'outcomes' ? renderToStaticMarkup(await ScalingGuardrailsPage()) : await area(section.id); + for (const m of section.metrics) expect(html).toContain(m.label); + } +}); + +it('renders an area with breadcrumb, as-of header, ceiling, metric details, gaps and a runs slot', async () => { + const html = await area('da'); + expect(html).toContain('href="/scaling">Scaling / DA'); + expect(html).toContain('As of 2026-01-02 00:00 UTC'); + expect(html).toMatch(/>Ceiling ]*>· Mgas\/s<\/span>/); + expect(html).toContain('da_bytes_per_gas · 0.0055 bytes/gas · observed · 2026-01-02'); + expect(html).toContain('synthetic.blob_utilization'); + expect(html).toContain('
  • Confirmed drain
  • '); + expect(html).toContain('Related runs'); +}); + +it('shows the ceiling and an empty state for an area without metrics', async () => { + const html = await area('networking'); + expect(html).toContain('gossip and peer limits unknown'); + expect(html).toContain('No detailed metrics yet'); +}); + +it('404s an unknown area', async () => { + await expect(area('bogus')).rejects.toMatchObject({ digest: 'NEXT_HTTP_ERROR_FALLBACK;404' }); }); it('links the first-screen guardrail strip to the guardrails page', async () => { diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index be99bb0..9889be5 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -1,14 +1,12 @@ import Link from 'next/link'; -import { Card } from '../components/ui/Card'; import { Text } from '../components/ui/Text'; import { CeilingCards } from './components/CeilingCards'; import { Guardrails } from './components/Guardrails'; import { Ladder } from './components/Ladder'; import { Progress } from './components/Progress'; -import { formatTime, Header, loadSnapshot, Unavailable } from './loadSnapshot'; -import type { Metric, Snapshot } from './snapshot.mjs'; +import { Header, loadSnapshot, Unavailable } from './loadSnapshot'; // The snapshot is a runtime file, so render per request rather than at build. export const dynamic = 'force-dynamic'; @@ -28,35 +26,6 @@ export default async function ScalingPage() { - {snapshot.sections.map((section) => ( -
    - - {section.title} - · {section.metrics.length} {section.metrics.length === 1 ? 'row' : 'rows'} - {section.summary} - - - {section.metrics.map((m) => ( -
    -
    - {m.label} - -
    - -
    - ))} -
    - {section.gaps.length > 0 ? ( -
    - Missing evidence -
      - {section.gaps.map((gap) =>
    • {gap}
    • )} -
    -
    - ) : null} -
    - ))} - Experiment results: Benchmarks ·{' '} Load tests @@ -64,51 +33,3 @@ export default async function ScalingPage() { ); } - -const STATUS_LABEL: Record = { - observed: '', partial: 'Partial', unavailable: 'Unavailable', 'not-measured': 'Not measured', -}; - -function Value({ metric }: { metric: Metric }) { - if (metric.value === null) { - return ( - - {STATUS_LABEL[metric.status]} - - ); - } - return ( - - - {metric.value.toLocaleString('en-US', { maximumFractionDigits: 2 })} - {metric.unit ? {metric.unit} : null} - - {metric.status === 'partial' ? Partial : null} - - ); -} - -function MetricDetails({ metric, snapshot }: { metric: Metric; snapshot: Snapshot }) { - const window = metric.window ? snapshot.windows[metric.window] : null; - return ( -
    - - {window ? `${window.label} · details` : metric.value === null ? 'No measurement yet · details' : 'Details'} - -
    - {window ?

    {formatTime(window.start)} → {formatTime(window.end)}

    : null} -

    {metric.note}

    - {metric.query ? {metric.query} : null} - {metric.source ? : null} -
    -
    - ); -} - -function ExternalLink({ link }: { link: { label: string; url: string } }) { - return ( - - {link.label} - - ); -}