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
139 lines
3.6 KiB
TypeScript
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',
|
|
]
|