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:
bobbanandClaude Sonnet 5 committed 2026-09-15 13:02:50 +02:00
1 parent 1f0e6a9d4c
commit b9409d3095
15 files changed
+2206 -140

No files matched your search

+26 -1
View File
@@ -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));
}