diff --git a/client/src/Components/v1/Charts/Utils/chartUtils.jsx b/client/src/Components/v1/Charts/Utils/chartUtils.jsx
index 1bab80595..10085116a 100644
--- a/client/src/Components/v1/Charts/Utils/chartUtils.jsx
+++ b/client/src/Components/v1/Charts/Utils/chartUtils.jsx
@@ -154,7 +154,10 @@ export const InfrastructureTooltip = ({
yKey,
yIdx = -1,
yLabel,
+ yKey2,
+ yLabel2,
dotColor,
+ dotColor2,
dateRange,
formatter = getFormattedPercentage,
}) => {
@@ -216,7 +219,37 @@ export const InfrastructureTooltip = ({
- {/* Display original value */}
+ {yKey2 && yLabel2 && (
+
+
+
+
+ {`${yLabel2} ${formatter(payload[0].payload[yKey2])}`}
+
+
+
+ )}
);
}
@@ -234,7 +267,10 @@ InfrastructureTooltip.propTypes = {
yKey: PropTypes.string,
yIdx: PropTypes.number,
yLabel: PropTypes.string,
+ yKey2: PropTypes.string,
+ yLabel2: PropTypes.string,
dotColor: PropTypes.string,
+ dotColor2: PropTypes.string,
dateRange: PropTypes.string,
};
diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx
index a38ac9fc6..8ff6c3a2e 100644
--- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx
+++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx
@@ -18,6 +18,7 @@ const useInfrastructureMonitorForm = () => {
usage_temperature: "",
secret: "",
selectedDisks: [],
+ enableProxmoxContainers: false,
});
const onChangeForm = (name, value) => {
@@ -50,6 +51,7 @@ const useInfrastructureMonitorForm = () => {
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
secret: "",
selectedDisks: [],
+ enableProxmoxContainers: false,
}));
}, []);
@@ -84,6 +86,7 @@ const useInfrastructureMonitorForm = () => {
: "",
secret: monitor.secret || "",
selectedDisks: monitor.selectedDisks || [],
+ enableProxmoxContainers: monitor.enableProxmoxContainers ?? false,
}));
}, []);
return {
diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx
index 04279cac4..cbfba9bb9 100644
--- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx
+++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx
@@ -31,6 +31,7 @@ const useInfrastructureSubmit = () => {
: {}),
secret: infrastructureMonitor.secret,
selectedDisks: infrastructureMonitor.selectedDisks,
+ enableProxmoxContainers: infrastructureMonitor.enableProxmoxContainers,
};
return form;
};
@@ -50,6 +51,7 @@ const useInfrastructureSubmit = () => {
temperature,
usage_temperature,
selectedDisks,
+ enableProxmoxContainers,
...rest
} = form;
@@ -67,6 +69,7 @@ const useInfrastructureSubmit = () => {
type: "hardware",
notifications: infrastructureMonitor.notifications,
selectedDisks,
+ enableProxmoxContainers,
thresholds,
};
// Handle create or update
diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx
index 21d70720d..c424ea969 100644
--- a/client/src/Pages/Infrastructure/Create/index.jsx
+++ b/client/src/Pages/Infrastructure/Create/index.jsx
@@ -5,7 +5,7 @@ import FieldWrapper from "@/Components/v1/Inputs/FieldWrapper/index.jsx";
import Link from "@/Components/v1/Link/index.jsx";
import Select from "@/Components/v1/Inputs/Select/index.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
-import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
+import { Box, Stack, Typography, Button, ButtonGroup, Switch, FormControlLabel } from "@mui/material";
import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
import MonitorStatusHeader from "./Components/MonitorStatusHeader.jsx";
import MonitorActionButtons from "./Components/MonitorActionButtons.jsx";
@@ -368,6 +368,34 @@ const CreateInfrastructureMonitor = () => {
/>
)}
+
+
+
+ {t("proxmoxContainerMonitoring")}
+
+
+ {t("proxmoxContainerMonitoringDescription")}
+
+
+
+
+ }
+ label={t("enableProxmoxContainers")}
+ />
+
+
+
{
+ const theme = useTheme();
+ const { t } = useTranslation();
+
+ const { formatBytesPerSecondString } = useHardwareUtils();
+
+ if (!containerData?.length) {
+ return {t("noContainerStatsAvailable")};
+ }
+
+ const formatPercentage = (value) => `${(value * 100).toFixed(1)}%`;
+
+ const configs = [
+ {
+ type: "container-cpu",
+ data: containerData,
+ dataKeys: ["cpuUsage"],
+ heading: t("cpuUsage"),
+ strokeColor: theme.palette.success.main,
+ gradientStartColor: theme.palette.success.main,
+ yLabel: t("usage"),
+ xTick: ,
+ yTick: ,
+ toolTip: (
+
+ ),
+ },
+ {
+ type: "container-memory",
+ data: containerData,
+ dataKeys: ["memoryUsage"],
+ heading: t("memoryUsage"),
+ strokeColor: theme.palette.accent.main,
+ gradientStartColor: theme.palette.accent.main,
+ yLabel: t("usage"),
+ xTick: ,
+ yTick: ,
+ toolTip: (
+
+ ),
+ },
+ {
+ type: "container-disk-io",
+ data: containerData,
+ dataKeys: ["diskRead", "diskWrite"],
+ heading: t("diskIO"),
+ // Don't pass strokeColor - let chart use distinct colors from palette
+ yLabel: t("rate"),
+ xTick: ,
+ yTick: ,
+ toolTip: (
+
+ ),
+ },
+ {
+ type: "container-network-io",
+ data: containerData,
+ dataKeys: ["netIn", "netOut"],
+ heading: t("networkIO"),
+ // Don't pass strokeColor - let chart use distinct colors from palette
+ yLabel: t("rate"),
+ xTick: ,
+ yTick: ,
+ toolTip: (
+
+ ),
+ },
+ ];
+
+ return (
+ *": {
+ flexBasis: `calc(50% - ${theme.spacing(8)})`,
+ maxWidth: `calc(50% - ${theme.spacing(8)})`,
+ },
+ }}
+ >
+ {configs.map((config) => (
+
+ ))}
+
+ );
+};
+
+ContainerCharts.propTypes = {
+ containerData: PropTypes.array.isRequired,
+ dateRange: PropTypes.string.isRequired,
+};
+
+export default ContainerCharts;
diff --git a/client/src/Pages/Infrastructure/Details/Components/ContainerStats/ContainerStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/ContainerStats/ContainerStatBoxes.jsx
new file mode 100644
index 000000000..b15d9ce97
--- /dev/null
+++ b/client/src/Pages/Infrastructure/Details/Components/ContainerStats/ContainerStatBoxes.jsx
@@ -0,0 +1,130 @@
+import PropTypes from "prop-types";
+import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
+import StatBox from "@/Components/v1/StatBox/index.jsx";
+import { Typography } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
+
+const formatUptime = (seconds) => {
+ if (!seconds || seconds === 0) return "0s";
+
+ const days = Math.floor(seconds / 86400);
+ const hours = Math.floor((seconds % 86400) / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+
+ if (days > 0) {
+ return `${days}d ${hours}h`;
+ } else if (hours > 0) {
+ return `${hours}h ${minutes}m`;
+ } else if (minutes > 0) {
+ return `${minutes}m`;
+ }
+ return `${Math.floor(seconds)}s`;
+};
+
+const formatPercentage = (value) => {
+ if (value === undefined || value === null) return "0%";
+ return `${(value * 100).toFixed(1)}%`;
+};
+
+// Map container status to Checkmate monitor status for theming
+const containerStatusToMonitorStatus = (status) => {
+ if (status === "running") return "up";
+ if (status === "stopped") return "down";
+ return "pending";
+};
+
+const ContainerStatBoxes = ({ shouldRender, container }) => {
+ const { t } = useTranslation();
+ const { formatBytes } = useHardwareUtils();
+
+ if (!container) {
+ return {t("noContainerSelected")};
+ }
+
+ const monitorStatus = containerStatusToMonitorStatus(container.status);
+
+ return (
+
+
+
+ {(container.type || "lxc").toUpperCase()}
+
+ }
+ />
+
+
+ {formatPercentage(container.cpu_usage)}
+
+ ({container.cpu_cores || 0} {t("cores")})
+
+ >
+ }
+ />
+
+ {formatBytes(container.memory_used)}
+ /
+ {formatBytes(container.memory_total)}
+ >
+ }
+ />
+
+ {formatBytes(container.disk_used)}
+ /
+ {formatBytes(container.disk_total)}
+ >
+ }
+ />
+
+ );
+};
+
+ContainerStatBoxes.propTypes = {
+ shouldRender: PropTypes.bool.isRequired,
+ container: PropTypes.shape({
+ vmid: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ name: PropTypes.string,
+ node: PropTypes.string,
+ status: PropTypes.string,
+ type: PropTypes.string,
+ uptime: PropTypes.number,
+ cpu_cores: PropTypes.number,
+ cpu_usage: PropTypes.number,
+ memory_used: PropTypes.number,
+ memory_total: PropTypes.number,
+ memory_usage: PropTypes.number,
+ swap_used: PropTypes.number,
+ swap_total: PropTypes.number,
+ disk_used: PropTypes.number,
+ disk_total: PropTypes.number,
+ disk_read: PropTypes.number,
+ disk_write: PropTypes.number,
+ network_in: PropTypes.number,
+ network_out: PropTypes.number,
+ }),
+};
+
+export default ContainerStatBoxes;
diff --git a/client/src/Pages/Infrastructure/Details/Components/ContainerStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/ContainerStats/index.jsx
new file mode 100644
index 000000000..dbece6aa5
--- /dev/null
+++ b/client/src/Pages/Infrastructure/Details/Components/ContainerStats/index.jsx
@@ -0,0 +1,125 @@
+import PropTypes from "prop-types";
+import { useState, useEffect } from "react";
+import { Box, Typography } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@emotion/react";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
+import ContainerStatBoxes from "./ContainerStatBoxes.jsx";
+import ContainerCharts from "./ContainerCharts.jsx";
+import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
+
+const getAvailableContainers = (containers) => {
+ return (containers || [])
+ .filter((c) => c.vmid && c.name)
+ .map((c) => ({
+ vmid: String(c.vmid),
+ name: c.name,
+ node: c.node,
+ status: c.status,
+ type: c.type,
+ }));
+};
+
+const getContainerData = (checks, vmid) => {
+ if (!vmid) return [];
+
+ return (checks || [])
+ .map((check) => {
+ const container = (check.containers || []).find(
+ (c) => String(c.vmid) === String(vmid)
+ );
+ if (!container) return null;
+ return {
+ _id: check._id,
+ cpuUsage: container.avgCpuUsage ?? 0,
+ memoryUsage: container.avgMemoryUsage ?? 0,
+ diskRead: container.deltaDiskRead ?? 0,
+ diskWrite: container.deltaDiskWrite ?? 0,
+ netIn: container.deltaNetIn ?? 0,
+ netOut: container.deltaNetOut ?? 0,
+ };
+ })
+ .filter(Boolean);
+};
+
+const ContainerStats = ({ containers, checks, isLoading, dateRange, setDateRange }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ const availableContainers = getAvailableContainers(containers);
+ const [selectedContainer, setSelectedContainer] = useState("");
+
+ useEffect(() => {
+ if (availableContainers.length > 0 && !selectedContainer) {
+ setSelectedContainer(availableContainers[0].vmid);
+ }
+ }, [availableContainers, selectedContainer]);
+
+ const containerData = getContainerData(checks, selectedContainer);
+ const currentContainer = containers?.find(
+ (c) => String(c.vmid) === String(selectedContainer)
+ );
+
+ if (!containers?.length) {
+ return (
+
+
+ {t("noContainersAvailable")}
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+ {availableContainers.length > 0 && (
+
+
+ >
+ );
+};
+
+ContainerStats.propTypes = {
+ containers: PropTypes.array,
+ checks: PropTypes.array,
+ isLoading: PropTypes.bool.isRequired,
+ dateRange: PropTypes.string.isRequired,
+ setDateRange: PropTypes.func.isRequired,
+};
+
+export default ContainerStats;
diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx
index 62a5c2642..8215760bf 100644
--- a/client/src/Pages/Infrastructure/Details/index.jsx
+++ b/client/src/Pages/Infrastructure/Details/index.jsx
@@ -8,6 +8,7 @@ import GaugeBoxes from "./Components/GaugeBoxes/index.jsx";
import AreaChartBoxes from "./Components/AreaChartBoxes/index.jsx";
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import NetworkStats from "./Components/NetworkStats/index.jsx";
+import ContainerStats from "./Components/ContainerStats/index.jsx";
import CustomTabList from "@/Components/v1/Tab/index.jsx";
import TabContext from "@mui/lab/TabContext";
@@ -102,6 +103,12 @@ const InfrastructureDetails = () => {
label={t("network")}
value="network"
/>
+ {monitor?.enableProxmoxContainers && (
+
+ )}
{tab === "details" && (
<>
@@ -134,6 +141,15 @@ const InfrastructureDetails = () => {
setDateRange={setDateRange}
/>
)}
+ {tab === "containers" && monitor?.enableProxmoxContainers && (
+
+ )}
);
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index 27d585074..64bcde7cb 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -450,6 +450,7 @@ const infrastructureMonitorValidation = joi.object({
}),
notifications: joi.array().items(joi.string()),
selectedDisks: joi.array().items(joi.string()).optional(),
+ enableProxmoxContainers: joi.boolean(),
});
const notificationValidation = joi.object({
diff --git a/client/src/locales/ar.json b/client/src/locales/ar.json
index 6cec253d8..aeac43bba 100644
--- a/client/src/locales/ar.json
+++ b/client/src/locales/ar.json
@@ -85,7 +85,9 @@
"networkError": "",
"fallback": {
"title": "",
- "checks": [""],
+ "checks": [
+ ""
+ ],
"actionButton": ""
},
"createButton": "",
@@ -930,7 +932,9 @@
"generalSettings": "",
"contents": "",
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -985,28 +989,36 @@
},
"infrastructureMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"maintenanceWindow": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"pageSpeed": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"uptimeMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -1104,5 +1116,8 @@
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
- "selectInterface": ""
+ "selectInterface": "",
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json
index 837b49803..f0bae30ff 100644
--- a/client/src/locales/cs.json
+++ b/client/src/locales/cs.json
@@ -85,7 +85,9 @@
"networkError": "",
"fallback": {
"title": "",
- "checks": [""],
+ "checks": [
+ ""
+ ],
"actionButton": ""
},
"createButton": "",
@@ -930,7 +932,9 @@
"generalSettings": "",
"contents": "",
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -985,28 +989,36 @@
},
"infrastructureMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"maintenanceWindow": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"pageSpeed": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"uptimeMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -1104,5 +1116,8 @@
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
- "selectInterface": ""
+ "selectInterface": "",
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/de.json b/client/src/locales/de.json
index fd3918378..0e758cc80 100644
--- a/client/src/locales/de.json
+++ b/client/src/locales/de.json
@@ -1136,5 +1136,8 @@
"disk_selection_info": "Momentan wurde keine Festplatte erkannt.",
"disk_selection_description": "Wählen Sie die spezifischen Festplatten aus, die Sie überwachen möchten."
}
- }
+ },
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 80d132a8d..a059632e4 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -181,6 +181,9 @@
"infrastructureServerUrlLabel": "Server URL",
"infrastructureDisplayNameLabel": "Display name",
"infrastructureAuthorizationSecretLabel": "Authorization secret",
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring",
"gb": "GB",
"mb": "MB",
"mem": "Mem",
diff --git a/client/src/locales/es.json b/client/src/locales/es.json
index ed6a98c5d..0083c77a5 100644
--- a/client/src/locales/es.json
+++ b/client/src/locales/es.json
@@ -85,7 +85,9 @@
"networkError": "",
"fallback": {
"title": "",
- "checks": [""],
+ "checks": [
+ ""
+ ],
"actionButton": ""
},
"createButton": "",
@@ -930,7 +932,9 @@
"generalSettings": "",
"contents": "",
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -985,28 +989,36 @@
},
"infrastructureMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"maintenanceWindow": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"pageSpeed": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"uptimeMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -1104,5 +1116,8 @@
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
- "selectInterface": ""
+ "selectInterface": "",
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json
index 4949817c7..d75f6f61e 100644
--- a/client/src/locales/fi.json
+++ b/client/src/locales/fi.json
@@ -85,7 +85,9 @@
"networkError": "",
"fallback": {
"title": "",
- "checks": [""],
+ "checks": [
+ ""
+ ],
"actionButton": ""
},
"createButton": "",
@@ -930,7 +932,9 @@
"generalSettings": "",
"contents": "",
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -985,28 +989,36 @@
},
"infrastructureMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"maintenanceWindow": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"pageSpeed": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"uptimeMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -1104,5 +1116,8 @@
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
- "selectInterface": ""
+ "selectInterface": "",
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json
index 9e76440f0..113c75c15 100644
--- a/client/src/locales/fr.json
+++ b/client/src/locales/fr.json
@@ -1136,5 +1136,8 @@
"disk_selection_info": "Aucun disque détecté pour le moment.",
"disk_selection_description": "Sélectionnez les disques spécifiques que vous souhaitez surveiller."
}
- }
+ },
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json
index 68d94139f..dcb6aadda 100644
--- a/client/src/locales/ja.json
+++ b/client/src/locales/ja.json
@@ -934,7 +934,9 @@
"generalSettings": "一般設定",
"contents": "コンテンツ",
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -989,28 +991,36 @@
},
"infrastructureMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"maintenanceWindow": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"pageSpeed": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"uptimeMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -1115,5 +1125,8 @@
"disk_selection_info": "現在ディスクが検出されていません。",
"disk_selection_description": "監視したい特定のディスクを選択してください。"
}
- }
+ },
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json
index 193c53788..cb350c72c 100644
--- a/client/src/locales/pt-BR.json
+++ b/client/src/locales/pt-BR.json
@@ -1136,5 +1136,8 @@
"disk_selection_info": "Nenhum disco detectado no momento.",
"disk_selection_description": "Selecione os discos específicos que você deseja monitorar."
}
- }
+ },
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json
index ab40ecb0d..8f216b95f 100644
--- a/client/src/locales/ru.json
+++ b/client/src/locales/ru.json
@@ -1136,5 +1136,8 @@
"disk_selection_info": "На данный момент диск не обнаружен.",
"disk_selection_description": "Выберите конкретные диски, которые вы хотите отслеживать."
}
- }
+ },
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/th.json b/client/src/locales/th.json
index d5f9ad137..464a40d9c 100644
--- a/client/src/locales/th.json
+++ b/client/src/locales/th.json
@@ -1136,5 +1136,8 @@
"disk_selection_info": "ไม่พบดิสก์ในขณะนี้",
"disk_selection_description": "เลือกดิสก์ที่คุณต้องการตรวจสอบ"
}
- }
+ },
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json
index 59f294533..62e933920 100644
--- a/client/src/locales/tr.json
+++ b/client/src/locales/tr.json
@@ -85,7 +85,9 @@
"networkError": "Ağ hatası oluştu",
"fallback": {
"title": "",
- "checks": [""],
+ "checks": [
+ ""
+ ],
"actionButton": ""
},
"createButton": "Bildirim kanalı oluştur",
@@ -930,7 +932,9 @@
"generalSettings": "Genel ayarlar",
"contents": "İçerikler",
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -985,28 +989,36 @@
},
"infrastructureMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"maintenanceWindow": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"pageSpeed": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
},
"uptimeMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": ""
}
@@ -1104,5 +1116,8 @@
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
- "selectInterface": ""
+ "selectInterface": "",
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/uk.json b/client/src/locales/uk.json
index 22b96487c..e70702bc9 100644
--- a/client/src/locales/uk.json
+++ b/client/src/locales/uk.json
@@ -86,7 +86,9 @@
"networkError": "",
"fallback": {
"title": "",
- "checks": [""]
+ "checks": [
+ ""
+ ]
},
"createButton": "",
"createTitle": "",
@@ -834,5 +836,8 @@
"selectEnabled": "",
"title": ""
}
- }
+ },
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/vi.json b/client/src/locales/vi.json
index 22b96487c..e70702bc9 100644
--- a/client/src/locales/vi.json
+++ b/client/src/locales/vi.json
@@ -86,7 +86,9 @@
"networkError": "",
"fallback": {
"title": "",
- "checks": [""]
+ "checks": [
+ ""
+ ]
},
"createButton": "",
"createTitle": "",
@@ -834,5 +836,8 @@
"selectEnabled": "",
"title": ""
}
- }
+ },
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/zh-CN.json b/client/src/locales/zh-CN.json
index 67b419445..3e2492b15 100644
--- a/client/src/locales/zh-CN.json
+++ b/client/src/locales/zh-CN.json
@@ -1004,14 +1004,22 @@
},
"maintenanceWindow": {
"fallback": {
- "checks": ["标记维护时段", "消除所有误解", "停止在维护窗口内发送警报"],
+ "checks": [
+ "标记维护时段",
+ "消除所有误解",
+ "停止在维护窗口内发送警报"
+ ],
"title": "维护时段用于:",
"actionButton": "让我们创建您的第一个维护时段!"
}
},
"pageSpeed": {
"fallback": {
- "checks": ["报告页面的用户体验", "帮助分析网页速度", "就如何改进页面提出建议"],
+ "checks": [
+ "报告页面的用户体验",
+ "帮助分析网页速度",
+ "就如何改进页面提出建议"
+ ],
"title": "PageSpeed 监视器用于:",
"actionButton": "让我们创建您的第一个 PageSpeed 监视器!"
}
@@ -1128,5 +1136,8 @@
"disk_selection_info": "目前没有检测到磁盘。",
"disk_selection_description": "选择您要监控的特定磁盘。"
}
- }
+ },
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json
index e5ee58e58..90ca7b8d1 100644
--- a/client/src/locales/zh-TW.json
+++ b/client/src/locales/zh-TW.json
@@ -934,7 +934,9 @@
"generalSettings": "",
"contents": "內容",
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": "讓我們建立您的第一個狀態頁面!"
}
@@ -989,28 +991,40 @@
},
"infrastructureMonitor": {
"fallback": {
- "checks": ["追蹤伺服器的效能", "識別瓶頸並優化資源使用", "透過即時監控確保可靠性"],
+ "checks": [
+ "追蹤伺服器的效能",
+ "識別瓶頸並優化資源使用",
+ "透過即時監控確保可靠性"
+ ],
"title": "",
"actionButton": "讓我們建立您的第一個基礎設施監視器!"
}
},
"maintenanceWindow": {
"fallback": {
- "checks": ["標記您的維護期間", "消除任何誤解", "在維護時段停止發送警報"],
+ "checks": [
+ "標記您的維護期間",
+ "消除任何誤解",
+ "在維護時段停止發送警報"
+ ],
"title": "",
"actionButton": "讓我們建立您的第一個維護時段!"
}
},
"pageSpeed": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": "讓我們建立您的第一個 PageSpeed 監視器!"
}
},
"uptimeMonitor": {
"fallback": {
- "checks": [""],
+ "checks": [
+ ""
+ ],
"title": "",
"actionButton": "讓我們建立您的第一個正常運行時間監視器!"
}
@@ -1108,5 +1122,8 @@
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
- "selectInterface": ""
+ "selectInterface": "",
+ "proxmoxContainerMonitoring": "Proxmox container monitoring",
+ "proxmoxContainerMonitoringDescription": "Enable monitoring of Proxmox LXC containers on this infrastructure node. Requires Capture agent to be configured with Proxmox API credentials.",
+ "enableProxmoxContainers": "Enable container monitoring"
}
diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts
index 94dfa873c..199ba2c99 100644
--- a/server/src/db/models/Check.ts
+++ b/server/src/db/models/Check.ts
@@ -4,6 +4,7 @@ import type {
Check,
CheckAudits,
CheckCaptureInfo,
+ CheckContainerInfo,
CheckCpuInfo,
CheckDiskInfo,
CheckErrorInfo,
@@ -143,6 +144,31 @@ const networkInterfaceSchema = new Schema(
{ _id: false }
);
+const containerSchema = new Schema(
+ {
+ vmid: { type: String, default: "" },
+ name: { type: String, default: "" },
+ node: { type: String, default: "" },
+ status: { type: String, default: "" },
+ type: { type: String, default: "lxc" },
+ uptime: { type: Number, default: 0 },
+ cpu_cores: { type: Number, default: 0 },
+ cpu_usage: { type: Number, default: 0 },
+ memory_used: { type: Number, default: 0 },
+ memory_total: { type: Number, default: 0 },
+ memory_usage: { type: Number, default: 0 },
+ swap_used: { type: Number, default: 0 },
+ swap_total: { type: Number, default: 0 },
+ disk_used: { type: Number, default: 0 },
+ disk_total: { type: Number, default: 0 },
+ disk_read: { type: Number, default: 0 },
+ disk_write: { type: Number, default: 0 },
+ network_in: { type: Number, default: 0 },
+ network_out: { type: Number, default: 0 },
+ },
+ { _id: false }
+);
+
const lighthouseAuditSchema = new Schema(
{
id: { type: String },
@@ -256,6 +282,10 @@ const CheckSchema = new Schema(
type: [networkInterfaceSchema],
default: () => [],
},
+ containers: {
+ type: [containerSchema],
+ default: () => [],
+ },
accessibility: {
type: Number,
},
diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts
index 17565a2e4..e8c3e7e8a 100644
--- a/server/src/db/models/Monitor.ts
+++ b/server/src/db/models/Monitor.ts
@@ -80,6 +80,10 @@ const MonitorSchema = new Schema(
type: Boolean,
default: false,
},
+ enableProxmoxContainers: {
+ type: Boolean,
+ default: false,
+ },
jsonPath: {
type: String,
},
diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts
index 7707a7b17..e7b55a606 100644
--- a/server/src/repositories/checks/MongoChecksRepistory.ts
+++ b/server/src/repositories/checks/MongoChecksRepistory.ts
@@ -3,6 +3,7 @@ import type {
Check,
CheckAudits,
CheckCaptureInfo,
+ CheckContainerInfo,
CheckCpuInfo,
CheckDiskInfo,
CheckErrorInfo,
@@ -137,6 +138,29 @@ class MongoChecksRepository implements IChecksRepository {
fifo_out: iface?.fifo_out ?? 0,
}));
+ const mapContainers = (containers?: CheckContainerInfo[]): CheckContainerInfo[] =>
+ (containers ?? []).map((container) => ({
+ vmid: container?.vmid ?? "",
+ name: container?.name ?? "",
+ node: container?.node ?? "",
+ status: container?.status ?? "",
+ type: container?.type ?? "lxc",
+ uptime: container?.uptime ?? 0,
+ cpu_cores: container?.cpu_cores ?? 0,
+ cpu_usage: container?.cpu_usage ?? 0,
+ memory_used: container?.memory_used ?? 0,
+ memory_total: container?.memory_total ?? 0,
+ memory_usage: container?.memory_usage ?? 0,
+ swap_used: container?.swap_used ?? 0,
+ swap_total: container?.swap_total ?? 0,
+ disk_used: container?.disk_used ?? 0,
+ disk_total: container?.disk_total ?? 0,
+ disk_read: container?.disk_read ?? 0,
+ disk_write: container?.disk_write ?? 0,
+ network_in: container?.network_in ?? 0,
+ network_out: container?.network_out ?? 0,
+ }));
+
const mapAudits = (audits?: CheckAudits): CheckAudits | undefined => {
if (!audits) {
return undefined;
@@ -174,6 +198,7 @@ class MongoChecksRepository implements IChecksRepository {
errors: mapErrors(doc.errors),
capture: mapCapture(doc.capture),
net: mapNet(doc.net),
+ containers: mapContainers(doc.containers),
accessibility: doc.accessibility,
bestPractices: doc.bestPractices,
seo: doc.seo,
@@ -374,6 +399,24 @@ class MongoChecksRepository implements IChecksRepository {
deltaFifoIn: iface?.deltaFifoIn ?? 0,
deltaFifoOut: iface?.deltaFifoOut ?? 0,
})),
+ containers: (metric.containers ?? []).map((container: { [key: string]: number | string | undefined }) => ({
+ vmid: container?.vmid ?? "",
+ name: container?.name ?? "",
+ node: container?.node ?? "",
+ status: container?.status ?? "",
+ type: container?.type ?? "",
+ cpu_cores: container?.cpu_cores ?? 0,
+ avgCpuUsage: container?.avgCpuUsage ?? 0,
+ avgMemoryUsage: container?.avgMemoryUsage ?? 0,
+ memoryTotal: container?.memoryTotal ?? 0,
+ swapTotal: container?.swapTotal ?? 0,
+ diskTotal: container?.diskTotal ?? 0,
+ diskUsed: container?.diskUsed ?? 0,
+ deltaDiskRead: container?.deltaDiskRead ?? 0,
+ deltaDiskWrite: container?.deltaDiskWrite ?? 0,
+ deltaNetIn: container?.deltaNetIn ?? 0,
+ deltaNetOut: container?.deltaNetOut ?? 0,
+ })),
}));
return {
@@ -451,6 +494,7 @@ class MongoChecksRepository implements IChecksRepository {
avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } },
disks: { $push: "$disk" },
net: { $push: "$net" },
+ containers: { $push: "$containers" },
updatedAts: { $push: "$updatedAt" },
sampleDoc: { $first: "$$ROOT" },
},
@@ -570,6 +614,66 @@ class MongoChecksRepository implements IChecksRepository {
},
},
},
+ containers: {
+ $map: {
+ input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.containers", []] } }] },
+ as: "cIdx",
+ in: {
+ vmid: { $arrayElemAt: ["$sampleDoc.containers.vmid", "$$cIdx"] },
+ name: { $arrayElemAt: ["$sampleDoc.containers.name", "$$cIdx"] },
+ node: { $arrayElemAt: ["$sampleDoc.containers.node", "$$cIdx"] },
+ status: { $arrayElemAt: ["$sampleDoc.containers.status", "$$cIdx"] },
+ type: { $arrayElemAt: ["$sampleDoc.containers.type", "$$cIdx"] },
+ cpu_cores: { $arrayElemAt: ["$sampleDoc.containers.cpu_cores", "$$cIdx"] },
+ avgCpuUsage: { $avg: { $map: { input: "$containers", as: "cA", in: { $arrayElemAt: ["$$cA.cpu_usage", "$$cIdx"] } } } },
+ avgMemoryUsage: { $avg: { $map: { input: "$containers", as: "cA", in: { $arrayElemAt: ["$$cA.memory_usage", "$$cIdx"] } } } },
+ memoryTotal: { $arrayElemAt: ["$sampleDoc.containers.memory_total", "$$cIdx"] },
+ swapTotal: { $arrayElemAt: ["$sampleDoc.containers.swap_total", "$$cIdx"] },
+ diskTotal: { $arrayElemAt: ["$sampleDoc.containers.disk_total", "$$cIdx"] },
+ diskUsed: { $avg: { $map: { input: "$containers", as: "cA", in: { $arrayElemAt: ["$$cA.disk_used", "$$cIdx"] } } } },
+ deltaDiskRead: {
+ $let: {
+ vars: {
+ tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
+ f: { $arrayElemAt: [{ $map: { input: { $first: "$containers" }, as: "c", in: "$$c.disk_read" } }, "$$cIdx"] },
+ l: { $arrayElemAt: [{ $map: { input: { $last: "$containers" }, as: "c", in: "$$c.disk_read" } }, "$$cIdx"] },
+ },
+ in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
+ },
+ },
+ deltaDiskWrite: {
+ $let: {
+ vars: {
+ tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
+ f: { $arrayElemAt: [{ $map: { input: { $first: "$containers" }, as: "c", in: "$$c.disk_write" } }, "$$cIdx"] },
+ l: { $arrayElemAt: [{ $map: { input: { $last: "$containers" }, as: "c", in: "$$c.disk_write" } }, "$$cIdx"] },
+ },
+ in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
+ },
+ },
+ deltaNetIn: {
+ $let: {
+ vars: {
+ tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
+ f: { $arrayElemAt: [{ $map: { input: { $first: "$containers" }, as: "c", in: "$$c.network_in" } }, "$$cIdx"] },
+ l: { $arrayElemAt: [{ $map: { input: { $last: "$containers" }, as: "c", in: "$$c.network_in" } }, "$$cIdx"] },
+ },
+ in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
+ },
+ },
+ deltaNetOut: {
+ $let: {
+ vars: {
+ tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
+ f: { $arrayElemAt: [{ $map: { input: { $first: "$containers" }, as: "c", in: "$$c.network_out" } }, "$$cIdx"] },
+ l: { $arrayElemAt: [{ $map: { input: { $last: "$containers" }, as: "c", in: "$$c.network_out" } }, "$$cIdx"] },
+ },
+ in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
+ },
+ },
+ },
+ },
+ },
},
},
{ $sort: { _id: 1 } },
diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts
index 878d6f7cf..2f21ec1f2 100644
--- a/server/src/repositories/monitors/MongoMonitorsRepository.ts
+++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts
@@ -225,6 +225,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
statusWindowThreshold: doc.statusWindowThreshold,
type: doc.type,
ignoreTlsErrors: doc.ignoreTlsErrors,
+ enableProxmoxContainers: doc.enableProxmoxContainers,
jsonPath: doc.jsonPath ?? undefined,
expectedValue: doc.expectedValue ?? undefined,
matchMethod: doc.matchMethod ?? undefined,
diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts
index 3e6c727b5..4eb372310 100644
--- a/server/src/service/business/checkService.ts
+++ b/server/src/service/business/checkService.ts
@@ -105,7 +105,7 @@ class CheckService {
if (type === "hardware") {
const hardwarePayload = payload as HardwareStatusPayload | undefined;
- const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {};
+ const { cpu, memory, disk, host, net, containers } = hardwarePayload?.data ?? {};
const errorsSource = Array.isArray(hardwarePayload?.errors)
? hardwarePayload?.errors
: (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors;
@@ -116,6 +116,7 @@ class CheckService {
check.errors = errorsSource;
check.capture = hardwarePayload?.capture;
check.net = net;
+ check.containers = containers;
}
return check;
};
diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts
index e4e5972af..6d119c001 100644
--- a/server/src/service/infrastructure/networkService.ts
+++ b/server/src/service/infrastructure/networkService.ts
@@ -304,7 +304,8 @@ class NetworkService implements INetworkService {
Object.assign(httpResponse, {
code: response.statusCode,
- status: response.ok,
+ // 207 Multi-Status is used by Capture when some metrics fail - still a success
+ status: response.ok || response.statusCode === 207,
message: response.statusMessage ?? "",
responseTime: response.timings.phases.total || 0,
payload,
@@ -424,7 +425,112 @@ class NetworkService implements INetworkService {
private async requestHardware(monitor: Monitor): Promise {
try {
- return await this.requestHttp(monitor);
+ // Fetch main metrics
+ this.logger.info({
+ message: "Fetching hardware metrics",
+ service: this.SERVICE_NAME,
+ method: "requestHardware",
+ details: { url: monitor.url },
+ });
+ const mainResponse = await this.requestHttp(monitor);
+
+ // If main request failed, return early
+ if (!mainResponse.status) {
+ this.logger.warn({
+ message: "Main metrics request failed",
+ service: this.SERVICE_NAME,
+ method: "requestHardware",
+ details: { url: monitor.url, code: mainResponse.code, msg: mainResponse.message },
+ });
+ return mainResponse;
+ }
+
+ // Only fetch Proxmox metrics if enabled for this monitor
+ if (monitor?.enableProxmoxContainers === true) {
+ try {
+ const proxmoxUrl = this.buildProxmoxUrl(monitor.url);
+ this.logger.info({
+ message: "Attempting to fetch Proxmox metrics",
+ service: this.SERVICE_NAME,
+ method: "requestHardware",
+ details: { proxmoxUrl },
+ });
+ if (proxmoxUrl) {
+ const proxmoxResponse = await this.requestHttp({
+ ...monitor,
+ url: proxmoxUrl,
+ });
+
+ // Proxmox response structure: { data: [...], errors: [...] | null, capture: {...} }
+ const proxmoxPayload = proxmoxResponse.payload as {
+ data?: unknown[] | null;
+ errors?: Array<{ metric: string[]; err: string }> | null;
+ };
+ const mainPayload = mainResponse.payload as {
+ data?: Record;
+ errors?: Array<{ metric: string[]; err: string }>;
+ };
+
+ // Handle based on Capture's response scenarios:
+ // 1. Not configured: data=[], errors=null -> log and skip
+ // 2. Connection failed: data=null, errors=[...] -> merge errors
+ // 3. Partial failure: data=[...], errors=[...] -> merge both
+ // 4. Success: data=[...], errors=null -> merge data
+
+ if (proxmoxResponse.status && mainPayload?.data && typeof mainPayload.data === "object") {
+ // Merge container data if available
+ if (proxmoxPayload?.data && Array.isArray(proxmoxPayload.data)) {
+ if (proxmoxPayload.data.length > 0) {
+ (mainPayload.data as Record).containers = proxmoxPayload.data;
+ this.logger.info({
+ message: "Successfully merged Proxmox container data",
+ service: this.SERVICE_NAME,
+ method: "requestHardware",
+ details: { containerCount: proxmoxPayload.data.length },
+ });
+ } else {
+ this.logger.debug({
+ message: "Proxmox returned empty container list (not configured or no containers running)",
+ service: this.SERVICE_NAME,
+ method: "requestHardware",
+ });
+ }
+ }
+
+ // Merge Proxmox errors into main errors array
+ if (proxmoxPayload?.errors && Array.isArray(proxmoxPayload.errors) && proxmoxPayload.errors.length > 0) {
+ if (!mainPayload.errors) {
+ mainPayload.errors = [];
+ }
+ mainPayload.errors.push(...proxmoxPayload.errors);
+ this.logger.warn({
+ message: "Proxmox returned errors",
+ service: this.SERVICE_NAME,
+ method: "requestHardware",
+ details: { errors: proxmoxPayload.errors },
+ });
+ }
+ } else if (!proxmoxResponse.status) {
+ this.logger.warn({
+ message: "Proxmox request failed",
+ service: this.SERVICE_NAME,
+ method: "requestHardware",
+ details: { status: proxmoxResponse.status, code: proxmoxResponse.code },
+ });
+ }
+ }
+ } catch (proxmoxErr: any) {
+ // Log but don't fail - Proxmox is optional
+ this.logger.warn({
+ message: "Proxmox fetch error (optional)",
+ service: this.SERVICE_NAME,
+ method: "requestHardware",
+ details: { error: proxmoxErr.message },
+ });
+ }
+ }
+
+ return mainResponse;
} catch (err: any) {
err.service = this.SERVICE_NAME;
err.method = "requestHardware";
@@ -432,6 +538,22 @@ class NetworkService implements INetworkService {
}
}
+ /**
+ * Builds the Proxmox metrics URL from the main metrics URL
+ * e.g., http://localhost:59232/api/v1/metrics -> http://localhost:59232/api/v1/metrics/proxmox
+ */
+ private buildProxmoxUrl(baseUrl: string | undefined): string | null {
+ if (!baseUrl) return null;
+ try {
+ const url = new URL(baseUrl);
+ // Append /proxmox to the path
+ url.pathname = url.pathname.replace(/\/?$/, "/proxmox");
+ return url.toString();
+ } catch {
+ return null;
+ }
+ }
+
private async requestDocker(monitor: Monitor): Promise {
try {
if (!monitor.url) {
diff --git a/server/src/types/check.ts b/server/src/types/check.ts
index 96c6f6e7f..5b6d7b667 100644
--- a/server/src/types/check.ts
+++ b/server/src/types/check.ts
@@ -64,6 +64,28 @@ export interface CheckNetworkInterfaceInfo {
fifo_out: number;
}
+export interface CheckContainerInfo {
+ vmid: string;
+ name: string;
+ node: string;
+ status: string;
+ type: string;
+ uptime: number;
+ cpu_cores: number;
+ cpu_usage: number;
+ memory_used: number;
+ memory_total: number;
+ memory_usage: number;
+ swap_used: number;
+ swap_total: number;
+ disk_used: number;
+ disk_total: number;
+ disk_read: number;
+ disk_write: number;
+ network_in: number;
+ network_out: number;
+}
+
export interface CheckAudits {
cls?: ILighthouseAudit;
si?: ILighthouseAudit;
@@ -99,6 +121,7 @@ export interface Check {
errors?: CheckErrorInfo[];
capture?: CheckCaptureInfo;
net?: CheckNetworkInterfaceInfo[];
+ containers?: CheckContainerInfo[];
accessibility?: number;
bestPractices?: number;
seo?: number;
diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts
index dc3bd8c09..6b0fbbc6d 100644
--- a/server/src/types/monitor.ts
+++ b/server/src/types/monitor.ts
@@ -22,6 +22,7 @@ export interface Monitor {
statusWindowThreshold: number;
type: MonitorType;
ignoreTlsErrors: boolean;
+ enableProxmoxContainers: boolean;
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
diff --git a/server/src/types/network.ts b/server/src/types/network.ts
index 10f89842f..57ffec6fa 100644
--- a/server/src/types/network.ts
+++ b/server/src/types/network.ts
@@ -1,5 +1,6 @@
import type {
CheckCaptureInfo,
+ CheckContainerInfo,
CheckCpuInfo,
CheckDiskInfo,
CheckErrorInfo,
@@ -75,6 +76,7 @@ export interface HardwareStatusMetrics {
disk?: CheckDiskInfo[];
host?: CheckHostInfo;
net?: CheckNetworkInterfaceInfo[];
+ containers?: CheckContainerInfo[];
}
export interface HardwareStatusPayload {
diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js
index 8533b4e44..6f233d7a9 100755
--- a/server/src/validation/joi.js
+++ b/server/src/validation/joi.js
@@ -158,6 +158,7 @@ const createMonitorBodyValidation = joi.object({
statusWindowThreshold: joi.number().min(1).max(100).default(60),
url: joi.string().required(),
ignoreTlsErrors: joi.boolean().default(false),
+ enableProxmoxContainers: joi.boolean().default(false),
port: joi.number(),
isActive: joi.boolean(),
interval: joi.number(),
@@ -193,6 +194,7 @@ const editMonitorBodyValidation = joi.object({
notifications: joi.array().items(joi.string()),
secret: joi.string(),
ignoreTlsErrors: joi.boolean(),
+ enableProxmoxContainers: joi.boolean(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string().allow(null, ""),