Add server hardware/network detail view with Proxmox sync and agent reporting
Servers & Tasks only tracked scheduled tasks — there was no overview of the servers themselves and no way to see CPU/RAM/disk/IP info. Add a clickable server overview (visible to every role, not just admins) that opens a per-server detail page. - servers table gains an agent-reported hardware/network snapshot (IPs, CPU model/cores/load, memory, disks) and an optional link to a Proxmox VM/LXC (integration + node + guest type + vmid). - Proxmox adapter gains getGuestDetail(): live cores/memory/disk from /config, live cpu/mem/uptime from /status/current, and IPs (LXC net config directly, QEMU via a best-effort guest-agent call that degrades gracefully when the agent isn't installed). - New GET /api/servers/:id/detail combines whichever hardware source applies (live Proxmox vs. last agent report) with a DNS reverse-lookup against the DNS module's own record cache, so matching hostnames show up next to each IP. New PATCH /api/servers/:id manages the Proxmox link. - agent/linux/report-tasks.sh now also collects and reports IPs, CPU/memory/disk info on every check-in (load-average-based CPU number, not instantaneous, to keep the agent a cheap oneshot). - Extracted the per-server task table into a shared ServerTaskTable component so the all-servers view and the new detail page render tasks identically. Verified server-side end-to-end against the real dev server (agent report -> detail endpoint -> DNS match) and the Proxmox adapter against a mock HTTPS server covering LXC/QEMU config parsing and the guest-agent-unavailable fallback. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f0e6a9d4c
commit
b9409d3095
15 files changed
+2206
-140
No files matched your search
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE `servers` ADD `ip_addresses` text;--> statement-breakpoint
|
||||
ALTER TABLE `servers` ADD `cpu_model` text;--> statement-breakpoint
|
||||
ALTER TABLE `servers` ADD `cpu_cores` integer;--> statement-breakpoint
|
||||
ALTER TABLE `servers` ADD `cpu_load_percent` real;--> statement-breakpoint
|
||||
ALTER TABLE `servers` ADD `mem_total_bytes` integer;--> statement-breakpoint
|
||||
ALTER TABLE `servers` ADD `mem_used_bytes` integer;--> statement-breakpoint
|
||||
ALTER TABLE `servers` ADD `disks` text;--> statement-breakpoint
|
||||
ALTER TABLE `servers` ADD `proxmox_integration_id` integer REFERENCES integrations(id);--> statement-breakpoint
|
||||
ALTER TABLE `servers` ADD `proxmox_node` text;--> statement-breakpoint
|
||||
ALTER TABLE `servers` ADD `proxmox_guest_type` text;--> statement-breakpoint
|
||||
ALTER TABLE `servers` ADD `proxmox_vmid` integer;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -8,6 +8,13 @@
|
||||
"when": 1789419241921,
|
||||
"tag": "0000_colossal_violations",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1789468464784,
|
||||
"tag": "0001_yummy_luke_cage",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+21
-1
@@ -1,5 +1,5 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { sqliteTable, text, integer, uniqueIndex } from "drizzle-orm/sqlite-core";
|
||||
import { sqliteTable, text, integer, real, uniqueIndex } from "drizzle-orm/sqlite-core";
|
||||
|
||||
// ─── Users & roles ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -150,6 +150,9 @@ export const dnsRecordsCache = sqliteTable("dns_records_cache", {
|
||||
|
||||
// ─── Servers & scheduled tasks (ported from Schedule Task Manager) ─────────
|
||||
|
||||
export const proxmoxGuestTypes = ["qemu", "lxc"] as const;
|
||||
export type ProxmoxGuestTypeCol = (typeof proxmoxGuestTypes)[number];
|
||||
|
||||
export const servers = sqliteTable("servers", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
@@ -162,6 +165,23 @@ export const servers = sqliteTable("servers", {
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
lastSeenAt: text("last_seen_at"),
|
||||
|
||||
// Agent-reported hardware/network snapshot — updated on every agent report.
|
||||
ipAddresses: text("ip_addresses"), // JSON string[]
|
||||
cpuModel: text("cpu_model"),
|
||||
cpuCores: integer("cpu_cores"),
|
||||
cpuLoadPercent: real("cpu_load_percent"),
|
||||
memTotalBytes: integer("mem_total_bytes"),
|
||||
memUsedBytes: integer("mem_used_bytes"),
|
||||
disks: text("disks"), // JSON string: {mount, sizeBytes, usedBytes}[]
|
||||
|
||||
// Optional link to a Proxmox VM/LXC — set by an admin, not the agent.
|
||||
proxmoxIntegrationId: integer("proxmox_integration_id").references(() => integrations.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
proxmoxNode: text("proxmox_node"),
|
||||
proxmoxGuestType: text("proxmox_guest_type").$type<ProxmoxGuestTypeCol>(),
|
||||
proxmoxVmid: integer("proxmox_vmid"),
|
||||
});
|
||||
|
||||
export const scheduledTasks = sqliteTable("scheduled_tasks", {
|
||||
|
||||
@@ -38,14 +38,54 @@ export interface ProxmoxGuest {
|
||||
uptime: number | null;
|
||||
}
|
||||
|
||||
export interface ProxmoxGuestDetail {
|
||||
vmid: number;
|
||||
node: string;
|
||||
type: ProxmoxGuestType;
|
||||
name: string;
|
||||
status: string;
|
||||
cpuCores: number | null;
|
||||
cpuUsagePercent: number | null;
|
||||
memoryBytes: number | null;
|
||||
memUsedBytes: number | null;
|
||||
diskBytes: number | null;
|
||||
uptime: number | null;
|
||||
ipAddresses: string[];
|
||||
/** QEMU only — false when the guest agent call failed (not installed/running). Always true for LXC (IPs read from config, no agent needed). */
|
||||
guestAgentAvailable: boolean;
|
||||
}
|
||||
|
||||
export interface ProxmoxAdapter {
|
||||
ping(): Promise<{ ok: boolean; latencyMs?: number; error?: string }>;
|
||||
listGuests(): Promise<ProxmoxGuest[]>;
|
||||
getGuestDetail(node: string, type: ProxmoxGuestType, vmid: number): Promise<ProxmoxGuestDetail>;
|
||||
startGuest(node: string, type: ProxmoxGuestType, vmid: number): Promise<void>;
|
||||
stopGuest(node: string, type: ProxmoxGuestType, vmid: number): Promise<void>;
|
||||
restartGuest(node: string, type: ProxmoxGuestType, vmid: number): Promise<void>;
|
||||
}
|
||||
|
||||
function parseSizeToBytes(size: string): number | null {
|
||||
const match = size.match(/^(\d+(?:\.\d+)?)\s*([KMGT])?$/i);
|
||||
if (!match) return null;
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = (match[2] ?? "").toUpperCase();
|
||||
const multiplier = ({ "": 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 } as Record<string, number>)[unit] ?? 1;
|
||||
return Math.round(value * multiplier);
|
||||
}
|
||||
|
||||
function extractSizeParam(configValue: string): number | null {
|
||||
const match = configValue.match(/(?:^|,)size=([\d.]+[KMGT]?)/i);
|
||||
return match ? parseSizeToBytes(match[1]) : null;
|
||||
}
|
||||
|
||||
function extractIpFromNetConfig(configValue: string): string | null {
|
||||
const match = configValue.match(/(?:^|,)ip=([^,]+)/i);
|
||||
if (!match) return null;
|
||||
const ip = match[1];
|
||||
if (ip.toLowerCase() === "dhcp" || ip.toLowerCase() === "manual") return null;
|
||||
return ip.split("/")[0];
|
||||
}
|
||||
|
||||
interface RawResponse {
|
||||
status: number;
|
||||
text: () => string;
|
||||
@@ -147,6 +187,79 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
||||
return perNode.flat();
|
||||
}
|
||||
|
||||
async function getGuestDetail(node: string, type: ProxmoxGuestType, vmid: number): Promise<ProxmoxGuestDetail> {
|
||||
const [config, status] = await Promise.all([
|
||||
api("GET", `/nodes/${node}/${type}/${vmid}/config`),
|
||||
api("GET", `/nodes/${node}/${type}/${vmid}/status/current`),
|
||||
]);
|
||||
|
||||
let cpuCores: number | null = null;
|
||||
let diskBytes: number | null = null;
|
||||
const ipAddresses: string[] = [];
|
||||
let guestAgentAvailable = true;
|
||||
|
||||
if (type === "lxc") {
|
||||
cpuCores = typeof config.cores === "number" ? config.cores : null;
|
||||
if (typeof config.rootfs === "string") {
|
||||
diskBytes = extractSizeParam(config.rootfs);
|
||||
}
|
||||
for (const key of Object.keys(config)) {
|
||||
if (/^net\d+$/.test(key) && typeof config[key] === "string") {
|
||||
const ip = extractIpFromNetConfig(config[key]);
|
||||
if (ip) ipAddresses.push(ip);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const sockets = typeof config.sockets === "number" ? config.sockets : 1;
|
||||
cpuCores = typeof config.cores === "number" ? config.cores * sockets : null;
|
||||
|
||||
let totalDisk = 0;
|
||||
let foundDisk = false;
|
||||
for (const key of Object.keys(config)) {
|
||||
if (/^(scsi|virtio|sata|ide)\d+$/.test(key) && typeof config[key] === "string") {
|
||||
const size = extractSizeParam(config[key]);
|
||||
if (size !== null) {
|
||||
totalDisk += size;
|
||||
foundDisk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
diskBytes = foundDisk ? totalDisk : null;
|
||||
|
||||
try {
|
||||
const agentData = await api("GET", `/nodes/${node}/qemu/${vmid}/agent/network-get-interfaces`);
|
||||
const interfaces = agentData?.result ?? [];
|
||||
for (const iface of interfaces) {
|
||||
for (const addr of iface["ip-addresses"] ?? []) {
|
||||
if (addr["ip-address-type"] === "ipv4" && addr["ip-address"] !== "127.0.0.1") {
|
||||
ipAddresses.push(addr["ip-address"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
guestAgentAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
const memoryBytes = typeof config.memory === "number" ? config.memory * 1024 * 1024 : null;
|
||||
|
||||
return {
|
||||
vmid,
|
||||
node,
|
||||
type,
|
||||
name: config.name ?? `${type}/${vmid}`,
|
||||
status: status.status,
|
||||
cpuCores,
|
||||
cpuUsagePercent: typeof status.cpu === "number" ? status.cpu * 100 : null,
|
||||
memoryBytes,
|
||||
memUsedBytes: typeof status.mem === "number" ? status.mem : null,
|
||||
diskBytes,
|
||||
uptime: typeof status.uptime === "number" ? status.uptime : null,
|
||||
ipAddresses,
|
||||
guestAgentAvailable: type === "qemu" ? guestAgentAvailable : true,
|
||||
};
|
||||
}
|
||||
|
||||
async function statusAction(node: string, type: ProxmoxGuestType, vmid: number, action: string): Promise<void> {
|
||||
await api("POST", `/nodes/${node}/${type}/${vmid}/status/${action}`);
|
||||
}
|
||||
@@ -165,5 +278,5 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
return { ping, listGuests, startGuest, stopGuest, restartGuest };
|
||||
return { ping, listGuests, getGuestDetail, startGuest, stopGuest, restartGuest };
|
||||
}
|
||||
@@ -7,10 +7,18 @@ import { asyncHandler } from "../utils/asyncHandler.js";
|
||||
|
||||
export const agentReportRouter = Router();
|
||||
|
||||
const systemSchema = z.object({
|
||||
ip_addresses: z.array(z.string()).optional(),
|
||||
cpu: z.object({ model: z.string().optional(), cores: z.number().optional(), load_percent: z.number().nullable().optional() }).optional(),
|
||||
memory: z.object({ total_bytes: z.number().optional(), used_bytes: z.number().optional() }).optional(),
|
||||
disks: z.array(z.object({ mount: z.string(), size_bytes: z.number(), used_bytes: z.number() })).optional(),
|
||||
});
|
||||
|
||||
const reportSchema = z.object({
|
||||
hostname: z.string().max(255).optional(),
|
||||
os_type: z.string().optional(),
|
||||
reported_at: z.string().optional(),
|
||||
system: systemSchema.optional(),
|
||||
tasks: z.array(
|
||||
z.object({
|
||||
schedule_type: z.enum(["cron", "systemd_timer"]),
|
||||
@@ -47,6 +55,7 @@ agentReportRouter.post("/", asyncHandler(async (req, res) => {
|
||||
|
||||
await syncServerTasks(server.id, {
|
||||
hostname: parsed.data.hostname,
|
||||
system: parsed.data.system,
|
||||
tasks: parsed.data.tasks.map((t) => ({
|
||||
scheduleType: t.schedule_type,
|
||||
name: t.name,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Router } from "express";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { db } from "../db/client.js";
|
||||
import { servers } from "../db/schema.js";
|
||||
import { servers, integrations, dnsRecordsCache, dnsZonesCache, dnsProviders } from "../db/schema.js";
|
||||
import { requireAuth, requireRole } from "../auth/middleware.js";
|
||||
import { generateApiToken } from "../services/tokens.js";
|
||||
import { recordAudit } from "../services/audit.js";
|
||||
import { asyncHandler } from "../utils/asyncHandler.js";
|
||||
import { loadIntegrationConfig } from "../integrations/loadIntegration.js";
|
||||
import { createProxmoxAdapter, type ProxmoxGuestType } from "../integrations/proxmox/adapter.js";
|
||||
|
||||
export const serversRouter = Router();
|
||||
serversRouter.use(requireAuth);
|
||||
@@ -85,6 +87,155 @@ serversRouter.post("/:id/rotate-token", requireRole("admin"), asyncHandler(async
|
||||
res.json({ server: serverOut, token });
|
||||
}));
|
||||
|
||||
const updateServerSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
hostname: z.string().max(255).optional(),
|
||||
description: z.string().max(500).optional(),
|
||||
proxmoxIntegrationId: z.number().int().nullable().optional(),
|
||||
proxmoxNode: z.string().nullable().optional(),
|
||||
proxmoxGuestType: z.enum(["qemu", "lxc"]).nullable().optional(),
|
||||
proxmoxVmid: z.number().int().nullable().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const proxmoxFields = [data.proxmoxIntegrationId, data.proxmoxNode, data.proxmoxGuestType, data.proxmoxVmid];
|
||||
if (proxmoxFields.every((f) => f === undefined)) return true;
|
||||
const allNull = proxmoxFields.every((f) => f === null);
|
||||
const allSet = proxmoxFields.every((f) => f !== undefined && f !== null);
|
||||
return allNull || allSet;
|
||||
},
|
||||
{ message: "proxmoxIntegrationId/proxmoxNode/proxmoxGuestType/proxmoxVmid must be set together or all cleared to null" },
|
||||
);
|
||||
|
||||
serversRouter.patch("/:id", requireRole("admin"), asyncHandler(async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: "invalid_id" });
|
||||
|
||||
const parsed = updateServerSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: "invalid_body", details: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
const [existing] = await db.select().from(servers).where(eq(servers.id, id)).limit(1);
|
||||
if (!existing) return res.status(404).json({ error: "not_found" });
|
||||
|
||||
if (parsed.data.proxmoxIntegrationId) {
|
||||
const [integration] = await db
|
||||
.select()
|
||||
.from(integrations)
|
||||
.where(eq(integrations.id, parsed.data.proxmoxIntegrationId))
|
||||
.limit(1);
|
||||
if (!integration || integration.type !== "proxmox") {
|
||||
return res.status(400).json({ error: "invalid_proxmox_integration" });
|
||||
}
|
||||
}
|
||||
|
||||
const [updated] = await db.update(servers).set(parsed.data).where(eq(servers.id, id)).returning();
|
||||
|
||||
await recordAudit({
|
||||
actor: req.currentUser!,
|
||||
category: "server",
|
||||
action: "update",
|
||||
targetType: "server",
|
||||
targetId: id,
|
||||
detail: { name: updated.name },
|
||||
});
|
||||
|
||||
const { apiTokenHash, ...serverOut } = updated;
|
||||
res.json({ server: serverOut });
|
||||
}));
|
||||
|
||||
serversRouter.get("/:id/detail", asyncHandler(async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: "invalid_id" });
|
||||
|
||||
const [server] = await db.select().from(servers).where(eq(servers.id, id)).limit(1);
|
||||
if (!server) return res.status(404).json({ error: "not_found" });
|
||||
|
||||
let ipAddresses: string[] = [];
|
||||
let hardware: Record<string, unknown>;
|
||||
|
||||
if (server.proxmoxIntegrationId && server.proxmoxNode && server.proxmoxGuestType && server.proxmoxVmid !== null) {
|
||||
const loaded = await loadIntegrationConfig(server.proxmoxIntegrationId);
|
||||
if (!loaded || loaded.integration.type !== "proxmox") {
|
||||
hardware = { source: "proxmox", error: "The linked Proxmox integration no longer exists or has changed type." };
|
||||
} else {
|
||||
try {
|
||||
const adapter = createProxmoxAdapter(loaded.config as { url: string; tokenId: string; tokenSecret: string; insecure?: boolean });
|
||||
const detail = await adapter.getGuestDetail(
|
||||
server.proxmoxNode,
|
||||
server.proxmoxGuestType as ProxmoxGuestType,
|
||||
server.proxmoxVmid,
|
||||
);
|
||||
ipAddresses = detail.ipAddresses;
|
||||
hardware = {
|
||||
source: "proxmox",
|
||||
integrationId: loaded.integration.id,
|
||||
integrationName: loaded.integration.name,
|
||||
status: detail.status,
|
||||
cpuCores: detail.cpuCores,
|
||||
cpuUsagePercent: detail.cpuUsagePercent,
|
||||
memTotalBytes: detail.memoryBytes,
|
||||
memUsedBytes: detail.memUsedBytes,
|
||||
diskBytes: detail.diskBytes,
|
||||
uptime: detail.uptime,
|
||||
guestAgentAvailable: detail.guestAgentAvailable,
|
||||
};
|
||||
} catch (err) {
|
||||
hardware = { source: "proxmox", error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
}
|
||||
} else if (server.cpuModel || server.cpuCores || server.memTotalBytes || server.disks) {
|
||||
ipAddresses = server.ipAddresses ? JSON.parse(server.ipAddresses) : [];
|
||||
hardware = {
|
||||
source: "agent",
|
||||
cpuModel: server.cpuModel,
|
||||
cpuCores: server.cpuCores,
|
||||
cpuLoadPercent: server.cpuLoadPercent,
|
||||
memTotalBytes: server.memTotalBytes,
|
||||
memUsedBytes: server.memUsedBytes,
|
||||
disks: server.disks ? JSON.parse(server.disks) : [],
|
||||
};
|
||||
} else {
|
||||
hardware = { source: "none" };
|
||||
}
|
||||
|
||||
let dnsMatches: { recordName: string; recordType: string; ip: string; zoneName: string | null; providerName: string; providerType: string }[] = [];
|
||||
if (ipAddresses.length > 0) {
|
||||
dnsMatches = await db
|
||||
.select({
|
||||
recordName: dnsRecordsCache.name,
|
||||
recordType: dnsRecordsCache.type,
|
||||
ip: dnsRecordsCache.content,
|
||||
zoneName: dnsZonesCache.zoneName,
|
||||
providerName: dnsProviders.name,
|
||||
providerType: dnsProviders.providerType,
|
||||
})
|
||||
.from(dnsRecordsCache)
|
||||
.innerJoin(dnsProviders, eq(dnsRecordsCache.providerId, dnsProviders.id))
|
||||
.leftJoin(
|
||||
dnsZonesCache,
|
||||
and(eq(dnsZonesCache.providerId, dnsRecordsCache.providerId), eq(dnsZonesCache.zoneId, dnsRecordsCache.zoneId)),
|
||||
)
|
||||
.where(inArray(dnsRecordsCache.content, ipAddresses));
|
||||
}
|
||||
|
||||
const {
|
||||
apiTokenHash,
|
||||
ipAddresses: _rawIpAddresses,
|
||||
disks: _rawDisks,
|
||||
cpuModel: _cpuModel,
|
||||
cpuCores: _cpuCores,
|
||||
cpuLoadPercent: _cpuLoadPercent,
|
||||
memTotalBytes: _memTotalBytes,
|
||||
memUsedBytes: _memUsedBytes,
|
||||
...serverOut
|
||||
} = server;
|
||||
|
||||
res.json({ server: serverOut, ipAddresses, hardware, dnsMatches });
|
||||
}));
|
||||
|
||||
serversRouter.delete("/:id", requireRole("admin"), asyncHandler(async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: "invalid_id" });
|
||||
|
||||
@@ -13,8 +13,16 @@ export interface IncomingTask {
|
||||
metadata?: unknown;
|
||||
}
|
||||
|
||||
export interface IncomingSystemInfo {
|
||||
ip_addresses?: string[];
|
||||
cpu?: { model?: string; cores?: number; load_percent?: number | null };
|
||||
memory?: { total_bytes?: number; used_bytes?: number };
|
||||
disks?: { mount: string; size_bytes: number; used_bytes: number }[];
|
||||
}
|
||||
|
||||
export interface AgentReport {
|
||||
hostname?: string;
|
||||
system?: IncomingSystemInfo;
|
||||
tasks: IncomingTask[];
|
||||
}
|
||||
|
||||
@@ -85,8 +93,25 @@ export async function syncServerTasks(serverId: number, report: AgentReport) {
|
||||
await db.update(scheduledTasks).set({ isStale: true }).where(agentScope);
|
||||
}
|
||||
|
||||
const system = report.system;
|
||||
await db
|
||||
.update(servers)
|
||||
.set({ lastSeenAt: now, ...(report.hostname ? { hostname: report.hostname } : {}) })
|
||||
.set({
|
||||
lastSeenAt: now,
|
||||
...(report.hostname ? { hostname: report.hostname } : {}),
|
||||
...(system?.ip_addresses ? { ipAddresses: JSON.stringify(system.ip_addresses) } : {}),
|
||||
...(system?.cpu?.model !== undefined ? { cpuModel: system.cpu.model } : {}),
|
||||
...(system?.cpu?.cores !== undefined ? { cpuCores: system.cpu.cores } : {}),
|
||||
...(system?.cpu?.load_percent !== undefined ? { cpuLoadPercent: system.cpu.load_percent } : {}),
|
||||
...(system?.memory?.total_bytes !== undefined ? { memTotalBytes: system.memory.total_bytes } : {}),
|
||||
...(system?.memory?.used_bytes !== undefined ? { memUsedBytes: system.memory.used_bytes } : {}),
|
||||
...(system?.disks
|
||||
? {
|
||||
disks: JSON.stringify(
|
||||
system.disks.map((d) => ({ mount: d.mount, sizeBytes: d.size_bytes, usedBytes: d.used_bytes })),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
.where(eq(servers.id, serverId));
|
||||
}
|
||||
Reference in new issue
Block a user