/** * S-12 — health endpoint. * * GET /health → { ok, sessions, bufferBytes, uptime, version } * * Also integrates the disk watchdog: on each health call we check free space * and total buffer size, returning a warning if caps are near. * * Owner: T-1.7 */ import { execFile } from "node:child_process"; import fs from "node:fs/promises"; import type { IncomingMessage, ServerResponse } from "node:http"; import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import { listSessions } from "../../tmux/manager.js"; import { sendJson } from "../util.js"; const execFileAsync = promisify(execFile); const _startedAt = Date.now(); export interface HealthOptions { stateDir?: string; } export async function handleHealth( _req: IncomingMessage, res: ServerResponse, opts: HealthOptions = {}, ): Promise { const stateDir = opts.stateDir ?? path.join(os.homedir(), ".local", "share", "pi-remote"); const [sessions, bufferBytes, freeBytes] = await Promise.all([ listSessions().catch(() => []), getTotalBufferBytes(stateDir), getFreeBytes(stateDir), ]); const freeGb = freeBytes / (1024 * 1024 * 1024); const bufferMb = bufferBytes / (1024 * 1024); sendJson(res, 200, { ok: true, uptime: Math.floor((Date.now() - _startedAt) / 1000), sessions: sessions.length, sessionIds: sessions.map((s) => s.id), bufferMb: Math.round(bufferMb * 10) / 10, diskFreeGb: Math.round(freeGb * 10) / 10, warnings: buildWarnings(freeGb, bufferMb), }); } function buildWarnings(freeGb: number, bufferMb: number): string[] { const warnings: string[] = []; if (freeGb < 1) warnings.push(`Low disk space: ${freeGb.toFixed(1)} GB free`); if (bufferMb > 900) warnings.push(`Buffer near cap: ${bufferMb.toFixed(0)} MB used`); return warnings; } async function getTotalBufferBytes(stateDir: string): Promise { const bufDir = path.join(stateDir, "buffers"); try { const entries = await fs.readdir(bufDir, { withFileTypes: true }); let total = 0; for (const e of entries) { if (!e.name.endsWith(".buf")) continue; try { const stat = await fs.stat(path.join(bufDir, e.name)); total += stat.size; } catch { // skip } } return total; } catch { return 0; } } async function getFreeBytes(dir: string): Promise { try { const { stdout } = await execFileAsync("df", ["-k", dir]); const lines = stdout.trim().split("\n"); const last = lines[lines.length - 1]; const parts = last.split(/\s+/); const availKb = parseInt(parts[3], 10); return availKb * 1024; } catch { return Number.POSITIVE_INFINITY; // unknown — don't warn } }