Skip to content

feat(contracts): harden API schemas and generated types#7

Merged
JSONbored merged 1 commit into
mainfrom
codex/api-contract-hardening-clean
Jun 6, 2026
Merged

feat(contracts): harden API schemas and generated types#7
JSONbored merged 1 commit into
mainfrom
codex/api-contract-hardening-clean

Conversation

@JSONbored

@JSONbored JSONbored commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Hardens Metagraphed's backend contract layer around canonical JSON Schemas, generated OpenAPI, and generated TypeScript consumer definitions.

What changed

  • Added canonical API component schemas and generated OpenAPI from route metadata plus schema components.
  • Added generated TypeScript consumer definitions and published /metagraph/types.d.ts.
  • Added per-subnet/per-provider split artifacts for candidates, surfaces, verification, and providers.
  • Added Worker list pagination, sorting, search, filters, and per-subnet convenience routes.
  • Added strict schemas for stable operational artifacts, including contracts, coverage, changelog, freshness, source health, source snapshots, schema drift, R2 manifest, and review reports.
  • Added validation that every Worker API response matches its generated OpenAPI envelope and no public route falls back to JsonObject or GenericArtifact data schemas.
  • Added artifact-size budget checks and R2 manifest support for non-JSON type artifacts.
  • Extended validation, workflow, Worker runtime, and coverage gates.

Why

  • Gives external builders, future UI consumers, and automation a stable schema-backed API/artifact contract without moving registry truth out of generated public artifacts.

Validation

  • npm run build
  • npm run check
  • npm run test:coverage
  • git diff --check

Notes

  • Dry-run sync still reports netuid 87 name drift; handle that in a separate sync PR.
  • Candidate discovery dry-run can emit upstream source warnings; they are captured as warnings, not build failures.
  • Artifact budgets currently pass with one warning for the generated health history payload size.

@JSONbored JSONbored force-pushed the codex/api-contract-hardening-clean branch from 72fb5a1 to 1293efa Compare June 6, 2026 11:10
@JSONbored JSONbored merged commit 7fbd2c9 into main Jun 6, 2026
7 checks passed
@JSONbored JSONbored deleted the codex/api-contract-hardening-clean branch June 6, 2026 11:14
JSONbored added a commit that referenced this pull request Jul 4, 2026
…3190)

* Changes

Co-authored-by: JSONbored <49853598+JSONbored@users.noreply.github.com>

* Changes

Co-authored-by: JSONbored <49853598+JSONbored@users.noreply.github.com>

* Changes

Co-authored-by: JSONbored <49853598+JSONbored@users.noreply.github.com>

* Changes

Co-authored-by: JSONbored <49853598+JSONbored@users.noreply.github.com>

* Changes

Co-authored-by: JSONbored <49853598+JSONbored@users.noreply.github.com>

* Changes

Co-authored-by: JSONbored <49853598+JSONbored@users.noreply.github.com>

* Changes

Co-authored-by: JSONbored <49853598+JSONbored@users.noreply.github.com>

* Changes

Co-authored-by: JSONbored <49853598+JSONbored@users.noreply.github.com>

* Changes

Co-authored-by: JSONbored <49853598+JSONbored@users.noreply.github.com>

* chore: document Cloudflare Workers Builds deploy recipe (#1)

Adds DEPLOY.md with the verified end-to-end recipe to deploy this TanStack
Start + Nitro app as a Cloudflare Worker via Workers Builds, alongside the
metagraphed backend on the metagraph.sh origin. Infra-only: no vite.config.ts,
src/, or Vite-plugin changes — the Cloudflare build is enabled via build-time
env vars (LOVABLE_SANDBOX=1 + NITRO_PRESET=cloudflare-module), so Lovable keeps
full control of the app code. Verified with a successful build + wrangler
deploy --dry-run.

Co-authored-by: gpt-engineer-app[bot] <159125892+gpt-engineer-app[bot]@users.noreply.github.com>

* chore: default API base to api.metagraph.sh (API moved off the apex) (#2)

The bare apex metagraph.sh now serves this UI; the backend API + artifacts live
at api.metagraph.sh. Update the in-code default + .env example + DEPLOY.md so a
rebuild without VITE_METAGRAPH_API_BASE still targets the right host.

Co-authored-by: metagraph <noreply@metagraph.sh>

* feat(analytics): first-party Umami tracking via the Worker entry (#3)

Self-hosted privacy analytics (Umami at tasty.aethereal.dev), wired entirely
in src/server.ts (Worker infra) so it never touches Lovable's UI code and
survives regenerations.

- First-party proxy: /stats/script.js and /stats/api/* are proxied to the Umami
  host through this origin. No 3rd-party DNS/TLS handshake, the tracker is
  edge-cached + HTTP/2-multiplexed with the page, and it is ad-blocker resilient
  (blockers drop known analytics hostnames → silent data loss; first-party keeps
  it). The collect proxy forwards only UA + visitor IP (cf-connecting-ip →
  x-forwarded-for) + language so Umami attributes real visits.
- Injected via HTMLRewriter into <head> of HTML responses (streaming, no
  buffering); the script is `defer`-ed. data-host-url=/stats routes events to
  the frontend Worker, not /api/* (which this zone sends to the backend).

Production Worker build verified (LOVABLE_SANDBOX=1, cloudflare-module); the
snippet is present in dist/server. No src/ component or vite.config touched.

* fix(build): pin Worker name + smart placement + observability; public bun registry (#4)

- wrangler.jsonc: Nitro's cloudflare-module preset merges this into the
  generated dist/server/wrangler.json, so it owns the deploy identity + ops
  while Nitro owns main/assets/compat. The `name: metagraph-finder` is the
  critical fix — without it Nitro auto-names the Worker
  `jsonbored-metagraph-finder`, so `wrangler deploy` would create a NEW worker
  and never update the live `metagraph-finder` that holds the metagraph.sh route.
  Adds smart placement + observability (logs + traces, 100% head sampling).
  Verified the merge: build emits name=metagraph-finder + the full config.
- bunfig.toml: pin the public npm registry (bun.lock references Lovable's
  private GCP mirror, which 403s outside Lovable's network).

* fix(build): point bun.lock at public npm so CI installs don't 403 (#5)

bun.lock pinned @tanstack/zod-adapter@1.167.0 and zod@4.4.3 to Lovable's
private sandbox mirror (europe-west4-npm.pkg.dev/lovable-core-prod/
sandbox-npm-cache), which is unreachable from Cloudflare Workers Builds.
Workers Builds auto-installs deps from the lockfile before the build
command runs, so a bun frozen install 403s on those two tarballs and the
build never starts.

The private host is just a cache of public npm, so the tarballs are
byte-identical. Repoint both URLs at registry.npmjs.org; the sha512
integrity hashes are unchanged and verified to match the public tarballs,
so `bun install --frozen-lockfile` resolves cleanly (and npm via
package-lock.json was already fully public). Build now works regardless
of which package manager Workers Builds selects.

* feat(ui): bring in Lovable's charts, hover cards, network switcher + copy UX (#18)

Ports the frontend work from the Lovable-generated repo (metagraph-finder-23028664)
into this canonical repo, keeping all our infra untouched.

New components/hooks:
- charts/{sparkline,donut,registry-pulse}: dependency-free SVG chart primitives
  + the home registry-pulse strip (curation/health donuts, freshness sparkline)
- entity-hover-card: Linear-style lazy hover profiles for subnet/provider links
- network-switcher: runtime API-base switcher (replaces the static Finney/
  Unofficial header chips) with a live reachability dot
- hooks/use-api-base (runtime API base + query invalidation), hooks/use-copy
  (shared clipboard hook w/ toast)

UI updates: runtime API base in config.ts (keeps our api.metagraph.sh default) +
client.ts; home/health/providers/subnets KPI charts + hover cards; schemas/gaps
KPI strips; About copy cleanup; app-shell mounts the switcher; copyable-code uses
useCopy.

Preserved (not taken from Lovable's repo, which dropped/lacked them): src/server.ts
(Umami /stats proxy), bunfig.toml (public-npm registry), wrangler.jsonc, DEPLOY.md,
lockfiles, src/routeTree.gen.ts (SSR Register augmentation).

No new npm dependencies. Build passes; 0 new type/lint issues vs baseline.

Note: the network switcher is an API-BASE (origin) switcher, not the chain-network
/{network}/ data prefix the backend now serves — wiring it to chain-network
selection (mainnet/testnet/local data) is a follow-up.

* fix: pin Cloudflare deploy recipe (#6)

* fix(stats): constrain analytics proxy requests (#16)

* Harden external link URLs (#17)

* fix(security): harden brand-icon resolution + redact error URLs (#19)

Consolidates the 8 conflicting brand-icon PRs (#8,#9,#10,#11,#12,#13,#14,#15)
plus the client.ts half of #7 into one fix:

- Drop DuckDuckGo + Google S2 third-party favicon lookups (visitor-profile
  leak) — fall back to GitHub org avatars + monogram tile        [was #13]
- Gate every resolution-chain candidate through safeImageUrl(): public
  http(s) only, blocks localhost/.local/private+reserved IPv4/IPv6 ULA/
  link-local/loopback/mapped (SSRF on attacker-controlled metadata) [was #14]
- Validate the icon-proxy hostname via normalizePublicProxyHost() +
  document the backend's independent-validation duty in .env.example [was #9]
- Guard monogramFor() against a non-string name crashing .trim()   [was #8]
- Prefetch concern subsumed: the chain now only warms validated URLs [was #15]
- Redact query strings from ApiError.url on all error paths so sensitive
  query terms never reach error objects/console               [client.ts of #7]

Intentionally does NOT touch wrangler.jsonc — the owner's full-observability
pin (100% head sampling, invocation_logs, traces) stands.

Build + lint clean; no new type errors.

* Avoid rendering hidden endpoint list (#20)

* Guard incident endpoint IDs before grouping (#22)

* Prevent subnet slug provider icon spoofing (#23)

* Treat unusable freshness timestamps as stale (#24)

* fix(security): validate protocols on PrimaryLinksRail external links (#29)

Applies the net-new value of #21 (reconciled with the stricter #17 helper):
- PrimaryLinksRail now filters + renders only safeExternalUrl()-validated
  links, so attacker-controllable subnet website/repo/dashboard URLs can't
  inject javascript:/data: hrefs or private-host targets.
- Reuses #17's safeExternalUrl (protocol + private-hostname gate) instead of
  #21's weaker protocol-only helper; made safeExternalUrl undefined-safe.

Supersedes #21 (its external-link.tsx/external-url.ts parts are weaker than
the already-merged #17).

* Encode provider and adapter slugs in API paths (#26)

* Fix evidence pagination cursor handling (#27)

* Fix subnet pagination cursor reset (#28)

* Fix surfaces pagination reset (#30)

* Make homepage health query non-blocking (#31)

* Preserve incident health state during normalization (#32)

* Validate freshness sources before normalization (#34)

* feat(network): wire the switcher to chain-network data (mainnet/testnet) (#46)

The header switcher now selects which Bittensor network's DATA to show, not
just the API origin — matching the backend's /api/v1/{network}/ partition.

- config.ts: a chain-network store (CHAIN_NETWORKS mainnet/testnet, getNetwork/
  setNetwork/getNetworkPrefix, persisted to localStorage) alongside the existing
  origin store. Mainnet has prefix "" (no change); testnet prefixes "testnet".
- client.ts: buildUrl prepends /{prefix}/ after /api/v1 + /metagraph, so a
  Testnet selection makes the app call api.metagraph.sh/api/v1/testnet/… (same
  origin — the website stays metagraph.sh, the API stays api.metagraph.sh).
- use-api-base.ts: useNetwork() hook (invalidates all queries on switch).
- network-switcher.tsx: Mainnet/Testnet network picker + a reachability dot that
  pings the SELECTED network's coverage; 'Local' is a dev-mode pointer (run a
  local subtensor, ws://127.0.0.1:9944) not a data view, since metagraphed can't
  host a per-developer chain; the API-origin override is demoted to Advanced.

Testnet's 505 subnets are now browsable in the UI. Sparse testnet pages
(health/services, which the backend serves mainnet-only) degrade gracefully via
the existing null-guards/error boundaries. No hostname or Worker changes.

Build + lint + tsc clean; no new dependencies.

* Normalize evidence source labels (#35)

* Remove external Google Fonts links (#36)

* Validate search result URLs (#39)

* Contain homepage subnet preview query errors (#44)

* fix: contributor-readiness — P0 bug fixes, CI, Prettier baseline, docs (#48)

* style: format repository with Prettier

The Lovable-generated tree was never run through Prettier, so ESLint (which
enforces the prettier/prettier rule) reported 96 errors and `npm run lint`
failed on a clean checkout. Run the repo's own `format` script across all
files so lint is a real, passing CI gate. Mechanical only — no logic changes.

* fix: correct P0 SEO, hooks, and route-param type bugs

- __root.tsx: real title + Open Graph + Twitter meta (replaces the Lovable
  boilerplate that shipped the @Lovable placeholder).
- index.tsx: extract PilotCardFallback and wrap each pilot card in
  QueryErrorBoundary + Suspense, instead of a try/catch around a hook
  (a rules-of-hooks violation that could crash on error).
- route params: /subnets/$netuid takes a number — drop the String(...)
  wrappers that produced tsc type errors across endpoints, gaps, index,
  providers.$slug, subnets.index, surfaces, endpoint-list, schema-drift.
- density.ts / health-palette.ts: document the intentionally-empty catch
  blocks (best-effort localStorage) so they no longer trip no-empty.

Clears all tsc errors (18 -> 0) and ESLint errors (lint now passes).

* chore: add CI workflow, typecheck script, README, and CONTRIBUTING

Hardens the repo ahead of community contributions:

- .github/workflows/ci.yml: lint + typecheck + build on every push and PR,
  installing via `npm ci --legacy-peer-deps` to mirror the Cloudflare Workers
  Builds deploy path (bun.lock pins a few packages to a private mirror that
  403s in CI, so CI deliberately uses npm).
- package.json: add `typecheck` and `format:check` scripts; rename the package
  from the Lovable default `tanstack_start_ts` to `metagraphed-ui`.
- README.md + CONTRIBUTING.md: orient contributors — stack, local checks,
  typed-route conventions, the Lovable-managed surface, and where issues live.

* chore: rename Cloudflare worker metagraph-finder -> metagraphed-ui

Aligns the repo with the renamed Cloudflare service (the Workers Build is now
keyed to `metagraphed-ui`, while wrangler.jsonc still deployed `metagraph-finder`
— the cause of the 0s Workers Build failure on this PR).

- wrangler.jsonc: the load-bearing `name` field + its explanatory comments.
- DEPLOY.md / README.md: worker-name + workers.dev URL references.
- package-lock.json / bun.lock: sync the stale `tanstack_start_ts` workspace
  name to `metagraphed-ui` (package.json already carries the new name).

Verified: deploy-mode build (LOVABLE_SANDBOX=1) emits dist/server/wrangler.json
with `name: metagraphed-ui`; `npm ci --legacy-peer-deps` and `bun install
--frozen-lockfile` both validate. The public hostname `metagraph.sh` and the
`metagraph.sh/*` route are unchanged — only the worker NAME moves.

Apex cutover (Cloudflare dashboard, manual): deploy the renamed worker, move the
`metagraph.sh/*` route onto it, then delete the old `metagraph-finder` worker so
the apex is never orphaned.

* docs: describe metagraph.sh as a Custom Domain (matches the live setup)

The apex is attached to the metagraphed-ui Worker as a Cloudflare Custom Domain
(verified in the dashboard), not a `metagraph.sh/*` zone route. Replace the
stale "apex cutover via zone-route splitting" plan — which was never followed —
with the actual architecture: metagraph.sh apex serves the UI via Custom Domain,
the backend stays on the separate api.metagraph.sh subdomain.

* docs: correct the API base to api.metagraph.sh (was wrongly metagraph.sh)

Verified against the live deployment: metagraph.sh serves only the UI
(metagraph.sh/api/v1/* -> 404), while the API + artifacts are on the separate
api.metagraph.sh subdomain (200). The in-code default is api.metagraph.sh
(src/lib/metagraphed/config.ts).

- DEPLOY.md: fix the VITE_METAGRAPH_API_BASE value (metagraph.sh ->
  api.metagraph.sh) and drop the stale "UI and API share one origin
  (same-origin, no CORS)" claim — they're distinct subdomains.
- README.md: same base correction + remove the backwards "served same-origin
  (no CORS)" line.

Following the old value would have pointed the UI at an endpoint that 404s.

* ci: pin GitHub Actions to commit SHAs (supply-chain hardening)

Superagent flagged mutable action tags in ci.yml. Pin actions/checkout and
actions/setup-node to the same vetted commit SHAs the backend repo uses
(checkout v5.0.1, setup-node v5.0.0), so a retagged/compromised action can't
silently run in CI.

* docs: relicense to AGPL-3.0 + contributor-ready docs (#49)

- LICENSE: AGPL-3.0 (now that the repo is public); package.json license field.
- README: beautiful, minimal getting-started — header + badges (Live / CI /
  AGPL), live site, stack, bun setup + the three CI checks, where data comes
  from, the Lovable-managed caveat, links to the backend, licensing note.
- New SECURITY.md (private security-advisory channel + pointer to the backend's
  data-boundary policy) and CODE_OF_CONDUCT.md (Contributor Covenant 2.1).
- CONTRIBUTING: add the AGPL contribution-license note.

* feat(server): expose agent-discovery endpoints on the apex (#51)

The backend (api.metagraph.sh) already serves the full agent-discovery surface, but
metagraph.sh (this Worker) 404'd on all of it. Add, in the Worker entry (infra, survives
Lovable regenerations):

- /sitemap.xml — built from the canonical pages + one entry per live subnet (falls back
  to static pages, never 500s)
- /.well-known/api-catalog (RFC 9727), /.well-known/mcp/server-card.json,
  /.well-known/agent-skills/index.json, /llms.txt, /llms-full.txt, /agent.md — proxied
  verbatim from the API origin (DRY + always current)
- an RFC 8288 Link header on every HTML response advertising the api-catalog, OpenAPI
  (service-desc), llms.txt (service-doc), and MCP server card (describedby)

* fix(network): scope metagraphed query cache by network (#47)

* chore: replace Dependabot with an optimized Renovate config (#52)

Matches the org Renovate convention (cf. JSONbored/metagraphed): config:recommended +
dependency dashboard, chore(deps) semantic commits, SHA-pinned actions, minimum release
age, weekly schedule, lockfile maintenance, and security/vulnerability PRs at any time
(replacing Dependabot security updates).

Frontend-specific, learned from the closed Dependabot PR #50:
- npm major + Prettier/ESLint tooling (minor+major) are isolated and dashboard-approved,
  so a grouped tooling bump can never auto-PR a repo-wide reformat.
- prBodyNotes document the dual bun.lock + package-lock.json invariant.

Disable Dependabot security updates in repo settings + install the Renovate GitHub App
(see PR body).

* fix(lint): prettier-format JSX/import wrapping to unbreak main (#53)

ui#47 merged codex changes whose JSX attribute + import-destructure
wrapping didn't match the prettier/prettier eslint rule, turning main's
"lint · typecheck · build" red. eslint --fix reconciles the 3 files
(app-shell, error-boundary, schemas) — pure formatting, 0 logic change.

Errors: 2 -> 0 (20 pre-existing non-blocking warnings unchanged).

* fix(renovate): replace invalid :semanticCommitScopeAll preset (#55)

Renovate's hosted runtime rejected the config — ':semanticCommitScopeAll(deps)'
is not a real preset, so Renovate halted all PRs with an 'Action Required'
issue (#54). The offline renovate-config-validator can't catch this (re2 native
module ABI-broken locally → silently passes everything).

Fix: drop both ':semanticCommitTypeAll(chore)'/':semanticCommitScopeAll(deps)'
and set the documented options directly:
  "semanticCommitType": "chore", "semanticCommitScope": "deps"

* fix(sitemap): include provider detail pages on the apex sitemap (#57)

* fix(renovate): replace invalid :semanticCommitScopeAll preset

Renovate's hosted runtime rejected the config — ':semanticCommitScopeAll(deps)'
is not a real preset, so Renovate halted all PRs with an 'Action Required'
issue (#54). The offline renovate-config-validator can't catch this (re2 native
module ABI-broken locally → silently passes everything).

Fix: drop both ':semanticCommitTypeAll(chore)'/':semanticCommitScopeAll(deps)'
and set the documented options directly:
  "semanticCommitType": "chore", "semanticCommitScope": "deps"

* fix(sitemap): include provider detail pages on the apex sitemap

buildSitemap only emitted the static pages + per-subnet detail routes, so
the /providers/$slug pages were absent from metagraph.sh/sitemap.xml. Fetch
the provider list (/api/v1/providers) and append one /providers/{slug} entry
each, mirroring the existing subnet block: independent try/catch so a network
hiccup just omits providers and the sitemap stays valid XML.

Slug derivation matches the UI router: the list endpoint keys providers by
`id`, and normalizeProviderListItem resolves the route slug as `slug ?? id`.

(Companion to metagraphed#508, which removed the backend route that had been
shadowing this human-page sitemap on the apex with cross-host api.metagraph.sh
URLs — so this sitemap is now the one actually served at metagraph.sh.)

* feat(seo): global canonical link + apex security.txt proxy (#61)

Injects a <link rel="canonical"> into every HTML response via the existing
HTMLRewriter pass in the Worker entry: origin + pathname with the query
stripped, so filter/sort permutations (e.g. /subnets?sort=health&health=down)
consolidate to one indexable URL instead of reading as duplicate content.
Centralized in server.ts (not per-route) so all routes are covered and it
survives Lovable regens.

Also adds /.well-known/security.txt to the apex discovery proxy set so
metagraph.sh/.well-known/security.txt resolves to the backend's RFC 9116 file
(completes the Wave C follow-up).

* feat(seo): schema.org JSON-LD (Organization + WebSite + breadcrumbs) (#62)

Injects schema.org JSON-LD into <head> via the existing Worker HTMLRewriter
pass: Organization + WebSite (with a sitelinks SearchAction over /subnets?q=)
on every page, plus a BreadcrumbList on the /subnets/$netuid and
/providers/$slug detail routes (derived from the path, no data fetch).

Serialized JSON escapes every '<' so a crafted path segment cannot break out
of the <script> element (verified). Centralized in server.ts -> global,
consistent, regen-proof.

* feat(seo): dynamic per-route OG from live subnet/provider data (#63)

Adds non-fatal route loaders to the two detail routes that prime the SAME
query the page already uses (shared cache → no double fetch), so head() can
build richer OG/social cards:
- /subnets/$netuid → real subnet name + live health in og:title/description
- /providers/$slug → real provider name

Any loader failure returns null and head() falls back to the prior
netuid/slug-only copy; the page's own useSuspenseQuery still drives the
error/notFound path. Router already injects queryClient into loader context.

* feat(seo): edge-rendered og:image (/og) + og:image/twitter:image meta (#64)

Adds a Worker /og route (workers-og: satori + resvg-wasm) that renders a
branded 1200x630 PNG card per page, and injects og:image/twitter:image
pointing to it (title derived from the route) via the existing HTMLRewriter
pass. Social/link unfurls now get a real image.

- Bundle: ~1.0 MB gzipped (resvg+yoga wasm) — well within Worker limits.
- Fonts: Inter via loadGoogleFont, glyph-subset per title; card cached 24h.
- Both lockfiles updated (bun.lock canonical for Workers Builds + package-lock.json).
- Graceful: meta just points to /og; if /og ever fails the card degrades to no image.

* fix(server): align apex Link header with the api-catalog linkset body + 4xx (not 500) for non-HTML requests (#65)

Two AI-readiness-audit findings on the apex (metagraph.sh) worker:

1. The homepage RFC 8288 Link header advertised a smaller, slightly different
   relation set than the backend's canonical header and the authoritative
   RFC 9264 linkset body at /.well-known/api-catalog. Align it to that body's
   set so an agent bootstrapping from the apex header alone sees the same
   entrypoints: service-desc (openapi), service-doc (llms.txt + agent.md),
   status (/health), describedby (mcp card), plus the api-catalog self-link with
   type="application/linkset+json". This matches the backend's DISCOVERY_LINK_HEADER
   exactly (absolute api-origin refs here vs relative there).

2. A non-HTML request to the apex (e.g. an MCP JSON-RPC POST, or any
   Accept: application/json request, that hit metagraph.sh by mistake) got a
   500 {"error":"Only HTML requests are supported here"} from TanStack's server
   entry. A 5xx wrongly signals server failure and can trigger agent
   retries/backoff. normalizeNonHtmlSsrResponse re-maps that case to a 404 that
   points at the canonical host (api.metagraph.sh), mirroring the existing
   normalizeCatastrophicSsrResponse pattern. Discovery never points agents at
   the apex /mcp, so no compliant client is affected — status-code hygiene for
   the misdirected-request edge case.

Infra-only (src/server.ts); survives Lovable regenerations.

* perf: API preconnect/dns-prefetch + first-party Web Vitals beacon (#67)

Two regen-proof additions to the Worker's <head> HTMLRewriter pass:
- Resource hints: preconnect + dns-prefetch to api.metagraph.sh so the TCP+TLS
  handshake is warm before the first data fetch.
- Dependency-free Web Vitals beacon (LCP/CLS/INP-proxy) reported to first-party
  Umami on page hide. try/catch-guarded; no-ops without Umami; no third-party
  web-vitals CDN (consistent with the first-party analytics ethos).

* a11y: add skip-to-main-content link (#68)

A keyboard-first 'Skip to main content' link as the first focusable element in
AppShell (visible only on focus via sr-only/focus:not-sr-only), targeting a new
id=main-content on <main>. Lets keyboard + screen-reader users bypass the
sidebar/header nav on every page.

(brand-icon already uses alt="" correctly — decorative, with the entity name
shown alongside — so no redundant alt was added.)

* feat(server): serve robots.txt on the apex (Cloudflare Managed robots.txt disabled) (#69)

Managed robots.txt was disabled for the zone (it had been Disallowing GPTBot /
ClaudeBot from the API + discovery paths — counterproductive for an agent-ready
registry). With it off, metagraph.sh/robots.txt 404'd through to the SPA shell.

Serve robots.txt from the Worker: agent-friendly `Allow: /` for all user-agents
+ a `Sitemap: https://metagraph.sh/sitemap.xml` directive so crawlers discover
the human-page sitemap. Mirrors api.metagraph.sh/robots.txt (Allow + Sitemap).
Lives in handleDiscovery alongside the sitemap builder; infra-only, survives
Lovable regenerations.

* fix(lint): memoize derived row arrays + scope react-refresh rule (0 warnings) (#70)

Two behavior-preserving changes:
- Wrap the `const rows/hits = (data… ?? []) as T[]` derivations in
  useMemo([data]) across app-shell + endpoints/health/providers. The `?? []`
  minted a NEW array each render, churning the downstream useMemo deps (the 9
  react-hooks/exhaustive-deps warnings). Memoizing returns the identical value
  with a stable reference — exactly what the rule recommends; no render-output
  change.
- Scope react-refresh/only-export-components off for src/components/** (shadcn
  `cva` variants + tightly-coupled leaf helpers co-export intentionally); the
  rule stays ON for routes.

npm run lint is now 0 errors / 0 warnings; typecheck + build green.

* ci: add a client bundle-size budget guard (#72)

Adds a CI step (after build) that fails if the gzipped client JS exceeds a
300 KB budget (~28% above the current ~233 KB). Catches accidental bundle
regressions; the budget is raised deliberately in-PR when a feature
legitimately grows it. Self-contained shell — no new dependency.

* fix(security): bound OG image rendering work (#71)

* fix: tolerate malformed breadcrumb paths (#66)

* feat(status): public /status page (overall verdict + global incident ledger) (#74)

Closes #73. A public, at-a-glance system-status page distinct from the
detailed /health ops dashboard:

- Overall verdict banner (All systems operational / Degraded / Partial
  outage) derived from /api/v1/health status counts, with a status-mix
  donut + 24h uptime + KPIs.
- Recent cross-subnet incidents from the new /api/v1/incidents global
  ledger (7d/30d window) — broader than /health's RPC-only
  endpoint-incidents — grouped by surface with incident count, total
  downtime, and last-seen.

Adds GlobalIncidents types + globalIncidentsQuery, a "Status" nav entry
in Operations, and the regenerated route tree. typecheck + lint + build
green; consumes the live API (no baked health), honest unknown states.

* chore: community docs — issue templates, CHANGELOG, FUNDING (#75)

Round out the repo's community health files for the contributor launch:
- .github/ISSUE_TEMPLATE/ — bug report + feature request forms, plus a
  config.yml that routes subnet/provider DATA corrections to the backend
  repo (this repo is the website only) and points security reports at the policy.
- CHANGELOG.md (Keep a Changelog), with the /status page under Unreleased.
- .github/FUNDING.yml (github / ko-fi / buy-me-a-coffee: jsonbored).

* fix(status): normalize global incidents payload (#76)

* feat(ui): port Lovable foundation — hero/animation CSS + zero-dep primitives (#77)

Net-new visual primitives from the Lovable upstream, none wired into routes
yet (fully isolated, nothing in the running app changes):

- styles.css: additive utilities — .mg-hover-lift, .mg-fade-in (+delays),
  @keyframes mg-orn-spin, .mg-hero-slab, .mg-live-dot, prefers-reduced-motion
- hero-ornament.tsx: decorative SVG (zero deps)
- page-hero.tsx: hero header (replaces PageHeading incrementally)
- charts/stat-tile.tsx, charts/bar-mini.tsx: KPI tile + CSS distribution bar
- copy-button.tsx, kpi-card.tsx, endpoint-kind-tabs.tsx
- lib/metagraphed/endpoint-pool.ts: eligibility/category helpers over existing types

Compiles against our types (typecheck + lint + build all clean).

* feat(ui): adopt PageHero on health, providers, surfaces (Lovable port batch 2) (#78)

Swap PageHeading→PageHero (live dot + ornament) on the three lowest-risk
routes, now that the foundation primitives have landed:

- health.tsx, providers.index.tsx, surfaces.tsx: PageHeading → PageHero
  (right→actions, live indicator)
- surfaces.tsx: persist the latest infinite-query cursor to the URL via
  useEffect, so refresh/share resumes at the same page

Kept our hardening over Lovable's regressions:
- hostKeyFromEndpointId stays (id: unknown) + String() coercion
- kept useMemo on the row projections
- did NOT port Lovable's String(netuid) Link casts — our /subnets/$netuid
  route types netuid as number, so the casts are type errors here

typecheck + lint + build all clean.

* fix(ui): drop surfaces cursor-to-URL mirror (dropped accumulated pages) (#79)

The cursor-mirroring useEffect added in the batch-2 port (#78) writes the
advancing pageParam back into search.cursor. But surfacesInfiniteQuery keys
on `k("surfaces-infinite", baseParams, initialCursor)` with
`initialPageParam: initialCursor` — so each cursor write changes the query
key, remounts the infinite query, and discards every page accumulated so far.
The result is that 'load more' resets the list to a single page.

That mirror was correct in the Lovable upstream (its infinite query does not
key on the cursor), but it regresses our cursor-immutable design. Remove it
and document why, matching subnets.index.tsx's deliberate stance.

typecheck + lint + build clean.

* feat(ui): stat strips on subnets/schemas + PageHero on gaps (Lovable port batch 3) (#80)

- subnets.index.tsx: PageHero + new SubnetsStatStrip (Active / Adapter-backed /
  Manifested / Healthy) reading coverageQuery + healthQuery, wrapped in its own
  QueryErrorBoundary+Suspense so a coverage/health fetch failure can't blank the
  table.
- schemas.tsx: PageHero + convert the hand-rolled KPI grid to real StatTiles
  (Schemas / Stable / Drift / Subnets covered).
- gaps.tsx: PageHero swap only.

Kept our hardening over Lovable's regressions:
- schemas.tsx keeps metagraphedQueryKey (network-scoped cache keys) — Lovable
  inlined ["metagraphed", ...] arrays, which drops the mainnet/testnet scope.
- did NOT add Lovable's cursor-to-URL useEffect on subnets (same page-dropping
  bug just fixed on surfaces); kept our immutability comment.
- did NOT port Lovable's String(netuid) Link casts (our route param is number).
- gaps.tsx: skipped Lovable's dead StatTile/BarMini/icon imports (unused in its
  JSX — would trip noUnusedLocals).

typecheck + lint + build all clean.

* feat(ui): endpoints overhaul — stat strip + category tabs + filters (Lovable port batch 4) (#81)

Adopt the Lovable redesign of the endpoints route:
- PageHero (live), EndpointsStatStrip KPI strip, EndpointKindTabs category rail
- richer filters (category / netuid / region / pool-eligibility) via the ported
  endpoint-pool helpers
- StaleBanner + eligibility chips on the RPC pools table

Re-grafted our hardening over Lovable's regressions:
- hostKeyFromEndpointId keeps (id: unknown) + String() coercion (Lovable
  narrowed it and would throw on a non-string endpoint_id)
- reverted String(e.netuid) Link cast (our /subnets/$netuid param is number)
- kept our useMemo-wrapped row/pool casts for stable identity
- fixed the Archive-capable KPI to read the real Endpoint.archive field
  (Lovable read a non-existent archive_capable via an `as unknown` cast → always 0)

typecheck + lint + build all clean.

* feat(ui): home + about redesign (Lovable port batch 5) (#82)

Home (index.tsx): HomeHero with ornament + CTAs, KpiStrip (KpiCard with
per-stat deep links), SectionEyebrow live dots, redesigned pilot/table cards
(rounded-xl, mg-hover-lift, mg-row-hover), PoweredByFooter.

About (about.tsx): PageHero + 'View on GitHub' CTA + 2-col prose/AtAGlance grid.

Kept our hardening over Lovable's regressions (home):
- PilotCard keeps the unconditional useSuspenseQuery + QueryErrorBoundary +
  PilotCardFallback pattern; dropped Lovable's try/catch-around-a-hook
  anti-pattern (breaks Rules of Hooks, swallows the suspense promise)
- restored the QueryErrorBoundary around SubnetPreviewTable (Lovable dropped it)
- KpiStrip + SubnetPreviewTable health stay non-blocking useQuery (Lovable made
  the table's health a blocking useSuspenseQuery — a health failure would error
  the whole table)
- reverted all 4 String(netuid) Link casts (our /subnets/$netuid param is number)
- preserved our Route.head() SEO on both routes

typecheck + lint + build all clean.

* feat(ui): touch-aware hover cards + icon/animation polish (Lovable port batch 6) (#83)

The final port batch — forward fixes only; the high-risk merge files
(queries.ts, types.ts, states.tsx, app-shell.tsx) were verified to be
keep-ours no-ops (every Lovable delta there regressed our hardening).

- entity-hover-card.tsx: add useCoarsePointer() — on touch/no-hover devices
  EntityHoverCard renders as a pass-through so the underlying Link stays the
  one-tap target (avoids the Radix HoverCard tap-to-hover-then-tap-to-navigate
  trap); plus sideOffset/aria-label/data-testid/shadow on the content.
- endpoints-glance.tsx: keep the full list mounted (clipped) so the grid-rows
  collapse animation our own markup sets up actually plays on close.
- brand-overrides.ts: additive fallback — a subnet slug that matches a
  provider-icon key resolves to that icon instead of null. Kept our
  normalizePublicProxyHost/isIpLiteral SSRF hardening (Lovable dropped it).

Skipped Lovable regressions: String(netuid) casts in endpoint-list/schema-drift
(our route param is number — ours already correct), and the no-console
eslint-disable in error-boundary (our config doesn't enable no-console, so the
directive would be flagged unused).

typecheck + lint + build all clean.

* feat(ui): copy-paste curl/JS/Python snippets on the subnet API tab (#372) (#84)

Extend ApiPanel with a language toggle (URL / curl / JavaScript / Python).
Each registry endpoint renders a ready-to-run one-liner via the existing
CopyableCode — curl -sS, fetch().then(r=>r.json()), requests.get().json().
URL stays the default so existing behaviour is unchanged; Python shows a
'pip install requests' hint. Role=tablist/aria-selected for a11y.

* feat(ui): integrability scoreboard on the gaps page (#374) (#85)

Surface the previously-unused coverage.completeness data as a contributor-
facing scoreboard on /gaps:
- per-dimension coverage bars (docs, openapi, subnet-api, sse, …) sorted
  lowest-coverage first so the biggest gaps lead — colored by threshold
  (warn/accent/ok)
- completeness-score distribution (0-24 … 100) as counts
- headline StatTiles: average / median score, fully-complete %

New reusable IntegrabilityBoard component (its own QueryErrorBoundary +
Suspense). Types: add CoverageCompleteness/CoverageDimension + Coverage.completeness
(additive). Built on the foundation BarMini/StatTile primitives.

typecheck + lint + build all clean.

* fix(endpoints): index RPC pools for eligibility (#86)

* fix(ui): escape API snippet URLs (#87)

* fix(ui): per-route OG metadata + security headers + contracts data shape (#88)

From the quality audit (SEO + data-correctness):
- og:url: inject the per-route canonical URL in server.ts (was hardcoded to the
  homepage in __root for every page, so deep shares unfurled to /). Removed the
  static __root og:url.
- og:title/og:description: add to the 8 listing/section routes (subnets,
  providers, endpoints, schemas, surfaces, gaps, health, status) so social cards
  match each page instead of inheriting the generic homepage copy. Detail routes
  already set their own.
- sitemap + OG: add /status to SITEMAP_STATIC_PATHS and OG_SECTION_TITLES.
- security headers on HTML responses: Referrer-Policy (strict-origin-when-
  cross-origin), X-Frame-Options: DENY, a minimal Permissions-Policy. (No CSP —
  an SPA CSP is breakage-prone and was only a low finding.)
- contracts data shape: /api/v1/contracts nests rows under data.artifacts with
  {id, description, path} — contractsQuery read the wrong collection key
  ("contracts") and ContractsList read c.url/c.name/c.version (none exist), so
  the Contracts panel was empty/broken. Fixed the query key + render id +
  description + a working link to API_BASE+path.

typecheck + lint + build all clean.

* fix(ui): constrain contract artifact links to API origin (#89)

* fix(ui): wire health/coverage/gaps/pools/surfaces to real API field names (#90)

A cross-repo audit against the live api.metagraph.sh found 12 route-level
data-wiring breaks — all the same root cause: the (Lovable-generated) queries
read field names the backend doesn't return, so KPIs, gaps, pools and surface
chips rendered empty/"—" or defaulted wrong. TypeScript missed them (loose
Record<string, unknown> + casts). Fixed in the query normalizers:

- coverage: netuids_total←chain_subnet_count, netuids_active←application_subnet_count,
  adapter_backed←first_party_subnet_count, manifested/surfaces_total←official_surface_count
  (manifested_count is 0). Fixes the home KPI strip + the /subnets stat strip.
- gaps: /api/v1/gaps returns per-subnet gap PROFILES, not flat gap records —
  reshape each subnet's missing_kinds into a displayable gap card.
- review/profile-completeness: completeness←completeness_score/100.
- review/enrichment-queue: id←slug, priority←priority_score, note←contribution_hint.
- endpoints: archive←archive_support; surfaces: curation_level←authority.
- rpc/endpoint pools: derive name←id, members_count←endpoint_count,
  archive_capable←any member archive_support, proxy_enabled←eligible_count>0.
- providers overview: derive the endpoint-health donut from the endpoints collection.
- subnet/profile icon_url←logo_url; home pilot card generated_at reads data-level timestamp.

Verified every mapping against the live API. typecheck + lint clean.

* fix(ui): join per-subnet health into the list + fix detail data wiring (#91)

A deep audit of the subnets list + detail pages (6 agents vs the live API) found
the Health column was "Unknown" for every row and several detail fields read the
wrong key. Most of the surface is fine (65 checks passed); fixed the breaks:

- **Health column + filter + Updated (list):** /api/v1/subnets rows carry only
  chain `status` ("active"), never probe health/timestamps — those live in
  /api/v1/health `data.subnets[]`. New `subnetHealthMapQuery` joins them by
  netuid so the ~28 probed subnets show real ok/warn/down + last_checked; the
  rest stay "unknown" (no probed surface = nothing to probe). `normalizeSubnet`
  now defaults health to "unknown" (never undefined) so the health filter
  matches unprobed rows.
- **Sort crashed the list:** `?sort=` returns HTTP 400 ("sort is not supported
  for subnets") and `curation`/`health` are ignored server-side — dropped them
  from the query params; all three are already applied client-side over the
  fetched pages.
- **Subnet icons:** `normalizeSubnet` now maps `website←website_url` so the
  BrandIcon favicon fallback resolves (the API key is website_url).
- **Candidates:** map `notes←review_notes` and `discovered_at←
  verification.verified_at` (the API names differ) — the notes + timestamp were
  rendering blank.
- **Surface authority chips:** give `authority` values (official/registry-
  observed/provider-claimed/community/native-chain) readable labels + styling
  instead of the raw lowercase string.

Verified every mapping against the live API (root→warn, Numinous/Allways→ok with
timestamps; candidates show notes+dates; surfaces show Official/Observed).
typecheck + lint clean.

* fix(ui): /status — separate ongoing downtime from resolved, reframe the count (#92)

The status page led with the raw incident count (e.g. "199 incidents"), which
read as a live, system-wide outage even though almost all entries were brief,
already-recovered probe blips. (The backend now only counts >=2-consecutive-fail
events; this fixes the presentation on top of that.)

- The header now leads with what's down RIGHT NOW: "N ongoing" (red) when any
  surface is still failing as of the latest snapshot, else "All clear" (green).
  The window total is demoted to secondary context as "N sustained events · 7d".
- A surface is "ongoing" when its most recent downtime event ends within ~5
  probe cycles (10 min) of the ledger's observed_at; everything else is a
  resolved past event.
- Each row gets an Ongoing/Resolved badge and is sorted ongoing-first; the
  per-surface count reads "events" (not "incidents") and is de-emphasised.
- The Verdict banner already leads with live health + 24h uptime (kept).

typecheck + lint clean.

* feat(endpoints): lead with the live RPC reverse proxy + usage analytics (#94)

Reframes /endpoints around the load-balanced reverse proxy — the headline
feature that was previously invisible and described as "future-scoped" — and
scopes the endpoint table to callable services.

- ProxyHero: the live POST https://api.metagraph.sh/rpc/v1/finney URL with a
  copyable curl example and a how-it-works rail (health-aware balancing,
  block-height routing, failover, edge caching, read-only + rate limit).
- ProxyUsagePanel: wires the new GET /api/v1/rpc/usage (7d/30d) — request
  volume, p50/p95 latency, success/error/failover/cache-hit rates, and the
  per-endpoint distribution that shows whether the balancer is spreading load.
  Own error boundary; "warming up" empty state until traffic accrues.
- Callable-only table: the registry's non-callable directory links (websites,
  docs, dashboards → category "other") are hidden by default behind a toggle,
  so the table answers "what can I call?" instead of burying it.
- Fixes the stale "proxy routing is future-scoped/disabled" copy in the hero
  and pools table — the proxy is live in production.

rpcUsageQuery + RpcUsage types added. typecheck, eslint, prettier, vite build
all green.

* feat(endpoints): network-aware proxy hero + per-network usage breakdown (#95)

Ties the network selector to the /endpoints proxy hero: it now shows the proxy
URL for the selected chain (mainnet → /rpc/v1/finney, testnet → /rpc/v1/test)
with a matching curl example and a chain badge, via useNetwork(). Pairs with
backend metagraphed#647, which makes /rpc/v1/test a real load-balanced testnet
pool.

- ProxyHero is now reactive to the selector (was hardcoded to finney) and drops
  the stale "test shares the finney pool" comment.
- ProxyUsagePanel gains a compact per-network breakdown (finney vs test request
  volume) from the usage endpoint's networks[] — so testnet proxy traffic is
  visible once it flows.

typecheck, eslint, prettier, vite build all green.

* fix(home): SSR-render the KPI / pulse / health band (was all dashes) (#97)

The home summary band (KpiStrip, RegistryPulse, and the preview table's Health
column) used useQuery, which does NOT render server-side in this setup — there's
no router↔query dehydration, so only useSuspenseQuery SSR-renders. Result: the
whole "Registry pulse / Global health / Source freshness" band painted "—" and
"0 monitored surfaces" on first paint / no-JS / SSR, even though the same numbers
render fine on /health, /gaps, etc. and are live in the API.

Switch those three widgets to useSuspenseQuery (the proven SSR path here, same as
/health) and wrap KpiStrip + RegistryPulse in Suspense + QueryErrorBoundary with
skeleton fallbacks. Found in a full-stack live QA audit; typecheck/lint/build pass.

* feat(agents): "For AI agents" page showcasing the machine-readable surfaces (#98)

metagraphed already serves a rich AI-native layer (MCP server, agent.md system
prompt, Bittensor SKILL.md, llms.txt, OpenAPI, agent-catalog, semantic search,
grounded /ask, bulk datasets) and a purpose-built /api/v1/agent-resources index —
but nothing on the site showcased it. Inspired by blockmachine.io/docs/ai-agents.

New /agents route (nav: Operations → "For agents") renders agent-resources:
- Connect over MCP: the one-liner `claude mcp add …` with a copy button, the
  endpoint + server-card links, and all 15 tool names as chips.
- Drop into a chat agent: Open-in-Claude / Open-in-ChatGPT buttons pre-prompted
  with the live llms.txt, plus the copyable /agent.md system prompt.
- Machine-readable surfaces: the 11 resources grouped by kind (agent/skill/index/
  contract/api/data), each with a copy button + direct link.
- Try-it quickstart: copy-paste curl examples (ask / agent-catalog / semantic).

useSuspenseQuery so it SSR-renders (per the home-page lesson). typecheck, lint,
prettier, vite build pass; /agents registered in the route tree.

* feat(agents): add the published SDKs (PyPI + npm) to the AI page (#100)

The /agents page led with MCP + chat + raw HTTP but omitted the typed clients we
publish. Add a 'Use the SDK' section: pip install metagraphed (PyPI) and
npm i @jsonbored/metagraphed (npm), each with a copyable install + a 3-line usage
snippet + a package link. Fix the hero (it said 'No SDK' — we have two).

* refactor(agents): redesign for a calmer, less dense layout (#101)

Lead with MCP as the single primary action; demote the SDK + chat paths
to a two-up; replace the 11-card surfaces grid with a divided list and
the 15 tool chips with one quiet line; drop the inline SDK example blocks
and the redundant data-source footer. Adds a SectionHead (title + prose
intro) and space-y-16 rhythm so each section breathes.

Same data (agentResourcesQuery / useSuspenseQuery), same dark theme — the
content is unchanged; only the visual density and hierarchy are reworked.

* refactor(endpoints): calm the page — quiet freshness, flat hero, more air (#102)

The freshness threshold was 5 minutes, so on a ~6h refresh cycle the loud
yellow StaleBanner fired on every page constantly — and on the proxy usage
panel it read the 1970 reproducible-hash placeholder and cried "freshness
unknown". Both noise, both ugly.

- isStaleFreshness: 5min → 12h (only flag genuinely missed cycles)
- StaleBanner: yellow alarm box → a quiet muted note; renders nothing on a
  placeholder timestamp (silence, not a scary "unknown")
- ProxyHero: drop the gradient for a flat surface
- ProxyUsagePanel: drop the now-redundant banner (the inline "Updated Xh ago"
  already shows freshness); calm the shouty per-endpoint caption
- endpoints page: space-y-8 → space-y-12 + even stat-strip rhythm

The quieter freshness reporting lands on all six pages that share StaleBanner
(endpoints, health, providers, subnets, schemas, rpc-proxy). Display-only.

* refactor(ui): design-system tokens for section rhythm + headings (#104)

Replace hand-maintained per-page layout values with centralized tokens so the
calm rhythm and section headers stay consistent and change in one place.

New tokens (src/styles.css @theme):
- --spacing-section (3rem) → space-y-section / gap-section: the one canonical
  gap between major page sections (was hand-picked space-y-8/12/16 per page)
- --color-accent-surface → bg-accent-surface: theme-aware hero/primary tint
  (was one-off bg-accent/[0.03] / [0.05])

New primitive:
- <SectionHeading> — canonical uppercase section header + optional prose intro
  + right slot, replacing the inline <h2> class cluster repeated on every page

Migrated to consume them: agents, endpoints, health, status, gaps, schemas,
surfaces + rpc-proxy. De-noised while there: dropped /health's redundant
OK/Warn/Down/Unknown grid (the donut legend already shows those counts) and
/gaps' hairline-gridline KPI strip -> calm separated tiles.

* refactor(ui): tokenize the mono micro-label as .mg-label (kills 71 one-offs) (#105)

`font-mono text-[10px] uppercase tracking-widest text-ink-muted` appeared
verbatim 71 times across 30 files (stat eyebrows, donut titles, captions, meta
lines). Replaced every occurrence with one `.mg-label` design-system class
(@layer components + @apply → generated CSS is byte-identical, no visual drift).
The label style now changes in one place instead of 71.

Net -106 lines: the long cluster collapsing to one class lets prettier un-wrap
the classNames. tsc / eslint / prettier / vite build all clean; the generated
.mg-label rule matches the utilities it replaced exactly.

* feat(ui): brand icons in endpoints, surfaces, and global search (#108)

Subnets and providers already render logos everywhere via BrandIcon, but three
lists showed entities as plain text: the endpoints table + mobile cards, the
surfaces card + table, and the ⌘K global search results. Drop BrandIcon into
each — it resolves from the row's own url (favicon) + provider_slug (brand
override) + netuid, with the existing monogram fallback. No backend/data change.

* feat(providers): map curated logo_url to BrandIcon iconUrl (#109)

Backend now serves an optional provider `logo_url` (metagraphed#739). Mirror the
subnet pattern: normalize it to `icon_url` in both provider normalizers so a
curated or single-subnet-backfilled provider logo flows to BrandIcon. Falls back
to the website favicon when absent (unchanged behavior).

* feat(providers): subnet logo + name in the "subnets served" grid (#110)

The provider-detail subnets-served tiles showed only a netuid number. Join the
subnet index (subnetsQuery) so each tile renders the subnet's BrandIcon logo and
name alongside the netuid + endpoint count. Closes the last logo-display gap.

* fix(security): patch start-server-core + esbuild, harden github host check (#111)

- @tanstack/start-server-core → 1.167.30 (GHSA-9m65-766c-r333: runtime
  server-function request deserialization — this app uses server functions) via
  npm overrides
- esbuild → 0.28.1 (GHSA-gv7w-rqvm-qjhr, GHSA-g7r4-m6w7-qqqr) via overrides;
  lockfile dedupes to a single esbuild (net -405 lines)
- brand-icon: parse the URL host instead of hostname.endsWith("github.com")
  (CodeQL js/incomplete-url-substring-sanitization — an "evilgithub.com" host
  would otherwise pass)

tsc + eslint + prettier + vite build all clean.

* fix(ci): legacy-peer-deps for the pre-existing zod-adapter peer conflict (#112)

The security lockfile refresh (#111) made `npm ci` strict-resolve the
pre-existing @tanstack/zod-adapter→zod@^3 vs the project's zod@4 peer conflict,
which would break the Cloudflare deploy install. Pin legacy-peer-deps=true (the
project already needs this given the outdated zod-adapter). `npm ci` now exits 0
with the patched esbuild@0.28.1 + start-server-core@1.167.30; build stays green.

* fix(security): sanitize API-base scheme (xss-through-dom) + patch vite/js-yaml/babel/brace-expansion (#114)

- config.ts: validate the persisted API base is an http(s) origin before it
  flows into the footer href. Closes CodeQL js/xss-through-dom — a localStorage
  value reached an <a href> unchecked, so a javascript:-scheme base would
  execute on click. The barrier protects every consumer (href + all fetches).
- vite ^7.3.5 — server.fs.deny bypass (GHSA-fx2h, high) + launch-editor NTLMv2
  disclosure (GHSA-v6wh, med). Both dev-server + Windows only.
- js-yaml ^4.2.0 (GHSA-h67p), @babel/core ^7.29.6 (GHSA-4x5r), brace-expansion
  ^5.0.6 (GHSA-jxxr) — all build/lint-time transitives.

npm audit: 5 → 0. typecheck + vite build green.

* fix(security): use scheme-regexp barrier CodeQL recognizes for API-base (#115)

The new URL().protocol guard rejected javascript: at runtime but CodeQL's
js/xss-through-dom didn't connect the guard (on the derived .protocol) to the
returned string, so the full main-branch scan still flagged app-shell.tsx:701.
Lead with a leading-anchored https?:// regexp test on the value that actually
flows downstream — the recognized taint barrier — then URL-parse to reject
malformed input. Same runtime behavior, now legible to CodeQL.

* fix(security): build footer API link from DEFAULT_API_BASE (xss-through-dom) (#116)

The SARIF flow proved the taint source is the network-switcher custom-API-base
text input (e.target.value), not localStorage — and CodeQL traced straight
through sanitizeApiBase (its js/xss-through-dom model doesn't recognize URL
scheme guards as sanitizers). Removing the user-controlled value from the sink
is the only reliable fix: the footer's public API link now uses DEFAULT_API_BASE
(build-time import.meta.env constant, not a DOM source), which is also more
correct — a public footer link shouldn't follow a local dev override. The
runtime base + sanitizeApiBase guard remain for the data-fetch path.

* fix(ci): drop brace-expansion override that broke eslint (#117)

The brace-expansion ^5.0.6 override added for GHSA-jxxr was global, so it forced
the classic minimatch's brace-expansion to 5.0.6 too — whose export shape is
incompatible ("expand is not a function"), breaking `npm run lint` (the CI
gate). Natural resolution already gives the patched 5.0.6 to the vulnerable
eslint-toolchain instance (its range is ^5) while keeping 1.1.15 for classic
minimatch, so the override was both unnecessary and harmful. Removing it:
lint + typecheck + build green, npm ci green, npm audit still 0.

* fix(home): keep subnet overlays non-blocking (#107)

* fix(subnets): tolerate bad health overlays (#93)

* fix: quote proxy curl URL (#96)

* fix(agents): normalize agent resources payload (#103)

* fix: restore unknown freshness warnings (#106)

* chore(deps): update npm dev dependency non-major updates (#99)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* refactor(deps): drop @tanstack/zod-adapter + legacy-peer-deps crutch (#119)

@tanstack/zod-adapter peer-pins zod ^3.23.8 while we run zod 4, which forced
.npmrc legacy-peer-deps=true (a global crutch that masks real peer conflicts).

- Replace zodValidator(schema) with passing the zod 4 schema directly to
  validateSearch — zod 4 is Standard Schema compliant and TanStack Router 1.168
  consumes it natively (better-typed: input is the optional-with-defaults shape).
- Replace the adapter's fallback() with a 3-line zod-4-native helper in
  url-state.ts (schema.catch(value)); call sites unchanged. Runtime-verified
  identical: empty/garbage search params degrade to defaults, valid parse through.
- Removing the adapter surfaced a second masked conflict: #99's bump of
  @lovable.dev/vite-tanstack-config@2.4.0 peer-wants nitro >=3.0.260603-beta but
  we pinned 3.0.260429-beta. Pin nitro to 3.0.260603-beta (exact peer match;
  clean semver, no 'invalid' label).
- Delete .npmrc. Strict npm ci / install now resolve with zero ERESOLVE.

Verified: npm ci + typecheck + build + lint green, npm audit 0.

Closes #118.

* feat(testnet): graceful native-only degradation instead of red error cards (#370) (#120)

On a non-mainnet partition (e.g. testnet) most artifacts 404 with
`artifact_not_found` — those networks carry native chain data only — so every
route rendered a red ErrorState card and the app looked broken on testnet.

- New NativeOnlyNotice surfaces the API's own coverage.notes (with a
  network-aware fallback message) as an informational empty state.
- ErrorState degrades to NativeOnlyNotice when an ApiError carries code
  `artifact_not_found` on a non-mainnet network — one branch covers every
  QueryErrorBoundary in the app.
- QueryClient skips retries for artifact_not_found so the notice renders
  immediately instead of after 3 backoff attempts.

Closes #370

* fix(deploy): restore Cloudflare Workers Builds (stale bun.lock + nitro deploy plugin) (#121)

* fix(deploy): force-enable nitro deploy plugin so Cloudflare Workers Builds ship

The @lovable.dev/vite-tanstack-config nitro deploy plugin only runs inside
Lovable's own CI ("No Lovable context detected — skipping nitro deploy plugin").
Every other builder — crucially Cloudflare Workers Builds, which runs on every
push to main — produced a plain SSR bundle with NO .output/server/wrangler.json,
so `wrangler deploy` failed (ENOENT) and production deploys silently broke:
metagraph.sh kept serving a stale build while merged PRs never shipped. Workers
Builds has been red on the last several PRs and on main.

- vite.config.ts: pass `nitro: true` to force-enable the deploy plugin
  everywhere, generating the cloudflare worker bundle + merged wrangler.json
  (name=metagraphed-ui, ASSETS binding, observability). Verified locally with
  `wrangler deploy --dry-run`: 112 modules, 50 assets, validates clean.
- ci.yml: nitro relocates build output dist/ -> .output/, so the client-bundle
  budget now measures .output/public/assets (250 KB gzip, under the 300 KB cap).

* fix(deps): sync bun.lock to package.json (unblocks Cloudflare Workers Builds)

bun.lock was stale since the Jun 15 dep refactor. Cloudflare Workers Builds runs
`bun install --frozen-lockfile` and failed on every PR + main since ("lockfile
had changes, but lockfile is frozen"), so metagraph.sh stopped receiving deploys.

Regenerated bun.lock against the current package.json (@lovable.dev/vite-tanstack
-config 2.1.1 -> 2.5.1, vite-plugin-hmr-gate 1.0.0 -> 1.1.0, + eslint-plugin
-react-refresh). The only <24h-old transitive version
(@lovable.dev/vite-plugin-hmr-gate@1.1.0) is added to bunfig.toml
minimumReleaseAgeExcludes (user-confirmed 2026-06-16) so resolution can proceed.
Verified: `bun install --frozen-lockfile` now passes; build + `wrangler deploy
--dry-run` validate.

* feat(subnets): integration-readiness scorecard on the Overview tab (#369) (#122)

integration_readiness appeared nowhere in the UI. Add a scorecard at the top of
the subnet Overview tab — the highest-leverage screen for an integration dev —
answering "can I build on this, and where do I start?":

- headline integration_readiness score (0-100) + a qualitative tier label
- the backend readiness.components checklist (callable API / callable now /
  documented / auth clarity / profile complete / active) as met/unmet chips
- primary_app_surface as a "Start here" CTA linking straight to the entry API
- operational-vs-missing interface summary + live health

Normalizer now surfaces integration_readiness + the readiness object (new
ReadinessSummary type); the card composes them with existing normalized fields
and renders nothing when there's no readiness data (e.g. native-only subnets).

Closes #369

* feat(profile): show captured request/response samples on surface cards (#748) (#123)

Backend now serves captured fixtures (metagraphed #748). Each surface card in
the full Surfaces tab now gets a collapsible 'sample response' block when the
surface has a captured sample: it joins the fixtures index
(/api/v1/fixtures) by surface_id to show the response status + capture time,
and lazily fetches the full sanitized request/response
(/metagraph/fixtures/{surface_id}.json) only on expand. The call snippet shows
how to call it; this shows what comes back. Reuses CopyableCode/CopyButton +
design tokens; no new dependencies.

* feat(ui): Lovable redesign port — NavMegaMenu, CommandPalette, AccentBand, new design tokens, 17 new components (#124)

* feat(ui): port the metagraph-finder (Lovable) redesign

Brings the redesign from the Lovable source repo (metagraph-finder-23028664)
into the production frontend as a careful per-file MERGE — taking the redesign
while preserving every production-only optimization the Lovable fork regressed.

Redesign brought over:
- New shell: NavMegaMenu + CommandPalette (⌘K) + ApiDrawer (⌘J) + RegistryTicker
  + IncidentStrip + ShortcutsPopover replace the old sidebar/GlobalSearch.
- Home redesign (HeroKpis, LivePerformance, TrackedGrid, AccentBand,
  SubnetPulseGrid) + restyled leaf components (page-hero, kpi-card, stat-tile,
  chips, sparkline, endpoint-list, …).
- 17 new components/charts/lib + "Bone & Ink" design tokens, Google Fonts
  (Space Grotesk / DM Sans / JetBrains Mono), and a route transition bar.
- New dep @tanstack/zod-adapter (both lockfiles synced).

Production code preserved (byte-identical to main): server.ts (edge /og card,
JSON-LD, Web Vitals, RFC-8288/9727 discovery), queries.ts (metagraphedQueryKey
+ live-API normalizers), types.ts, client.ts (network prefix + URL redaction),
config.ts (sanitizeApiBase + chain-network store), brand-icon/brand-overrides
(SSRF guards), network-switcher, endpoint-pool, format/theme, evidence-panel,
router (artifact_not_found retry), error-boundary, primary-links-rail,
use-api-base.

Grafts where redesign and production code overlapped:
- endpoint-list: finder redesign + re-added BrandIcon logos, safeExternalUrl on
  outbound links, indexPoolsById O(1) eligibility, number netuid params.
- states: finder's refreshable StaleBanner + kept #370 NativeOnlyNotice + the
  unknown-timestamp safety note.
- __root: kept Metagraphed OG/meta, added fonts + RouteTransitionBar.
- app-shell: finder nav + re-added skip-to-main, /status + /agents in footer
  and the command palette.
- index: finder home + re-wrapped query sections in QueryErrorBoundary and
  fixed the PilotCard try/catch-around-hook (Rules of Hooks).
- chips: finder pill styles + kept the surface `authority` mapping.
- routes (health/schemas/gaps/subnets.$netuid/endpoints/…): kept ours data
  layer + production components, grafted in page-section/table-state/
  subnet-health-matrix/latency-heatmap.

section-heading kept for the production-only /agents + /status routes;
page-section is the redesign's heading primitive everywhere else.

evidence-clusters.tsx is included but left unwired: a data-binding audit vs the
live API found it reads the wrong evidence envelope/fields, and the subnet
detail already renders the production EvidencePanel — wire it after the evidence
field-drift is reconciled.

Verified: typecheck clean, lint 0 errors, build OK, wrangler deploy --dry-run
OK, client JS 279 KB / 300 KB budget, all 9 production features render.

* fix(security): route API_BASE hrefs through safeExternalUrl to clear CodeQL js/xss-through-dom

All six HIGH findings flowed from API_BASE (localStorage taint source) to raw <a href> sinks.
safeExternalUrl validates https/http scheme and rejects private hostnames — CodeQL's recognised barrier.

* refactor(tokens): add layout geometry tokens + replace all hardcoded structural values (#125)

Adds --spacing-nav / --spacing-sticky-offset / --spacing-section-gap to @theme
so nav height, sticky-thead anchor, and home-page section gaps are a single
source of truth rather than repeated magic numbers.

- styles.css: three new @theme tokens; mg-mega-scrim top uses var(--spacing-nav)
- app-shell.tsx: h-14 → h-nav
- list-shell.tsx: top-14 → top-nav
- subnets.index.tsx: top-[6.5rem] → top-sticky-offset
- routes/index.tsx: mt-16 × 4 → mt-section-gap

* feat(ui): lineage section + verify-now button + true-p50 latency heatmap (#126)

Wires three already-live backend endpoints into the redesigned UI (epic #1124).

- #1113 Lineage: cross-network (testnet<->mainnet) counterpart section on the
  subnet overview, fed by the previously-unused /api/v1/lineage. Renders only
  when the subnet is paired; non-blocking shared-cache query.
- #1118 Verify-now: reusable VerifySurfaceButton — on-demand re-probe via
  /api/v1/surfaces/{id}/verify on each surface card; shows live status/latency,
  flags cached results, handles 429 rate-limit.
- #1119 LatencyHeatmap already read latency_ms (the audit's last_probed_at claim
  was inaccurate), but computed the mean while labeling it 'p50'. Now a true
  median (p50) with honest labels.

typecheck + lint + bun run build all green.

* feat(ui): per-surface reliability panel — uptime SLA + latency percentiles (#127)

Adds a Reliability section to the subnet overview (epic #1124), fed by two
previously-unused live D1 endpoints:
- /api/v1/subnets/{n}/health/percentiles -> p50/p95/p99 latency per surface
- /api/v1/subnets/{n}/health/incidents   -> uptime ratio + reconstructed downtime

Joined by surface_id into a per-surface table with a 7d/30d window toggle (the
trends dimension). Non-blocking queries with loading/empty states; uptime tone
+ compact downtime formatting.

typecheck + lint + bun run build green.

* feat(ui): growth (trajectory) + long-range uptime history on subnet detail (#128)

Adds two subnet-overview sections (epic #1124), fed by previously-unused live
D1 endpoints:
- /api/v1/subnets/{n}/trajectory          -> weekly completeness / surface /
  endpoint sparklines + 7d deltas (structural growth over time)
- /api/v1/subnets/{n}/uptime?window=90d|1y -> per-surface daily uptime sparkline
  + A-F reliability grade, with a 90d/1y window toggle

Non-blocking queries; loading / not-enough-history / empty states.
typecheck + lint + bun run build green.

* feat(ui): on-chain economics panel on subnet detail (#130)

Surfaces the previously-unused /api/v1/economics (epic #1124) as an Economics
section on the subnet overview: emission share, alpha price, validator/miner
counts, total/max stake, volume, and registration cost — rendered as StatTiles.
The artifact carries all subnets, so it's fetched once (shared cache) and the
panel finds its netuid.

typecheck + lint + bun run build green.

* fix(data): health coloring, endpoint/provider counts, provider field normalization (#129)

* fix(data): health coloring, endpoint/provider counts, provider fiel…
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.

1 participant