This commit is contained in:
1 parent
21a61068fb
commit
60278b9e71
38 files changed
+3452
No files matched your search
@@ -0,0 +1,101 @@
|
||||
import { withAdmin } from '@/lib/api-handler'
|
||||
import { apiError } from '@/lib/api-response'
|
||||
import { PERMS } from '@/lib/permission-slugs'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
const VALID_REPORTS = new Set(['registrations', 'online-by-hour', 'economy'])
|
||||
|
||||
function csvEscape(value: unknown): string {
|
||||
if (value == null) return ''
|
||||
const str = String(value)
|
||||
if (/[",\n\r]/.test(str)) {
|
||||
return `"${str.replace(/"/g, '""')}"`
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
function csvRow(values: unknown[]): string {
|
||||
return `${values.map(csvEscape).join(',')}\n`
|
||||
}
|
||||
|
||||
async function streamRegistrations(): Promise<ReadableStream<Uint8Array>> {
|
||||
const rows = await prisma.$queryRaw<{ day: string; cnt: bigint }[]>`
|
||||
SELECT FROM_UNIXTIME(account_created, '%Y-%m-%d') AS day, COUNT(*) AS cnt
|
||||
FROM users
|
||||
WHERE account_created > UNIX_TIMESTAMP(NOW() - INTERVAL 90 DAY)
|
||||
GROUP BY day
|
||||
ORDER BY day ASC
|
||||
`
|
||||
const encoder = new TextEncoder()
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(csvRow(['day', 'registrations'])))
|
||||
for (const r of rows) {
|
||||
controller.enqueue(encoder.encode(csvRow([r.day, Number(r.cnt)])))
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function streamOnlineByHour(): Promise<ReadableStream<Uint8Array>> {
|
||||
const rows = await prisma.$queryRaw<{ hour: number; cnt: bigint }[]>`
|
||||
SELECT HOUR(FROM_UNIXTIME(last_online)) AS hour, COUNT(*) AS cnt
|
||||
FROM users
|
||||
WHERE last_online > UNIX_TIMESTAMP(NOW() - INTERVAL 30 DAY)
|
||||
GROUP BY hour
|
||||
ORDER BY hour ASC
|
||||
`
|
||||
const encoder = new TextEncoder()
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(csvRow(['hour', 'active_users'])))
|
||||
for (const r of rows) {
|
||||
controller.enqueue(encoder.encode(csvRow([Number(r.hour), Number(r.cnt)])))
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function streamEconomy(): Promise<ReadableStream<Uint8Array>> {
|
||||
const [totals] = await prisma.$queryRaw<{ total_credits: bigint; total_pixels: bigint }[]>`
|
||||
SELECT SUM(credits) AS total_credits, SUM(pixels) AS total_pixels FROM users
|
||||
`
|
||||
const encoder = new TextEncoder()
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(csvRow(['metric', 'value'])))
|
||||
controller.enqueue(
|
||||
encoder.encode(csvRow(['total_credits', Number(totals?.total_credits ?? 0)])),
|
||||
)
|
||||
controller.enqueue(
|
||||
encoder.encode(csvRow(['total_pixels', Number(totals?.total_pixels ?? 0)])),
|
||||
)
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const GET = withAdmin({ permission: PERMS.ANALYTICS_EXPORT }, async (request) => {
|
||||
const url = new URL(request.url)
|
||||
const report = url.searchParams.get('report') ?? ''
|
||||
if (!VALID_REPORTS.has(report)) {
|
||||
return apiError(`Unknown report. Use one of: ${[...VALID_REPORTS].join(', ')}`, 400)
|
||||
}
|
||||
|
||||
const stream =
|
||||
report === 'registrations'
|
||||
? await streamRegistrations()
|
||||
: report === 'online-by-hour'
|
||||
? await streamOnlineByHour()
|
||||
: await streamEconomy()
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': `attachment; filename="${report}.csv"`,
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import net from 'node:net'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { env } from '@/env'
|
||||
import { withAdmin } from '@/lib/api-handler'
|
||||
import { PERMS } from '@/lib/permissions'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
function checkRcon(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const host = env.RCON_HOST
|
||||
const port = env.RCON_PORT
|
||||
|
||||
const socket = new net.Socket()
|
||||
const timeout = setTimeout(() => {
|
||||
socket.destroy()
|
||||
resolve(false)
|
||||
}, 3000)
|
||||
|
||||
socket.connect(port, host, () => {
|
||||
clearTimeout(timeout)
|
||||
socket.destroy()
|
||||
resolve(true)
|
||||
})
|
||||
|
||||
socket.on('error', () => {
|
||||
clearTimeout(timeout)
|
||||
socket.destroy()
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const GET = withAdmin({ permission: PERMS.DEVOPS_VIEW }, async () => {
|
||||
// DB health
|
||||
let dbOk = false
|
||||
let dbLatency = 0
|
||||
try {
|
||||
const start = Date.now()
|
||||
await prisma.$queryRaw`SELECT 1`
|
||||
dbLatency = Date.now() - start
|
||||
dbOk = true
|
||||
} catch {
|
||||
dbOk = false
|
||||
}
|
||||
|
||||
// Emulator health (RCON TCP check)
|
||||
const emulator = await checkRcon()
|
||||
|
||||
// Online users
|
||||
const onlineUsers = await prisma.user.count({ where: { online: '1' } })
|
||||
|
||||
return NextResponse.json({
|
||||
database: dbOk,
|
||||
dbLatency,
|
||||
emulator,
|
||||
onlineUsers,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user