From 67a4f4a275b69e2357108d30ff0c0cccc45d29a6 Mon Sep 17 00:00:00 2001 From: jayteemoney Date: Fri, 9 Oct 2026 16:16:20 +0100 Subject: [PATCH 1/2] feat(frontend): show the total streams on mainnet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app never displayed the protocol-wide stream count: the dashboard shows the connected wallet's streams and /organisations showed only streams linked to an organisation, so the 11 streams opened on mainnet were visible only in the raw /api/stats JSON. - /organisations: the readout now shows "Streams on mainnet" (get-stream-nonce, opened by anyone) beside "From organisations" (linked to a listed team), and /api/organisations returns streamsCreated. - Home page: a live line under the hero buttons, "N streams on mainnet · N organisations", linking to /organisations. It renders nothing until the numbers arrive, so no placeholder figure is ever shown. --- frontend/src/app/organisations/page.tsx | 17 +++--- frontend/src/components/landing/hero.tsx | 11 ++++ .../src/components/landing/live-stats.tsx | 54 +++++++++++++++++++ frontend/src/lib/organisations.ts | 14 ++++- 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/landing/live-stats.tsx diff --git a/frontend/src/app/organisations/page.tsx b/frontend/src/app/organisations/page.tsx index f92ee3f..a591f1c 100644 --- a/frontend/src/app/organisations/page.tsx +++ b/frontend/src/app/organisations/page.tsx @@ -166,18 +166,23 @@ function Hero({ directory }: { directory: OrganisationDirectory | null }) {

-

Streams tracked

+

Streams on mainnet

- {streams ?? "–"} + {directory?.streamsCreated ?? "–"}

+

opened by anyone

+

From organisations

+

+ {streams ?? "–"} +

+

linked to a listed team

+
+

Newest

- {newest ? newest.name : "–"} -

-

- {newest ? formatDate(newest.registeredAt) : ""} + {newest ? `${newest.name} · ${formatDate(newest.registeredAt)}` : "–"}

diff --git a/frontend/src/components/landing/hero.tsx b/frontend/src/components/landing/hero.tsx index 83d60f9..068af86 100644 --- a/frontend/src/components/landing/hero.tsx +++ b/frontend/src/components/landing/hero.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; import { LiveStreamCard } from "@/components/landing/live-stream-card"; +import { LiveStats } from "@/components/landing/live-stats"; import { ArrowRight, Zap } from "lucide-react"; export function Hero() { @@ -81,6 +82,16 @@ export function Hero() { + {/* Live usage, read from the contract */} + + + + {/* Animated streaming preview */} { + const res = await fetch(`${OPENCLAW_API_URL}/api/stats`); + if (!res.ok) throw new Error(`stats ${res.status}`); + return (await res.json()) as Stats; +} + +/** + * The live usage line under the hero. Read from the contract through + * /api/stats; it renders nothing until the numbers arrive, so a visitor never + * sees a placeholder or a guessed figure. + */ +export function LiveStats() { + const { data } = useQuery({ + queryKey: ["live-stats"], + queryFn: fetchStats, + refetchInterval: 60_000, + staleTime: 30_000, + }); + if (!data) return null; + + const orgs = data.workspacesRegistered; + return ( + + + + + + + {data.streamsCreated} streams on + mainnet + + · + + {orgs}{" "} + {orgs === 1 ? "organisation" : "organisations"} + + · read live from the contract + + ); +} diff --git a/frontend/src/lib/organisations.ts b/frontend/src/lib/organisations.ts index 1a3af10..2984ad4 100644 --- a/frontend/src/lib/organisations.ts +++ b/frontend/src/lib/organisations.ts @@ -15,6 +15,7 @@ import { getDao, getDaoCount, getNetwork, + getStreamNonce, HIRO_API_BASE, STREAM_FACTORY_CONTRACT, } from "./openclaw-server"; @@ -36,6 +37,12 @@ export interface OrganisationDirectory { network: "mainnet" | "testnet"; /** `get-dao-count`, the authoritative total. */ total: number; + /** + * Every stream ever opened on mainnet (`get-stream-nonce`), by anyone, whether + * or not an organisation sent it. Distinct from the streams organisations + * have linked, which is the sum of `streamsTracked`. + */ + streamsCreated: number; /** Newest first. */ organisations: Organisation[]; /** @@ -148,7 +155,11 @@ let cached: { directory: OrganisationDirectory; expires: number } | null = null; export async function getOrganisationDirectory(): Promise { if (cached && Date.now() < cached.expires) return cached.directory; - const [rows, total] = await Promise.all([fetchFactoryTransactions(), getDaoCount()]); + const [rows, total, streamsCreated] = await Promise.all([ + fetchFactoryTransactions(), + getDaoCount(), + getStreamNonce(), + ]); const registrations = parseRegistrations(rows, STREAM_FACTORY_CONTRACT); // The contract is the source of truth for each record. A registration whose @@ -172,6 +183,7 @@ export async function getOrganisationDirectory(): Promise const directory: OrganisationDirectory = { network: getNetwork(), total, + streamsCreated, organisations, unlisted: Math.max(0, total - organisations.length), asOf: new Date().toISOString(), From bb98ff258c689dee4f2e8d55b08159ee0b5609f4 Mon Sep 17 00:00:00 2001 From: jayteemoney Date: Fri, 9 Oct 2026 16:32:21 +0100 Subject: [PATCH 2/2] feat(frontend): stream total as prominent as the organisation count - /organisations: "Organisations" and "Streams" now sit side by side at the same size, with "From organisations" and "Newest" below. - Dashboard overview: a "StackStream on mainnet" panel at the top shows streams created and organisations registered, linking to /organisations. It also shows before a wallet is connected, and the wallet's own figures now sit under a "Your streams" heading. - Both read one shared hook (useNetworkStats) over /api/stats, which the home page line now uses too. Every new stream or registration, by anyone, raises the numbers within about a minute. --- frontend/src/app/dashboard/page.tsx | 19 ++++--- frontend/src/app/organisations/page.tsx | 33 +++++++----- .../src/components/landing/live-stats.tsx | 21 +------- .../src/components/layout/network-stats.tsx | 54 +++++++++++++++++++ frontend/src/hooks/use-network-stats.ts | 33 ++++++++++++ 5 files changed, 122 insertions(+), 38 deletions(-) create mode 100644 frontend/src/components/layout/network-stats.tsx create mode 100644 frontend/src/hooks/use-network-stats.ts diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index dc7c2bb..8120333 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -20,6 +20,7 @@ import { useTokenMetadata } from "@/hooks/use-token-metadata"; import type { StreamData } from "@/lib/stacks"; import { STREAM_STATUS, unresolvableTokenLabel } from "@/lib/constants"; import { TopUpDialog } from "@/components/stream/top-up-dialog"; +import { NetworkStats } from "@/components/layout/network-stats"; import { Skeleton } from "@/components/ui/skeleton"; import Link from "next/link"; import { PlusCircle, Zap, Users, Coins, TrendingUp } from "lucide-react"; @@ -47,17 +48,23 @@ export default function DashboardPage() { if (!isConnected) { return ( - } - title="Connect your wallet" - description="Connect a Stacks wallet to open streams and manage real-time payments, settled on Bitcoin." - /> +
+ + } + title="Connect your wallet" + description="Connect a Stacks wallet to open streams and manage real-time payments, settled on Bitcoin." + /> +
); } return (
- {/* Stats row */} + + + {/* Your streams */} +

Your streams

{isLoading ? ( Array.from({ length: 4 }).map((_, i) => ( diff --git a/frontend/src/app/organisations/page.tsx b/frontend/src/app/organisations/page.tsx index a591f1c..9118eff 100644 --- a/frontend/src/app/organisations/page.tsx +++ b/frontend/src/app/organisations/page.tsx @@ -158,31 +158,38 @@ function Hero({ directory }: { directory: OrganisationDirectory | null }) { style={delay(2)} >
-

- Organisations registered -

-

- {total ?? "–"} -

-
+
-

Streams on mainnet

-

+

+ Organisations +

+

+ {total ?? "–"} +

+
+
+

+ Streams +

+

{directory?.streamsCreated ?? "–"}

-

opened by anyone

+
+

From organisations

{streams ?? "–"}

-

linked to a listed team

-
+

Newest

- {newest ? `${newest.name} · ${formatDate(newest.registeredAt)}` : "–"} + {newest ? newest.name : "–"} +

+

+ {newest ? formatDate(newest.registeredAt) : ""}

diff --git a/frontend/src/components/landing/live-stats.tsx b/frontend/src/components/landing/live-stats.tsx index 2247640..95e3862 100644 --- a/frontend/src/components/landing/live-stats.tsx +++ b/frontend/src/components/landing/live-stats.tsx @@ -1,19 +1,7 @@ "use client"; import Link from "next/link"; -import { useQuery } from "@tanstack/react-query"; -import { OPENCLAW_API_URL } from "@/lib/constants"; - -interface Stats { - streamsCreated: number; - workspacesRegistered: number; -} - -async function fetchStats(): Promise { - const res = await fetch(`${OPENCLAW_API_URL}/api/stats`); - if (!res.ok) throw new Error(`stats ${res.status}`); - return (await res.json()) as Stats; -} +import { useNetworkStats } from "@/hooks/use-network-stats"; /** * The live usage line under the hero. Read from the contract through @@ -21,12 +9,7 @@ async function fetchStats(): Promise { * sees a placeholder or a guessed figure. */ export function LiveStats() { - const { data } = useQuery({ - queryKey: ["live-stats"], - queryFn: fetchStats, - refetchInterval: 60_000, - staleTime: 30_000, - }); + const { stats: data } = useNetworkStats(); if (!data) return null; const orgs = data.workspacesRegistered; diff --git a/frontend/src/components/layout/network-stats.tsx b/frontend/src/components/layout/network-stats.tsx new file mode 100644 index 0000000..c02f395 --- /dev/null +++ b/frontend/src/components/layout/network-stats.tsx @@ -0,0 +1,54 @@ +"use client"; + +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useNetworkStats } from "@/hooks/use-network-stats"; + +/** + * StackStream's live usage on mainnet, shown at the top of the dashboard + * whether or not a wallet is connected. Both figures come from the contract. + */ +export function NetworkStats() { + const { stats, isLoading } = useNetworkStats(); + + if (isLoading) return ; + if (!stats) return null; + + return ( +
+
+
+

+ + + + + StackStream on mainnet +

+ + See organisations + +
+
+
+

+ {stats.streamsCreated} +

+

Streams created

+
+
+

+ {stats.workspacesRegistered} +

+

+ {stats.workspacesRegistered === 1 ? "Organisation registered" : "Organisations registered"} +

+
+
+
+ ); +} diff --git a/frontend/src/hooks/use-network-stats.ts b/frontend/src/hooks/use-network-stats.ts new file mode 100644 index 0000000..8964930 --- /dev/null +++ b/frontend/src/hooks/use-network-stats.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { OPENCLAW_API_URL } from "@/lib/constants"; + +export interface NetworkStats { + /** Every stream ever opened on mainnet, by anyone (stream-manager's stream counter). */ + streamsCreated: number; + /** Every organisation registered on stream-factory. */ + workspacesRegistered: number; + asOf: string; +} + +async function fetchNetworkStats(): Promise { + const res = await fetch(`${OPENCLAW_API_URL}/api/stats`); + if (!res.ok) throw new Error(`stats ${res.status}`); + return (await res.json()) as NetworkStats; +} + +/** + * Network-wide usage, read from the contract through /api/stats. Both numbers + * only go up: each new stream and each new registration adds one, whoever makes + * it. Refreshed every minute, which is also the API's cache window. + */ +export function useNetworkStats() { + const query = useQuery({ + queryKey: ["network-stats"], + queryFn: fetchNetworkStats, + refetchInterval: 60_000, + staleTime: 30_000, + }); + return { stats: query.data ?? null, isLoading: query.isLoading }; +}