import { NextRequest } from 'next/server' import { validateSession, hasServerPermission } from '@/lib/auth' import connectToDatabase from '@/lib/mongodb' import { Server } from '@/lib/models' import { isValidObjectId } from '@/lib/input-validation' import { getContainerByName } from '@/lib/docker' interface DockerCpuStats { cpu_stats: { cpu_usage: { total_usage: number } system_cpu_usage: number online_cpus: number } precpu_stats: { cpu_usage: { total_usage: number } system_cpu_usage: number } } interface DockerMemoryStats { memory_stats: { usage: number limit: number stats?: { cache?: number } } } type DockerStats = DockerCpuStats & DockerMemoryStats function calculateCpuPercent(stats: DockerStats): number { const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage const cpuCount = stats.cpu_stats.online_cpus || 1 if (systemDelta <= 0 || cpuDelta < 0) return 0 return Math.min(100, (cpuDelta / systemDelta) * cpuCount * 100) } // GET /api/servers/[id]/stats — SSE stream of container CPU & memory stats export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const session = await validateSession(request) if (!session) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 }) } const { id } = await params if (!isValidObjectId(id)) { return new Response(JSON.stringify({ error: 'Invalid server ID' }), { status: 400 }) } await connectToDatabase() const server = await Server.findById(id).lean() if (!server) { return new Response(JSON.stringify({ error: 'Server not found' }), { status: 404 }) } const adminIds = (server.admins ?? []).map((a) => a.toString()) if (!hasServerPermission(session, 'servers:view', adminIds)) { return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403 }) } const container = await getContainerByName(`mc-${server._id}`).catch(() => null) if (!container) { return new Response(JSON.stringify({ error: 'Container not found' }), { status: 404 }) } // Check if container is running try { const inspect = await container.inspect() if (!inspect.State?.Running) { return new Response(JSON.stringify({ error: 'Container is not running' }), { status: 400 }) } } catch { return new Response(JSON.stringify({ error: 'Failed to inspect container' }), { status: 500 }) } const encoder = new TextEncoder() let cancelled = false const stream = new ReadableStream({ async start(controller) { try { const statsStream = await container.stats({ stream: true }) as NodeJS.ReadableStream let buffer = '' const onData = (chunk: Buffer) => { if (cancelled) return buffer += chunk.toString() const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (!line.trim()) continue try { const stats: DockerStats = JSON.parse(line) const cpuPercent = calculateCpuPercent(stats) const memUsage = stats.memory_stats.usage || 0 const memLimit = stats.memory_stats.limit || 0 const memCache = stats.memory_stats.stats?.cache || 0 const memActual = memUsage - memCache const payload = { cpu: Math.round(cpuPercent * 100) / 100, memUsed: memActual, memLimit: memLimit, memPercent: memLimit > 0 ? Math.round((memActual / memLimit) * 10000) / 100 : 0, timestamp: Date.now(), } controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`)) } catch { // Skip malformed JSON lines } } } const onEnd = () => { if (!cancelled) { controller.close() } } const onError = () => { if (!cancelled) { controller.close() } } statsStream.on('data', onData) statsStream.on('end', onEnd) statsStream.on('error', onError) // Cleanup on cancel request.signal.addEventListener('abort', () => { cancelled = true statsStream.removeListener('data', onData) statsStream.removeListener('end', onEnd) statsStream.removeListener('error', onError) try { (statsStream as any).destroy?.() } catch {} try { controller.close() } catch {} }) } catch { controller.close() } }, }) return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, }) }