feat(contracts): harden API schemas and generated types#7
Merged
Conversation
72fb5a1 to
1293efa
Compare
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What changed
/metagraph/types.d.ts.JsonObjectorGenericArtifactdata schemas.Why
Validation
npm run buildnpm run checknpm run test:coveragegit diff --checkNotes