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>
73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
import { Router } from "express";
|
|
import { z } from "zod";
|
|
import { db } from "../db/client.js";
|
|
import { hashToken } from "../services/tokens.js";
|
|
import { syncServerTasks } from "../services/taskSync.js";
|
|
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"]),
|
|
name: z.string().min(1),
|
|
command: z.string().optional(),
|
|
schedule_expression: z.string().optional(),
|
|
source: z.string().optional(),
|
|
enabled: z.boolean().optional(),
|
|
next_run_at: z.string().optional(),
|
|
metadata: z.unknown().optional(),
|
|
}),
|
|
),
|
|
});
|
|
|
|
agentReportRouter.post("/", asyncHandler(async (req, res) => {
|
|
const authHeader = req.header("authorization") ?? "";
|
|
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
if (!match) {
|
|
return res.status(401).json({ error: "missing_token" });
|
|
}
|
|
|
|
const tokenHash = hashToken(match[1]);
|
|
const server = await db.query.servers.findFirst({
|
|
where: (s, { eq }) => eq(s.apiTokenHash, tokenHash),
|
|
});
|
|
if (!server) {
|
|
return res.status(401).json({ error: "invalid_token" });
|
|
}
|
|
|
|
const parsed = reportSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
return res.status(400).json({ error: "invalid_body", details: parsed.error.flatten() });
|
|
}
|
|
|
|
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,
|
|
command: t.command,
|
|
scheduleExpression: t.schedule_expression,
|
|
source: t.source,
|
|
enabled: t.enabled,
|
|
nextRunAt: t.next_run_at,
|
|
metadata: t.metadata,
|
|
})),
|
|
});
|
|
|
|
res.status(202).json({ ok: true, taskCount: parsed.data.tasks.length });
|
|
}));
|