diff --git a/src/components/hl-builder-dashboard.tsx b/src/components/hl-builder-dashboard.tsx index eb571f800..a11e6428a 100644 --- a/src/components/hl-builder-dashboard.tsx +++ b/src/components/hl-builder-dashboard.tsx @@ -5,6 +5,7 @@ import { HlCoinDistribution } from "@/components/hl-coin-distribution"; import { HlUserPercentile } from "@/components/hl-user-percentile"; import { HlTopUsersTable } from "@/components/hl-top-users-table"; import { HlFeedCoverageNote } from "@/components/hl-feed-coverage-note"; +import { HlKpiStrip } from "@/components/hl-kpi-strip"; /** * Per-builder HyperTracker-parity dashboard on /products/[slug]. @@ -36,44 +37,9 @@ export function HlBuilderDashboard({ return (
-

- Hyperliquid frontend dashboard -

- -
- - - - - - -
+ 0 - ? "text-emerald-600 dark:text-emerald-400" - : delta < 0 - ? "text-red-600 dark:text-red-400" - : "text-ink-faint"; - return ( -
-

{label}

-

- {value} -

- {delta !== undefined && ( -

{fmtDelta(delta)}

- )} -
- ); -} - function Milestone({ label, value, @@ -235,26 +159,5 @@ function fmtUSD(v: number): string { return `$${v.toFixed(4)}`; } -function fmtCount(v: number): string { - if (!Number.isFinite(v) || v === 0) return "0"; - const abs = Math.abs(v); - if (abs >= 1_000_000) return `${(v / 1_000_000).toFixed(2)}M`; - if (abs >= 1_000) return `${(v / 1_000).toFixed(1)}K`; - return Math.round(v).toLocaleString("en-US"); -} -function fmtPct(v: number): string { - if (!Number.isFinite(v) || v === 0) return "0%"; - const pct = v * 100; - if (pct >= 10) return `${pct.toFixed(1)}%`; - if (pct >= 1) return `${pct.toFixed(2)}%`; - return `${pct.toFixed(3)}%`; -} -function fmtDelta(v: number): string { - if (!Number.isFinite(v) || v === 0) return "0%"; - const pct = v * 100; - const sign = v > 0 ? "+" : ""; - if (Math.abs(pct) >= 10) return `${sign}${pct.toFixed(0)}%`; - return `${sign}${pct.toFixed(1)}%`; -} diff --git a/src/components/hl-kpi-strip.tsx b/src/components/hl-kpi-strip.tsx new file mode 100644 index 000000000..3dc5d3c84 --- /dev/null +++ b/src/components/hl-kpi-strip.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useState } from "react"; +import { HL_WINDOWS, HL_WINDOW_LABEL } from "@/lib/hl-feed"; +import type { HlWindow } from "@/lib/hl-feed"; +import type { HlBuilderStats } from "@/lib/hl-builder-stats"; + +/** + * The headline figures for one Hyperliquid frontend, with a control for the + * window they cover. + * + * The strip was fixed at 30 days, which is the complaint that started this: + * a reader who wants to know what a frontend did yesterday had no way to ask. + * The harness has published 24h, 7d and 30d for revenue, volume, users and + * the period-over-period delta all along; only the page was pinned. + * + * Two of the six cards do not move with the control, and say so rather than + * silently reusing a 30d number under a "feed day" heading: + * + * - Annualised is the window's revenue scaled to a year, so it is derived + * from the window and changes, but its multiplier changes with it. + * - Cohort share is defined as a share of the cohort's 24h notional. There + * is no 7d or 30d version of it in the harness, and computing one here + * from figures that are not published would be inventing a number. + */ + +/** Days each window spans, for the annualised projection. "24h" is one + * complete UTC feed day. */ +const WINDOW_DAYS: Record = { "24h": 1, "7d": 7, "30d": 30 }; + +export function HlKpiStrip({ stats }: { stats: HlBuilderStats }) { + const [timeframe, setTimeframe] = useState("30d"); + + // A stats object cached before byWindow existed would otherwise render a + // strip of zeros for one cache cycle. + const w = stats.byWindow?.[timeframe] ?? { + revenue: stats.revenue30d, + volume: stats.volume30d, + users: stats.users30d, + revenueDelta: stats.revenueDelta30d, + }; + + const label = HL_WINDOW_LABEL[timeframe]; + const days = WINDOW_DAYS[timeframe]; + const perUser = w.users > 0 ? w.revenue / w.users : 0; + + return ( + <> +
+

+ Hyperliquid frontend dashboard +

+
+ {HL_WINDOWS.map((win) => ( + + ))} +
+
+ +
+ + + + + + +
+ + ); +} + +function KpiCard({ + label, + value, + delta, + tip, + tone, +}: { + label: string; + value: string; + delta?: number; + tip?: string; + tone?: "primary"; +}) { + const arrowTone = + delta === undefined + ? "" + : delta > 0 + ? "text-emerald-600 dark:text-emerald-400" + : delta < 0 + ? "text-red-600 dark:text-red-400" + : "text-ink-faint"; + return ( +
+

{label}

+

+ {value} +

+ {delta !== undefined && ( +

{fmtDelta(delta)}

+ )} +
+ ); +} + +function fmtUSD(v: number): string { + if (!Number.isFinite(v) || v === 0) return "$0"; + const abs = Math.abs(v); + if (abs >= 1_000_000_000) return `$${(v / 1_000_000_000).toFixed(2)}B`; + if (abs >= 1_000_000) return `$${(v / 1_000_000).toFixed(2)}M`; + if (abs >= 1_000) return `$${(v / 1_000).toFixed(1)}K`; + if (abs >= 1) return `$${v.toFixed(2)}`; + return `$${v.toFixed(4)}`; +} + +function fmtCount(v: number): string { + if (!Number.isFinite(v) || v === 0) return "0"; + const abs = Math.abs(v); + if (abs >= 1_000_000) return `${(v / 1_000_000).toFixed(2)}M`; + if (abs >= 1_000) return `${(v / 1_000).toFixed(1)}K`; + return Math.round(v).toLocaleString("en-US"); +} + +function fmtPct(v: number): string { + if (!Number.isFinite(v) || v === 0) return "0%"; + const pct = v * 100; + if (pct >= 10) return `${pct.toFixed(1)}%`; + if (pct >= 1) return `${pct.toFixed(2)}%`; + return `${pct.toFixed(3)}%`; +} + +function fmtDelta(v: number): string { + if (!Number.isFinite(v) || v === 0) return "0%"; + const pct = v * 100; + const sign = v > 0 ? "+" : ""; + if (Math.abs(pct) >= 10) return `${sign}${pct.toFixed(0)}%`; + return `${sign}${pct.toFixed(1)}%`; +} diff --git a/src/lib/hl-builder-stats.ts b/src/lib/hl-builder-stats.ts index 6ae4251fd..c4f3f9464 100644 --- a/src/lib/hl-builder-stats.ts +++ b/src/lib/hl-builder-stats.ts @@ -59,6 +59,14 @@ export type HlBuilderStats = { /** Feed coverage for the window these figures were summed over. Null when * the gauges are not published yet (pre-2026-10-07 harness builds). */ feed: HlFeedCoverage | null; + /** The same headline figures for each window the harness publishes, so the + * dashboard's timeframe control has something to switch to. The flat + * fields above stay as the 30d view, which is what callers that never + * wanted a control already read. */ + byWindow: Record< + HlWindow, + { revenue: number; volume: number; users: number; revenueDelta: number } + >; }; /** @@ -161,6 +169,14 @@ export async function fetchHlBuilderStatsFresh( revenueDelta30d, volume30d, users30d, + revenue24h, + volume24h, + users24h, + revenueDelta24h, + revenue7d, + volume7d, + users7d, + revenueDelta7d, cohortShare, biggestDay, biggestDayUnix, @@ -176,6 +192,14 @@ export async function fetchHlBuilderStatsFresh( prom.scalar(`hl_frontend_revenue_delta_pct_v2{builder="${slug}",window="30d"}`), prom.scalar(`hl_frontend_volume_usd_30d_v2${sel}`), prom.scalar(`hl_frontend_users_30d_v2${sel}`), + prom.scalar(`hl_frontend_fees_usd_24h_v2${sel}`), + prom.scalar(`hl_frontend_volume_usd_24h_v2${sel}`), + prom.scalar(`hl_frontend_users_24h_v2${sel}`), + prom.scalar(`hl_frontend_revenue_delta_pct_v2{builder="${slug}",window="24h"}`), + prom.scalar(`hl_frontend_fees_usd_7d_v2${sel}`), + prom.scalar(`hl_frontend_volume_usd_7d_v2${sel}`), + prom.scalar(`hl_frontend_users_7d_v2${sel}`), + prom.scalar(`hl_frontend_revenue_delta_pct_v2{builder="${slug}",window="7d"}`), prom.scalar(`hl_frontend_global_volume_share_24h_v2${sel}`), prom.scalar(`hl_frontend_biggest_day_revenue_usd_v2${sel}`), prom.scalar(`hl_frontend_biggest_day_unix_v2${sel}`), @@ -239,6 +263,26 @@ export async function fetchHlBuilderStatsFresh( percentileShares30d, profitableUserPct30d: profitableUserPct30d ?? 0, feed, + byWindow: { + "24h": { + revenue: revenue24h ?? 0, + volume: volume24h ?? 0, + users: users24h ?? 0, + revenueDelta: revenueDelta24h ?? 0, + }, + "7d": { + revenue: revenue7d ?? 0, + volume: volume7d ?? 0, + users: users7d ?? 0, + revenueDelta: revenueDelta7d ?? 0, + }, + "30d": { + revenue: rev, + volume: volume30d ?? 0, + users: usr, + revenueDelta: revenueDelta30d ?? 0, + }, + }, }; }