refactor(backend): reorganize top-level layout

- ssh/ renamed to hosts/ (it covers SSH, RDP, VNC, Telnet, Docker, metrics)
- serial/serial.ts and guacamole/ moved inside hosts/
- dashboard.ts and homepage.ts moved to services/
- swagger.ts moved to utils/ with adjusted scan globs

Import paths and the generate:openapi script updated; ports and endpoints
unchanged.
This commit is contained in:
LukeGus
2026-07-16 04:29:26 -05:00
parent 5e7090542d
commit 8b6037b7ff
121 changed files with 66 additions and 58 deletions
@@ -0,0 +1,130 @@
import type { Express, RequestHandler } from "express";
import type { AuthenticatedRequest } from "../../types/index.js";
import { createCurrentHostMetricsHistoryRepository } from "../database/repositories/factory.js";
import { statsLogger } from "../utils/logger.js";
type HistoryRoutesDeps = {
validateHostId: RequestHandler;
canAccessHost: (
userId: string,
hostId: number,
level: "read" | "write" | "execute" | "delete" | "share",
) => Promise<boolean>;
};
const RANGE_OFFSETS: Record<string, number> = {
"1h": 1 * 60 * 60 * 1000,
"6h": 6 * 60 * 60 * 1000,
"24h": 24 * 60 * 60 * 1000,
"7d": 7 * 24 * 60 * 60 * 1000,
"30d": 30 * 24 * 60 * 60 * 1000,
};
export function registerHostMetricsHistoryRoutes(
app: Express,
{ validateHostId, canAccessHost }: HistoryRoutesDeps,
): void {
/**
* @openapi
* /metrics/history/{id}:
* get:
* summary: Get historical metrics for a host
* tags:
* - Host Metrics
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* - in: query
* name: range
* schema:
* type: string
* enum: [1h, 6h, 24h, 7d, 30d]
* - in: query
* name: from
* schema:
* type: string
* format: date-time
* - in: query
* name: to
* schema:
* type: string
* format: date-time
* responses:
* 200:
* description: Array of metric history rows.
* 403:
* description: Access denied.
* 404:
* description: Host not found or no access.
*/
app.get("/metrics/history/:id", validateHostId, async (req, res) => {
const hostId = Number(req.params.id);
const userId = (req as AuthenticatedRequest).userId;
try {
const hasAccess = await canAccessHost(userId, hostId, "read");
if (!hasAccess) {
return res.status(403).json({ error: "Access denied" });
}
const { range, from, to } = req.query as Record<
string,
string | undefined
>;
let fromTs: string;
let toTs: string = new Date().toISOString();
if (range) {
const offsetMs = RANGE_OFFSETS[range];
if (!offsetMs) {
return res
.status(400)
.json({ error: "Invalid range. Use 1h, 6h, 24h, 7d, or 30d" });
}
fromTs = new Date(Date.now() - offsetMs).toISOString();
} else if (from && to) {
const fromDate = new Date(from);
const toDate = new Date(to);
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) {
return res.status(400).json({ error: "Invalid from/to date format" });
}
fromTs = fromDate.toISOString();
toTs = toDate.toISOString();
} else {
fromTs = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
}
// SQLite CURRENT_TIMESTAMP stores 'YYYY-MM-DD HH:MM:SS' (no T/Z).
// Normalize both sides to that format for reliable string comparison.
const toSqlite = (iso: string) =>
iso.replace("T", " ").replace(/\.\d{3}Z$/, "");
const rows = (
await createCurrentHostMetricsHistoryRepository().listRange(
hostId,
toSqlite(fromTs),
toSqlite(toTs),
)
).map((row) => ({
ts: row.ts,
cpu_percent: row.cpuPercent,
mem_percent: row.memPercent,
disk_percent: row.diskPercent,
net_rx_bytes: row.netRxBytes,
net_tx_bytes: row.netTxBytes,
}));
res.json({ rows, fromTs, toTs });
} catch (error) {
statsLogger.error("Failed to fetch metrics history", {
operation: "metrics_history_fetch_error",
hostId,
error: error instanceof Error ? error.message : String(error),
});
res.status(500).json({ error: "Failed to fetch metrics history" });
}
});
}