import { Elysia } from "elysia"; import { db } from "@/lib/db/index"; import { games, performanceEntries, reports, communitySuggestions, user, } from "@/lib/db/schema"; import { eq, count, sql, gte, and, desc } from "drizzle-orm"; import { requireContributorOrAdmin } from "@/lib/auth/guard"; // In-memory cache for dashboard stats (TTL 300s) interface DashboardStatsResult { overview: { totalGames: number; totalBenchmarks: number; totalUsers: number; pendingReports: number; pendingSuggestions: number; }; recent: { benchmarksLast30Days: number; gamesLast30Days: number; }; topContributors: Array<{ userId: string | null; name: string; count: number; }>; syncHealth: Record; gamesBySource: Record; playabilityDistribution: Record; } interface CacheEntry { data: T; expiresAt: number; } const statsCache = new Map>(); const STATS_CACHE_TTL = 300_000; // 5 minutes function getCached(key: string): T | null { const entry = statsCache.get(key) as CacheEntry | undefined; if (!entry) return null; if (Date.now() > entry.expiresAt) { statsCache.delete(key); return null; } return entry.data; } function setCached(key: string, data: T): void { statsCache.set(key, { data, expiresAt: Date.now() + STATS_CACHE_TTL }); } export const dashboardRoutes = new Elysia({ prefix: "/dashboard", detail: { tags: ["Dashboard"] } }).get( "/stats", async ({ request, set }) => { const guard = await requireContributorOrAdmin(request.headers); if (!guard.ok) { set.status = guard.status; return { error: guard.error }; } // Check cache const cached = getCached("dashboard_stats"); if (cached) return cached; const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const [ totalGames, totalBenchmarks, totalUsers, pendingReports, pendingSuggestions, recentBenchmarks, recentGames, topContributors, syncHealth, gamesBySource, playabilityDistribution, ] = await Promise.all([ // Total games db.select({ count: count() }).from(games), // Total benchmarks db .select({ count: count() }) .from(performanceEntries) .where(eq(performanceEntries.isRemoved, false)), // Total users db.select({ count: count() }).from(user), // Pending reports db .select({ count: count() }) .from(reports) .where(eq(reports.status, "open")), // Pending suggestions db .select({ count: count() }) .from(communitySuggestions) .where(eq(communitySuggestions.status, "pending")), // Benchmarks in last 30 days db .select({ count: count() }) .from(performanceEntries) .where( and( eq(performanceEntries.isRemoved, false), gte(performanceEntries.createdAt, thirtyDaysAgo), ), ), // Games added in last 30 days db .select({ count: count() }) .from(games) .where(gte(games.createdAt, thirtyDaysAgo)), // Top contributors db .select({ userId: performanceEntries.userId, userName: user.name, entryCount: count(), }) .from(performanceEntries) .leftJoin(user, eq(performanceEntries.userId, user.id)) .where(eq(performanceEntries.isRemoved, false)) .groupBy(performanceEntries.userId, user.name) .orderBy(desc(count())) .limit(10), // Sync health db .select({ status: games.syncStatus, count: count(), }) .from(games) .where(sql`${games.steamAppId} IS NOT NULL`) .groupBy(games.syncStatus), // Games by source db .select({ source: games.source, count: count(), }) .from(games) .groupBy(games.source), // Playability distribution db .select({ status: games.playabilityStatus, count: count(), }) .from(games) .where(sql`${games.playabilityStatus} IS NOT NULL`) .groupBy(games.playabilityStatus), ]); const result = { overview: { totalGames: totalGames[0]?.count ?? 0, totalBenchmarks: totalBenchmarks[0]?.count ?? 0, totalUsers: totalUsers[0]?.count ?? 0, pendingReports: pendingReports[0]?.count ?? 0, pendingSuggestions: pendingSuggestions[0]?.count ?? 0, }, recent: { benchmarksLast30Days: recentBenchmarks[0]?.count ?? 0, gamesLast30Days: recentGames[0]?.count ?? 0, }, topContributors: topContributors.map((c) => ({ userId: c.userId, name: c.userName ?? "Anonymous", count: c.entryCount, })), syncHealth: syncHealth.reduce( (acc, s) => { acc[s.status ?? "unknown"] = s.count; return acc; }, {} as Record, ), gamesBySource: gamesBySource.reduce( (acc, s) => { acc[s.source ?? "unknown"] = s.count; return acc; }, {} as Record, ), playabilityDistribution: playabilityDistribution.reduce( (acc, p) => { acc[p.status ?? "unknown"] = p.count; return acc; }, {} as Record, ), }; setCached("dashboard_stats", result); return result; }, );