feat: add admin analytics API endpoint with time-series data
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { Elysia } from "elysia"
|
||||
import { db } from "@/lib/db/index"
|
||||
import {
|
||||
performanceEntries,
|
||||
gameVersions,
|
||||
games,
|
||||
hardware,
|
||||
user,
|
||||
gameComments,
|
||||
} from "@/lib/db/schema"
|
||||
import { eq, count, sql, desc } from "drizzle-orm"
|
||||
import { requireModeratorOrAdmin } from "@/lib/auth/guard"
|
||||
|
||||
export const adminAnalyticsRoutes = new Elysia({
|
||||
prefix: "/admin/analytics",
|
||||
detail: { tags: ["Admin"] },
|
||||
})
|
||||
|
||||
// ── Overview: time-series data for charts ────────────────────────
|
||||
.get(
|
||||
"/overview",
|
||||
async ({ request, set }) => {
|
||||
const guard = await requireModeratorOrAdmin(request.headers)
|
||||
if (!guard.ok) {
|
||||
set.status = guard.status
|
||||
return { error: guard.error }
|
||||
}
|
||||
|
||||
const ninetyDaysAgo = new Date()
|
||||
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90)
|
||||
|
||||
// Benchmark submissions per day (last 90 days)
|
||||
const benchmarkTimeline = await db.execute(sql`
|
||||
SELECT
|
||||
DATE(pe.created_at) AS day,
|
||||
COUNT(*)::int AS count
|
||||
FROM performance_entries pe
|
||||
WHERE pe.created_at >= ${ninetyDaysAgo}
|
||||
AND pe.is_removed = false
|
||||
GROUP BY DATE(pe.created_at)
|
||||
ORDER BY day ASC
|
||||
`)
|
||||
|
||||
// User registrations per day (last 90 days)
|
||||
const userTimeline = await db.execute(sql`
|
||||
SELECT
|
||||
DATE(u.created_at) AS day,
|
||||
COUNT(*)::int AS count
|
||||
FROM "user" u
|
||||
WHERE u.created_at >= ${ninetyDaysAgo}
|
||||
GROUP BY DATE(u.created_at)
|
||||
ORDER BY day ASC
|
||||
`)
|
||||
|
||||
// Device distribution (total benchmarks per hardware)
|
||||
const deviceDistribution = await db
|
||||
.select({
|
||||
hardwareSlug: performanceEntries.hardwareSlug,
|
||||
hardwareName: hardware.name,
|
||||
count: count(),
|
||||
})
|
||||
.from(performanceEntries)
|
||||
.innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug))
|
||||
.where(eq(performanceEntries.isRemoved, false))
|
||||
.groupBy(performanceEntries.hardwareSlug, hardware.name)
|
||||
.orderBy(desc(count()))
|
||||
|
||||
// Genre popularity (top 10 genres by benchmark count)
|
||||
const genrePopularity = await db.execute(sql`
|
||||
SELECT
|
||||
genre,
|
||||
COUNT(*)::int AS count
|
||||
FROM games g
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(g.genres) AS genre
|
||||
JOIN game_versions gv ON gv.game_id = g.id
|
||||
JOIN performance_entries pe ON pe.version_id = gv.id
|
||||
WHERE pe.is_removed = false
|
||||
AND g.genres IS NOT NULL
|
||||
GROUP BY genre
|
||||
ORDER BY count DESC
|
||||
LIMIT 10
|
||||
`)
|
||||
|
||||
// Comment activity per day (last 90 days)
|
||||
const commentTimeline = await db.execute(sql`
|
||||
SELECT
|
||||
DATE(gc.created_at) AS day,
|
||||
COUNT(*)::int AS count
|
||||
FROM game_comments gc
|
||||
WHERE gc.created_at >= ${ninetyDaysAgo}
|
||||
AND gc.is_removed = false
|
||||
GROUP BY DATE(gc.created_at)
|
||||
ORDER BY day ASC
|
||||
`)
|
||||
|
||||
return {
|
||||
benchmarkTimeline: benchmarkTimeline.rows,
|
||||
userTimeline: userTimeline.rows,
|
||||
deviceDistribution,
|
||||
genrePopularity: genrePopularity.rows,
|
||||
commentTimeline: commentTimeline.rows,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
adminCommentRoutes,
|
||||
} from "@/lib/api"
|
||||
import { adminStorageRoutes } from "@/lib/api/admin-storage"
|
||||
import { adminAnalyticsRoutes } from "@/lib/api/admin-analytics"
|
||||
import { steamSearchRoutes } from "@/lib/api/steam-search"
|
||||
import { steamdbVersionRoutes } from "@/lib/api/steamdb-version"
|
||||
import { searchUnifiedRoutes } from "@/lib/api/search-unified"
|
||||
@@ -198,6 +199,7 @@ export const app = new Elysia({ prefix: "/api" })
|
||||
.use(adminPerformanceRoutes)
|
||||
.use(adminCommentRoutes)
|
||||
.use(adminStorageRoutes)
|
||||
.use(adminAnalyticsRoutes)
|
||||
)
|
||||
// ── Strict rate limit (public forms, no auth) ───────────────
|
||||
.group("", (app) =>
|
||||
|
||||
@@ -16,6 +16,7 @@ export { adminReportRoutes } from "./admin-reports"
|
||||
export { adminPerformanceRoutes } from "./admin-performance"
|
||||
export { adminCommentRoutes } from "./admin-comments"
|
||||
export { adminStorageRoutes } from "./admin-storage"
|
||||
export { adminAnalyticsRoutes } from "./admin-analytics"
|
||||
export { steamdbVersionRoutes } from "./steamdb-version"
|
||||
export { steamgridProxyRoutes } from "./steamgrid-proxy"
|
||||
export { gamesManualRoutes } from "./games-manual"
|
||||
|
||||
Reference in New Issue
Block a user