From 8f0051a046794956f748d298e950393943875a5f Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 10 Jul 2026 14:38:36 +0200 Subject: [PATCH 1/3] chore: replace defi-saver logo with the official one --- public/logos/defi-saver.jpg | Bin 0 -> 18690 bytes public/logos/defi-saver.svg | 33 --------------------------------- src/lib/logo-manifest.ts | 2 +- 3 files changed, 1 insertion(+), 34 deletions(-) create mode 100644 public/logos/defi-saver.jpg delete mode 100644 public/logos/defi-saver.svg diff --git a/public/logos/defi-saver.jpg b/public/logos/defi-saver.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1173f20d7b08f9f084f55fafdce5ed88ca271ee3 GIT binary patch literal 18690 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 literal 0 HcmV?d00001 diff --git a/public/logos/defi-saver.svg b/public/logos/defi-saver.svg deleted file mode 100644 index 61eb4ce2..00000000 --- a/public/logos/defi-saver.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 6d3d7c3d..bd116a8d 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -310,7 +310,7 @@ const RAW: Record = { // Identified via on-chain HL referral codes + brand cross-reference // (DFS, UNITYWALLET, INVO, MARSGO, BITGETWALLET). See builders.json // notes on the HL node for the provenance trail per address. - "defi-saver": "/logos/defi-saver.svg", + "defi-saver": "/logos/defi-saver.jpg", unitywallet: "/logos/unitywallet.png", invo: "/logos/invo.png", marsgo: "/logos/marsgo.jpg", From a0cf6d54923a3e7dddd36e6f15b84b0a02c6e7ad Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:41:44 +0200 Subject: [PATCH 2/3] venue KPI strips: read cohort KV blobs first, add pm/perp toggle on product pages (#1049) Co-authored-by: Florent Tapponnier --- src/app/products/[slug]/page.tsx | 64 +++++++++++++++++++--------- src/components/venue-kpi-toggle.tsx | 66 +++++++++++++++++++++++++++++ src/lib/perp-stats.ts | 2 +- src/lib/perp-venue-data.ts | 42 +++++++++++++++++- src/lib/pm-stats.ts | 2 +- src/lib/pm-venue-data.ts | 47 +++++++++++++++++++- 6 files changed, 198 insertions(+), 25 deletions(-) create mode 100644 src/components/venue-kpi-toggle.tsx diff --git a/src/app/products/[slug]/page.tsx b/src/app/products/[slug]/page.tsx index 53d458f7..06f156af 100644 --- a/src/app/products/[slug]/page.tsx +++ b/src/app/products/[slug]/page.tsx @@ -28,6 +28,7 @@ import { PERP_PRODUCT_PILL_SLUGS, } from "@/lib/perp-venue-context"; import { PerpVenueSection } from "@/components/perp-venue-section"; +import { VenueKpiToggle } from "@/components/venue-kpi-toggle"; import { PmDataFeedSection } from "@/components/pm-data-feed-section"; import { RpcProviderChainsSection } from "@/components/rpc-provider-chains-section"; @@ -540,16 +541,48 @@ export default async function ProviderPage({ {hlStats && } - {pmContext?.kind === "venue" && ( - - )} + {(() => { + // Cohort sections. When a product belongs to BOTH the PM venue + // cohort and the perp cohort, wrap the two sections in a pill + // toggle so both stay reachable on the same page. With a single + // cohort the section renders directly, no toggle bar. + const pmVenueSection = + pmContext?.kind === "venue" ? ( + + ) : null; + const perpVenueSection = perpContext ? ( + + ) : null; + if (pmVenueSection && perpVenueSection) { + return ( + + ); + } + return pmVenueSection ?? perpVenueSection; + })()} {pmContext?.kind === "feed" && ( )} - {perpContext && ( - - )} - {reg && (

diff --git a/src/components/venue-kpi-toggle.tsx b/src/components/venue-kpi-toggle.tsx new file mode 100644 index 00000000..33435515 --- /dev/null +++ b/src/components/venue-kpi-toggle.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useState } from "react"; + +/** + * Pill toggle that swaps between pre-rendered venue sections on + * /products/ without navigation. The sections are server + * components rendered by the page and passed in as ReactNode content, + * so switching tabs costs zero network round trips. + * + * Callers only mount this when at least two sections exist; with a + * single cohort the page renders the section directly (a one-button + * toggle bar would be noise). Inactive sections stay in the DOM under + * `hidden` so tab switches are instant and anchors keep working. + * + * Pill styling mirrors PmHubTabs / PerpHubTabs. + */ + +export type VenueToggleSection = { + id: string; + label: string; + content: React.ReactNode; +}; + +export function VenueKpiToggle({ + sections, +}: { + sections: VenueToggleSection[]; +}) { + const [active, setActive] = useState(sections[0]?.id ?? ""); + + if (sections.length === 0) return null; + if (sections.length === 1) return <>{sections[0].content}; + + return ( +

+
+ {sections.map((s) => ( + + ))} +
+ {sections.map((s) => ( + + ))} +
+ ); +} diff --git a/src/lib/perp-stats.ts b/src/lib/perp-stats.ts index 08efd8e5..7702b489 100644 --- a/src/lib/perp-stats.ts +++ b/src/lib/perp-stats.ts @@ -112,7 +112,7 @@ function promUrl(): string | null { * Bumped any time the PerpCohortSummary shape changes so a stale-shape * blob from a previous deploy can never deserialize into a misaligned * payload. The cohort-snapshot module appends its own `:v1` suffix. */ -const PERP_COHORT_KEY = "perp-cohort"; +export const PERP_COHORT_KEY = "perp-cohort"; /** * Fetch the cohort in one Promise.all fan out. Returns null when Prom diff --git a/src/lib/perp-venue-data.ts b/src/lib/perp-venue-data.ts index f134f53d..f0bc396c 100644 --- a/src/lib/perp-venue-data.ts +++ b/src/lib/perp-venue-data.ts @@ -11,6 +11,8 @@ import { unstable_cache } from "next/cache"; import { Prometheus } from "@/lib/prometheus"; +import { readCohortSnapshot } from "@/lib/cohort-snapshot"; +import { PERP_COHORT_KEY, type PerpCohortSummary } from "@/lib/perp-stats"; export type PerpVenueKpis = { /** USD 24h traded volume. */ @@ -47,9 +49,46 @@ function perpFeesLabel(slug: string): string { return slug === "gmx-v2" ? "gmx" : slug; } +/** + * Snapshot-first venue KPIs. The materialize worker writes the + * perp-cohort blob to KV every minute; the Vercel site has no + * PROMETHEUS_URL, so the blob is the only data source that works in + * prod. Returns null when the blob is missing, stale, or has no usable + * row for the venue, and the caller falls through to Prom. + */ +async function fetchPerpVenueKpisFromSnapshot( + slug: string, +): Promise { + const snap = await readCohortSnapshot(PERP_COHORT_KEY); + const row = snap?.data?.venues?.find((v) => v.slug === slug); + if (!row) return null; + + const kpis: PerpVenueKpis = { + volume24h: row.volume24h ?? null, + volume30d: row.volume30d ?? null, + openInterest: row.openInterest ?? null, + fees30d: row.fees30d ?? null, + activeMarkets: row.activeMarkets ?? null, + topMarketVolume24h: row.topMarketVolume24h ?? null, + health: row.health ?? null, + allInFeeBpsEth: row.allInFeeBpsEth ?? null, + funding24hBpsEth: row.funding24hBpsEth ?? null, + }; + + // A row with every field null means the harness has not published + // this venue yet; let the Prom fallback take a shot instead. + const hasData = Object.values(kpis).some((v) => v !== null); + return hasData ? kpis : null; +} + async function fetchPerpVenueKpisRaw( slug: string, ): Promise { + const fromSnapshot = await fetchPerpVenueKpisFromSnapshot(slug); + if (fromSnapshot) return fromSnapshot; + + // Fallback: direct Prom probes. Only works where PROMETHEUS_URL is + // set (the materialize worker context), never on the Vercel site. const url = promUrl(); if (!url) return null; let prom: Prometheus; @@ -112,7 +151,8 @@ async function fetchPerpVenueKpisRaw( const fetchPerpVenueKpisCached = unstable_cache( fetchPerpVenueKpisRaw, - ["perp-venue-kpis-v1"], + // v2: snapshot-first (perp-cohort blob) with Prom fallback. + ["perp-venue-kpis-v2"], { revalidate: 120, tags: ["perp-venue"] }, ); diff --git a/src/lib/pm-stats.ts b/src/lib/pm-stats.ts index 23e866e6..5afd3552 100644 --- a/src/lib/pm-stats.ts +++ b/src/lib/pm-stats.ts @@ -102,7 +102,7 @@ function promUrl(): string | null { * worker after every tierA sweep. Bump the suffix if the summary shape * changes so a stale-shape blob can never deserialize into a misaligned * payload. The cohort-snapshot module appends its own `:v1`. */ -const PM_HUB_KEY = "pm-hub"; +export const PM_HUB_KEY = "pm-hub"; /** * Fetch the venue + data feed cohort in one Promise.all fan out. Returns diff --git a/src/lib/pm-venue-data.ts b/src/lib/pm-venue-data.ts index 379882f6..d3b65b37 100644 --- a/src/lib/pm-venue-data.ts +++ b/src/lib/pm-venue-data.ts @@ -18,6 +18,8 @@ import { unstable_cache } from "next/cache"; import { Prometheus } from "@/lib/prometheus"; +import { readCohortSnapshot } from "@/lib/cohort-snapshot"; +import { PM_HUB_KEY, type PmCohortSummary } from "@/lib/pm-stats"; // `include_tag=true` is required: without it gamma-api returns // `tags: null` for every market, every classifyCategory call falls @@ -254,9 +256,51 @@ function promUrl(): string | null { return process.env.PROMETHEUS_URL?.trim() || null; } +/** + * Snapshot-first venue KPIs. The materialize worker writes the pm-hub + * cohort blob to KV every minute; the Vercel site has no PROMETHEUS_URL, + * so the blob is the only data source that works in prod. The blob lacks + * the p99 / uptime / rate-limit fields, which stay null (the strip skips + * null cards). Returns null when the blob is missing, stale, or has no + * usable row for the venue, and the caller falls through to Prom. + */ +async function fetchPmVenueKpisFromSnapshot( + slug: string, +): Promise { + const snap = await readCohortSnapshot(PM_HUB_KEY); + const row = snap?.data?.venues?.find((v) => v.slug === slug); + if (!row) return null; + + const kpis: PmVenueKpis = { + volume30d: row.volume30d ?? null, + openInterest: row.openInterest ?? null, + activeMarkets: row.activeMarkets ?? null, + medianResolutionSec: + row.medianResolutionDelayMin != null + ? row.medianResolutionDelayMin * 60 + : null, + apiP50Ms: row.p50ApiLatencyMs ?? null, + // Not in the cohort blob; the strip hides these cards. + apiP99Ms: null, + uptime24h: null, + rateLimitHeadroom: null, + marketsAbove1m: row.marketsAbove1m ?? null, + }; + + // A row with every field null means the harness has not published + // this venue yet; let the Prom fallback take a shot instead. + const hasData = Object.values(kpis).some((v) => v !== null); + return hasData ? kpis : null; +} + async function fetchPmVenueKpisRaw( slug: string, ): Promise { + const fromSnapshot = await fetchPmVenueKpisFromSnapshot(slug); + if (fromSnapshot) return fromSnapshot; + + // Fallback: direct Prom probes. Only works where PROMETHEUS_URL is + // set (the materialize worker context), never on the Vercel site. const url = promUrl(); if (!url) return null; let prom: Prometheus; @@ -321,7 +365,8 @@ async function fetchPmVenueKpisRaw( const fetchPmVenueKpisCached = unstable_cache( fetchPmVenueKpisRaw, - ["pm-venue-kpis-v1"], + // v2: snapshot-first (pm-hub cohort blob) with Prom fallback. + ["pm-venue-kpis-v2"], { revalidate: 120, tags: ["pm-venue"] }, ); From 82fe81d71e5a18b0960b0b02a232187be56db14c Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 10 Jul 2026 14:46:11 +0200 Subject: [PATCH 3/3] perps: by-asset matrix worker-written snapshot (Vercel has no Prom access), official aster logo --- public/logos/aster.jpg | Bin 9197 -> 0 bytes public/logos/aster.svg | 36 ++++++++++++++++++------------------ src/lib/perp-stats.ts | 29 +++++++++++++++++++++++++++-- worker/index.ts | 4 +++- 4 files changed, 48 insertions(+), 21 deletions(-) delete mode 100644 public/logos/aster.jpg diff --git a/public/logos/aster.jpg b/public/logos/aster.jpg deleted file mode 100644 index b94df50b5196b5cd9a9f2a09ef5cf14d20d09cf7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9197 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 Vp+m2VF(KB2^J0bU6<}CA{XfwIXU+fs diff --git a/public/logos/aster.svg b/public/logos/aster.svg index 6559bc7b..13845d55 100644 --- a/public/logos/aster.svg +++ b/public/logos/aster.svg @@ -1,25 +1,25 @@ - - - - - - + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + diff --git a/src/lib/perp-stats.ts b/src/lib/perp-stats.ts index 7702b489..5e3e6eaf 100644 --- a/src/lib/perp-stats.ts +++ b/src/lib/perp-stats.ts @@ -352,7 +352,7 @@ export async function fetchPerpCohort(): Promise { * three asset columns plus the fee column round-trip Prom at most once * every two minutes regardless of page traffic. */ -async function fetchPerpByAssetMatrixRaw(): Promise { +export async function fetchPerpByAssetMatrixFresh(): Promise { const url = promUrl(); if (!url) return []; let prom: Prometheus; @@ -427,9 +427,34 @@ async function fetchPerpByAssetMatrixRaw(): Promise { ); } +const PERP_BY_ASSET_KEY = "perp-by-asset"; + +// Snapshot-first, mirroring fetchPerpCohortRaw: Vercel has no +// PROMETHEUS_URL (the VPS Prom is not public), so the live path only +// works for the worker; readers get the worker-written blob. +async function fetchPerpByAssetMatrixRaw(): Promise { + const snapshot = await readCohortSnapshot(PERP_BY_ASSET_KEY); + if (snapshot && Array.isArray(snapshot.data) && snapshot.data.length > 0) { + return snapshot.data; + } + const fresh = await fetchPerpByAssetMatrixFresh(); + if (fresh.length > 0) { + try { + await writeCohortSnapshot(PERP_BY_ASSET_KEY, fresh); + } catch (err) { + console.warn( + `perp-by-asset writeback failed: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + } + } + return fresh; +} + const fetchPerpByAssetMatrixCached = unstable_cache( fetchPerpByAssetMatrixRaw, - ["perp-by-asset-matrix-v1"], + ["perp-by-asset-matrix-v2"], { revalidate: 120, tags: ["perp-by-asset"] }, ); diff --git a/worker/index.ts b/worker/index.ts index be61811a..5ce7be12 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -43,7 +43,8 @@ import { cohortSnapshotConfigured, writeCohortSnapshot, } from "@/lib/cohort-snapshot"; -import { fetchPerpCohortFresh } from "@/lib/perp-stats"; +import { fetchPerpCohortFresh, + fetchPerpByAssetMatrixFresh } from "@/lib/perp-stats"; import { fetchHlBuilderStatsFresh, fetchHlCohortFresh, @@ -369,6 +370,7 @@ async function sweep(iteration: number): Promise { build: () => Promise; }> = [ { key: "perp-cohort", build: () => fetchPerpCohortFresh() }, + { key: "perp-by-asset", build: () => fetchPerpByAssetMatrixFresh() }, { key: "hl-frontends", build: () => Promise.resolve(hlCohort) }, { key: "hl-hip3", build: () => fetchHlHip3CohortFresh() }, { key: "hl-history", build: () => fetchHlHistoryFresh() },