feat(web): public mesh stats counter + /api/public/stats endpoint
Live social-proof counter on the landing page, tied to the E2E
narrative. Formatted as understated mono footer, not hero brag.
Backend — new GET /api/public/stats (unauthed, 60s in-memory cache):
{
messagesRouted: SELECT COUNT(*) FROM mesh.message_queue,
meshesCreated: SELECT COUNT(*) FROM mesh.mesh WHERE archivedAt IS NULL,
peersActive: SELECT COUNT(*) FROM mesh.presence WHERE disconnectedAt IS NULL,
lastUpdated: ISO timestamp,
}
Aggregate counts only — no ids, no names, no ciphertext, no routing
metadata. Safe for public consumption. cache-control header sets
public/s-maxage=60 for edge caching. `x-cache: HIT|MISS` for debug.
Frontend — new MeshStats Server Component at
modules/marketing/home/mesh-stats.tsx. Reads the endpoint server-side
via the ~/lib/api/server client, renders monospace footer:
ciphertext routed → 4,217 messages · 12 meshes · 8 peers online
broker sees none of it
Graceful zero state: when messagesRouted === 0 shows
"ciphertext → ready to route" instead of embarrassing zeros. Tabular-
nums for the numeric spans so they don't jitter across renders.
Mounted between <CallToAction /> and <LatestNewsToaster />. Page-level
`export const revalidate = 60` so Next.js ISR refreshes the counter
every minute without a DB hit on every request (combined with the
API cache = two-layer 60s TTL, DB sees ~1 query/minute).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,14 @@ import { DemoDashboard } from "~/modules/marketing/home/demo-dashboard";
|
||||
import { WhatIsClaudemesh } from "~/modules/marketing/home/what-is-claudemesh";
|
||||
import { FAQ } from "~/modules/marketing/home/faq";
|
||||
import { CallToAction } from "~/modules/marketing/home/cta";
|
||||
import { MeshStats } from "~/modules/marketing/home/mesh-stats";
|
||||
import { LatestNewsToaster } from "~/modules/marketing/home/toaster";
|
||||
|
||||
// Revalidate the page every 60s so the mesh-stats counter stays fresh
|
||||
// without hammering the DB. The /api/public/stats endpoint has its own
|
||||
// 60s in-memory cache too.
|
||||
export const revalidate = 60;
|
||||
|
||||
const HomePage = () => {
|
||||
return (
|
||||
<div
|
||||
@@ -28,6 +34,7 @@ const HomePage = () => {
|
||||
<BeyondTerminal />
|
||||
<FAQ />
|
||||
<CallToAction />
|
||||
<MeshStats />
|
||||
<LatestNewsToaster />
|
||||
</div>
|
||||
);
|
||||
|
||||
72
apps/web/src/modules/marketing/home/mesh-stats.tsx
Normal file
72
apps/web/src/modules/marketing/home/mesh-stats.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
publicStatsResponseSchema,
|
||||
type PublicStatsResponse,
|
||||
} from "@turbostarter/api/schema";
|
||||
import { handle } from "@turbostarter/api/utils";
|
||||
|
||||
import { api } from "~/lib/api/server";
|
||||
|
||||
const ZERO_STATS: PublicStatsResponse = {
|
||||
messagesRouted: 0,
|
||||
meshesCreated: 0,
|
||||
peersActive: 0,
|
||||
lastUpdated: new Date(0).toISOString(),
|
||||
};
|
||||
|
||||
const fetchStats = async (): Promise<PublicStatsResponse> => {
|
||||
try {
|
||||
return await handle(api.public.stats.$get, {
|
||||
schema: publicStatsResponseSchema,
|
||||
})();
|
||||
} catch {
|
||||
return ZERO_STATS;
|
||||
}
|
||||
};
|
||||
|
||||
const nf = new Intl.NumberFormat("en-US");
|
||||
|
||||
export const MeshStats = async () => {
|
||||
const stats = await fetchStats();
|
||||
const empty = stats.messagesRouted === 0;
|
||||
|
||||
return (
|
||||
<section className="border-t border-[var(--cm-border)] bg-[var(--cm-bg)] px-6 py-10 md:px-12">
|
||||
<div className="mx-auto max-w-[var(--cm-max-w)]">
|
||||
<div
|
||||
className="flex flex-col items-center gap-1 text-center text-[13px] text-[var(--cm-fg-tertiary)] md:flex-row md:justify-center md:gap-2"
|
||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||
>
|
||||
<span className="text-[var(--cm-fg-secondary)]">
|
||||
ciphertext routed
|
||||
</span>
|
||||
<span className="text-[var(--cm-clay)]">→</span>
|
||||
{empty ? (
|
||||
<span className="text-[var(--cm-fg-secondary)]">
|
||||
ready to route
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="tabular-nums text-[var(--cm-fg)]">
|
||||
{nf.format(stats.messagesRouted)} messages
|
||||
</span>
|
||||
<span className="hidden text-[var(--cm-border)] md:inline">·</span>
|
||||
<span className="tabular-nums text-[var(--cm-fg-secondary)]">
|
||||
{nf.format(stats.meshesCreated)} meshes
|
||||
</span>
|
||||
<span className="hidden text-[var(--cm-border)] md:inline">·</span>
|
||||
<span className="tabular-nums text-[var(--cm-fg-secondary)]">
|
||||
{nf.format(stats.peersActive)} peers online
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
className="mt-2 text-center text-[11px] text-[var(--cm-fg-tertiary)]/70"
|
||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||
>
|
||||
broker sees none of it
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { authRouter } from "./modules/auth/router";
|
||||
import { billingRouter } from "./modules/billing/router";
|
||||
import { myRouter } from "./modules/mesh/router";
|
||||
import { organizationRouter } from "./modules/organization/router";
|
||||
import { publicRouter } from "./modules/public/router";
|
||||
import { storageRouter } from "./modules/storage/router";
|
||||
import { onError } from "./utils/on-error";
|
||||
|
||||
@@ -51,6 +52,7 @@ const appRouter = new Hono()
|
||||
.route("/billing", billingRouter)
|
||||
.route("/my", myRouter)
|
||||
.route("/organizations", organizationRouter)
|
||||
.route("/public", publicRouter)
|
||||
.route("/storage", storageRouter)
|
||||
.onError(onError);
|
||||
|
||||
|
||||
57
packages/api/src/modules/public/router.ts
Normal file
57
packages/api/src/modules/public/router.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Hono } from "hono";
|
||||
|
||||
import { count, isNull } from "@turbostarter/db";
|
||||
import { mesh, messageQueue, presence } from "@turbostarter/db/schema";
|
||||
import { db } from "@turbostarter/db/server";
|
||||
|
||||
/**
|
||||
* Unauthed public stats for the landing page counter.
|
||||
*
|
||||
* In-memory 60s cache. Results are aggregate counts only — no ids,
|
||||
* no names, no ciphertext, no routing metadata. Safe for public consumption.
|
||||
*/
|
||||
const CACHE_TTL_MS = 60_000;
|
||||
|
||||
interface PublicStats {
|
||||
messagesRouted: number;
|
||||
meshesCreated: number;
|
||||
peersActive: number;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
let cachedStats: { value: PublicStats; expiresAt: number } | null = null;
|
||||
|
||||
const fetchStats = async (): Promise<PublicStats> => {
|
||||
const [[messagesRouted], [meshesCreated], [peersActive]] = await Promise.all([
|
||||
db.select({ c: count() }).from(messageQueue),
|
||||
db
|
||||
.select({ c: count() })
|
||||
.from(mesh)
|
||||
.where(isNull(mesh.archivedAt)),
|
||||
db
|
||||
.select({ c: count() })
|
||||
.from(presence)
|
||||
.where(isNull(presence.disconnectedAt)),
|
||||
]);
|
||||
|
||||
return {
|
||||
messagesRouted: messagesRouted?.c ?? 0,
|
||||
meshesCreated: meshesCreated?.c ?? 0,
|
||||
peersActive: peersActive?.c ?? 0,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
};
|
||||
|
||||
export const publicRouter = new Hono().get("/stats", async (c) => {
|
||||
const now = Date.now();
|
||||
if (cachedStats && cachedStats.expiresAt > now) {
|
||||
c.header("x-cache", "HIT");
|
||||
return c.json(cachedStats.value);
|
||||
}
|
||||
|
||||
const value = await fetchStats();
|
||||
cachedStats = { value, expiresAt: now + CACHE_TTL_MS };
|
||||
c.header("x-cache", "MISS");
|
||||
c.header("cache-control", "public, max-age=60, s-maxage=60");
|
||||
return c.json(value);
|
||||
});
|
||||
@@ -186,6 +186,18 @@ export type GetMyMeshStreamResponse = z.infer<
|
||||
typeof getMyMeshStreamResponseSchema
|
||||
>;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Public stats (unauthed landing counter)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
export const publicStatsResponseSchema = z.object({
|
||||
messagesRouted: z.number(),
|
||||
meshesCreated: z.number(),
|
||||
peersActive: z.number(),
|
||||
lastUpdated: z.string(),
|
||||
});
|
||||
export type PublicStatsResponse = z.infer<typeof publicStatsResponseSchema>;
|
||||
|
||||
export const getMyInvitesResponseSchema = z.object({
|
||||
sent: z.array(
|
||||
z.object({
|
||||
|
||||
Reference in New Issue
Block a user