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 (
+
+
+
+ {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}
+
+ {c.inputs.map((i) => - {input(i)}
)}
+
+
+ );
+}
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) => (
-
- ))}
-
- {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}
-
- );
-}