Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
339 lines
9.7 KiB
TypeScript
339 lines
9.7 KiB
TypeScript
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<DockerStats> {
|
|
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<string, unknown>) => {
|
|
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<SystemStats> {
|
|
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<void> {
|
|
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 }
|
|
)
|
|
}
|
|
}
|