refactor: convert to bun workspaces monorepo

- Move web app into apps/web/
- Create packages/shared/ with shared types
- Create plugins/decky-vault/ scaffold
- Root package.json manages workspaces only
This commit is contained in:
2026-06-28 05:20:28 +08:00
parent c4bede20d4
commit cd72b7a948
345 changed files with 488 additions and 126 deletions
+215
View File
@@ -0,0 +1,215 @@
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<string, number>;
gamesBySource: Record<string, number>;
playabilityDistribution: Record<string, number>;
}
interface CacheEntry<T> {
data: T;
expiresAt: number;
}
const statsCache = new Map<string, CacheEntry<unknown>>();
const STATS_CACHE_TTL = 300_000; // 5 minutes
function getCached<T>(key: string): T | null {
const entry = statsCache.get(key) as CacheEntry<T> | undefined;
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
statsCache.delete(key);
return null;
}
return entry.data;
}
function setCached<T>(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<DashboardStatsResult>("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<string, number>,
),
gamesBySource: gamesBySource.reduce(
(acc, s) => {
acc[s.source ?? "unknown"] = s.count;
return acc;
},
{} as Record<string, number>,
),
playabilityDistribution: playabilityDistribution.reduce(
(acc, p) => {
acc[p.status ?? "unknown"] = p.count;
return acc;
},
{} as Record<string, number>,
),
};
setCached("dashboard_stats", result);
return result;
},
);