refactor(hosts): group host modules into per-feature directories

file-manager/, metrics/ (incl. widgets, managers, alert-engine),
terminal/, tmux/ and tunnel/ each own their files; docker/ gains
container-runtime. Genuinely shared helpers (jump-host chain, host
resolver, connection pool, opkssh, vault, serial) stay at hosts/ root.
Pure file moves with import path updates; mirrored test paths follow.
This commit is contained in:
LukeGus
2026-07-16 04:34:22 -05:00
parent 7f43932429
commit 9c8528e4d2
82 changed files with 233 additions and 232 deletions
@@ -0,0 +1,92 @@
import type { Express } from "express";
import { execCommand } from "../widgets/common-utils.js";
import { execElevated } from "./exec-elevated.js";
import { managerHandler, ManagerInputError } from "./route-helpers.js";
import { shellSingleQuote } from "./exec-elevated.js";
import { isAllowedPath, isValidSystemdUnit } from "./validation.js";
import type { ManagerRoutesDeps } from "./types.js";
/** Directories from which arbitrary log files may be tailed. */
export const LOG_PATH_ALLOWLIST = ["/var/log"];
const COMMON_LOGS = [
"/var/log/syslog",
"/var/log/messages",
"/var/log/auth.log",
"/var/log/secure",
"/var/log/kern.log",
"/var/log/dpkg.log",
"/var/log/nginx/access.log",
"/var/log/nginx/error.log",
];
const LIST_LOGS_CMD = `ls -1 ${LOG_PATH_ALLOWLIST.map(shellSingleQuote).join(" ")} 2>/dev/null`;
export function clampLines(n: unknown): number {
const v = typeof n === "string" ? Number(n) : n;
if (typeof v !== "number" || !Number.isFinite(v)) return 200;
return Math.min(2000, Math.max(1, Math.round(v)));
}
export function buildTailCommand(path: string, lines: number): string {
// Keep stderr intact so execElevated can detect a permission error and
// escalate; suppressing it (2>/dev/null) would hide the denial and return an
// empty log with no chance to retry under sudo.
return `tail -n ${lines} ${shellSingleQuote(path)}`;
}
export function buildJournalCommand(unit: string, lines: number): string {
return `journalctl -u ${shellSingleQuote(unit)} -n ${lines} --no-pager`;
}
export function registerLogRoutes(
app: Express,
{ validateHostId, runOnHost }: ManagerRoutesDeps,
): void {
app.get(
"/host-metrics/managers/logs/:id/files",
validateHostId,
managerHandler(runOnHost, "read", "logs_list", async (client) => {
const { stdout } = await execCommand(client, LIST_LOGS_CMD, 10000);
const found = stdout
.split("\n")
.map((l) => l.trim())
.filter(Boolean)
.map((name) => `/var/log/${name}`);
return { common: COMMON_LOGS, files: found };
}),
);
app.get(
"/host-metrics/managers/logs/:id",
validateHostId,
managerHandler(
runOnHost,
"read",
"logs_tail",
async (client, host, req) => {
const path = req.query.path as string | undefined;
const unit = req.query.unit as string | undefined;
const lines = clampLines(req.query.lines);
let cmd: string;
if (unit) {
if (!isValidSystemdUnit(unit))
throw new ManagerInputError("Invalid unit");
cmd = buildJournalCommand(unit, lines);
} else if (path) {
if (!isAllowedPath(path, LOG_PATH_ALLOWLIST)) {
throw new ManagerInputError("Path not allowed");
}
cmd = buildTailCommand(path, lines);
} else {
throw new ManagerInputError("Provide a path or unit");
}
// Try unprivileged; many logs need root (auth.log, etc.).
const result = await execElevated(client, cmd, host.sudoPassword);
return { content: result.stdout, lines };
},
),
);
}