Skip to content

refactor: skeleton layout, OpenAPI-first API, and Chain Console web overhaul - #85

Merged
Savid merged 8 commits into
masterfrom
refactor/skeleton-openapi-web-overhaul
Jun 10, 2026
Merged

Savid merged 8 commits into
masterfrom
refactor/skeleton-openapi-web-overhaul

Conversation

@Savid

@Savid Savid commented Jun 10, 2026

Copy link
Copy Markdown
Member

Summary

Ground-up modernization of forky: the Go service moves to the ethpandaops skeleton layout with an OpenAPI-first, ogen-generated HTTP API, and the web UI gets a full overhaul — new build/test stack, typed routing, a Storybook-first component library, and a complete "Chain Console" visual redesign. Master's stale-node filtering and graph tail truncation (#84) are merged in and re-expressed in the new design system.

Go: skeleton layout + OpenAPI-first API

  • Application root moves from pkg/forky to the repo root; subpackages become internal/* (store, source, service, db, api, ethereum, human, types, version, yaml). New cmd/forky/main.go cobra entrypoint.
  • api/openapi.yaml is now the source of truth for the HTTP API on both sides: the Go server (api/rest, generated by ogen, implemented by internal/api) and the typed web client (web/src/api, generated by @hey-api/openapi-ts). make generate regenerates both together; wire format matches by construction.
  • Serving via http.ServeMux: /api/ → generated server, /openapi.yaml → embedded spec, / → embedded SPA fallback. Hand-rolled httprouter layer deleted; Go bumped to 1.26.

Web: stack + architecture

  • npm → pnpm, Vite 8, React 19, TS 6, Tailwind v4, TanStack Router (typed file-based routes) + TanStack Query.
  • Typed API client generated from the spec with zod runtime validation; MSW mocks are typed against the generated response types so tests fail if mocks drift from the spec.
  • Storybook 10 with stories across the component/parts library, run headless via Vitest browser mode; test suites split into unit (vitest.config.unit.ts, 125 tests) and Storybook interaction tests (84 stories/assertions).
  • Routing: concrete typed routes for /, /byo, /events, /node/*, /snapshot/:id and their /events overlay variants. Node ids contain literal slashes (ethpandaops/mainnet/node-001), so /node is a splat route — pretty multi-segment node URLs deep-link correctly. parseAppRoute in utils/routes.ts is the single tested boundary between plain path strings and typed route targets.
  • Persistent player state: the root route renders one persistent <App> and derives view props from the location, so navigating between aggregated / node / snapshot views (or opening the events overlay) never remounts the app — the focused time and play/pause state survive navigation, with no query params involved.

Web: "Chain Console" redesign

Dark-first mission-control aesthetic; light mode is a faithful inversion.

  • Design tokens (index.css): two-tier token architecture retained (custom ESLint rules still enforce semantic-tokens-only in app code). New cool near-black "ink" neutral ladder, alpha hairline borders, and the six fork-choice status colors normalized in OKLCH (shared lightness/chroma bands) so every status holds equal contrast in both themes. One electric-cyan accent owns focus rings, links, the playhead and LIVE state.
  • Typography: Inter Variable for UI, JetBrains Mono for hashes/slots/epochs with tabular numerals everywhere numbers tick.
  • Chrome: flush blurred-glass header (graph pans underneath) with gradient wordmark, network pill and a new vector logo mark; floating glass console dock with labeled mono fields (slot / epoch / time), transport controls and LIVE indicator; dark dials with a full-height cyan playhead. Fully responsive — the dock reflows from mobile (slot + play + live) up through xl (everything + epoch dial).
  • Graph: nodes use a dim status-tinted core with a bright status rim (the weight arc) and soft glow; hairline slot boundaries on a dot-grid canvas; glass overlay pills for Sources/Snapshot/Share/focus.
  • Drawers, events table, summary cards, footers and fallback pages all follow the same language.

Merged from master: stale nodes + tail truncation (#84)

Logic merged intact, visuals re-expressed in the design system:

  • Behind-node filtering: frames whose head lags their wall clock by more than ~5 epochs are excluded from aggregation; the Sources panel lists them struck through in red with their slot lag. Single-node views bypass the check.
  • Tail truncation (MAX_GRAPH_SLOTS = 128): long graphs render only the most recent window; finalized/justified checkpoints that fall outside are pinned into compact columns with their own slot boundaries, bridged by dashed "N slots hidden" gap markers and a rail.
  • New TruncationMarker component (opaque, dashed neutral ring — no graph node has a transparent fill).
  • Storybook coverage: a long-tail truncation scenario (auto-pans to the pinned checkpoints on open), an aggregated-with-stuck-node scenario, and the marker primitive.

Testing

  • pnpm lint, pnpm typecheck, pnpm test (125 unit + 84 Storybook browser tests), pnpm build all green; tsc -b production build verified.
  • Manually verified against live mainnet data at mobile/md/lg/xl breakpoints in both themes: aggregated, node, snapshot, BYO, events overlay, share links (?t=), and player-state persistence across navigation.

Screenshots

All live mainnet (localhost:5173) unless noted.

Aggregated view — dark

Aggregated view — light

Single node view

Events drawer (live reorg events)

Mobile (390px)

About drawer

Tail truncation: pinned finalized/justified checkpoints + gap markers (Storybook scenario — healthy mainnet doesn't truncate)

Stale node excluded from aggregation, flagged in Sources (Storybook scenario)

🤖 Generated with Claude Code

Savid and others added 7 commits June 10, 2026 12:32
Restructure the project around the ethpandaops skeleton layout, replace
the hand-rolled HTTP API with an OpenAPI-first ogen-generated server,
and modernize the web UI stack.

Go restructure:
- Move the application root from pkg/forky to the repo root (package
  forky: forkchoice.go, http.go, config.go); subpackages move from
  pkg/forky/* to internal/* (store, source, service, db, api, ethereum,
  human, types, version, yaml).
- Replace main.go + cmd/root.go with a cmd/forky/main.go cobra
  entrypoint that builds the logger, loads config, and runs the server.
- Serve on a http.ServeMux: /api/ -> generated server, /openapi.yaml ->
  embedded spec, / -> embedded SPA fallback.
- Bump Go to 1.26.2 and refresh dependencies.

OpenAPI-first HTTP API:
- api/openapi.yaml is now the source of truth for the HTTP API.
- api/rest holds the ogen-generated server/client (go generate);
  internal/api implements the generated rest.Handler interface and maps
  domain types onto the spec schemas.
- Delete the hand-rolled httprouter routing/response layer
  (pkg/forky/api/http); drop julienschmidt/httprouter, pkg/errors and
  lib/pq in favour of ogen with otel instrumentation.
- Add .redocly.yaml for spec linting; make generate regenerates the Go
  server and the typed web client together.

Web UI overhaul:
- Switch npm -> pnpm (pnpm-lock.yaml, packageManager pin, CI updates).
- Replace react-router-dom/wouter with TanStack Router using file-based
  routes (web/src/routes) and a generated route tree.
- Replace hand-written API fetchers with a @hey-api/openapi-ts generated
  typed client (web/src/api) integrated with TanStack Query and zod.
- Move Tailwind to v4 CSS-first config: tailwind.config.js removed,
  semantic color tokens defined in web/src/index.css and enforced by
  custom ESLint rules (no-hardcoded-colors, no-primitive-color-scales).
- Embed built assets via web/embed.go behind the webui build tag (with
  a stub for tag-less builds), replacing web/static.go.
- Rework major views (Events, Header, Selection, summaries) onto the
  semantic token theme; bump React 19.2, Vite 8, TypeScript 6, ESLint
  10 and Vitest 4.

Tooling and docs:
- New help-driven Makefile (build, build-web, generate, lint, audit,
  vuln, modernize); .golangci.yml rewritten for golangci-lint v2.
- CI: Go 1.26.x, golangci-lint v2.12.2, pnpm-based web jobs.
- Add AGENTS.md, CLAUDE.md, web/CLAUDE.md, CONTRIBUTING.md,
  .editorconfig, .tool-versions and .dockerignore.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Continue the web overhaul by replacing the catch-all splat route with
typed file-based TanStack routes, adding a Storybook setup, and splitting
the Vitest suite into unit and Storybook browser tests.

Routing:
- Drop the single splat route (`routes/$.tsx`, `routes/-RouteView.tsx`) in
  favor of concrete routes: `byo`, `events`, `node.$nodeId`,
  `snapshot.$frameId`, plus their non-nested `_.events` overlay variants.
- Add `utils/routes.ts` (`parseAppRoute`, `nodePath`, `snapshotPath`,
  `eventsPathFor`, and `*EventsPath` helpers) as the single boundary that
  turns plain app path strings into typed route targets; cover it with
  `utils/__tests__/routes.test.ts`.
- Rework `Link.tsx` and `useAppNavigation.ts` to dispatch parsed routes to
  typed `RouterLink`/`navigate` targets, falling back to a plain anchor /
  `window.location.assign` for unknown paths.
- Thread events overlay state through props: `App` takes `eventsOpen` /
  `eventsCloseTo`, `Events` consumes them instead of sniffing the pathname,
  and each `_.events` route opens the overlay with the right close target.
- Wire a `notFoundComponent` on the root route and simplify Header links.

Storybook:
- Add Storybook 10 (`.storybook/` config, preview, story routes, MSW
  setup) and stories for App, Fallbacks, and the component/parts library.
- Add `src/stories/` fixtures and helpers shared across stories.
- Register `eslint-plugin-storybook` and `tsconfig.storybook.json`.

Testing:
- Split Vitest: `vitest.config.unit.ts` (jsdom unit tests) and
  `vitest.config.ts` (Storybook tests in a Playwright/Chromium browser);
  add `test:unit*`, `test:storybook*`, and `test:ui` scripts.
- Extract `ErrorBoundaryFallback` so the fallback can be story-tested.

Tooling:
- Bump msw to 2.14.6, add Storybook/Vitest browser/Playwright and supporting
  dev deps; ignore `storybook-static` and `.tanstack`.
Reskin the operator UI to the dark-first "Chain Console" design language,
collapse the node routes onto a single splat route, and render one
persistent <App> from the root route.

Theming (src/index.css)
- Rewrite the two-tier token system around a cool-neutral "ink" ramp
  (hue 270), an electric-cyan accent and six status families that share
  OKLCH lightness/chroma per role, so every status holds equal contrast
  against the surface ladder in both themes.
- Add semantic accent tokens (--color-accent, --color-accent-hover) and
  glass-chrome / stage-grid / dot-grid utilities; drop the old
  Tailwind-palette primitives and the unused inset-shadow/pulse helpers.
- Adopt Inter Variable + JetBrains Mono Variable via @fontsource
  (added to package.json); restyle Header, Control, Share, Events,
  AggregatedNode, WeightedNode, Graph, Timeline and the rest to match
  (glass header/dock, network-name badge, icon buttons).

Routing
- Replace /node/$nodeId and /node_/$nodeId/events with one splat route
  (node.$.tsx) so multi-segment node ids with literal slashes
  (ethpandaops/mainnet/node-001) deep-link cleanly. Add parseNodeSplat
  and per-segment encodeNodeId in utils/routes.ts; a trailing /events
  segment always means the overlay.
- Remove the -AppRoute.tsx wrapper. The root route now renders the
  single, persistent <App> for every view and derives its props from the
  location via parseAppRoute; leaf route files are matchers only. One App
  (and the focus provider) stays mounted across navigation, so switching
  views no longer resets player time or play/pause.
- Regenerate routeTree.gen.ts; expand routes tests for multi-segment and
  encoded-slash node ids.

Branding & metadata
- Add SVG wordmark marks (forky-mark.svg, forky-mark-small.svg) and use
  them in the header; re-optimize the PNG favicons/logos (much smaller).
- Redesign favicon.svg; set theme-color/tile color to #090a0e; fix the
  placeholder name in site.webmanifest; remove humans.txt.
- Drop a stale @ts-expect-error in index.tsx; refresh web/CLAUDE.md
  routing docs.

typecheck, lint, 125 unit tests and 81 storybook tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports PR #84 onto the Chain Console web overhaul: behind-node
partitioning (Stage), struck-through lagging sources and truncation
rendering (Graph), pinned checkpoint columns and gap markers
(useGraph), and the TruncationMarker component restyled to the
semantic token system. Master's styling is intentionally not taken;
the BYO header link fix was already present on this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Long-tail scenario exercising pinned finalized/justified checkpoints
with gap markers, an aggregated view with a stuck source struck
through in the Sources panel, and the TruncationMarker primitive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the tail

The TruncationMarker in the primitives story overlapped the progress
circles; it now sits on its own row at the graph's real radius, and the
marker typography is sized so comma'd counts stay inside the disc. The
truncated long-tail graph stories auto-pan (via real mouse events, so
pan/zoom state stays consistent) from the head to the pinned
checkpoints and gap markers on load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ills

Graph primitives stories now render every node at the graph's actual
radius (150) so the fixed node typography keeps its real proportions,
with layouts re-spaced and boundary labels moved clear of the discs.
TruncationMarker and ConcatNode backgrounds are fully opaque — no graph
node lets edges or rails bleed through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm/action-setup found no version (the packageManager pin lives in
web/package.json, not the repo root) — pin pnpm 10.30.1 explicitly.
golangci-lint-action v3 passes --out-format, removed in golangci-lint
v2; bump to v9.2.0 and drop only-new-issues (full repo is clean, and
this PR's diff exceeds the API's 20k-line limit). Install Playwright
Chromium before web tests for the Vitest browser-mode Storybook suite,
and move core actions to current SHA-pinned releases ahead of the
Node 20 runner deprecation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Savid
Savid merged commit efead04 into master Jun 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants