diff --git a/freebuff/web/src/app/api/live/route.ts b/freebuff/web/src/app/api/live/route.ts index dd39d7c632..16f33a0dbd 100644 --- a/freebuff/web/src/app/api/live/route.ts +++ b/freebuff/web/src/app/api/live/route.ts @@ -9,7 +9,8 @@ export async function GET() { const stats = await getFreebuffLiveStats() return NextResponse.json(stats, { headers: { - 'Cache-Control': 'no-store, max-age=0', + 'Cache-Control': + 'public, max-age=0, s-maxage=60, stale-while-revalidate=30', }, }) } diff --git a/freebuff/web/src/app/home-client.tsx b/freebuff/web/src/app/home-client.tsx index e55454dd05..6b076688d7 100644 --- a/freebuff/web/src/app/home-client.tsx +++ b/freebuff/web/src/app/home-client.tsx @@ -13,6 +13,7 @@ import { CopyButton } from '@/components/copy-button' import { HeroGrid } from '@/components/hero-grid' import { Icons } from '@/components/icons' import { cn } from '@/lib/utils' +import { CompactLiveStats } from './live/live-client' const INSTALL_COMMAND = 'npm install -g freebuff' @@ -567,6 +568,8 @@ export default function HomeClient() { + + ) } diff --git a/freebuff/web/src/app/live/live-client.tsx b/freebuff/web/src/app/live/live-client.tsx index f41e4f2a33..2bf3995eed 100644 --- a/freebuff/web/src/app/live/live-client.tsx +++ b/freebuff/web/src/app/live/live-client.tsx @@ -7,6 +7,7 @@ import Link from 'next/link' import { useEffect, useState } from 'react' import { CopyButton } from '@/components/copy-button' +import { cn } from '@/lib/utils' import { COUNTRY_POINTS, WORLD_LAND_PATHS } from './world-map-data' @@ -14,9 +15,15 @@ import type { FreebuffLiveStats } from '@/server/live-stats' import type { LucideIcon } from 'lucide-react' const INSTALL_COMMAND = 'npm install -g freebuff' -const POLL_MS = 15_000 +const POLL_MS = 60_000 const MAP_SIZE = { width: 1000, height: 520 } const REGION_NAMES = new Intl.DisplayNames(['en'], { type: 'region' }) +const EMPTY_LIVE_STATS: FreebuffLiveStats = { + totalLiveUsers: 0, + countries: [], + models: [], + generatedAt: '1970-01-01T00:00:00.000Z', +} type CountryPoint = readonly [lat: number, lon: number] type PlottedCountry = FreebuffLiveStats['countries'][number] & { point: CountryPoint @@ -106,7 +113,10 @@ function isPlottedCountry( return country !== null } -function useLiveStats(initialStats: FreebuffLiveStats) { +function useLiveStats( + initialStats: FreebuffLiveStats, + options: { refreshOnMount?: boolean } = {}, +) { const [stats, setStats] = useState(initialStats) useEffect(() => { @@ -123,12 +133,16 @@ function useLiveStats(initialStats: FreebuffLiveStats) { } } + if (options.refreshOnMount) { + void refresh() + } + const interval = window.setInterval(refresh, POLL_MS) return () => { isMounted = false window.clearInterval(interval) } - }, []) + }, [options.refreshOnMount]) return stats } @@ -186,7 +200,15 @@ function EmptyState({ children }: { children: React.ReactNode }) { ) } -function WorldMap({ stats }: { stats: FreebuffLiveStats }) { +function WorldMap({ + stats, + compact = false, + isLoading = false, +}: { + stats: FreebuffLiveStats + compact?: boolean + isLoading?: boolean +}) { const maxCount = Math.max(1, ...stats.countries.map((row) => row.count)) const plottedCountries = stats.countries .map((country) => { @@ -199,20 +221,25 @@ function WorldMap({ stats }: { stats: FreebuffLiveStats }) { return (
-
-
- Active countries -
-
- {stats.countries.length.toLocaleString()} + {!compact && ( +
+
+ Active countries +
+
+ {stats.countries.length.toLocaleString()} +
-
+ )} - {plottedCountries.length === 0 && ( + {plottedCountries.length === 0 && isLoading && ( +
+
Loading live map
+
+ )} + {plottedCountries.length === 0 && !isLoading && (
Standing by
@@ -363,7 +395,7 @@ function WorldMap({ stats }: { stats: FreebuffLiveStats }) {
)} - {unplottedCount > 0 && ( + {!compact && unplottedCount > 0 && (
{unplottedCount} region{unplottedCount === 1 ? '' : 's'} listed off-map @@ -373,6 +405,46 @@ function WorldMap({ stats }: { stats: FreebuffLiveStats }) { ) } +export function CompactLiveStats({ + initialStats = EMPTY_LIVE_STATS, +}: { + initialStats?: FreebuffLiveStats +}) { + const stats = useLiveStats(initialStats, { refreshOnMount: true }) + const isLoading = stats.generatedAt === EMPTY_LIVE_STATS.generatedAt + + return ( +
+
+
+
+
+
+ + + Active users + +
+
+ {isLoading ? '...' : stats.totalLiveUsers.toLocaleString()} +
+
+
+ + +
+
+ ) +} + function ModelBars({ stats }: { stats: FreebuffLiveStats }) { const maxCount = Math.max(1, ...stats.models.map((model) => model.count)) diff --git a/freebuff/web/src/app/page.tsx b/freebuff/web/src/app/page.tsx index 334631f395..0de8eb7b99 100644 --- a/freebuff/web/src/app/page.tsx +++ b/freebuff/web/src/app/page.tsx @@ -8,7 +8,7 @@ import { siteConfig } from '@/lib/constant' export async function generateMetadata(): Promise { const canonicalUrl = env.NEXT_PUBLIC_CODEBUFF_APP_URL - const title = "Freebuff — the free coding agent" + const title = 'Freebuff — the free coding agent' const description = siteConfig.description return { diff --git a/freebuff/web/src/server/live-stats.ts b/freebuff/web/src/server/live-stats.ts index 359a85ff29..3e41720eeb 100644 --- a/freebuff/web/src/server/live-stats.ts +++ b/freebuff/web/src/server/live-stats.ts @@ -21,6 +21,12 @@ export interface FreebuffLiveStats { generatedAt: string } +const LIVE_STATS_CACHE_MS = 60_000 +let cachedLiveStats: { + expiresAt: number + stats: FreebuffLiveStats +} | null = null + const MODEL_LABELS = Object.fromEntries( SUPPORTED_FREEBUFF_MODELS.map( (model) => [model.id, model.displayName] as const, @@ -48,8 +54,16 @@ function sortCounts(rows: T[]): T[] { } export async function getFreebuffLiveStats( - now = new Date(), + now?: Date, + options: { cache?: boolean } = {}, ): Promise { + const useCache = options.cache ?? now === undefined + const requestTime = now ?? new Date() + + if (useCache && cachedLiveStats && cachedLiveStats.expiresAt > Date.now()) { + return cachedLiveStats.stats + } + const [countryRows, modelRows] = await Promise.all([ db .select({ @@ -57,7 +71,7 @@ export async function getFreebuffLiveStats( count: count(), }) .from(schema.freeSession) - .where(liveSessionWhere(now)) + .where(liveSessionWhere(requestTime)) .groupBy(schema.freeSession.country_code), db .select({ @@ -65,7 +79,7 @@ export async function getFreebuffLiveStats( count: count(), }) .from(schema.freeSession) - .where(liveSessionWhere(now)) + .where(liveSessionWhere(requestTime)) .groupBy(schema.freeSession.model), ]) @@ -84,10 +98,19 @@ export async function getFreebuffLiveStats( })), ) - return { + const stats = { totalLiveUsers: models.reduce((sum, row) => sum + row.count, 0), countries, models, - generatedAt: now.toISOString(), + generatedAt: requestTime.toISOString(), } + + if (useCache) { + cachedLiveStats = { + expiresAt: Date.now() + LIVE_STATS_CACHE_MS, + stats, + } + } + + return stats }