import { NextRequest, NextResponse } from 'next/server' /** * Mac Mini System Monitoring API * * Provides system stats and Docker container management * Requires Docker socket mounted at /var/run/docker.sock */ interface ContainerInfo { id: string name: string image: string status: string state: string created: string ports: string[] cpu_percent: number memory_usage: string memory_limit: string memory_percent: number network_rx: string network_tx: string } interface SystemStats { hostname: string platform: string arch: string uptime: number cpu: { model: string cores: number usage_percent: number } memory: { total: string used: string free: string usage_percent: number } disk: { total: string used: string free: string usage_percent: number } timestamp: string } interface DockerStats { containers: ContainerInfo[] total_containers: number running_containers: number stopped_containers: number } // Helper to format bytes function formatBytes(bytes: number): string { if (bytes === 0) return '0 B' const k = 1024 const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } // Helper to format uptime function formatUptime(seconds: number): string { const days = Math.floor(seconds / 86400) const hours = Math.floor((seconds % 86400) / 3600) const minutes = Math.floor((seconds % 3600) / 60) if (days > 0) return `${days}d ${hours}h ${minutes}m` if (hours > 0) return `${hours}h ${minutes}m` return `${minutes}m` } // Get Docker stats via socket async function getDockerStats(): Promise { const DOCKER_SOCKET = process.env.DOCKER_HOST || 'unix:///var/run/docker.sock' try { // Fetch container list const containersResponse = await fetch(`${DOCKER_SOCKET.replace('unix://', 'http://localhost')}/containers/json?all=true`, { // @ts-expect-error - Node.js fetch supports unix sockets via socketPath socketPath: '/var/run/docker.sock', }) if (!containersResponse.ok) { throw new Error('Failed to fetch containers') } const containers = await containersResponse.json() // Get stats for running containers const containerInfos: ContainerInfo[] = await Promise.all( containers.map(async (container: Record) => { const names = container.Names as string[] const name = names?.[0]?.replace(/^\//, '') || 'unknown' const state = container.State as string const status = container.Status as string const image = container.Image as string const created = container.Created as number const ports = container.Ports as Array<{ PrivatePort: number; PublicPort?: number; Type: string }> let cpu_percent = 0 let memory_usage = '0 B' let memory_limit = '0 B' let memory_percent = 0 let network_rx = '0 B' let network_tx = '0 B' // Get live stats for running containers if (state === 'running') { try { const statsResponse = await fetch( `http://localhost/containers/${container.Id}/stats?stream=false`, { // @ts-expect-error - Node.js fetch supports unix sockets socketPath: '/var/run/docker.sock', } ) if (statsResponse.ok) { const stats = await statsResponse.json() // Calculate CPU usage const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage || 0) const systemDelta = stats.cpu_stats.system_cpu_usage - (stats.precpu_stats?.system_cpu_usage || 0) const cpuCount = stats.cpu_stats.online_cpus || 1 if (systemDelta > 0 && cpuDelta > 0) { cpu_percent = (cpuDelta / systemDelta) * cpuCount * 100 } // Memory usage const memUsage = stats.memory_stats?.usage || 0 const memLimit = stats.memory_stats?.limit || 0 memory_usage = formatBytes(memUsage) memory_limit = formatBytes(memLimit) memory_percent = memLimit > 0 ? (memUsage / memLimit) * 100 : 0 // Network stats const networks = stats.networks || {} let rxBytes = 0 let txBytes = 0 Object.values(networks).forEach((net: unknown) => { const network = net as { rx_bytes?: number; tx_bytes?: number } rxBytes += network.rx_bytes || 0 txBytes += network.tx_bytes || 0 }) network_rx = formatBytes(rxBytes) network_tx = formatBytes(txBytes) } } catch { // Stats not available, use defaults } } return { id: (container.Id as string).substring(0, 12), name, image: (image as string).split(':')[0].split('/').pop() || image, status, state, created: new Date(created * 1000).toISOString(), ports: ports?.map(p => p.PublicPort ? `${p.PublicPort}:${p.PrivatePort}/${p.Type}` : `${p.PrivatePort}/${p.Type}` ) || [], cpu_percent: Math.round(cpu_percent * 100) / 100, memory_usage, memory_limit, memory_percent: Math.round(memory_percent * 100) / 100, network_rx, network_tx, } }) ) // Sort by name containerInfos.sort((a, b) => a.name.localeCompare(b.name)) return { containers: containerInfos, total_containers: containerInfos.length, running_containers: containerInfos.filter(c => c.state === 'running').length, stopped_containers: containerInfos.filter(c => c.state !== 'running').length, } } catch (error) { console.error('Docker stats error:', error) // Return empty stats if Docker socket not available return { containers: [], total_containers: 0, running_containers: 0, stopped_containers: 0, } } } // Get system stats async function getSystemStats(): Promise { const os = await import('os') const cpus = os.cpus() const totalMem = os.totalmem() const freeMem = os.freemem() const usedMem = totalMem - freeMem // Calculate CPU usage from cpus let totalIdle = 0 let totalTick = 0 cpus.forEach(cpu => { for (const type in cpu.times) { totalTick += cpu.times[type as keyof typeof cpu.times] } totalIdle += cpu.times.idle }) const cpuUsage = 100 - (totalIdle / totalTick * 100) // Disk stats (root partition) let diskTotal = 0 let diskUsed = 0 let diskFree = 0 try { const { execSync } = await import('child_process') const dfOutput = execSync('df -k / | tail -1').toString() const parts = dfOutput.split(/\s+/) diskTotal = parseInt(parts[1]) * 1024 diskUsed = parseInt(parts[2]) * 1024 diskFree = parseInt(parts[3]) * 1024 } catch { // Disk stats not available } return { hostname: os.hostname(), platform: os.platform(), arch: os.arch(), uptime: os.uptime(), cpu: { model: cpus[0]?.model || 'Unknown', cores: cpus.length, usage_percent: Math.round(cpuUsage * 100) / 100, }, memory: { total: formatBytes(totalMem), used: formatBytes(usedMem), free: formatBytes(freeMem), usage_percent: Math.round((usedMem / totalMem) * 100 * 100) / 100, }, disk: { total: formatBytes(diskTotal), used: formatBytes(diskUsed), free: formatBytes(diskFree), usage_percent: diskTotal > 0 ? Math.round((diskUsed / diskTotal) * 100 * 100) / 100 : 0, }, timestamp: new Date().toISOString(), } } // Container action (start/stop/restart) async function containerAction(containerId: string, action: 'start' | 'stop' | 'restart'): Promise { const response = await fetch( `http://localhost/containers/${containerId}/${action}`, { method: 'POST', // @ts-expect-error - Node.js fetch supports unix sockets socketPath: '/var/run/docker.sock', } ) if (!response.ok && response.status !== 304) { const error = await response.text() throw new Error(`Failed to ${action} container: ${error}`) } } // GET - Fetch system and Docker stats export async function GET() { try { const [system, docker] = await Promise.all([ getSystemStats(), getDockerStats(), ]) return NextResponse.json({ system, docker, timestamp: new Date().toISOString(), }) } catch (error) { console.error('Mac Mini stats error:', error) return NextResponse.json( { error: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ) } } // POST - Container actions (start/stop/restart) export async function POST(request: NextRequest) { try { const body = await request.json() const { container_id, action } = body if (!container_id || !action) { return NextResponse.json( { error: 'container_id and action required' }, { status: 400 } ) } if (!['start', 'stop', 'restart'].includes(action)) { return NextResponse.json( { error: 'Invalid action. Use: start, stop, restart' }, { status: 400 } ) } await containerAction(container_id, action) return NextResponse.json({ success: true, message: `Container ${action} successful`, container_id, action, }) } catch (error) { console.error('Container action error:', error) return NextResponse.json( { error: error instanceof Error ? error.message : 'Action failed' }, { status: 500 } ) } }