diff --git a/client/src/Components/v2/design-elements/Tabs.tsx b/client/src/Components/v2/design-elements/Tabs.tsx
new file mode 100644
index 000000000..149c0bd93
--- /dev/null
+++ b/client/src/Components/v2/design-elements/Tabs.tsx
@@ -0,0 +1,64 @@
+import MuiTabs from "@mui/material/Tabs";
+import type { TabsProps } from "@mui/material/Tabs";
+import { useTheme } from "@mui/material/styles";
+interface CustomTabsProps extends TabsProps {}
+
+export const Tabs = (props: CustomTabsProps) => {
+ const theme = useTheme();
+ return (
+
+ {props.children}
+
+ );
+};
+
+import MuiTab from "@mui/material/Tab";
+import type { TabProps } from "@mui/material/Tab";
+interface CustomTabProps extends TabProps {}
+
+export const Tab = (props: CustomTabProps) => {
+ const theme = useTheme();
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx
index 000dab1b2..33248a345 100644
--- a/client/src/Components/v2/design-elements/index.tsx
+++ b/client/src/Components/v2/design-elements/index.tsx
@@ -16,3 +16,4 @@ export * from "./Tooltip";
export * from "./StatBox";
export * from "./BaseChart";
export * from "./Gauge";
+export * from "./Tabs";
diff --git a/client/src/Components/v2/monitors/charts/HistogramInfrastructure.tsx b/client/src/Components/v2/monitors/charts/HistogramInfrastructure.tsx
new file mode 100644
index 000000000..7ef82ba0c
--- /dev/null
+++ b/client/src/Components/v2/monitors/charts/HistogramInfrastructure.tsx
@@ -0,0 +1,206 @@
+import { BaseChart } from "@/Components/v2/design-elements";
+import {
+ AreaChart,
+ Area,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ ResponsiveContainer,
+} from "recharts";
+import { Fragment, useId } from "react";
+import { XTick } from "@/Components/v2/monitors";
+
+import { useTheme } from "@mui/material/styles";
+import type { HardwareCheckStats } from "@/Types/Monitor";
+
+const AREA_COLORS = [
+ // Blues
+ "#3182bd", // Deep blue
+ "#6baed6", // Medium blue
+ "#9ecae1", // Light blue
+
+ // Greens
+ "#74c476", // Soft green
+ "#a1d99b", // Light green
+ "#c7e9c0", // Pale green
+
+ // Oranges
+ "#fdae6b", // Warm orange
+ "#fdd0a2", // Light orange
+ "#feedde", // Pale orange
+
+ // Purples
+ "#9467bd", // Lavender
+ "#a55194", // Deep magenta
+ "#c994c7", // Soft magenta
+
+ // Reds
+ "#ff9896", // Soft red
+ "#de2d26", // Deep red
+ "#fc9272", // Medium red
+
+ // Cyans/Teals
+ "#17becf", // Cyan
+ "#7fcdbb", // Teal
+ "#a1dab4", // Light teal
+
+ // Yellows
+ "#fec44f", // Mustard
+ "#fee391", // Light yellow
+ "#ffffd4", // Pale yellow
+
+ // Additional colors
+ "#e377c2", // Soft pink
+ "#bcbd22", // Olive
+ "#2ca02c", // Vibrant green
+];
+
+const createGradient = ({
+ id,
+ startColor,
+ endColor,
+ startOpacity = 0.8,
+ endOpacity = 0,
+ direction = "vertical",
+}: {
+ id: string;
+ startColor: string;
+ endColor: string;
+ startOpacity?: number;
+ endOpacity?: number;
+ direction?: "vertical" | "horizontal";
+}) => (
+
+
+
+
+
+
+);
+
+export const HistogramInfrastructure = ({
+ dateRange,
+ title,
+ type,
+ idx: _idx,
+ checks,
+ xKey,
+ yDomain,
+ dataKeys,
+ gradient = false,
+ gradientDirection = "vertical",
+ gradientStartColor,
+ gradientEndColor,
+ strokeColor,
+ fillColor,
+ yAxisFormatter,
+}: {
+ dateRange: string;
+ title: string;
+ type: string;
+ idx: number | null;
+ checks: HardwareCheckStats[];
+ xKey: string;
+ yDomain?: number[];
+ dataKeys: string[];
+ gradient?: boolean;
+ gradientDirection?: "vertical" | "horizontal";
+ gradientStartColor?: string;
+ gradientEndColor?: string;
+ strokeColor: string;
+ fillColor?: string;
+ yAxisFormatter?: (value: number) => string;
+}) => {
+ const theme = useTheme();
+ const uniqueId = useId();
+ let data = checks.reverse();
+
+ let avgTemps: { bucketDate: string; avg_temp: number | null }[] = [];
+ let tempYDomain: number[] = [];
+ if (type === "temp") {
+ avgTemps = data.map((check) => {
+ const temps = check.avgTemperature || [];
+ if (temps.length === 0) return { bucketDate: check.bucketDate, avg_temp: null };
+ const totalTemp = temps.reduce((sum, temp) => sum + (temp || 0), 0);
+ const avgTemp = totalTemp / temps.length;
+ return { bucketDate: check.bucketDate, avg_temp: avgTemp };
+ });
+
+ const maxTemp: number = avgTemps.reduce((max, item) => {
+ return item.avg_temp && item.avg_temp > max ? item.avg_temp : max;
+ }, 0);
+
+ tempYDomain = [0, Math.ceil((maxTemp * 1.3) / 10) * 10];
+ }
+
+ return (
+
+
+
+ (
+
+ )}
+ />
+
+
+
+ {dataKeys?.map((dataKey, index) => {
+ const gradientId = `gradient-${uniqueId}-${index}`;
+ return (
+
+ {gradient === true &&
+ createGradient({
+ id: gradientId,
+ startColor: gradientStartColor || AREA_COLORS[index],
+ endColor: gradientEndColor || "transparent",
+ direction: gradientDirection,
+ })}
+
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx
index 4efaae65c..ce90f1b8c 100644
--- a/client/src/Components/v2/monitors/index.tsx
+++ b/client/src/Components/v2/monitors/index.tsx
@@ -9,3 +9,4 @@ export * from "./charts/PiePageSpeed";
export * from "./charts/PiePageSpeedLegend";
export * from "./charts/HistogramPageSpeedDetails";
export * from "./charts/HistogramPageSpeedDetailsTooltip";
+export * from "./charts/HistogramInfrastructure";
diff --git a/client/src/Pages/Infrastructure/Details/Components/Charts.tsx b/client/src/Pages/Infrastructure/Details/Components/Charts.tsx
new file mode 100644
index 000000000..a293c310e
--- /dev/null
+++ b/client/src/Pages/Infrastructure/Details/Components/Charts.tsx
@@ -0,0 +1,104 @@
+import Grid from "@mui/material/Grid2";
+import { HistogramInfrastructure } from "@/Components/v2/monitors";
+
+import { useTranslation } from "react-i18next";
+import type { HardwareCheckStats } from "@/Types/Monitor";
+import { useMemo } from "react";
+import { useTheme } from "@mui/material/styles";
+import useMediaQuery from "@mui/material/useMediaQuery";
+const formatPercent = (value: number) => `${(value * 100).toFixed(0)}%`;
+const formatTemp = (value: number) => `${value.toFixed(0)}°C`;
+
+const getChartConfigs = (theme: any, checks: HardwareCheckStats[], t: any) => {
+ return [
+ {
+ title: t("pages.infrastructure.charts.labels.memory"),
+ type: "memory",
+ dataKeys: ["avgMemoryUsage"],
+ strokeColor: theme.palette.primary.main,
+ gradientStartColor: theme.palette.primary.main,
+ yDomain: [0, 1],
+ yAxisFormatter: formatPercent,
+ idx: null,
+ },
+ {
+ title: t("pages.infrastructure.charts.labels.cpu"),
+ type: "cpu",
+ dataKeys: ["avgCpuUsage"],
+ strokeColor: theme.palette.success.main,
+ gradientStartColor: theme.palette.success.main,
+ yDomain: [0, 1],
+ yAxisFormatter: formatPercent,
+ idx: null,
+ },
+ {
+ title: t("pages.infrastructure.charts.labels.temp"),
+ type: "temp",
+ dataKeys: ["avg_temp"],
+ strokeColor: theme.palette.error.main,
+ gradientStartColor: theme.palette.error.main,
+ yDomain: [0, 150],
+ yAxisFormatter: formatTemp,
+ idx: null,
+ },
+ ...(checks[0]?.disks?.map((_, idx) => ({
+ title: t("pages.infrastructure.charts.labels.disk", { idx }),
+ type: "disk",
+ dataKeys: [`disks[${idx}].usagePercent`],
+ strokeColor: theme.palette.warning.main,
+ gradientStartColor: theme.palette.warning.main,
+ yDomain: [0, 1],
+ yAxisFormatter: formatPercent,
+ idx,
+ })) || []),
+ ];
+};
+
+export const InfraDetailsCharts = ({
+ checks,
+ dateRange,
+}: {
+ checks: HardwareCheckStats[];
+ dateRange: string;
+}) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ const { t } = useTranslation();
+ const chartConfigs = useMemo(
+ () => getChartConfigs(theme, checks, t),
+ [theme, checks, t]
+ );
+
+ return (
+
+ {chartConfigs.map((config) => {
+ return (
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx b/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx
new file mode 100644
index 000000000..a911e00e3
--- /dev/null
+++ b/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx
@@ -0,0 +1,106 @@
+import Grid from "@mui/material/Grid2";
+import { HistogramInfrastructure } from "@/Components/v2/monitors";
+
+import { useTranslation } from "react-i18next";
+import type { HardwareCheckStats } from "@/Types/Monitor";
+import { useMemo } from "react";
+import { useTheme } from "@mui/material/styles";
+import useMediaQuery from "@mui/material/useMediaQuery";
+
+const formatBytesToMB = (value: number) => `${(value / (1024 * 1024)).toFixed(2)} MB`;
+
+interface ChartConfig {
+ title: string;
+ type: string;
+ dataKeys: string[];
+ strokeColor: string;
+ gradientStartColor: string;
+ idx: number | null;
+ interfaceName?: string;
+}
+
+const getChartConfigs = (
+ theme: any,
+ checks: HardwareCheckStats[],
+ t: any
+): ChartConfig[] => {
+ const configs: ChartConfig[] = [];
+
+ const netInterfaces = checks[0]?.net || [];
+
+ netInterfaces.forEach((iface, idx) => {
+ configs.push(
+ {
+ title: t("pages.infrastructure.charts.labels.netBytesSent", {
+ name: iface.name,
+ }),
+ type: "netBytesSent",
+ dataKeys: [`net[${idx}].bytesSentPerSecond`],
+ strokeColor: theme.palette.primary.main,
+ gradientStartColor: theme.palette.primary.main,
+ idx,
+ interfaceName: iface.name,
+ },
+ {
+ title: t("pages.infrastructure.charts.labels.netBytesRecv", {
+ name: iface.name,
+ }),
+ type: "netBytesRecv",
+ dataKeys: [`net[${idx}].deltaBytesRecv`],
+ strokeColor: theme.palette.success.main,
+ gradientStartColor: theme.palette.success.main,
+ idx,
+ interfaceName: iface.name,
+ }
+ );
+ });
+
+ return configs;
+};
+
+export const InfraNetworkCharts = ({
+ checks,
+ dateRange,
+}: {
+ checks: HardwareCheckStats[];
+ dateRange: string;
+}) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ const { t } = useTranslation();
+ const chartConfigs = useMemo(
+ () => getChartConfigs(theme, checks, t),
+ [theme, checks, t]
+ );
+
+ return (
+
+ {chartConfigs.map((config) => {
+ return (
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx b/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx
new file mode 100644
index 000000000..a72530db4
--- /dev/null
+++ b/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx
@@ -0,0 +1,109 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { BaseChart, Gauge } from "@/Components/v2/design-elements";
+
+import { useTranslation } from "react-i18next";
+import { getGbs, getFrequency } from "@/Utils/InfraUtils";
+import { useTheme } from "@mui/material";
+import useMediaQuery from "@mui/material/useMediaQuery";
+import type { CheckSnapshot } from "@/Types/Check";
+
+const InfraDetailGauge = ({
+ title,
+ progress,
+ upperLabel,
+ upperValue,
+ lowerLabel,
+ lowerValue,
+}: {
+ title: string;
+ progress: number;
+ upperLabel?: string;
+ upperValue?: string | number;
+ lowerLabel?: string;
+ lowerValue?: string | number;
+}) => {
+ const theme = useTheme();
+ return (
+
+
+
+
+
+ {upperLabel}
+ {upperValue}
+
+
+ {lowerLabel}
+ {lowerValue}
+
+
+ );
+};
+
+export const InfraDetailsGauges = ({
+ snapshot,
+}: {
+ snapshot: CheckSnapshot | undefined;
+}) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ if (!snapshot) {
+ return null;
+ }
+
+ return (
+
+
+
+ {snapshot?.disk?.map((disk, idx) => {
+ return (
+
+ );
+ })}
+
+ );
+};
diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx
new file mode 100644
index 000000000..7b1d80c40
--- /dev/null
+++ b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx
@@ -0,0 +1,73 @@
+import Stack from "@mui/material/Stack";
+import { StatBox } from "@/Components/v2/design-elements";
+
+import { useTheme } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import type { Monitor } from "@/Types/Monitor";
+import {
+ getAvgTemp,
+ getCores,
+ getFrequency,
+ getGbs,
+ getDiskTotalGbs,
+ getOsAndPlatform,
+} from "@/Utils/InfraUtils";
+
+export const StatusBoxes = ({ monitor }: { monitor: Monitor }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ const latestCheck = monitor?.recentChecks?.[0];
+ // Get data from latest check
+ const physicalCores = getCores(latestCheck?.cpu?.physical_core);
+ const logicalCores = getCores(latestCheck?.cpu?.logical_core);
+ const cpuFrequency = getFrequency(latestCheck?.cpu?.frequency);
+ const cpuTemps = latestCheck?.cpu?.temperature ?? [];
+ const cpuTemperature = getAvgTemp(cpuTemps);
+ const memoryTotalBytes = getGbs(latestCheck?.memory?.total_bytes);
+ const diskTotalBytes = getDiskTotalGbs(latestCheck?.disk);
+ const os = getOsAndPlatform(latestCheck?.host);
+
+ const platform = latestCheck?.host?.platform ?? undefined;
+ const osPlatform =
+ typeof os === "undefined" && typeof platform === "undefined"
+ ? undefined
+ : `${os} ${platform}`;
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Infrastructure/Details/Components/TabNetwork.tsx b/client/src/Pages/Infrastructure/Details/Components/TabNetwork.tsx
new file mode 100644
index 000000000..612040cb1
--- /dev/null
+++ b/client/src/Pages/Infrastructure/Details/Components/TabNetwork.tsx
@@ -0,0 +1,29 @@
+import Stack from "@mui/material/Stack";
+import { InfraNetworkCharts } from "./ChartsNetwork";
+
+import { useTheme } from "@mui/material";
+import type { HardwareStats } from "@/Types/Monitor";
+
+export const TabNetwork = ({
+ stats,
+ dateRange,
+}: {
+ stats: HardwareStats | undefined;
+ dateRange: string;
+}) => {
+ const theme = useTheme();
+
+ if (!stats) {
+ return null;
+ }
+
+ const checks = stats?.checks || [];
+ return (
+
+
+
+ );
+};
diff --git a/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx b/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx
new file mode 100644
index 000000000..447a2cce6
--- /dev/null
+++ b/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx
@@ -0,0 +1,34 @@
+import Stack from "@mui/material/Stack";
+import { InfraDetailsGauges } from "@/Pages/Infrastructure/Details/Components/Gauges";
+import { StatusBoxes } from "@/Pages/Infrastructure/Details/Components/StatusBoxes";
+import { InfraDetailsCharts } from "@/Pages/Infrastructure/Details/Components/Charts";
+
+import { useTheme } from "@mui/material";
+import type { HardwareStats, Monitor } from "@/Types/Monitor";
+
+export const TabOverview = ({
+ monitor,
+ stats,
+ dateRange,
+}: {
+ monitor: Monitor | undefined;
+ stats: HardwareStats | undefined;
+ dateRange: string;
+}) => {
+ const theme = useTheme();
+ if (!monitor) {
+ return null;
+ }
+
+ const checks = stats?.checks || [];
+ return (
+
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Infrastructure/Details/index.tsx b/client/src/Pages/Infrastructure/Details/index.tsx
new file mode 100644
index 000000000..42be78a63
--- /dev/null
+++ b/client/src/Pages/Infrastructure/Details/index.tsx
@@ -0,0 +1,90 @@
+import { BasePage, Tab, Tabs } from "@/Components/v2/design-elements";
+import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common";
+import { MonitorStatBoxes } from "@/Components/v2/monitors";
+import { TabNetwork } from "@/Pages/Infrastructure/Details/Components/TabNetwork";
+import { TabOverview } from "@/Pages/Infrastructure/Details/Components/TabOverview";
+
+import { useMemo, useState } from "react";
+import { useParams } from "react-router-dom";
+import { useGet } from "@/Hooks/UseApi";
+import type { HardwareDetailsResponse } from "@/Types/Monitor";
+import { useIsAdmin } from "@/Hooks/useIsAdmin";
+import { useTranslation } from "react-i18next";
+
+const InfrastructureDetails = () => {
+ const { t } = useTranslation();
+ const isAdmin = useIsAdmin();
+
+ const { monitorId } = useParams<{ monitorId: string }>();
+
+ const [dateRange, setDateRange] = useState("recent");
+ const [selectedTab, setSelectedTab] = useState(0);
+
+ const monitorDetailsUrl = useMemo(() => {
+ if (!monitorId) {
+ return null;
+ }
+ const params = new URLSearchParams();
+ params.append("dateRange", dateRange);
+ return `/monitors/hardware/details/${monitorId}?${params.toString()}`;
+ }, [monitorId, dateRange]);
+
+ const {
+ data: monitorDetailsData,
+ isLoading: monitorIsLoading,
+ refetch: refetchMonitor,
+ } = useGet(
+ monitorDetailsUrl,
+ {},
+ { refreshInterval: 10000, keepPreviousData: true }
+ );
+
+ const monitor = monitorDetailsData?.monitor;
+ const monitorStats = monitorDetailsData?.monitorStats ?? null;
+ const stats = monitorDetailsData?.stats;
+
+ return (
+
+
+
+
+ {
+ setSelectedTab(value);
+ }}
+ >
+
+
+
+ {selectedTab === 0 && (
+
+ )}
+ {selectedTab === 1 && (
+
+ )}
+
+ );
+};
+
+export default InfrastructureDetails;
diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/old.jsx
similarity index 100%
rename from client/src/Pages/Infrastructure/Details/index.jsx
rename to client/src/Pages/Infrastructure/Details/old.jsx
diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx
index 1a6177ddc..f32ae1989 100644
--- a/client/src/Pages/Uptime/Details/index.tsx
+++ b/client/src/Pages/Uptime/Details/index.tsx
@@ -8,6 +8,7 @@ import {
} from "@/Components/v2/monitors";
import { TrendingUp, AlertTriangle } from "lucide-react";
import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable";
+import { MonitorStatBoxes } from "@/Components/v2/monitors";
import { useTheme } from "@mui/material/styles";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
@@ -18,7 +19,6 @@ import { useGet } from "@/Hooks/UseApi";
import type { MonitorDetailsResponse } from "@/Types/Monitor";
import type { ChecksResponse } from "@/Types/Check";
import type { RootState } from "@/Types/state";
-import { MonitorStatBoxes } from "@/Components/v2/monitors";
import { formatDateWithTz } from "@/Utils/timeUtilsLegacy";
import { t } from "i18next";
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 780663f68..f9b5cb8a1 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -27,7 +27,7 @@ import PageSpeedCreate from "../Pages/PageSpeed/Create/index.jsx";
// Infrastructure
import Infrastructure from "../Pages/Infrastructure/Monitors";
import InfrastructureCreate from "../Pages/Infrastructure/Create/index.jsx";
-import InfrastructureDetails from "../Pages/Infrastructure/Details/index.jsx";
+import InfrastructureDetails from "../Pages/Infrastructure/Details/index";
// Server Status
import ServerUnreachable from "../Pages/ServerUnreachable.jsx";
@@ -165,7 +165,13 @@ const Routes = () => {
/>
}
+ element={
+ <>
+
+
+
+ >
+ }
/>
{
+ if (!frequency) return "N/A";
+ const ghz = (frequency / 1000).toFixed(2);
+ return `${ghz} GHz`;
+};
+
+export const getCores = (cores: number | undefined) => {
+ if (!cores) return "N/A";
+ if (cores === 1) return `${cores} core`;
+ return `${cores} cores`;
+};
+
+export const getAvgTemp = (temps: number[]): string => {
+ if (!temps || temps.length === 0) return "N/A";
+ const avgTemp = temps.reduce((a, b) => a + b, 0) / temps.length;
+ return `${avgTemp?.toFixed(2)} °C`;
+};
+
+export const getGbs = (bytes: number | undefined): string => {
+ if (!bytes) {
+ return "N/A";
+ }
+ if (bytes === 0) {
+ return "0 GB";
+ }
+
+ const GB = bytes / (1024 * 1024 * 1024);
+ const MB = bytes / (1024 * 1024);
+
+ if (GB >= 1) {
+ return GB.toFixed(2) + " GB";
+ } else {
+ return MB.toFixed(2) + " MB";
+ }
+};
+
+export const getDiskTotalGbs = (disk?: Partial[]): string => {
+ if (!disk) {
+ return getGbs(0);
+ }
+ const totalBytes = disk?.reduce((acc, disk) => acc + (disk.total_bytes || 0), 0) || 0;
+ return getGbs(totalBytes);
+};
+
+export const getOsAndPlatform = (hostInfo: CheckHostInfo | undefined): string => {
+ if (!hostInfo) {
+ return "N/A";
+ }
+ const os = hostInfo?.pretty_name || hostInfo?.os || "N/A";
+ const platform = hostInfo?.platform || "N/A";
+ return `${os} (${platform})`;
+};
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index d015f8f46..2c79562f0 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -284,6 +284,48 @@
"disk": "Disk",
"memory": "Memory"
}
+ },
+ "tabs": {
+ "labels": {
+ "overview": "Overview",
+ "network": "Network"
+ }
+ },
+ "statBoxes": {
+ "cpuPhysical": "CPU (Physical)",
+ "cpuLogical": "CPU (Logical)",
+ "cpuFrequency": "CPU Frequency",
+ "avgCpuTemperature": "Average CPU Temperature",
+ "memory": "Memory",
+ "disk": "Disk",
+ "os": "OS"
+ },
+ "gauges": {
+ "cpu": {
+ "lowerLabel": "Max frequency",
+ "title": "CPU usage",
+ "upperLabel": "Current frequency"
+ },
+ "disk": {
+ "lowerLabel": "Free",
+ "title": "Disk {{idx}} usage",
+ "upperLabel": "Used"
+ },
+ "memory": {
+ "lowerLabel": "Free",
+ "title": "Memory usage",
+ "upperLabel": "Used"
+ }
+ },
+ "charts": {
+ "labels": {
+ "memory": "Memory usage",
+ "cpu": "CPU usage",
+ "temp": "Temp",
+ "disk": "Disk usage",
+ "netBytesSent": "{{name}} - Bytes Sent",
+ "netBytesRecv": "{{name}} - Bytes Received"
+ }
}
}
},
diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts
index 721565f59..df1945fcc 100644
--- a/server/src/controllers/monitorController.ts
+++ b/server/src/controllers/monitorController.ts
@@ -93,7 +93,7 @@ class MonitorController {
const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
const teamId = requireTeamId(req?.user?.teamId);
- const monitor = await this.monitorService.getHardwareDetailsById({
+ const data = await this.monitorService.getHardwareDetailsById({
teamId,
monitorId,
dateRange,
@@ -102,7 +102,7 @@ class MonitorController {
return res.status(200).json({
success: true,
msg: "Hardware details retrieved successfully",
- data: monitor,
+ data: data,
});
} catch (error) {
next(error);
diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts
index 4d9c499de..7bd1cec6f 100644
--- a/server/src/db/models/Check.ts
+++ b/server/src/db/models/Check.ts
@@ -71,6 +71,7 @@ const cpuSchema = new Schema(
physical_core: { type: Number, default: 0 },
logical_core: { type: Number, default: 0 },
frequency: { type: Number, default: 0 },
+ current_frequency: { type: Number, default: 0 },
temperature: { type: [Number], default: [] },
free_percent: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
@@ -92,11 +93,18 @@ const diskSchema = new Schema(
{
device: { type: String, default: "" },
mountpoint: { type: String, default: "" },
- read_speed_bytes: { type: Number, default: 0 },
- write_speed_bytes: { type: Number, default: 0 },
total_bytes: { type: Number, default: 0 },
free_bytes: { type: Number, default: 0 },
+ used_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
+ total_inodes: { type: Number, default: 0 },
+ free_inodes: { type: Number, default: 0 },
+ used_inodes: { type: Number, default: 0 },
+ inodes_usage_percent: { type: Number, default: 0 },
+ read_bytes: { type: Number, default: 0 },
+ write_bytes: { type: Number, default: 0 },
+ read_time: { type: Number, default: 0 },
+ write_time: { type: Number, default: 0 },
},
{ _id: false }
);
@@ -106,6 +114,7 @@ const hostSchema = new Schema(
os: { type: String, default: "" },
platform: { type: String, default: "" },
kernel_version: { type: String, default: "" },
+ pretty_name: { type: String, default: "" },
},
{ _id: false }
);
diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts
index 1f65673f3..a36788e2c 100644
--- a/server/src/repositories/checks/MongoChecksRepistory.ts
+++ b/server/src/repositories/checks/MongoChecksRepistory.ts
@@ -117,11 +117,18 @@ class MongoChecksRepository implements IChecksRepository {
(disks ?? []).map((disk) => ({
device: disk?.device ?? "",
mountpoint: disk?.mountpoint ?? "",
- read_speed_bytes: disk?.read_speed_bytes ?? 0,
- write_speed_bytes: disk?.write_speed_bytes ?? 0,
total_bytes: disk?.total_bytes ?? 0,
free_bytes: disk?.free_bytes ?? 0,
+ used_bytes: disk?.used_bytes ?? 0,
usage_percent: disk?.usage_percent ?? 0,
+ total_inodes: disk?.total_inodes ?? 0,
+ free_inodes: disk?.free_inodes ?? 0,
+ used_inodes: disk?.used_inodes ?? 0,
+ inodes_usage_percent: disk?.inodes_usage_percent ?? 0,
+ read_bytes: disk?.read_bytes ?? 0,
+ write_bytes: disk?.write_bytes ?? 0,
+ read_time: disk?.read_time ?? 0,
+ write_time: disk?.write_time ?? 0,
}));
const mapErrors = (errors?: CheckErrorInfo[]): CheckErrorInfo[] =>
@@ -522,7 +529,7 @@ class MongoChecksRepository implements IChecksRepository {
};
const checks = (hardwareMetrics ?? []).map((metric) => ({
- _id: metric._id,
+ bucketDate: metric._id,
avgCpuUsage: metric.avgCpuUsage ?? 0,
avgMemoryUsage: metric.avgMemoryUsage ?? 0,
avgTemperature: metric.avgTemperature ?? [],
@@ -628,8 +635,8 @@ class MongoChecksRepository implements IChecksRepository {
as: "dIdx",
in: {
name: { $concat: ["disk", { $toString: "$$dIdx" }] },
- readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } },
- writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } },
+ readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_bytes", "$$dIdx"] } } } },
+ writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_bytes", "$$dIdx"] } } } },
totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } },
freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } },
usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } },
diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts
index b950c9a32..b824908b5 100644
--- a/server/src/service/business/monitorService.ts
+++ b/server/src/service/business/monitorService.ts
@@ -257,9 +257,12 @@ export class MonitorService implements IMonitorService {
checks: checksData.checks,
};
+ const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
+
return {
- ...monitor,
+ monitor,
stats,
+ monitorStats,
};
};
diff --git a/server/src/types/check.ts b/server/src/types/check.ts
index 8e223d7c8..c02b03099 100644
--- a/server/src/types/check.ts
+++ b/server/src/types/check.ts
@@ -12,6 +12,7 @@ export interface CheckCpuInfo {
physical_core?: number;
logical_core?: number;
frequency?: number;
+ current_frequency?: number;
temperature?: number[];
free_percent?: number;
usage_percent?: number;
@@ -28,6 +29,7 @@ export interface CheckHostInfo {
os?: string;
platform?: string;
kernel_version?: string;
+ pretty_name?: string;
}
export interface CheckCaptureInfo {
@@ -38,11 +40,18 @@ export interface CheckCaptureInfo {
export interface CheckDiskInfo {
device?: string;
mountpoint?: string;
- read_speed_bytes?: number;
- write_speed_bytes?: number;
total_bytes?: number;
free_bytes?: number;
+ used_bytes?: number;
usage_percent?: number;
+ total_inodes?: number;
+ free_inodes?: number;
+ used_inodes?: number;
+ inodes_usage_percent?: number;
+ read_bytes?: number;
+ write_bytes?: number;
+ read_time?: number;
+ write_time?: number;
}
export interface CheckErrorInfo {
@@ -127,7 +136,7 @@ export interface HardwareChecksResult {
totalChecks: number;
};
checks: Array<{
- _id: string;
+ bucketDate: string;
avgCpuUsage: number;
avgMemoryUsage: number;
avgTemperature: number[];
diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts
index b2bf360dc..62ced091d 100644
--- a/server/src/types/monitor.ts
+++ b/server/src/types/monitor.ts
@@ -75,42 +75,52 @@ export interface UptimeDetailsResult {
monitorStats: import("./monitorStats.js").MonitorStats | null;
}
-export interface HardwareDetailsResult extends Monitor {
- stats: {
- aggregateData: {
- totalChecks: number;
- };
- upChecks: {
- totalChecks: number;
- };
- checks: Array<{
- _id: string;
- avgCpuUsage: number;
- avgMemoryUsage: number;
- avgTemperature: number[];
- disks: Array<{
- name: string;
- readSpeed: number;
- writeSpeed: number;
- totalBytes: number;
- freeBytes: number;
- usagePercent: number;
- }>;
- net: Array<{
- name: string;
- bytesSentPerSecond: number;
- deltaBytesRecv: number;
- deltaPacketsSent: number;
- deltaPacketsRecv: number;
- deltaErrIn: number;
- deltaErrOut: number;
- deltaDropIn: number;
- deltaDropOut: number;
- deltaFifoIn: number;
- deltaFifoOut: number;
- }>;
- }>;
+export interface HardwareDiskStats {
+ name: string;
+ readSpeed: number;
+ writeSpeed: number;
+ totalBytes: number;
+ freeBytes: number;
+ usagePercent: number;
+}
+
+export interface HardwareNetStats {
+ name: string;
+ bytesSentPerSecond: number;
+ deltaBytesRecv: number;
+ deltaPacketsSent: number;
+ deltaPacketsRecv: number;
+ deltaErrIn: number;
+ deltaErrOut: number;
+ deltaDropIn: number;
+ deltaDropOut: number;
+ deltaFifoIn: number;
+ deltaFifoOut: number;
+}
+
+export interface HardwareCheckStats {
+ bucketDate: string;
+ avgCpuUsage: number;
+ avgMemoryUsage: number;
+ avgTemperature: number[];
+ disks: HardwareDiskStats[];
+ net: HardwareNetStats[];
+}
+
+export interface HardwareStats {
+ aggregateData: {
+ totalChecks: number;
};
+ upChecks: {
+ totalChecks: number;
+ };
+ checks: HardwareCheckStats[];
+}
+
+export interface HardwareDetailsResult {
+ monitor: Monitor;
+ stats: HardwareStats;
+ monitorStats: import("./monitorStats.js").MonitorStats | null;
}
export interface PageSpeedDetailsResult {