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 {