Fix "No online Proxmox nodes found" masking real host-stats errors
listNodeStats() was silently dropping any node whose /status or /storage call failed and filtering it out of the result — with every node dropped, the page showed the same "no nodes" empty state as a genuinely nodeless tailnet, even though the node was online and the existing VM/LXC table below it worked fine. The likely real cause: Sys.Audit (host status) and Datastore.Audit (storage) are different ACL privileges from the VM/LXC management scope this integration originally needed, so a token created before this feature existed may lack them. getNodeStats() now fetches /status and /storage independently via Promise.allSettled instead of Promise.all, so one endpoint failing doesn't discard data the other successfully returned, and records a per-endpoint error message. listNodeStats() no longer filters failed nodes out at all -- it always returns one entry per online node, with `error` set and the rest of the fields null when nothing could be fetched. The Proxmox page now shows that error inline on the node's card instead of it vanishing. Verified against a mock Proxmox API returning 403 on /storage only (partial data still shows) and on both endpoints (node stays visible with its error surfaced, not silently dropped) — reproducing the reported bug and confirming the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2d062a9ac4
commit
5919929dfb
3 files changed
+57
-17
No files matched your search
@@ -67,6 +67,8 @@ export interface ProxmoxStorage {
|
||||
|
||||
export interface ProxmoxNodeStats {
|
||||
node: string;
|
||||
/** Set (with every other field null/empty) when this node's status/storage couldn't be fetched — e.g. the API token lacks Sys.Audit/Datastore.Audit. */
|
||||
error: string | null;
|
||||
uptime: number | null;
|
||||
cpuUsagePercent: number | null;
|
||||
cpuCores: number | null;
|
||||
@@ -289,13 +291,46 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
||||
};
|
||||
}
|
||||
|
||||
const emptyNodeStats = (node: string, error: string | null): ProxmoxNodeStats => ({
|
||||
node,
|
||||
error,
|
||||
uptime: null,
|
||||
cpuUsagePercent: null,
|
||||
cpuCores: null,
|
||||
loadAverage: null,
|
||||
memTotalBytes: null,
|
||||
memUsedBytes: null,
|
||||
swapTotalBytes: null,
|
||||
swapUsedBytes: null,
|
||||
rootfsTotalBytes: null,
|
||||
rootfsUsedBytes: null,
|
||||
pveVersion: null,
|
||||
storages: [],
|
||||
});
|
||||
|
||||
function errorMessage(reason: unknown): string {
|
||||
return reason instanceof Error ? reason.message : String(reason);
|
||||
}
|
||||
|
||||
async function getNodeStats(node: string): Promise<ProxmoxNodeStats> {
|
||||
const [status, storages] = await Promise.all([
|
||||
// Host status and storage usage need different ACL privileges
|
||||
// (Sys.Audit vs Datastore.Audit) — a token scoped only for VM/LXC
|
||||
// management (this integration's original scope) may have one but not
|
||||
// the other, so fetch them independently rather than losing both to
|
||||
// Promise.all's fail-fast behavior.
|
||||
const [statusResult, storageResult] = await Promise.allSettled([
|
||||
api("GET", `/nodes/${node}/status`),
|
||||
api("GET", `/nodes/${node}/storage`),
|
||||
]);
|
||||
|
||||
const loadavgRaw = Array.isArray(status.loadavg) ? status.loadavg.map((v: string) => Number(v)) : null;
|
||||
const status = statusResult.status === "fulfilled" ? statusResult.value : null;
|
||||
const storages = storageResult.status === "fulfilled" ? storageResult.value : null;
|
||||
|
||||
const errors: string[] = [];
|
||||
if (statusResult.status === "rejected") errors.push(`host stats: ${errorMessage(statusResult.reason)}`);
|
||||
if (storageResult.status === "rejected") errors.push(`storage: ${errorMessage(storageResult.reason)}`);
|
||||
|
||||
const loadavgRaw = Array.isArray(status?.loadavg) ? status.loadavg.map((v: string) => Number(v)) : null;
|
||||
const loadAverage: [number, number, number] | null =
|
||||
loadavgRaw && loadavgRaw.length === 3 && loadavgRaw.every((n: number) => Number.isFinite(n))
|
||||
? (loadavgRaw as [number, number, number])
|
||||
@@ -303,17 +338,18 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
||||
|
||||
return {
|
||||
node,
|
||||
uptime: typeof status.uptime === "number" ? status.uptime : null,
|
||||
cpuUsagePercent: typeof status.cpu === "number" ? status.cpu * 100 : null,
|
||||
cpuCores: typeof status.cpuinfo?.cpus === "number" ? status.cpuinfo.cpus : null,
|
||||
error: errors.length > 0 ? errors.join("; ") : null,
|
||||
uptime: typeof status?.uptime === "number" ? status.uptime : null,
|
||||
cpuUsagePercent: typeof status?.cpu === "number" ? status.cpu * 100 : null,
|
||||
cpuCores: typeof status?.cpuinfo?.cpus === "number" ? status.cpuinfo.cpus : null,
|
||||
loadAverage,
|
||||
memTotalBytes: typeof status.memory?.total === "number" ? status.memory.total : null,
|
||||
memUsedBytes: typeof status.memory?.used === "number" ? status.memory.used : null,
|
||||
swapTotalBytes: typeof status.swap?.total === "number" ? status.swap.total : null,
|
||||
swapUsedBytes: typeof status.swap?.used === "number" ? status.swap.used : null,
|
||||
rootfsTotalBytes: typeof status.rootfs?.total === "number" ? status.rootfs.total : null,
|
||||
rootfsUsedBytes: typeof status.rootfs?.used === "number" ? status.rootfs.used : null,
|
||||
pveVersion: typeof status.pveversion === "string" ? status.pveversion : null,
|
||||
memTotalBytes: typeof status?.memory?.total === "number" ? status.memory.total : null,
|
||||
memUsedBytes: typeof status?.memory?.used === "number" ? status.memory.used : null,
|
||||
swapTotalBytes: typeof status?.swap?.total === "number" ? status.swap.total : null,
|
||||
swapUsedBytes: typeof status?.swap?.used === "number" ? status.swap.used : null,
|
||||
rootfsTotalBytes: typeof status?.rootfs?.total === "number" ? status.rootfs.total : null,
|
||||
rootfsUsedBytes: typeof status?.rootfs?.used === "number" ? status.rootfs.used : null,
|
||||
pveVersion: typeof status?.pveversion === "string" ? status.pveversion : null,
|
||||
storages: (Array.isArray(storages) ? storages : []).map((s: any) => ({
|
||||
id: s.storage,
|
||||
type: s.type,
|
||||
@@ -328,17 +364,19 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
|
||||
|
||||
async function listNodeStats(): Promise<ProxmoxNodeStats[]> {
|
||||
const nodes = await listNodes();
|
||||
const perNode = await Promise.all(
|
||||
return Promise.all(
|
||||
nodes.map(async (node) => {
|
||||
try {
|
||||
return await getNodeStats(node);
|
||||
} catch {
|
||||
// one unreachable/offline node shouldn't take down the whole stats view
|
||||
return null;
|
||||
} catch (err) {
|
||||
// Surface the failure on this node's card instead of silently
|
||||
// dropping it — that previously showed a misleading "no online
|
||||
// nodes" empty state even when nodes existed but stats couldn't
|
||||
// be fetched (e.g. missing ACL privileges on the API token).
|
||||
return emptyNodeStats(node, errorMessage(err));
|
||||
}
|
||||
}),
|
||||
);
|
||||
return perNode.filter((n): n is ProxmoxNodeStats => n !== null);
|
||||
}
|
||||
|
||||
async function statusAction(node: string, type: ProxmoxGuestType, vmid: number, action: string): Promise<void> {
|
||||
|
||||
@@ -424,6 +424,7 @@ export interface ProxmoxStorage {
|
||||
|
||||
export interface ProxmoxNodeStats {
|
||||
node: string;
|
||||
error: string | null;
|
||||
uptime: number | null;
|
||||
cpuUsagePercent: number | null;
|
||||
cpuCores: number | null;
|
||||
|
||||
@@ -184,6 +184,7 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{n.error && <div className="alert alert-danger">{n.error}</div>}
|
||||
<div className="row g-3">
|
||||
<div className="col-sm-6">
|
||||
<div className="text-secondary small">Uptime</div>
|
||||
|
||||
Reference in new issue
Block a user