Files
nixpanel-server/server/src/core/system.ts
T
NixReaper 8ff00c544c feat: v0.4.0 — Apache service stack, shell access, log viewer, changelog
Service stack (backend):
- webserver.ts rewritten: apache2/php-fpm routes replacing nginx; per-account
  Apache log endpoint; PHP-FPM reload per version; apache2ctl configtest/reload
- accounts.ts: added PUT /:id/shell to toggle /bin/bash vs /usr/sbin/nologin,
  runs usermod -s and writes audit log

New UI features (nixserver):
- ShellAccess page: grant/revoke SSH access per account via AccountPicker
- ApacheLogs page: live log viewer (access/error) per account; replaces stub
- NginxLogs: re-exports ApacheLogs (NixPanel uses Apache, not NGINX)
- WebServer page: shows apache2, php8.2-fpm, php8.3-fpm status with reload
  buttons; config test; Apache log viewer

Bug fixes:
- Accounts.tsx: Lucide icon size prop type widened (string|number); removed
  unused Users import; dropped duplicate NGINX log card
- Removed soon badge from ShellAccess and ApacheLogs entries

Project:
- CHANGELOG.md created with full history from 0.2.x through 0.4.0
- All packages bumped from 0.3.2 to 0.4.0
2026-04-11 05:17:49 -05:00

139 lines
3.6 KiB
TypeScript

import { readFile } from 'fs/promises'
import { exec } from './exec.js'
import type { SystemStats, ServiceStatus } from '../types/index.js'
/**
* Read CPU usage (0-100%) from /proc/stat.
*/
async function getCpuUsage(): Promise<number> {
try {
const stat1 = await readFile('/proc/stat', 'utf8')
await new Promise(r => setTimeout(r, 200))
const stat2 = await readFile('/proc/stat', 'utf8')
const parse = (s: string) => {
const line = s.split('\n')[0].split(' ').filter(Boolean).slice(1).map(Number)
const idle = line[3] + line[4]
const total = line.reduce((a, b) => a + b, 0)
return { idle, total }
}
const s1 = parse(stat1)
const s2 = parse(stat2)
const idleDiff = s2.idle - s1.idle
const totalDiff = s2.total - s1.total
return Math.round((1 - idleDiff / totalDiff) * 100)
} catch {
return 0
}
}
/**
* Get memory stats from /proc/meminfo (bytes).
*/
async function getMemoryStats(): Promise<SystemStats['memory']> {
try {
const content = await readFile('/proc/meminfo', 'utf8')
const get = (key: string) => {
const match = content.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`))
return match ? parseInt(match[1], 10) * 1024 : 0
}
const total = get('MemTotal')
const free = get('MemFree')
const buffers = get('Buffers')
const cached = get('Cached')
const used = total - free - buffers - cached
return {
total,
used,
free: total - used,
percent: Math.round((used / total) * 100),
}
} catch {
return { total: 0, used: 0, free: 0, percent: 0 }
}
}
/**
* Get disk usage for / (bytes).
*/
async function getDiskStats(): Promise<SystemStats['disk']> {
try {
const result = await exec('df', ['-B1', '/'])
const lines = result.stdout.split('\n')
const parts = lines[1]?.split(/\s+/)
if (!parts) return { total: 0, used: 0, free: 0, percent: 0 }
const total = parseInt(parts[1], 10)
const used = parseInt(parts[2], 10)
const free = parseInt(parts[3], 10)
return { total, used, free, percent: Math.round((used / total) * 100) }
} catch {
return { total: 0, used: 0, free: 0, percent: 0 }
}
}
/**
* Get system load averages from /proc/loadavg.
*/
async function getLoadAvg(): Promise<[number, number, number]> {
try {
const content = await readFile('/proc/loadavg', 'utf8')
const parts = content.split(' ')
return [parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2])]
} catch {
return [0, 0, 0]
}
}
/**
* Gather all system stats in parallel.
*/
export async function getSystemStats(): Promise<SystemStats> {
const [cpu, memory, disk, load] = await Promise.all([
getCpuUsage(),
getMemoryStats(),
getDiskStats(),
getLoadAvg(),
])
return {
cpu,
memory,
disk,
load,
uptime: Math.floor(process.uptime()),
}
}
/**
* Check the status of a systemd service.
*/
export async function getServiceStatus(name: string): Promise<ServiceStatus> {
const [activeResult, enabledResult] = await Promise.all([
exec('systemctl', ['is-active', name]),
exec('systemctl', ['is-enabled', name]),
])
const active = activeResult.stdout.trim()
const enabled = enabledResult.stdout.trim() === 'enabled'
let status: ServiceStatus['status'] = 'unknown'
if (active === 'active') status = 'running'
else if (active === 'inactive') status = 'stopped'
else if (active === 'failed') status = 'failed'
return { name, status, enabled }
}
export const MANAGED_SERVICES = [
'apache2',
'php8.2-fpm',
'php8.3-fpm',
'exim4',
'dovecot',
'pdns',
'mariadb',
'fail2ban',
'ssh',
]