chore: initialize repository with deployment baseline
This commit is contained in:
637
backend/src/routes/proxmox.routes.ts
Normal file
637
backend/src/routes/proxmox.routes.ts
Normal file
@@ -0,0 +1,637 @@
|
||||
import { OperationTaskType, Prisma } from "@prisma/client";
|
||||
import { Router } from "express";
|
||||
import { z } from "zod";
|
||||
import { HttpError } from "../lib/http-error";
|
||||
import { prisma } from "../lib/prisma";
|
||||
import { authorize, requireAuth } from "../middleware/auth";
|
||||
import {
|
||||
addVmDisk,
|
||||
clusterUsageGraphs,
|
||||
deleteVm,
|
||||
migrateVm,
|
||||
nodeUsageGraphs,
|
||||
vmUsageGraphs,
|
||||
reinstallVm,
|
||||
reconfigureVmNetwork,
|
||||
restartVm,
|
||||
resumeVm,
|
||||
shutdownVm,
|
||||
startVm,
|
||||
stopVm,
|
||||
suspendVm,
|
||||
syncNodesAndVirtualMachines,
|
||||
updateVmConfiguration,
|
||||
vmConsoleTicket
|
||||
} from "../services/proxmox.service";
|
||||
import { logAudit } from "../services/audit.service";
|
||||
import {
|
||||
createOperationTask,
|
||||
markOperationTaskFailed,
|
||||
markOperationTaskRunning,
|
||||
markOperationTaskSuccess
|
||||
} from "../services/operations.service";
|
||||
|
||||
const router = Router();
|
||||
const consoleTypeSchema = z.enum(["novnc", "spice", "xterm"]);
|
||||
const graphTimeframeSchema = z.enum(["hour", "day", "week", "month", "year"]);
|
||||
|
||||
function vmRuntimeType(vm: { type: "QEMU" | "LXC" }) {
|
||||
return vm.type === "LXC" ? "lxc" : "qemu";
|
||||
}
|
||||
|
||||
function withUpid(payload: Prisma.InputJsonObject, upid?: string): Prisma.InputJsonObject {
|
||||
if (!upid) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return {
|
||||
...payload,
|
||||
upid
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchVm(vmId: string) {
|
||||
const vm = await prisma.virtualMachine.findUnique({ where: { id: vmId } });
|
||||
if (!vm) {
|
||||
throw new HttpError(404, "VM not found", "VM_NOT_FOUND");
|
||||
}
|
||||
return vm;
|
||||
}
|
||||
|
||||
async function resolveConsoleProxyTarget(node: string, consoleType: "novnc" | "spice" | "xterm") {
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: {
|
||||
key: "console_proxy"
|
||||
}
|
||||
});
|
||||
|
||||
const raw = setting?.value as
|
||||
| {
|
||||
mode?: "cluster" | "per_node";
|
||||
cluster?: Record<string, unknown>;
|
||||
nodes?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mode = raw.mode ?? "cluster";
|
||||
if (mode === "per_node") {
|
||||
const nodeConfig = raw.nodes?.[node];
|
||||
if (nodeConfig && typeof nodeConfig[consoleType] === "string") {
|
||||
return String(nodeConfig[consoleType]);
|
||||
}
|
||||
}
|
||||
|
||||
if (raw.cluster && typeof raw.cluster[consoleType] === "string") {
|
||||
return String(raw.cluster[consoleType]);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
router.post("/sync", requireAuth, authorize("node:manage"), async (req, res, next) => {
|
||||
try {
|
||||
const task = await createOperationTask({
|
||||
taskType: OperationTaskType.SYSTEM_SYNC,
|
||||
requestedBy: req.user?.email,
|
||||
payload: { source: "manual_sync" }
|
||||
});
|
||||
|
||||
await markOperationTaskRunning(task.id);
|
||||
|
||||
try {
|
||||
const result = await syncNodesAndVirtualMachines();
|
||||
await markOperationTaskSuccess(task.id, {
|
||||
node_count: result.node_count
|
||||
});
|
||||
|
||||
await logAudit({
|
||||
action: "proxmox_sync",
|
||||
resource_type: "NODE",
|
||||
actor_email: req.user!.email,
|
||||
actor_role: req.user!.role,
|
||||
details: {
|
||||
node_count: result.node_count,
|
||||
task_id: task.id
|
||||
},
|
||||
ip_address: req.ip
|
||||
});
|
||||
|
||||
res.json({
|
||||
...result,
|
||||
task_id: task.id
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Proxmox sync failed";
|
||||
await markOperationTaskFailed(task.id, message);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
const actionSchema = z.object({
|
||||
action: z.enum(["start", "stop", "restart", "shutdown", "suspend", "resume", "delete"])
|
||||
});
|
||||
|
||||
router.post("/vms/:id/actions/:action", requireAuth, authorize("vm:update"), async (req, res, next) => {
|
||||
try {
|
||||
const { action } = actionSchema.parse(req.params);
|
||||
const vm = await fetchVm(req.params.id);
|
||||
const type = vmRuntimeType(vm);
|
||||
|
||||
const taskType = action === "delete" ? OperationTaskType.VM_DELETE : OperationTaskType.VM_POWER;
|
||||
const task = await createOperationTask({
|
||||
taskType,
|
||||
vm: {
|
||||
id: vm.id,
|
||||
name: vm.name,
|
||||
node: vm.node
|
||||
},
|
||||
requestedBy: req.user?.email,
|
||||
payload: { action }
|
||||
});
|
||||
|
||||
await markOperationTaskRunning(task.id);
|
||||
|
||||
let upid: string | undefined;
|
||||
|
||||
try {
|
||||
if (action === "start") {
|
||||
upid = await startVm(vm.node, vm.vmid, type);
|
||||
await prisma.virtualMachine.update({ where: { id: vm.id }, data: { status: "RUNNING", proxmox_upid: upid } });
|
||||
} else if (action === "stop") {
|
||||
upid = await stopVm(vm.node, vm.vmid, type);
|
||||
await prisma.virtualMachine.update({ where: { id: vm.id }, data: { status: "STOPPED", proxmox_upid: upid } });
|
||||
} else if (action === "restart") {
|
||||
upid = await restartVm(vm.node, vm.vmid, type);
|
||||
await prisma.virtualMachine.update({ where: { id: vm.id }, data: { status: "RUNNING", proxmox_upid: upid } });
|
||||
} else if (action === "shutdown") {
|
||||
upid = await shutdownVm(vm.node, vm.vmid, type);
|
||||
await prisma.virtualMachine.update({ where: { id: vm.id }, data: { status: "STOPPED", proxmox_upid: upid } });
|
||||
} else if (action === "suspend") {
|
||||
upid = await suspendVm(vm.node, vm.vmid, type);
|
||||
await prisma.virtualMachine.update({ where: { id: vm.id }, data: { status: "PAUSED", proxmox_upid: upid } });
|
||||
} else if (action === "resume") {
|
||||
upid = await resumeVm(vm.node, vm.vmid, type);
|
||||
await prisma.virtualMachine.update({ where: { id: vm.id }, data: { status: "RUNNING", proxmox_upid: upid } });
|
||||
} else {
|
||||
upid = await deleteVm(vm.node, vm.vmid, type);
|
||||
await prisma.virtualMachine.delete({ where: { id: vm.id } });
|
||||
}
|
||||
|
||||
const taskResult = withUpid(
|
||||
{
|
||||
vm_id: vm.id,
|
||||
action
|
||||
},
|
||||
upid
|
||||
);
|
||||
|
||||
await markOperationTaskSuccess(task.id, taskResult, upid);
|
||||
|
||||
await logAudit({
|
||||
action: `vm_${action}`,
|
||||
resource_type: "VM",
|
||||
resource_id: vm.id,
|
||||
resource_name: vm.name,
|
||||
actor_email: req.user!.email,
|
||||
actor_role: req.user!.role,
|
||||
details: {
|
||||
...taskResult,
|
||||
task_id: task.id
|
||||
},
|
||||
ip_address: req.ip
|
||||
});
|
||||
|
||||
res.json({ success: true, action, upid, task_id: task.id });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "VM action failed";
|
||||
await markOperationTaskFailed(task.id, message);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
const migrateSchema = z.object({
|
||||
target_node: z.string().min(1)
|
||||
});
|
||||
|
||||
router.post("/vms/:id/migrate", requireAuth, authorize("vm:update"), async (req, res, next) => {
|
||||
try {
|
||||
const payload = migrateSchema.parse(req.body);
|
||||
const vm = await fetchVm(req.params.id);
|
||||
const type = vmRuntimeType(vm);
|
||||
|
||||
const task = await createOperationTask({
|
||||
taskType: OperationTaskType.VM_MIGRATION,
|
||||
vm: {
|
||||
id: vm.id,
|
||||
name: vm.name,
|
||||
node: vm.node
|
||||
},
|
||||
requestedBy: req.user?.email,
|
||||
payload
|
||||
});
|
||||
|
||||
await markOperationTaskRunning(task.id);
|
||||
|
||||
try {
|
||||
const upid = await migrateVm(vm.node, vm.vmid, payload.target_node, type);
|
||||
await prisma.virtualMachine.update({
|
||||
where: { id: vm.id },
|
||||
data: { node: payload.target_node, status: "MIGRATING", proxmox_upid: upid }
|
||||
});
|
||||
|
||||
const migrationResult = withUpid(
|
||||
{
|
||||
vm_id: vm.id,
|
||||
from_node: vm.node,
|
||||
target_node: payload.target_node
|
||||
},
|
||||
upid
|
||||
);
|
||||
|
||||
await markOperationTaskSuccess(task.id, migrationResult, upid);
|
||||
res.json({ success: true, upid, target_node: payload.target_node, task_id: task.id });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "VM migrate failed";
|
||||
await markOperationTaskFailed(task.id, message);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
const configSchema = z
|
||||
.object({
|
||||
hostname: z.string().min(1).optional(),
|
||||
iso_image: z.string().min(1).optional(),
|
||||
boot_order: z.string().min(1).optional(),
|
||||
ssh_public_key: z.string().min(10).optional(),
|
||||
qemu_guest_agent: z.boolean().optional()
|
||||
})
|
||||
.refine((value) => Object.keys(value).length > 0, {
|
||||
message: "At least one configuration field is required"
|
||||
});
|
||||
|
||||
router.patch("/vms/:id/config", requireAuth, authorize("vm:update"), async (req, res, next) => {
|
||||
try {
|
||||
const payload = configSchema.parse(req.body ?? {});
|
||||
const vm = await fetchVm(req.params.id);
|
||||
const type = vmRuntimeType(vm);
|
||||
|
||||
const config: Record<string, string | number | boolean> = {};
|
||||
if (payload.hostname) config.name = payload.hostname;
|
||||
if (payload.boot_order) config.boot = payload.boot_order;
|
||||
if (payload.ssh_public_key) config.sshkeys = payload.ssh_public_key;
|
||||
if (payload.iso_image && vm.type === "QEMU") config.ide2 = `${payload.iso_image},media=cdrom`;
|
||||
if (typeof payload.qemu_guest_agent === "boolean" && vm.type === "QEMU") {
|
||||
config.agent = payload.qemu_guest_agent ? 1 : 0;
|
||||
}
|
||||
|
||||
const task = await createOperationTask({
|
||||
taskType: OperationTaskType.VM_CONFIG,
|
||||
vm: { id: vm.id, name: vm.name, node: vm.node },
|
||||
requestedBy: req.user?.email,
|
||||
payload
|
||||
});
|
||||
|
||||
await markOperationTaskRunning(task.id);
|
||||
|
||||
try {
|
||||
const upid = await updateVmConfiguration(vm.node, vm.vmid, type, config);
|
||||
const configResult = withUpid(
|
||||
{
|
||||
vm_id: vm.id,
|
||||
config: config as unknown as Prisma.InputJsonValue
|
||||
},
|
||||
upid
|
||||
);
|
||||
await markOperationTaskSuccess(task.id, configResult, upid);
|
||||
|
||||
await logAudit({
|
||||
action: "vm_config_update",
|
||||
resource_type: "VM",
|
||||
resource_id: vm.id,
|
||||
resource_name: vm.name,
|
||||
actor_email: req.user!.email,
|
||||
actor_role: req.user!.role,
|
||||
details: {
|
||||
config: config as unknown as Prisma.InputJsonValue,
|
||||
task_id: task.id,
|
||||
...(upid ? { upid } : {})
|
||||
},
|
||||
ip_address: req.ip
|
||||
});
|
||||
|
||||
res.json({ success: true, upid, task_id: task.id, config_applied: config });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "VM config update failed";
|
||||
await markOperationTaskFailed(task.id, message);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
const networkSchema = z.object({
|
||||
interface_name: z.string().optional(),
|
||||
bridge: z.string().min(1),
|
||||
vlan_tag: z.number().int().min(0).max(4094).optional(),
|
||||
rate_mbps: z.number().int().positive().optional(),
|
||||
firewall: z.boolean().optional(),
|
||||
ip_mode: z.enum(["dhcp", "static"]).default("dhcp"),
|
||||
ip_cidr: z.string().optional(),
|
||||
gateway: z.string().optional()
|
||||
});
|
||||
|
||||
router.patch("/vms/:id/network", requireAuth, authorize("vm:update"), async (req, res, next) => {
|
||||
try {
|
||||
const payload = networkSchema.parse(req.body ?? {});
|
||||
if (payload.ip_mode === "static" && !payload.ip_cidr) {
|
||||
throw new HttpError(400, "ip_cidr is required when ip_mode=static", "INVALID_NETWORK_PAYLOAD");
|
||||
}
|
||||
|
||||
const vm = await fetchVm(req.params.id);
|
||||
const type = vmRuntimeType(vm);
|
||||
|
||||
const task = await createOperationTask({
|
||||
taskType: OperationTaskType.VM_NETWORK,
|
||||
vm: { id: vm.id, name: vm.name, node: vm.node },
|
||||
requestedBy: req.user?.email,
|
||||
payload
|
||||
});
|
||||
|
||||
await markOperationTaskRunning(task.id);
|
||||
|
||||
try {
|
||||
const networkInput: Parameters<typeof reconfigureVmNetwork>[3] = {
|
||||
interface_name: payload.interface_name,
|
||||
bridge: payload.bridge,
|
||||
vlan_tag: payload.vlan_tag,
|
||||
rate_mbps: payload.rate_mbps,
|
||||
firewall: payload.firewall,
|
||||
ip_mode: payload.ip_mode,
|
||||
ip_cidr: payload.ip_cidr,
|
||||
gateway: payload.gateway
|
||||
};
|
||||
const upid = await reconfigureVmNetwork(vm.node, vm.vmid, type, networkInput);
|
||||
const networkResult = withUpid(
|
||||
{
|
||||
vm_id: vm.id,
|
||||
network: payload as unknown as Prisma.InputJsonValue
|
||||
},
|
||||
upid
|
||||
);
|
||||
await markOperationTaskSuccess(task.id, networkResult, upid);
|
||||
res.json({ success: true, upid, task_id: task.id });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "VM network update failed";
|
||||
await markOperationTaskFailed(task.id, message);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
const diskSchema = z.object({
|
||||
storage: z.string().min(1),
|
||||
size_gb: z.number().int().positive(),
|
||||
bus: z.enum(["scsi", "sata", "virtio", "ide"]).default("scsi"),
|
||||
mount_point: z.string().optional()
|
||||
});
|
||||
|
||||
router.post("/vms/:id/disks", requireAuth, authorize("vm:update"), async (req, res, next) => {
|
||||
try {
|
||||
const payload = diskSchema.parse(req.body ?? {});
|
||||
const vm = await fetchVm(req.params.id);
|
||||
const type = vmRuntimeType(vm);
|
||||
|
||||
const task = await createOperationTask({
|
||||
taskType: OperationTaskType.VM_CONFIG,
|
||||
vm: { id: vm.id, name: vm.name, node: vm.node },
|
||||
requestedBy: req.user?.email,
|
||||
payload
|
||||
});
|
||||
|
||||
await markOperationTaskRunning(task.id);
|
||||
|
||||
try {
|
||||
const diskInput: Parameters<typeof addVmDisk>[3] = {
|
||||
storage: payload.storage,
|
||||
size_gb: payload.size_gb,
|
||||
bus: payload.bus,
|
||||
mount_point: payload.mount_point
|
||||
};
|
||||
const upid = await addVmDisk(vm.node, vm.vmid, type, diskInput);
|
||||
const diskResult = withUpid(
|
||||
{
|
||||
vm_id: vm.id,
|
||||
disk: payload as unknown as Prisma.InputJsonValue
|
||||
},
|
||||
upid
|
||||
);
|
||||
await markOperationTaskSuccess(task.id, diskResult, upid);
|
||||
res.status(201).json({ success: true, upid, task_id: task.id });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "VM disk attach failed";
|
||||
await markOperationTaskFailed(task.id, message);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
const reinstallSchema = z.object({
|
||||
backup_before_reinstall: z.boolean().default(false),
|
||||
iso_image: z.string().optional(),
|
||||
ssh_public_key: z.string().optional()
|
||||
});
|
||||
|
||||
router.post("/vms/:id/reinstall", requireAuth, authorize("vm:update"), async (req, res, next) => {
|
||||
try {
|
||||
const payload = reinstallSchema.parse(req.body ?? {});
|
||||
const vm = await fetchVm(req.params.id);
|
||||
const type = vmRuntimeType(vm);
|
||||
|
||||
if (payload.backup_before_reinstall) {
|
||||
await prisma.backup.create({
|
||||
data: {
|
||||
vm_id: vm.id,
|
||||
vm_name: vm.name,
|
||||
node: vm.node,
|
||||
status: "PENDING",
|
||||
type: "FULL",
|
||||
schedule: "MANUAL",
|
||||
notes: "Auto-created before VM reinstall"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const task = await createOperationTask({
|
||||
taskType: OperationTaskType.VM_REINSTALL,
|
||||
vm: { id: vm.id, name: vm.name, node: vm.node },
|
||||
requestedBy: req.user?.email,
|
||||
payload
|
||||
});
|
||||
|
||||
await markOperationTaskRunning(task.id);
|
||||
|
||||
try {
|
||||
const upid = await reinstallVm(vm.node, vm.vmid, type, {
|
||||
iso_image: payload.iso_image,
|
||||
ssh_public_key: payload.ssh_public_key
|
||||
});
|
||||
|
||||
await prisma.virtualMachine.update({
|
||||
where: { id: vm.id },
|
||||
data: {
|
||||
status: "RUNNING",
|
||||
proxmox_upid: upid ?? undefined
|
||||
}
|
||||
});
|
||||
|
||||
const reinstallResult = withUpid(
|
||||
{
|
||||
vm_id: vm.id,
|
||||
reinstall: payload as unknown as Prisma.InputJsonValue
|
||||
},
|
||||
upid
|
||||
);
|
||||
|
||||
await markOperationTaskSuccess(task.id, reinstallResult, upid);
|
||||
|
||||
res.json({ success: true, upid, task_id: task.id });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "VM reinstall failed";
|
||||
await markOperationTaskFailed(task.id, message);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/vms/:id/console", requireAuth, authorize("vm:read"), async (req, res, next) => {
|
||||
try {
|
||||
const vm = await fetchVm(req.params.id);
|
||||
const type = vmRuntimeType(vm);
|
||||
const consoleType = consoleTypeSchema.parse(
|
||||
typeof req.query.console_type === "string"
|
||||
? req.query.console_type.toLowerCase()
|
||||
: "novnc"
|
||||
);
|
||||
const ticket = await vmConsoleTicket(vm.node, vm.vmid, type, consoleType);
|
||||
const proxyTarget = await resolveConsoleProxyTarget(vm.node, consoleType);
|
||||
|
||||
res.json({
|
||||
...ticket,
|
||||
console_type: consoleType,
|
||||
proxy_target: proxyTarget ?? null
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/vms/:id/usage-graphs", requireAuth, authorize("vm:read"), async (req, res, next) => {
|
||||
try {
|
||||
const vm = await fetchVm(req.params.id);
|
||||
const type = vmRuntimeType(vm);
|
||||
const timeframe = graphTimeframeSchema.parse(
|
||||
typeof req.query.timeframe === "string" ? req.query.timeframe.toLowerCase() : "day"
|
||||
);
|
||||
|
||||
const graph = await vmUsageGraphs(vm.node, vm.vmid, type, timeframe, {
|
||||
cpu_usage: vm.cpu_usage,
|
||||
ram_usage: vm.ram_usage,
|
||||
disk_usage: vm.disk_usage,
|
||||
network_in: vm.network_in,
|
||||
network_out: vm.network_out
|
||||
});
|
||||
|
||||
return res.json({
|
||||
vm_id: vm.id,
|
||||
vm_name: vm.name,
|
||||
vm_type: vm.type,
|
||||
node: vm.node,
|
||||
timeframe: graph.timeframe,
|
||||
source: graph.source,
|
||||
summary: graph.summary,
|
||||
points: graph.points
|
||||
});
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/nodes/:id/usage-graphs", requireAuth, authorize("node:read"), async (req, res, next) => {
|
||||
try {
|
||||
const node = await prisma.proxmoxNode.findFirst({
|
||||
where: {
|
||||
OR: [{ id: req.params.id }, { hostname: req.params.id }, { name: req.params.id }]
|
||||
}
|
||||
});
|
||||
|
||||
if (!node) {
|
||||
throw new HttpError(404, "Node not found", "NODE_NOT_FOUND");
|
||||
}
|
||||
|
||||
const timeframe = graphTimeframeSchema.parse(
|
||||
typeof req.query.timeframe === "string" ? req.query.timeframe.toLowerCase() : "day"
|
||||
);
|
||||
|
||||
const graph = await nodeUsageGraphs(node.hostname, timeframe, {
|
||||
cpu_usage: node.cpu_usage,
|
||||
ram_used_mb: node.ram_used_mb,
|
||||
ram_total_mb: node.ram_total_mb,
|
||||
disk_used_gb: node.disk_used_gb,
|
||||
disk_total_gb: node.disk_total_gb
|
||||
});
|
||||
|
||||
return res.json({
|
||||
node_id: node.id,
|
||||
node_name: node.name,
|
||||
node_hostname: node.hostname,
|
||||
timeframe: graph.timeframe,
|
||||
source: graph.source,
|
||||
summary: graph.summary,
|
||||
points: graph.points
|
||||
});
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/cluster/usage-graphs", requireAuth, authorize("node:read"), async (req, res, next) => {
|
||||
try {
|
||||
const timeframe = graphTimeframeSchema.parse(
|
||||
typeof req.query.timeframe === "string" ? req.query.timeframe.toLowerCase() : "day"
|
||||
);
|
||||
const graph = await clusterUsageGraphs(timeframe);
|
||||
|
||||
return res.json({
|
||||
timeframe: graph.timeframe,
|
||||
source: graph.source,
|
||||
node_count: graph.node_count,
|
||||
nodes: graph.nodes,
|
||||
summary: graph.summary,
|
||||
points: graph.points
|
||||
});
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user