fix(admin): align dashboard stats data sources

This commit is contained in:
saltbo
2026-07-09 21:00:10 -04:00
committed by Jasper Van
parent ab81f138bd
commit 8eb388ea01
4 changed files with 171 additions and 2166 deletions
File diff suppressed because it is too large Load Diff
+95 -6
View File
@@ -24,6 +24,7 @@ import {
orgQuotas,
shares,
siteInvitations,
statsRollupsDaily,
storages,
webhookEvents,
} from '../../db/schema'
@@ -38,6 +39,10 @@ import type {
const RUNNING_DOWNLOAD_STATUSES = ['queued', 'assigned', 'running', 'downloading', 'ingesting']
const DOWNLOAD_ACTIVITY_ACTIONS = ['share_download', 'object_download']
const AUDITED_DOWNLOAD_SOURCES = new Set(['landing_share', 'object_download'])
const STORAGE_USED_ROLLUP_METRIC = 'storage.used.bytes'
const GLOBAL_ROLLUP_ORG_ID = ''
const GLOBAL_ROLLUP_DIMENSION_KEY = ''
const GLOBAL_ROLLUP_DIMENSION_VALUE = ''
export function createAdminStatsRepo(db: Database): AdminStatsRepo {
return {
@@ -547,7 +552,7 @@ async function getDashboardOverviewStats(
const [trendNewUsers, activeByDay, storageUsedByDay, uploadByDay, downloadByDay] = await Promise.all([
getNewUsersByDay(db, range),
getActiveUsersByDay(db, range),
getStorageUsedByDay(db, range),
getStorageUsedByDay(db, range, now),
getActivityBytesByDay(db, range, ['upload_confirm']),
getDownloadBytesByDay(db, range),
])
@@ -651,7 +656,7 @@ async function getDashboardStorageStats(
.select({ id: matters.id, type: matters.type, size: matters.size, createdAt: matters.createdAt })
.from(matters)
.where(and(eq(matters.status, 'active'), eq(matters.dirtype, 0))),
getStorageUsedByDay(db, range),
getStorageUsedByDay(db, range, now),
getStorageByType(db, range),
])
const fileItems = files.map((row) => ({ bytes: row.size ?? 0, createdAt: row.createdAt }))
@@ -1083,18 +1088,94 @@ async function getActivityMetadataRows(db: Database, range: AdminStatsDateRange,
)
}
async function getStorageUsedByDay(db: Database, range: AdminStatsDateRange): Promise<Map<string, number>> {
async function getStorageUsedByDay(db: Database, range: AdminStatsDateRange, now: Date): Promise<Map<string, number>> {
const buckets = createDateBuckets(range)
if (buckets.length === 0) return new Map()
const firstBucketStart = dateKeyStart(buckets[0])
const lastBucketStart = dateKeyStart(buckets[buckets.length - 1])
const rows = await db
.select({
bucketStart: statsRollupsDaily.bucketStart,
bytes: statsRollupsDaily.bytes,
})
.from(statsRollupsDaily)
.where(
and(
eq(statsRollupsDaily.metricKey, STORAGE_USED_ROLLUP_METRIC),
eq(statsRollupsDaily.orgId, GLOBAL_ROLLUP_ORG_ID),
eq(statsRollupsDaily.dimensionKey, GLOBAL_ROLLUP_DIMENSION_KEY),
eq(statsRollupsDaily.dimensionValue, GLOBAL_ROLLUP_DIMENSION_VALUE),
gte(statsRollupsDaily.bucketStart, firstBucketStart),
lte(statsRollupsDaily.bucketStart, lastBucketStart),
),
)
const byDay = new Map(rows.map((row) => [dayKey(row.bucketStart), toNumber(row.bytes)]))
const today = dayKey(now)
const missingOrMutableBuckets = buckets.filter((date) => !byDay.has(date) || date === today)
if (missingOrMutableBuckets.length === 0) return byDay
const computed = await computeStorageUsedByDayFromCurrentFiles(db, missingOrMutableBuckets)
const updatedAt = now
for (const date of missingOrMutableBuckets) {
const bytes = computed.get(date) ?? 0
byDay.set(date, bytes)
const row = {
id: storageUsedRollupId(date),
bucketStart: dateKeyStart(date),
orgId: GLOBAL_ROLLUP_ORG_ID,
metricKey: STORAGE_USED_ROLLUP_METRIC,
dimensionKey: GLOBAL_ROLLUP_DIMENSION_KEY,
dimensionValue: GLOBAL_ROLLUP_DIMENSION_VALUE,
count: 0,
bytes,
uniqueCount: 0,
metadata: JSON.stringify({ source: 'current_state_backfill' }),
updatedAt,
}
const insert = db.insert(statsRollupsDaily).values(row)
if (date === today) {
await insert.onConflictDoUpdate({
target: [
statsRollupsDaily.bucketStart,
statsRollupsDaily.orgId,
statsRollupsDaily.metricKey,
statsRollupsDaily.dimensionKey,
statsRollupsDaily.dimensionValue,
],
set: { bytes, metadata: row.metadata, updatedAt },
})
} else {
await insert.onConflictDoNothing({
target: [
statsRollupsDaily.bucketStart,
statsRollupsDaily.orgId,
statsRollupsDaily.metricKey,
statsRollupsDaily.dimensionKey,
statsRollupsDaily.dimensionValue,
],
})
}
}
return byDay
}
async function computeStorageUsedByDayFromCurrentFiles(db: Database, buckets: string[]): Promise<Map<string, number>> {
const sortedBuckets = [...buckets].sort()
const lastDayEnd = endOfDateKey(sortedBuckets[sortedBuckets.length - 1])
const files = await db
.select({ size: matters.size, createdAt: matters.createdAt })
.from(matters)
.where(and(eq(matters.status, 'active'), eq(matters.dirtype, 0), lte(matters.createdAt, range.to)))
.where(and(eq(matters.status, 'active'), eq(matters.dirtype, 0), lte(matters.createdAt, lastDayEnd)))
const sortedFiles = files
.map((row) => ({ bytes: row.size ?? 0, createdAt: row.createdAt }))
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
const byDay = new Map<string, number>()
let cursor = 0
let runningBytes = 0
for (const date of createDateBuckets(range)) {
for (const date of sortedBuckets) {
const dayEnd = endOfDateKey(date)
while (cursor < sortedFiles.length && sortedFiles[cursor].createdAt <= dayEnd) {
runningBytes += sortedFiles[cursor].bytes
@@ -1236,7 +1317,7 @@ async function getTopSharesByActivity(
counts.set(row.targetId, item)
}
const topIds = [...counts.entries()]
.sort((a, b) => b[1].views + b[1].downloads - (a[1].views + a[1].downloads))
.sort((a, b) => b[1].views - a[1].views || b[1].downloads - a[1].downloads)
.slice(0, 8)
.map(([id]) => id)
if (topIds.length === 0) return []
@@ -1482,10 +1563,18 @@ function endOfDateKey(date: string): Date {
return new Date(`${date}T23:59:59.999Z`)
}
function dateKeyStart(date: string): Date {
return new Date(`${date}T00:00:00.000Z`)
}
function dayKey(date: Date): string {
return date.toISOString().slice(0, 10)
}
function storageUsedRollupId(date: string): string {
return `daily:${STORAGE_USED_ROLLUP_METRIC}:${date}`
}
function unixSeconds(date: Date): number {
return Math.floor(date.getTime() / 1000)
}
@@ -99,6 +99,42 @@ describe('admin stats routes', () => {
expect(res.status).toBe(400)
})
it('does not publish admin stats routes in the OpenAPI document', async () => {
const { app } = await createTestApp()
const res = await app.request('/api/openapi.json')
const body = (await res.json()) as { paths: Record<string, unknown> }
expect(res.status).toBe(200)
expect(Object.keys(body.paths).some((path) => path.startsWith('/api/admin/stats'))).toBe(false)
})
it('reads storage waterline trends from daily rollups when present', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await seedProLicense(db)
await seedStatsFixture(db)
await db.run(sql`
INSERT INTO stats_rollups_daily (
id, bucket_start, org_id, metric_key, dimension_key, dimension_value,
count, bytes, unique_count, metadata, updated_at
)
VALUES (
'storage-used-2026-01-01', ${Date.UTC(2026, 0, 1)}, '', 'storage.used.bytes', '', '',
0, 4096, 0, NULL, ${Date.UTC(2026, 0, 2)}
)
`)
const res = await app.request('/api/admin/stats/storage?from=2026-01-01&to=2026-01-01', { headers })
const body = (await res.json()) as {
storageTrend: Array<{ date: string; usedBytes: number; newBytes: number; newFiles: number }>
}
expect(res.status).toBe(200)
expect(body.storageTrend).toEqual([{ date: '2026-01-01', usedBytes: 4096, newBytes: 0, newFiles: 0 }])
})
it('returns traffic dashboard stats from audit-backed download events for Pro admins', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
@@ -191,6 +227,34 @@ describe('admin stats routes', () => {
expect(oldRanking.storageByType).toEqual([])
expect(oldRanking.topShares).toEqual([])
})
it('orders top share rankings by views before downloads', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
await seedProLicense(db)
const { orgId, userId } = await seedStatsFixture(db)
const nowSec = Math.floor(Date.now() / 1000)
const futureSec = nowSec + 7 * 24 * 60 * 60
await db.run(sql`
INSERT INTO shares (id, token, kind, matter_id, org_id, creator_id, expires_at, download_limit, views, downloads, status, created_at)
VALUES ('share-download-heavy', 'share-download-heavy', 'landing', 'stats-file', ${orgId}, ${userId}, ${futureSec}, 10, 0, 3, 'active', ${nowSec})
`)
await db.run(sql`
INSERT INTO activity_events (id, org_id, user_id, actor_type, action, target_type, target_id, target_name, metadata, created_at)
VALUES
('activity-download-heavy-1', ${orgId}, NULL, 'anonymous', 'share_download', 'share', 'share-download-heavy', 'report.pdf', '{"bytes":512,"source":"landing_share","anonymous":true}', ${nowSec}),
('activity-download-heavy-2', ${orgId}, NULL, 'anonymous', 'share_download', 'share', 'share-download-heavy', 'report.pdf', '{"bytes":512,"source":"landing_share","anonymous":true}', ${nowSec}),
('activity-download-heavy-3', ${orgId}, NULL, 'anonymous', 'share_download', 'share', 'share-download-heavy', 'report.pdf', '{"bytes":512,"source":"landing_share","anonymous":true}', ${nowSec})
`)
const res = await app.request('/api/admin/stats/ranking', { headers })
const body = (await res.json()) as { topShares: Array<{ token: string; views: number; downloads: number }> }
expect(res.status).toBe(200)
expect(body.topShares[0]).toMatchObject({ token: 'share-token-1', views: 1, downloads: 1 })
expect(body.topShares[1]).toMatchObject({ token: 'share-download-heavy', views: 0, downloads: 3 })
})
})
async function seedStatsFixture(db: Awaited<ReturnType<typeof createTestApp>>['db']) {
+12 -432
View File
@@ -1,4 +1,6 @@
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { z } from 'zod'
import { requireAdmin } from '../middleware/auth'
import type { Env } from '../middleware/platform'
import { requireFeature } from '../middleware/require-feature'
@@ -12,137 +14,6 @@ import {
getAdminDashboardTrafficStats,
getAdminDetailedStats,
} from '../usecases/admin-stats'
import { errorResponse, jsonContent } from './openapi'
const coreStatsSchema = z
.object({
generatedAt: z.string(),
users: z.object({
total: z.number().int(),
admins: z.number().int(),
activeLast30Days: z.number().int(),
newLast7Days: z.number().int(),
}),
spaces: z.object({
total: z.number().int(),
personal: z.number().int(),
team: z.number().int(),
newLast30Days: z.number().int(),
}),
storage: z.object({
usedBytes: z.number().int(),
quotaBytes: z.number().int(),
quotaUtilization: z.number(),
capacityBytes: z.number().int(),
backendCount: z.number().int(),
activeBackendCount: z.number().int(),
}),
traffic: z.object({
usedBytes: z.number().int(),
quotaBytes: z.number().int(),
utilization: z.number(),
period: z.string(),
}),
sharing: z.object({
totalShares: z.number().int(),
activeShares: z.number().int(),
views: z.number().int(),
downloads: z.number().int(),
}),
operations: z.object({
pendingInvitations: z.number().int(),
failedBackgroundJobs: z.number().int(),
offlineDownloaders: z.number().int(),
runningDownloadTasks: z.number().int(),
}),
})
.openapi('AdminCoreStats')
const statusCountSchema = z.object({ status: z.string(), count: z.number().int() })
const detailedStatsSchema = z
.object({
generatedAt: z.string(),
periodDays: z.number().int(),
trends: z.array(
z.object({
date: z.string(),
signups: z.number().int(),
activeUsers: z.number().int(),
shareViews: z.number().int(),
shareDownloads: z.number().int(),
remoteTasks: z.number().int(),
failedJobs: z.number().int(),
}),
),
usageBySpace: z.array(
z.object({
orgId: z.string(),
orgName: z.string(),
orgType: z.string(),
usedBytes: z.number().int(),
quotaBytes: z.number().int(),
utilization: z.number(),
}),
),
storageByType: z.array(z.object({ type: z.string(), bytes: z.number().int(), files: z.number().int() })),
topShares: z.array(
z.object({
id: z.string(),
token: z.string(),
name: z.string(),
creatorId: z.string(),
creatorName: z.string(),
views: z.number().int(),
downloads: z.number().int(),
status: z.string(),
}),
),
sharing: z.object({
expiredShares: z.number().int(),
revokedShares: z.number().int(),
downloadLimitHitShares: z.number().int(),
conversionRate: z.number(),
}),
remoteDownloads: z.object({
total: z.number().int(),
completed: z.number().int(),
failed: z.number().int(),
running: z.number().int(),
successRate: z.number(),
byStatus: z.array(statusCountSchema),
failureReasons: z.array(z.object({ reason: z.string(), count: z.number().int() })),
byDownloader: z.array(
z.object({
downloaderId: z.string(),
name: z.string(),
status: z.string(),
tasks: z.number().int(),
failedTasks: z.number().int(),
lastHeartbeatAt: z.string().nullable(),
}),
),
}),
reliability: z.object({
backgroundJobs: z.object({
total: z.number().int(),
failed: z.number().int(),
failureRate: z.number(),
byStatus: z.array(statusCountSchema),
failures: z.array(
z.object({ id: z.string(), type: z.string(), errorMessage: z.string().nullable(), createdAt: z.string() }),
),
}),
cloudTrafficReports: z.object({ pending: z.number().int(), failed: z.number().int() }),
license: z.object({
active: z.boolean(),
edition: z.string().nullable(),
lastRefreshAt: z.string().nullable(),
lastRefreshError: z.string().nullable(),
}),
}),
})
.openapi('AdminDetailedStats')
const detailsQuerySchema = z.object({
periodDays: z.coerce.number().int().min(7).max(90).default(30),
@@ -157,180 +28,6 @@ const rangeQuerySchema = z.object({
to: dashboardDateSchema.optional(),
})
const statsRangeFields = {
generatedAt: z.string(),
from: z.string(),
to: z.string(),
}
const deltaSchema = z.object({
value: z.number(),
previousValue: z.number(),
changePercent: z.number(),
})
const topShareWithPercentSchema = z.object({
id: z.string(),
token: z.string(),
name: z.string(),
creatorId: z.string(),
creatorName: z.string(),
views: z.number(),
downloads: z.number(),
status: z.string(),
viewPercent: z.number(),
downloadPercent: z.number(),
})
const usageBySpaceSchema = z.object({
orgId: z.string(),
orgName: z.string(),
orgType: z.string(),
usedBytes: z.number(),
quotaBytes: z.number(),
utilization: z.number(),
})
const storageByTypeSchema = z.object({
type: z.string(),
bytes: z.number(),
files: z.number(),
})
const namedPercentValueSchema = z.object({
name: z.string(),
value: z.number(),
percent: z.number(),
})
const namedBytesBreakdownSchema = z.object({
name: z.string(),
bytes: z.number(),
files: z.number(),
percent: z.number(),
})
const dashboardOverviewStatsSchema = z
.object({
...statsRangeFields,
totals: z.object({
users: z.number(),
newUsers: deltaSchema,
activeUsers: deltaSchema,
storageUsedBytes: z.number(),
storageQuotaBytes: z.number(),
trafficBytes: deltaSchema,
uploadBytes: deltaSchema,
downloadBytes: deltaSchema,
activeShares: z.number(),
shareViews: deltaSchema,
shareDownloads: deltaSchema,
}),
trends: z.array(
z.object({
date: z.string(),
newUsers: z.number(),
activeUsers: z.number(),
storageUsedBytes: z.number(),
uploadBytes: z.number(),
downloadBytes: z.number(),
}),
),
})
.openapi('AdminDashboardOverviewStats')
const dashboardGrowthStatsSchema = z
.object({
...statsRangeFields,
summary: z.object({
totalUsers: z.number(),
newUsers: deltaSchema,
activeUsers: deltaSchema,
verifiedUsers: z.number(),
bannedUsers: z.number(),
silentUsers: z.number(),
}),
userScaleTrend: z.array(z.object({ date: z.string(), newUsers: z.number(), totalUsers: z.number() })),
activeUserTrend: z.array(z.object({ date: z.string(), dau: z.number(), wau: z.number(), mau: z.number() })),
userStatus: z.array(namedPercentValueSchema),
registrationSources: z.array(namedPercentValueSchema),
})
.openapi('AdminDashboardGrowthStats')
const dashboardStorageStatsSchema = z
.object({
...statsRangeFields,
summary: z.object({
storageUsedBytes: z.number(),
quotaBytes: z.number(),
fileCount: z.number(),
newFiles: deltaSchema,
newBytes: deltaSchema,
coldFileBytes: z.number(),
}),
storageTrend: z.array(
z.object({ date: z.string(), usedBytes: z.number(), newBytes: z.number(), newFiles: z.number() }),
),
typeBreakdown: z.array(storageByTypeSchema.extend({ percent: z.number() })),
sizeBreakdown: z.array(namedBytesBreakdownSchema),
ageBreakdown: z.array(namedBytesBreakdownSchema),
})
.openapi('AdminDashboardStorageStats')
const dashboardTrafficStatsSchema = z
.object({
...statsRangeFields,
summary: z.object({
totalBytes: deltaSchema,
requestCount: deltaSchema,
issuedDownloads: z.number(),
blockedDownloads: z.number(),
issueRate: z.number(),
peakDailyBytes: z.number(),
}),
trafficTrend: z.array(
z.object({ date: z.string(), uploadBytes: z.number(), downloadBytes: z.number(), requests: z.number() }),
),
sourceBreakdown: z.array(
z.object({ name: z.string(), bytes: z.number(), requests: z.number(), percent: z.number() }),
),
issueStatus: z.array(z.object({ status: z.string(), count: z.number(), percent: z.number() })),
bandwidthTrend: z.array(z.object({ date: z.string(), bytes: z.number() })),
successTrend: z.array(
z.object({ date: z.string(), uploadSuccessRate: z.number(), downloadSuccessRate: z.number() }),
),
failureReasons: z.array(namedPercentValueSchema),
})
.openapi('AdminDashboardTrafficStats')
const dashboardSharingStatsSchema = z
.object({
...statsRangeFields,
summary: z.object({
activeShares: z.number(),
createdShares: deltaSchema,
views: deltaSchema,
downloads: deltaSchema,
saves: deltaSchema,
downloadConversionRate: z.number(),
}),
funnel: z.array(namedPercentValueSchema),
trend: z.array(z.object({ date: z.string(), views: z.number(), downloads: z.number(), saves: z.number() })),
typeBreakdown: z.array(namedPercentValueSchema),
sourceBreakdown: z.array(namedPercentValueSchema),
topShares: z.array(topShareWithPercentSchema),
})
.openapi('AdminDashboardSharingStats')
const dashboardRankingStatsSchema = z
.object({
...statsRangeFields,
topShares: z.array(topShareWithPercentSchema),
topSpaces: z.array(usageBySpaceSchema),
storageByType: z.array(storageByTypeSchema),
})
.openapi('AdminDashboardRankingStats')
function parseRange(query: z.infer<typeof rangeQuerySchema>): { from?: Date; to?: Date } {
return {
from: query.from ? parseDashboardDate(query.from, 'start') : undefined,
@@ -353,144 +50,27 @@ function parseDashboardDate(value: string, boundary: 'start' | 'end'): Date {
return new Date(value)
}
const coreRoute = createRoute({
operationId: 'getAdminCoreStats',
summary: 'Get admin dashboard core stats',
tags: ['Admin Stats'],
method: 'get',
path: '/core',
middleware: [requireAdmin] as const,
responses: {
200: jsonContent(coreStatsSchema, 'Admin core stats'),
401: errorResponse('Unauthorized'),
},
})
const detailsRoute = createRoute({
operationId: 'getAdminDetailedStats',
summary: 'Get admin dashboard detailed stats',
tags: ['Admin Stats'],
method: 'get',
path: '/details',
middleware: [requireAdmin, requireFeature('analytics')] as const,
request: { query: detailsQuerySchema },
responses: {
200: jsonContent(detailedStatsSchema, 'Admin detailed stats'),
402: errorResponse('Feature not available'),
},
})
const overviewRoute = createRoute({
operationId: 'getAdminDashboardOverviewStats',
summary: 'Get admin dashboard overview stats',
tags: ['Admin Stats'],
method: 'get',
path: '/overview',
middleware: [requireAdmin] as const,
request: { query: rangeQuerySchema },
responses: {
200: jsonContent(dashboardOverviewStatsSchema, 'Admin dashboard overview stats'),
400: errorResponse('Invalid query'),
401: errorResponse('Unauthorized'),
},
})
const growthRoute = createRoute({
operationId: 'getAdminDashboardGrowthStats',
summary: 'Get admin dashboard growth stats',
tags: ['Admin Stats'],
method: 'get',
path: '/growth',
middleware: [requireAdmin, requireFeature('analytics')] as const,
request: { query: rangeQuerySchema },
responses: {
200: jsonContent(dashboardGrowthStatsSchema, 'Admin dashboard growth stats'),
400: errorResponse('Invalid query'),
402: errorResponse('Feature not available'),
},
})
const storageRoute = createRoute({
operationId: 'getAdminDashboardStorageStats',
summary: 'Get admin dashboard storage stats',
tags: ['Admin Stats'],
method: 'get',
path: '/storage',
middleware: [requireAdmin, requireFeature('analytics')] as const,
request: { query: rangeQuerySchema },
responses: {
200: jsonContent(dashboardStorageStatsSchema, 'Admin dashboard storage stats'),
400: errorResponse('Invalid query'),
402: errorResponse('Feature not available'),
},
})
const trafficRoute = createRoute({
operationId: 'getAdminDashboardTrafficStats',
summary: 'Get admin dashboard traffic stats',
tags: ['Admin Stats'],
method: 'get',
path: '/traffic',
middleware: [requireAdmin, requireFeature('analytics')] as const,
request: { query: rangeQuerySchema },
responses: {
200: jsonContent(dashboardTrafficStatsSchema, 'Admin dashboard traffic stats'),
400: errorResponse('Invalid query'),
402: errorResponse('Feature not available'),
},
})
const sharingRoute = createRoute({
operationId: 'getAdminDashboardSharingStats',
summary: 'Get admin dashboard sharing stats',
tags: ['Admin Stats'],
method: 'get',
path: '/sharing',
middleware: [requireAdmin, requireFeature('analytics')] as const,
request: { query: rangeQuerySchema },
responses: {
200: jsonContent(dashboardSharingStatsSchema, 'Admin dashboard sharing stats'),
400: errorResponse('Invalid query'),
402: errorResponse('Feature not available'),
},
})
const rankingRoute = createRoute({
operationId: 'getAdminDashboardRankingStats',
summary: 'Get admin dashboard ranking stats',
tags: ['Admin Stats'],
method: 'get',
path: '/ranking',
middleware: [requireAdmin, requireFeature('analytics')] as const,
request: { query: rangeQuerySchema },
responses: {
200: jsonContent(dashboardRankingStatsSchema, 'Admin dashboard ranking stats'),
400: errorResponse('Invalid query'),
402: errorResponse('Feature not available'),
},
})
export const adminStats = new OpenAPIHono<Env>()
.openapi(coreRoute, async (c) => c.json(await getAdminCoreStats(c.get('deps')), 200))
.openapi(detailsRoute, async (c) => {
export const adminStats = new Hono<Env>()
.get('/core', requireAdmin, async (c) => c.json(await getAdminCoreStats(c.get('deps')), 200))
.get('/details', requireAdmin, requireFeature('analytics'), zValidator('query', detailsQuerySchema), async (c) => {
const { periodDays } = c.req.valid('query')
return c.json(await getAdminDetailedStats(c.get('deps'), { periodDays }), 200)
})
.openapi(overviewRoute, async (c) =>
.get('/overview', requireAdmin, zValidator('query', rangeQuerySchema), async (c) =>
c.json(await getAdminDashboardOverviewStats(c.get('deps'), parseRange(c.req.valid('query'))), 200),
)
.openapi(growthRoute, async (c) =>
.get('/growth', requireAdmin, requireFeature('analytics'), zValidator('query', rangeQuerySchema), async (c) =>
c.json(await getAdminDashboardGrowthStats(c.get('deps'), parseRange(c.req.valid('query'))), 200),
)
.openapi(storageRoute, async (c) =>
.get('/storage', requireAdmin, requireFeature('analytics'), zValidator('query', rangeQuerySchema), async (c) =>
c.json(await getAdminDashboardStorageStats(c.get('deps'), parseRange(c.req.valid('query'))), 200),
)
.openapi(trafficRoute, async (c) =>
.get('/traffic', requireAdmin, requireFeature('analytics'), zValidator('query', rangeQuerySchema), async (c) =>
c.json(await getAdminDashboardTrafficStats(c.get('deps'), parseRange(c.req.valid('query'))), 200),
)
.openapi(sharingRoute, async (c) =>
.get('/sharing', requireAdmin, requireFeature('analytics'), zValidator('query', rangeQuerySchema), async (c) =>
c.json(await getAdminDashboardSharingStats(c.get('deps'), parseRange(c.req.valid('query'))), 200),
)
.openapi(rankingRoute, async (c) =>
.get('/ranking', requireAdmin, requireFeature('analytics'), zValidator('query', rangeQuerySchema), async (c) =>
c.json(await getAdminDashboardRankingStats(c.get('deps'), parseRange(c.req.valid('query'))), 200),
)