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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ pnpm check:og-image # postbuild social-card guard over ./out (runs inside `pnp
- `components/site/` — page sections composed by `app/page.tsx` (Hero, Compare, Method, Faq, Contact, …) plus chrome (`Chrome`, `Footer`, `ScrollReveal`, …). Section components are named after their section `id` (e.g. `Contact.tsx` for `id="contact"`); `Hero` is the idiomatic exception for the top `id="intro"` section.
- `components/ui/` — low-level primitives (Frame, SectionHeader, Prose, Figure, …).
- `lib/` — shared helpers (`links.ts`, `blog.ts`, `faq.ts`, `legal.ts`, `jsonld.tsx`, `schema.ts`, …). `lib/schema.ts` holds the site-level JSON-LD nodes; the per-post `BlogPosting` node is `blogPostingNode` in `lib/blog.ts` because it derives from a post rather than from the site — `blogNode` nests it and `app/blog/[slug]/page.tsx` emits it top-level, so they cannot diverge under the same `@id`.
- `lib/copy.ts` — the site's prose, as plain data. New or edited homepage copy goes here, not inline in a component: the comparison table maps over `COMPARE_ROWS` and `app/llms-full.txt/route.ts` serialises the same strings as markdown, so copy inlined in JSX silently drifts from the mirror. This is not hypothetical — the hero and method stat strips were inline `<Figure>` attributes restated by hand in the mirror, and had already drifted. Values as well as sentences: `HERO_FIGURES`, `STACK`, `DEMO_CAPTIONS`. Render prose through `highlightBrand` (`components/ui/brand.tsx`) wherever the sentence names the product.
- `lib/copy.ts` — the site's prose, as plain data. New or edited homepage copy goes here, not inline in a component: the comparison table maps over `COMPARE_ROWS` and `app/llms-full.txt/route.ts` serialises the same strings as markdown, so copy inlined in JSX silently drifts from the mirror. This is not hypothetical — the hero and method stat strips were inline `<Figure>` attributes restated by hand in the mirror, and had already drifted. Values as well as sentences: `HERO_FIGURES`, `STACK`, `PANEL_CAPTIONS`. Render prose through `highlightBrand` (`components/ui/brand.tsx`) wherever the sentence names the product.
- `lib/stats.ts` — the live testnet figures behind the hero terminal and the Contact fleet panel. The page stays a static export: both panels render a loading state into the HTML and fill in after mount from `https://data.decdn.org/stats-421614.json`, which the `decdn/stats` indexer rewrites every five minutes (schema: `worker/src/stats.ts` there). Shape check, formatting and view models are pure and unit-tested; `lib/use-stats.ts` is the one-fetch-per-page client hook. The CSP in `public/_headers` allowlists the origin under `connect-src` — change both together.
- `test-utils/` — test-only helpers (`react-tree.ts` walks the element tree a server component returns). Nothing under `app/` or `components/` imports from here.
- `scripts/` — `check-out.mjs`, the postbuild assertions `pnpm build` runs against `./out`.
- `content/blog/` — MDX posts loaded by `lib/blog.ts` and rendered via `app/blog/[slug]/page.tsx`.
Expand Down
87 changes: 32 additions & 55 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -303,10 +303,11 @@ a.cta-primary:focus-visible {
}

/* --- hero terminal -----------------------------------------------------
Mocks a single decdn CLI session: probe → stream → verify → settle.
Replays recent on-chain settlements from the live stats file
(lib/stats.ts), one per cycle: tx → block → operator → bytes → paid.
All lines share one 4.5s loop; each line fades in at its own delay.
Progress bar fills during the "streaming" phase. Cursor blinks
independently. One terminal session restarts every cycle. */
Progress bar fills during the "delivered" line. Cursor blinks
independently. The cascade restarts every cycle. */

.terminal {
font-family:
Expand Down Expand Up @@ -498,9 +499,11 @@ a.cta-primary:focus-visible {
/* --- fleet status panel (§05) -----------------------------------------
Sits on the paper Contact section as an inverted panel. Same "tool
panel" vocabulary as .terminal — square corners, dashed border, hard
offset shadow — with an ops-dashboard layout instead of a CLI log: a
list of nodes with pulsing serving indicators plus two aggregate stat
bars that breathe. Not interactive, so no hover transform. */
offset shadow — with an ops-dashboard layout instead of a CLI log:
registered nodes (region, operator, last settlement) with a pulse for
each one that settled recently, plus two 24h
aggregates over hourly spark strips. Filled from the live stats file
(lib/stats.ts). Not interactive, so no hover transform. */

.fleet {
font-family:
Expand Down Expand Up @@ -663,68 +666,42 @@ a.cta-primary:focus-visible {
font-variant-numeric: tabular-nums;
}

.fleet-bar {
/* Full-width line for a status or overflow note in place of node rows. */
.fleet-note {
padding: 2px 0;
opacity: 0.5;
}

/* Hourly spark strip under each aggregate: one cell per hour of the 24h
window, oldest left, each cell's height its share of the busiest hour
(--level, 0–1). A zero hour keeps a hairline so the strip reads as a
timeline rather than a gap. */
.fleet-spark {
display: flex;
align-items: flex-end;
gap: 2px;
width: 100%;
height: 5px;
border: 1px solid color-mix(in srgb, var(--paper) 22%, transparent);
position: relative;
overflow: hidden;
height: 14px;
margin: 4px 0 4px;
border-bottom: 1px solid color-mix(in srgb, var(--paper) 22%, transparent);
}

.fleet-bar-fill {
position: absolute;
inset: 0;
width: 70%;
.fleet-spark-cell {
flex: 1;
height: max(1px, calc(var(--level, 0) * 100%));
background: var(--whisper);
}

.fb-throughput {
animation: fleet-wave 7s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

.fb-revenue {
animation: fleet-wave 8.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
animation-delay: 2.3s;
}

/* subtle live-data jitter around a steady-state value — not a dramatic
breathe. Bar stays in a 72–82% band; the two bars run on different
durations + a phase offset so they never sync. */
@keyframes fleet-wave {
0%,
100% {
width: 74%;
}
15% {
width: 79%;
}
32% {
width: 73%;
}
48% {
width: 81%;
}
63% {
width: 76%;
}
80% {
width: 78%;
}
transition: height 0.6s cubic-bezier(0.3, 0, 0.2, 1);
}

@media (prefers-reduced-motion: reduce) {
.fleet-pulse-active,
.fleet-bar-fill {
animation: none;
}
.fleet-pulse-active {
animation: none;
opacity: 1;
transform: none;
box-shadow: none;
}
.fleet-bar-fill {
width: 80%;
.fleet-spark-cell {
transition: none;
}
}

Expand Down
18 changes: 5 additions & 13 deletions app/llms-full.txt/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "@/lib/copy";
import { FAQ_ITEMS } from "@/lib/faq";
import { getLegalDoc, LEGAL_SLUGS } from "@/lib/legal";
import { STATS_URL } from "@/lib/stats";

const response = GET();
const body = await response.text();
Expand Down Expand Up @@ -161,19 +162,10 @@ describe("homepage mirror", () => {
expect(body).toContain("the three legal documents");
});

// The terminal and fleet-dashboard figures are invented sample values. In
// HTML they sit inside a captioned <figure>; plain text strips that framing
// entirely, so mirroring them would be worse than omitting them.
it.each([
"15.1 GB/s",
"$0.151 /s",
"$0.1309",
"$0.0055",
"$0.0411",
"550 MB",
"probing 18 peers",
])("omits the demo figure %s", (figure) => {
expect(body).not.toContain(figure);
// The settlement feed and fleet panel are live readings; a mirror built at
// deploy time would freeze them, so it points at their source instead.
it("points at the live stats file rather than mirroring the panels", () => {
expect(body).toContain(STATS_URL);
});
});

Expand Down
9 changes: 5 additions & 4 deletions app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { FAQ_ITEMS } from "@/lib/faq";
import { getLegalDoc, LEGAL_SLUGS, legalUrl } from "@/lib/legal";
import { DOCS_ORIGIN, EMAIL, links, SITE_URL } from "@/lib/links";
import { STATS_URL } from "@/lib/stats";

// The whole origin as one plain-text document, so an agent grounding on deCDN
// fetches this instead of stripping HTML from the homepage, the blog index,
Expand All @@ -32,9 +33,9 @@ import { DOCS_ORIGIN, EMAIL, links, SITE_URL } from "@/lib/links";
// Post and legal bodies come from the same loaders the routes use.
//
// Deliberately excluded: the HeroTerminal and FleetStatus figures. They are
// invented sample values, and a plain-text mirror strips exactly the visual
// framing that marks them as a demo, so quoting them here would be worse than
// not mirroring them at all. The tests assert their absence.
// live testnet readings fetched in the browser, and a mirror built once at
// deploy time would freeze a stale copy of them. The mirror points at the
// stats file instead.

// Required by Next 16 under `output: "export"` for route handlers that emit
// static files at build time. GET-only is the only verb supported in export.
Expand Down Expand Up @@ -193,7 +194,7 @@ This is every page on decdn.org as one document: the homepage copy, the traditio

${statusBlock("throughout")}

Two notes on reading this file. Field notes and legal documents are reproduced verbatim except that their headings are demoted to nest under the \`###\` each is given here, so the outline of this document is its own. And the homepage's terminal and fleet-dashboard widgets are not mirrored at all: their figures are invented sample values for demonstration, and plain text strips the framing that marks them as a demo.
Two notes on reading this file. Field notes and legal documents are reproduced verbatim except that their headings are demoted to nest under the \`###\` each is given here, so the outline of this document is its own. And the homepage's settlement feed and fleet panel are not mirrored: they show live Arbitrum Sepolia testnet readings, fetched in the browser from ${STATS_URL}, which the deCDN indexer rewrites every five minutes. Read that file for current figures.

## Homepage

Expand Down
127 changes: 127 additions & 0 deletions components/site/FleetStatus.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { describe, expect, it } from "vitest";
import { FleetPanel } from "./FleetStatus";
import { PANEL_CAPTIONS } from "@/lib/copy";
import type { Stats, StatsResult } from "@/lib/stats";
import { attrs, findAll, findOne, textOf } from "@/test-utils/react-tree";

// Both panels delegate the <figure>/<figcaption> structure to
// components/ui/PanelFigure, whose own test asserts that the caption sits
// outside the aria-hidden subtree. What is left to check here is that the
// fleet panel composes it with the right caption, and renders each state.
//
// FleetStatus itself calls useStats (useState), so the tree walker can't
// invoke it; FleetPanel is its render half. HeroTerminal is all client state —
// scripts/check-out.mjs greps the built HTML for both captions, which is the
// only check that sees HeroTerminal render.

const stats: Stats = {
chainId: 421614,
updatedAt: "2026-09-27T12:00:00.000Z",
lastBlock: 100,
caughtUp: true,
totals: {
valueSettled: "1500000",
bytesServed: "3000000000",
settlementCount: 2,
},
hourly: [
{
hour: "2026-09-27T11",
valueSettled: "500000",
bytesServed: "1000000000",
settlementCount: 1,
registeredNodes: 3,
},
{
hour: "2026-09-27T12",
valueSettled: "1000000",
bytesServed: "2000000000",
settlementCount: 1,
registeredNodes: 3,
},
],
settlements: [
{
txHash: "0xabc",
logIndex: 0,
blockNumber: 100,
timestamp: 1_790_000_000 - 4 * 60,
operator: "0x00000000000000000000000000000000000000a1",
bytesDelivered: "1000000000",
amount: "500000",
epoch: 7,
},
],
nodes: {
"0x01": {
operator: "0x00000000000000000000000000000000000000a1",
region: "DE",
},
"0x02": {
operator: "0x00000000000000000000000000000000000000a2",
region: "DE",
},
"0x03": {
operator: "0x00000000000000000000000000000000000000a3",
region: "unknown",
},
},
};

const fetchedAt = 1_790_000_000_000;

const panel = (result: StatsResult) => {
const tree = FleetPanel({ result, className: "block w-full" });
const [hidden] = findAll(tree, "div").filter(
(el) => attrs(el)["aria-hidden"] === true,
);
return { tree, text: textOf(hidden) };
};

describe("FleetPanel", () => {
it("renders through PanelFigure", () => {
const { tree } = panel({ status: "loading" });
const figure = findOne(tree, "figure");
expect(attrs(figure).className).toBe("block w-full");
const panels = findAll(tree, "div").filter(
(el) => attrs(el)["aria-hidden"] === true,
);
expect(panels).toHaveLength(1);
expect(attrs(panels[0]).className).toBe("fleet");
});

it("captions the panel with the fleet caption", () => {
const { tree } = panel({ status: "loading" });
expect(textOf(findOne(tree, "figcaption"))).toBe(PANEL_CAPTIONS.fleet);
});

// The static HTML is this state: it must carry no figures at all.
it("renders no figures while loading", () => {
const { text } = panel({ status: "loading" });
expect(text).toContain("loading");
expect(text.replaceAll("24h", "")).not.toMatch(/\d/);
});

it("says so when the stats file is unavailable", () => {
expect(panel({ status: "error" }).text).toContain("stats unavailable");
});

it("renders one row per registered node and the 24h aggregates", () => {
const { text } = panel({ status: "ok", stats, fetchedAt });
expect(text).toContain("3nodes registered");
expect(text).toContain("de0x0000…00a1 · settled 4 min ago");
expect(text).toContain("de0x0000…00a2 · idle");
expect(text).toContain("n/a0x0000…00a3 · idle");
expect(text).toContain("$1.50");
expect(text).not.toContain("catching up");
});

it("flags a backfilling index", () => {
const { text } = panel({
status: "ok",
stats: { ...stats, caughtUp: false },
fetchedAt,
});
expect(text).toContain("catching up");
});
});
Loading
Loading