97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<number> {
|
|
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<number> {
|
|
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
|
|
}
|
|
}
|