dev_arc_aws/backend/src/ssh/metrics/cpu.ts
Claude f32d93947b
Add host metrics widgets (Phase 6): CPU/mem/disk/network/processes/ports/firewall/login dashboard
Ports Termix's per-host metrics collector logic onto ArchNest's own SSH
connection helpers (not its multi-user/cache/session scaffolding), exposed via
a new authenticated REST endpoint and a dedicated /host-metrics page with
client-side polling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-19 15:38:30 +00:00

54 lines
2 KiB
TypeScript

import type { Client } from 'ssh2'
import { execCommand, toFixedNum } from './common.js'
function parseCpuLine(cpuLine: string): { total: number; idle: number } | undefined {
const parts = cpuLine.trim().split(/\s+/).slice(1).map(Number)
if (parts.length < 4 || parts.some((n) => !Number.isFinite(n))) return undefined
const idle = parts[3] + (parts[4] ?? 0)
const total = parts.reduce((sum, n) => sum + n, 0)
return { total, idle }
}
export async function collectCpuMetrics(
client: Client,
): Promise<{ percent: number | null; cores: number | null; load: [number, number, number] | null }> {
let percent: number | null = null
let cores: number | null = null
let load: [number, number, number] | null = null
try {
const work = (async () => {
const first = await execCommand(client, "grep '^cpu ' /proc/stat")
await new Promise((r) => setTimeout(r, 500))
const [second, loadOut, coresOut] = await Promise.all([
execCommand(client, "grep '^cpu ' /proc/stat"),
execCommand(client, 'cat /proc/loadavg'),
execCommand(client, 'nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo'),
])
const a = parseCpuLine(first.stdout)
const b = parseCpuLine(second.stdout)
if (a && b) {
const totalDiff = b.total - a.total
const idleDiff = b.idle - a.idle
if (totalDiff > 0) {
percent = toFixedNum(Math.min(100, Math.max(0, ((totalDiff - idleDiff) / totalDiff) * 100)))
}
}
const loadParts = loadOut.stdout.trim().split(/\s+/).slice(0, 3).map(Number)
if (loadParts.length === 3 && loadParts.every(Number.isFinite)) {
load = loadParts as [number, number, number]
}
const coreCount = Number(coresOut.stdout.trim())
cores = Number.isFinite(coreCount) ? coreCount : null
})()
await Promise.race([work, new Promise((_, reject) => setTimeout(() => reject(new Error('cpu metrics timeout')), 25000))])
} catch {
// best-effort; leave nulls
}
return { percent, cores, load }
}