From 7b963ca87777b16031f7a3c7e265b19afda80d65 Mon Sep 17 00:00:00 2001 From: owiaseimdad Date: Sun, 3 Aug 2025 00:30:04 +0530 Subject: [PATCH 001/131] Network tab first implementation --- .../Components/NetworkStats/NetworkCharts.jsx | 59 +++++++++++ .../NetworkStats/NetworkStatBoxes.jsx | 82 ++++++++++++++ .../Details/Components/NetworkStats/index.jsx | 100 ++++++++++++++++++ .../Components/NetworkStats/skeleton.jsx | 56 ++++++++++ .../Pages/Infrastructure/Details/index.jsx | 69 ++++++++---- 5 files changed, 347 insertions(+), 19 deletions(-) create mode 100644 client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx create mode 100644 client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx create mode 100644 client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx create mode 100644 client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx new file mode 100644 index 000000000..497fa1410 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -0,0 +1,59 @@ +// NetworkCharts.jsx +import { Grid, Card, CardContent, Typography } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import AreaChart from "../../../../../Components/Charts/AreaChart"; +import { TzTick } from '../../../../../Components/Charts/Utils/chartUtils'; + +const BytesTick = ({ x, y, payload }) => { + const value = payload.value; + const label = + value >= 1024 ** 3 + ? `${(value / 1024 ** 3).toFixed(2)} GB` + : value >= 1024 ** 2 + ? `${(value / 1024 ** 2).toFixed(2)} MB` + : `${(value / 1024).toFixed(2)} KB`; + + return {label}; +}; + +const NetworkCharts = ({ eth0Data, dateRange }) => { + const theme = useTheme(); + const textColor = theme.palette.primary.contrastTextTertiary; + + const charts = [ + { title: "eth0 Bytes/sec", key: "bytesPerSec", color: theme.palette.info.main, yTick: }, + { title: "eth0 Packets/sec", key: "packetsPerSec", color: theme.palette.success.main }, + { title: "eth0 Errors", key: "errors", color: theme.palette.error.main }, + { title: "eth0 Drops", key: "drops", color: theme.palette.warning.main } + ]; + + return ( + + {charts.map((chart) => ( + + + + + {chart.title} + + } + strokeColor={chart.color} + gradient + gradientStartColor={chart.color} + gradientEndColor="#fff" + height={200} + /> + + + + ))} + + ); +}; + +export default NetworkCharts; \ No newline at end of file diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx new file mode 100644 index 000000000..abe67fa8e --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -0,0 +1,82 @@ +// NetworkStatBoxes.jsx +import DataUsageIcon from "@mui/icons-material/DataUsage"; +import NetworkCheckIcon from "@mui/icons-material/NetworkCheck"; +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import StatusBoxes from "../../../../../Components/StatusBoxes"; +import StatBox from "../../../../../Components/StatBox"; +import { Typography } from "@mui/material"; + +const INTERFACE_LABELS = { + en0: "Ethernet/Wi-Fi (Primary)", + wlan0: "Wi-Fi (Secondary)", +}; + +function formatBytes(bytes) { + if (bytes === 0 || bytes == null) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; +} + +// Format numbers with commas +function formatNumber(num) { + return num != null ? num.toLocaleString() : "0"; +} + +const NetworkStatBoxes = ({ shouldRender, net }) => { + const filtered = net?.filter( + (iface) => iface.name === "en0" || iface.name === "wlan0" + ) || []; + + if (!net?.length) { + return No network stats available.; + } + + return ( + + {filtered.map((iface) => ( + <> + + + + + + + + ))} + + ); +}; + +export default NetworkStatBoxes; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx new file mode 100644 index 000000000..ecb62e769 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -0,0 +1,100 @@ +import NetworkStatBoxes from "./NetworkStatBoxes"; +import NetworkCharts from "./NetworkCharts"; +import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; + +function filterByDateRange(data, dateRange) { + if (!Array.isArray(data)) return []; + const now = Date.now(); + let cutoff; + switch (dateRange) { + case "recent": + cutoff = now - 2 * 60 * 60 * 1000; // last 2 hours + break; + case "day": + cutoff = now - 24 * 60 * 60 * 1000; // last 24 hours + break; + case "week": + cutoff = now - 7 * 24 * 60 * 60 * 1000; // last 7 days + break; + case "month": + cutoff = now - 30 * 24 * 60 * 60 * 1000; // last 30 days + break; + default: + cutoff = 0; + } + return data.filter((d) => new Date(d.time).getTime() >= cutoff); +} + +const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { + const eth0Data = getEth0TimeSeries(checks); + const xAxisFormatter = getXAxisFormatter(checks); + const filteredEth0Data = filterByDateRange(eth0Data, dateRange); + + return ( + <> + + + + + ); +}; + +export default Network; + +/* ---------- Helper functions ---------- */ +function getEth0TimeSeries(checks) { + const sorted = [...(checks || [])].sort((a, b) => new Date(a._id) - new Date(b._id)); + const series = []; + let prev = null; + + for (const check of sorted) { + const eth = (check.net || []).find((iface) => iface.name === "en0"); + if (!eth) { + prev = check; + continue; + } + if (prev) { + const prevEth = (prev.net || []).find((iface) => iface.name === "en0"); + const t1 = new Date(check._id); + const t0 = new Date(prev._id); + if (!prevEth || isNaN(t1) || isNaN(t0)) { + prev = check; + continue; + } + const dt = (t1 - t0) / 1000; + if (dt > 0) { + series.push({ + time: check._id, + bytesPerSec: (eth.bytesSent - prevEth.bytesSent) / dt, + packetsPerSec: (eth.packetsSent - prevEth.packetsSent) / dt, + errors: (eth.errIn ?? 0) + (eth.errOut ?? 0), + drops: (eth.dropIn ?? 0) + (eth.dropOut ?? 0), + }); + } + } + prev = check; + } + + return series; +} + +function getXAxisFormatter(checks) { + if (!checks || checks.length === 0) return (val) => val; + const sorted = [...checks].sort((a, b) => new Date(a._id) - new Date(b._id)); + const first = new Date(sorted[0]._id); + const last = new Date(sorted[sorted.length - 1]._id); + const diffDays = (last - first) / (1000 * 60 * 60 * 24); + + return diffDays > 2 + ? (val) => new Date(val).toLocaleDateString(undefined, { month: "short", day: "numeric" }) + : (val) => + new Date(val).toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); +} diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx new file mode 100644 index 000000000..52fcfad87 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx @@ -0,0 +1,56 @@ +import { + Card, + CardContent, + Skeleton, + Table, + TableHead, + TableRow, + TableCell, + TableBody, +} from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + + + + Name + Bytes Sent + Bytes Received + Packets Sent + Packets Received + Errors In + Errors Out + Drops In + Drops Out + + + + {Array.from({ length: 5 }).map((_, idx) => ( + + {Array.from({ length: 9 }).map((__, colIdx) => ( + + + + ))} + + ))} + +
+
+
+ ); +}; + +export default SkeletonLayout; diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx index 30840851f..78b78fa78 100644 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ b/client/src/Pages/Infrastructure/Details/index.jsx @@ -1,5 +1,5 @@ // Components -import { Stack, Typography } from "@mui/material"; +import { Stack, Typography, Tab } from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader"; import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; @@ -7,6 +7,9 @@ import StatusBoxes from "./Components/StatusBoxes"; import GaugeBoxes from "./Components/GaugeBoxes"; import AreaChartBoxes from "./Components/AreaChartBoxes"; import GenericFallback from "../../../Components/GenericFallback"; +import NetworkStats from "./Components/NetworkStats"; +import CustomTabList from "../../../Components/Tab"; +import TabContext from "@mui/lab/TabContext"; // Utils import { useTheme } from "@emotion/react"; @@ -22,11 +25,11 @@ const BREADCRUMBS = [ { name: "details", path: "" }, ]; const InfrastructureDetails = () => { - // Redux state // Local state const [dateRange, setDateRange] = useState("recent"); const [trigger, setTrigger] = useState(false); + const [tab, setTab] = useState("details"); // Utils const theme = useTheme(); @@ -87,23 +90,51 @@ const InfrastructureDetails = () => { monitor={monitor} triggerUpdate={triggerUpdate} /> - - - - + + setTab(v)} + > + + + + {tab === "details" && ( + <> + + + + + + )} + {tab === "network" && ( + + )} + ); }; From 10c5463892617e1a8254ec36c2f084dc0f09c429 Mon Sep 17 00:00:00 2001 From: owiaseimdad Date: Sun, 3 Aug 2025 01:26:13 +0530 Subject: [PATCH 002/131] Fixed the tool tip. --- .../Components/Charts/Utils/chartUtils.jsx | 5 +- .../Components/NetworkStats/NetworkCharts.jsx | 80 +++++++-- .../NetworkStats/NetworkStatBoxes.jsx | 118 ++++++------- .../Details/Components/NetworkStats/index.jsx | 163 +++++++++--------- 4 files changed, 214 insertions(+), 152 deletions(-) diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/Charts/Utils/chartUtils.jsx index 414e1ff9c..bbf81ae38 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/Charts/Utils/chartUtils.jsx @@ -112,6 +112,7 @@ export const InfrastructureTooltip = ({ yLabel, dotColor, dateRange, + formatter = getFormattedPercentage, }) => { const uiTimezone = useSelector((state) => state.ui.timezone); const theme = useTheme(); @@ -166,8 +167,8 @@ export const InfrastructureTooltip = ({ sx={{ opacity: 0.8 }} > {yIdx >= 0 - ? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}` - : `${yLabel} ${getFormattedPercentage(payload[0].payload[yKey])}`} + ? `${yLabel} ${formatter(payload[0].payload[hardwareType][yIdx][metric])}` + : `${yLabel} ${formatter(payload[0].payload[yKey])}`} diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 497fa1410..9ef548b2e 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -2,7 +2,7 @@ import { Grid, Card, CardContent, Typography } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import AreaChart from "../../../../../Components/Charts/AreaChart"; -import { TzTick } from '../../../../../Components/Charts/Utils/chartUtils'; +import { TzTick, InfrastructureTooltip } from '../../../../../Components/Charts/Utils/chartUtils'; const BytesTick = ({ x, y, payload }) => { const value = payload.value; @@ -16,37 +16,89 @@ const BytesTick = ({ x, y, payload }) => { return {label}; }; +const getFormattedNetworkMetric = (value) => { + if (typeof value !== "number" || isNaN(value)) return "0"; + if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(1)} GB/s`; + if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(1)} MB/s`; + if (value >= 1024) return `${(value / 1024).toFixed(1)} KB/s`; + return `${Math.round(value)} B/s`; +}; + const NetworkCharts = ({ eth0Data, dateRange }) => { const theme = useTheme(); const textColor = theme.palette.primary.contrastTextTertiary; const charts = [ - { title: "eth0 Bytes/sec", key: "bytesPerSec", color: theme.palette.info.main, yTick: }, - { title: "eth0 Packets/sec", key: "packetsPerSec", color: theme.palette.success.main }, - { title: "eth0 Errors", key: "errors", color: theme.palette.error.main }, - { title: "eth0 Drops", key: "drops", color: theme.palette.warning.main } + { title: "Bytes per second", key: "bytesPerSec", color: theme.palette.info.main, yTick: }, + { title: "Packets per second", key: "packetsPerSec", color: theme.palette.success.main }, + { title: "Errors", key: "errors", color: theme.palette.error.main }, + { title: "Drops", key: "drops", color: theme.palette.warning.main } ]; + const formatYAxis = (key, value) => { + if (key === "bytesPerSec") { + // Format as MB/s or GB/s if large + if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(1)} GB/s`; + if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(1)} MB/s`; + if (value >= 1024) return `${(value / 1024).toFixed(1)} KB/s`; + return `${Math.round(value)} B/s`; + } + return Math.round(value).toLocaleString(); + }; + + const CustomTick = ({ x, y, payload, chartKey }) => { + // Ensure value is always rounded for display + let value = payload.value; + if (typeof value === 'number') { + value = Math.round(value); + } + return ( + + {formatYAxis(chartKey, value)} + + ); + }; + + const chartConfigs = charts.map((chart) => ({ + data: eth0Data, + dataKeys: [chart.key], + heading: chart.title, + strokeColor: chart.color, + gradientStartColor: chart.color, + yTick: chart.yTick || , + xTick: , + toolTip: ( + + ), + })); + return ( - {charts.map((chart) => ( - + {chartConfigs.map((config, idx) => ( + - {chart.title} + {config.heading} } - strokeColor={chart.color} + yTick={config.yTick} + xTick={config.xTick} + strokeColor={config.strokeColor} gradient - gradientStartColor={chart.color} + gradientStartColor={config.gradientStartColor} gradientEndColor="#fff" height={200} + customTooltip={config.toolTip} /> diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx index abe67fa8e..5253742a6 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -7,76 +7,78 @@ import StatBox from "../../../../../Components/StatBox"; import { Typography } from "@mui/material"; const INTERFACE_LABELS = { - en0: "Ethernet/Wi-Fi (Primary)", - wlan0: "Wi-Fi (Secondary)", + en0: "Ethernet/Wi-Fi (Primary)", + wlan0: "Wi-Fi (Secondary)", }; function formatBytes(bytes) { - if (bytes === 0 || bytes == null) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB", "TB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; + if (bytes === 0 || bytes == null) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; } // Format numbers with commas function formatNumber(num) { - return num != null ? num.toLocaleString() : "0"; + return num != null ? num.toLocaleString() : "0"; } const NetworkStatBoxes = ({ shouldRender, net }) => { - const filtered = net?.filter( - (iface) => iface.name === "en0" || iface.name === "wlan0" - ) || []; + const filtered = + net?.filter((iface) => iface.name === "en0" || iface.name === "wlan0") || []; - if (!net?.length) { - return No network stats available.; - } + if (!net?.length) { + return No network stats available.; + } - return ( - - {filtered.map((iface) => ( - <> - - - - - - - - ))} - - ); + return ( + + {filtered.map((iface) => ( + <> + + + + + + + + ))} + + ); }; export default NetworkStatBoxes; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index ecb62e769..60825be0f 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -3,98 +3,105 @@ import NetworkCharts from "./NetworkCharts"; import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; function filterByDateRange(data, dateRange) { - if (!Array.isArray(data)) return []; - const now = Date.now(); - let cutoff; - switch (dateRange) { - case "recent": - cutoff = now - 2 * 60 * 60 * 1000; // last 2 hours - break; - case "day": - cutoff = now - 24 * 60 * 60 * 1000; // last 24 hours - break; - case "week": - cutoff = now - 7 * 24 * 60 * 60 * 1000; // last 7 days - break; - case "month": - cutoff = now - 30 * 24 * 60 * 60 * 1000; // last 30 days - break; - default: - cutoff = 0; - } - return data.filter((d) => new Date(d.time).getTime() >= cutoff); + if (!Array.isArray(data)) return []; + const now = Date.now(); + let cutoff; + switch (dateRange) { + case "recent": + cutoff = now - 2 * 60 * 60 * 1000; // last 2 hours + break; + case "day": + cutoff = now - 24 * 60 * 60 * 1000; // last 24 hours + break; + case "week": + cutoff = now - 7 * 24 * 60 * 60 * 1000; // last 7 days + break; + case "month": + cutoff = now - 30 * 24 * 60 * 60 * 1000; // last 30 days + break; + default: + cutoff = 0; + } + return data.filter((d) => new Date(d.time).getTime() >= cutoff); } const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { - const eth0Data = getEth0TimeSeries(checks); - const xAxisFormatter = getXAxisFormatter(checks); - const filteredEth0Data = filterByDateRange(eth0Data, dateRange); + const eth0Data = getEth0TimeSeries(checks); + const xAxisFormatter = getXAxisFormatter(checks); + const filteredEth0Data = filterByDateRange(eth0Data, dateRange); - return ( - <> - - - - - ); + return ( + <> + + + + + ); }; export default Network; /* ---------- Helper functions ---------- */ function getEth0TimeSeries(checks) { - const sorted = [...(checks || [])].sort((a, b) => new Date(a._id) - new Date(b._id)); - const series = []; - let prev = null; + const sorted = [...(checks || [])].sort((a, b) => new Date(a._id) - new Date(b._id)); + const series = []; + let prev = null; - for (const check of sorted) { - const eth = (check.net || []).find((iface) => iface.name === "en0"); - if (!eth) { - prev = check; - continue; - } - if (prev) { - const prevEth = (prev.net || []).find((iface) => iface.name === "en0"); - const t1 = new Date(check._id); - const t0 = new Date(prev._id); - if (!prevEth || isNaN(t1) || isNaN(t0)) { - prev = check; - continue; - } - const dt = (t1 - t0) / 1000; - if (dt > 0) { - series.push({ - time: check._id, - bytesPerSec: (eth.bytesSent - prevEth.bytesSent) / dt, - packetsPerSec: (eth.packetsSent - prevEth.packetsSent) / dt, - errors: (eth.errIn ?? 0) + (eth.errOut ?? 0), - drops: (eth.dropIn ?? 0) + (eth.dropOut ?? 0), - }); - } - } - prev = check; - } + for (const check of sorted) { + const eth = (check.net || []).find((iface) => iface.name === "en0"); + if (!eth) { + prev = check; + continue; + } + if (prev) { + const prevEth = (prev.net || []).find((iface) => iface.name === "en0"); + const t1 = new Date(check._id); + const t0 = new Date(prev._id); + if (!prevEth || isNaN(t1) || isNaN(t0)) { + prev = check; + continue; + } + const dt = (t1 - t0) / 1000; + if (dt > 0) { + series.push({ + time: check._id, + bytesPerSec: (eth.bytesSent - prevEth.bytesSent) / dt, + packetsPerSec: (eth.packetsSent - prevEth.packetsSent) / dt, + errors: (eth.errIn ?? 0) + (eth.errOut ?? 0), + drops: (eth.dropIn ?? 0) + (eth.dropOut ?? 0), + }); + } + } + prev = check; + } - return series; + return series; } function getXAxisFormatter(checks) { - if (!checks || checks.length === 0) return (val) => val; - const sorted = [...checks].sort((a, b) => new Date(a._id) - new Date(b._id)); - const first = new Date(sorted[0]._id); - const last = new Date(sorted[sorted.length - 1]._id); - const diffDays = (last - first) / (1000 * 60 * 60 * 24); + if (!checks || checks.length === 0) return (val) => val; + const sorted = [...checks].sort((a, b) => new Date(a._id) - new Date(b._id)); + const first = new Date(sorted[0]._id); + const last = new Date(sorted[sorted.length - 1]._id); + const diffDays = (last - first) / (1000 * 60 * 60 * 24); - return diffDays > 2 - ? (val) => new Date(val).toLocaleDateString(undefined, { month: "short", day: "numeric" }) - : (val) => - new Date(val).toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - hour12: false, - }); + return diffDays > 2 + ? (val) => + new Date(val).toLocaleDateString(undefined, { month: "short", day: "numeric" }) + : (val) => + new Date(val).toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); } From e42b4b28081760fef67cd8638bb0e62e1c8d41ad Mon Sep 17 00:00:00 2001 From: Owaise Date: Tue, 5 Aug 2025 20:46:42 +0530 Subject: [PATCH 003/131] fixed the minor changes for FE. --- .../Components/Charts/Utils/chartUtils.jsx | 2 +- .../Components/NetworkStats/NetworkCharts.jsx | 219 ++++++++++-------- .../NetworkStats/NetworkStatBoxes.jsx | 64 ++--- .../Details/Components/NetworkStats/index.jsx | 132 +++++++---- 4 files changed, 234 insertions(+), 183 deletions(-) diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/Charts/Utils/chartUtils.jsx index bbf81ae38..af5f27c66 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/Charts/Utils/chartUtils.jsx @@ -88,7 +88,7 @@ PercentTick.propTypes = { */ const getFormattedPercentage = (value) => { if (typeof value !== "number") return value; - return `${(value * 100).toFixed(2)}.%`; + return `${(value * 100).toFixed(2)}%`; }; /** diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 9ef548b2e..343f7681b 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -1,111 +1,130 @@ // NetworkCharts.jsx -import { Grid, Card, CardContent, Typography } from "@mui/material"; -import { useTheme } from "@mui/material/styles"; -import AreaChart from "../../../../../Components/Charts/AreaChart"; -import { TzTick, InfrastructureTooltip } from '../../../../../Components/Charts/Utils/chartUtils'; +import PropTypes from "prop-types"; +import { Stack } from "@mui/material"; +import InfraAreaChart from "../../../../../Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart"; -const BytesTick = ({ x, y, payload }) => { - const value = payload.value; - const label = - value >= 1024 ** 3 - ? `${(value / 1024 ** 3).toFixed(2)} GB` - : value >= 1024 ** 2 - ? `${(value / 1024 ** 2).toFixed(2)} MB` - : `${(value / 1024).toFixed(2)} KB`; - - return {label}; -}; +// Utils +import { + TzTick, + InfrastructureTooltip, +} from "../../../../../Components/Charts/Utils/chartUtils"; +import { useTheme } from "@emotion/react"; const getFormattedNetworkMetric = (value) => { - if (typeof value !== "number" || isNaN(value)) return "0"; - if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(1)} GB/s`; - if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(1)} MB/s`; - if (value >= 1024) return `${(value / 1024).toFixed(1)} KB/s`; - return `${Math.round(value)} B/s`; + if (typeof value !== "number" || isNaN(value)) return "0"; + if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(1)} GB/s`; + if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(1)} MB/s`; + if (value >= 1024) return `${(value / 1024).toFixed(1)} KB/s`; + return `${Math.round(value)} B/s`; }; const NetworkCharts = ({ eth0Data, dateRange }) => { - const theme = useTheme(); - const textColor = theme.palette.primary.contrastTextTertiary; + const theme = useTheme(); - const charts = [ - { title: "Bytes per second", key: "bytesPerSec", color: theme.palette.info.main, yTick: }, - { title: "Packets per second", key: "packetsPerSec", color: theme.palette.success.main }, - { title: "Errors", key: "errors", color: theme.palette.error.main }, - { title: "Drops", key: "drops", color: theme.palette.warning.main } - ]; + const configs = [ + { + type: "network-bytes", + data: eth0Data, + dataKeys: ["bytesPerSec"], + heading: "Bytes per second", + strokeColor: theme.palette.info.main, + gradientStartColor: theme.palette.info.main, + yLabel: "Bytes per second", + xTick: , + toolTip: ( + + ), + }, + { + type: "network-packets", + data: eth0Data, + dataKeys: ["packetsPerSec"], + heading: "Packets per second", + strokeColor: theme.palette.success.main, + gradientStartColor: theme.palette.success.main, + yLabel: "Packets per second", + xTick: , + toolTip: ( + Math.round(value).toLocaleString()} + /> + ), + }, + { + type: "network-errors", + data: eth0Data, + dataKeys: ["errors"], + heading: "Errors", + strokeColor: theme.palette.error.main, + gradientStartColor: theme.palette.error.main, + yLabel: "Errors", + xTick: , + toolTip: ( + Math.round(value).toLocaleString()} + /> + ), + }, + { + type: "network-drops", + data: eth0Data, + dataKeys: ["drops"], + heading: "Drops", + strokeColor: theme.palette.warning.main, + gradientStartColor: theme.palette.warning.main, + yLabel: "Drops", + xTick: , + toolTip: ( + Math.round(value).toLocaleString()} + /> + ), + }, + ]; - const formatYAxis = (key, value) => { - if (key === "bytesPerSec") { - // Format as MB/s or GB/s if large - if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(1)} GB/s`; - if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(1)} MB/s`; - if (value >= 1024) return `${(value / 1024).toFixed(1)} KB/s`; - return `${Math.round(value)} B/s`; - } - return Math.round(value).toLocaleString(); - }; - - const CustomTick = ({ x, y, payload, chartKey }) => { - // Ensure value is always rounded for display - let value = payload.value; - if (typeof value === 'number') { - value = Math.round(value); - } - return ( - - {formatYAxis(chartKey, value)} - - ); - }; - - const chartConfigs = charts.map((chart) => ({ - data: eth0Data, - dataKeys: [chart.key], - heading: chart.title, - strokeColor: chart.color, - gradientStartColor: chart.color, - yTick: chart.yTick || , - xTick: , - toolTip: ( - - ), - })); - - return ( - - {chartConfigs.map((config, idx) => ( - - - - - {config.heading} - - - - - - ))} - - ); + return ( + *": { + flexBasis: `calc(50% - ${theme.spacing(8)})`, + maxWidth: `calc(50% - ${theme.spacing(8)})`, + }, + }} + > + {configs.map((config) => ( + + ))} + + ); }; -export default NetworkCharts; \ No newline at end of file +NetworkCharts.propTypes = { + eth0Data: PropTypes.array.isRequired, + dateRange: PropTypes.string.isRequired, +}; + +export default NetworkCharts; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx index 5253742a6..b2f2f7841 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -1,16 +1,9 @@ // NetworkStatBoxes.jsx -import DataUsageIcon from "@mui/icons-material/DataUsage"; -import NetworkCheckIcon from "@mui/icons-material/NetworkCheck"; -import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import PropTypes from "prop-types"; import StatusBoxes from "../../../../../Components/StatusBoxes"; import StatBox from "../../../../../Components/StatBox"; import { Typography } from "@mui/material"; -const INTERFACE_LABELS = { - en0: "Ethernet/Wi-Fi (Primary)", - wlan0: "Wi-Fi (Secondary)", -}; - function formatBytes(bytes) { if (bytes === 0 || bytes == null) return "0 B"; const k = 1024; @@ -37,48 +30,57 @@ const NetworkStatBoxes = ({ shouldRender, net }) => { shouldRender={shouldRender} flexWrap="wrap" > - {filtered.map((iface) => ( - <> + {filtered + .map((iface) => [ + />, + />, + />, + />, + />, - - ))} + />, + ]) + .flat()} ); }; +NetworkStatBoxes.propTypes = { + shouldRender: PropTypes.bool.isRequired, + net: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + bytes_sent: PropTypes.number, + bytes_recv: PropTypes.number, + packets_sent: PropTypes.number, + packets_recv: PropTypes.number, + err_in: PropTypes.number, + err_out: PropTypes.number, + }) + ), +}; + export default NetworkStatBoxes; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index 60825be0f..6b8ccb85a 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -1,34 +1,10 @@ +import PropTypes from "prop-types"; import NetworkStatBoxes from "./NetworkStatBoxes"; import NetworkCharts from "./NetworkCharts"; import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; -function filterByDateRange(data, dateRange) { - if (!Array.isArray(data)) return []; - const now = Date.now(); - let cutoff; - switch (dateRange) { - case "recent": - cutoff = now - 2 * 60 * 60 * 1000; // last 2 hours - break; - case "day": - cutoff = now - 24 * 60 * 60 * 1000; // last 24 hours - break; - case "week": - cutoff = now - 7 * 24 * 60 * 60 * 1000; // last 7 days - break; - case "month": - cutoff = now - 30 * 24 * 60 * 60 * 1000; // last 30 days - break; - default: - cutoff = 0; - } - return data.filter((d) => new Date(d.time).getTime() >= cutoff); -} - const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { const eth0Data = getEth0TimeSeries(checks); - const xAxisFormatter = getXAxisFormatter(checks); - const filteredEth0Data = filterByDateRange(eth0Data, dateRange); return ( <> @@ -42,66 +18,120 @@ const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { setDateRange={setDateRange} /> ); }; +Network.propTypes = { + net: PropTypes.array, + checks: PropTypes.array, + isLoading: PropTypes.bool.isRequired, + dateRange: PropTypes.string.isRequired, + setDateRange: PropTypes.func.isRequired, +}; + export default Network; /* ---------- Helper functions ---------- */ function getEth0TimeSeries(checks) { + console.log(`[NetworkStats] Processing ${checks?.length || 0} checks`); + if (checks && checks.length > 0) { + console.log("[NetworkStats] First check _id:", checks[0]._id); + console.log("[NetworkStats] Last check _id:", checks[checks.length - 1]._id); + console.log( + "[NetworkStats] Sample check structure:", + JSON.stringify(checks[0], null, 2) + ); + } + const sorted = [...(checks || [])].sort((a, b) => new Date(a._id) - new Date(b._id)); const series = []; let prev = null; for (const check of sorted) { + console.log(`[NetworkStats] Processing check: ${check._id}`); const eth = (check.net || []).find((iface) => iface.name === "en0"); if (!eth) { + console.log("[NetworkStats] No en0 interface found in check:", check._id); prev = check; continue; } + + console.log(`[NetworkStats] Found en0 interface in check ${check._id}:`, eth); + if (prev) { + console.log(`[NetworkStats] Have previous check: ${prev._id}`); const prevEth = (prev.net || []).find((iface) => iface.name === "en0"); const t1 = new Date(check._id); const t0 = new Date(prev._id); + console.log(`[NetworkStats] Time difference: ${t1 - t0}ms`); + if (!prevEth || isNaN(t1) || isNaN(t0)) { + console.log("[NetworkStats] Skipping - invalid prev data or time"); prev = check; continue; } + const dt = (t1 - t0) / 1000; + console.log(`[NetworkStats] Delta time: ${dt}s`); + if (dt > 0) { - series.push({ - time: check._id, - bytesPerSec: (eth.bytesSent - prevEth.bytesSent) / dt, - packetsPerSec: (eth.packetsSent - prevEth.packetsSent) / dt, - errors: (eth.errIn ?? 0) + (eth.errOut ?? 0), - drops: (eth.dropIn ?? 0) + (eth.dropOut ?? 0), + const bytesField = eth.avgBytesSent; + const prevBytesField = prevEth.avgBytesSent; + + console.log(`[NetworkStats] Bytes comparison:`, { + current: bytesField, + previous: prevBytesField, + diff: bytesField - prevBytesField, }); + + if (bytesField !== undefined && prevBytesField !== undefined) { + const dataPoint = { + _id: check._id, // Use _id instead of time to match AreaChartBoxes + bytesPerSec: (bytesField - prevBytesField) / dt, + packetsPerSec: (eth.avgPacketsSent - prevEth.avgPacketsSent) / dt, + errors: (eth.avgErrIn ?? 0) + (eth.avgErrOut ?? 0), + drops: 0, // Skip drops for now since we don't have avgDropIn/Out + }; + console.log(`[NetworkStats] Adding data point:`, dataPoint); + series.push(dataPoint); + } else { + console.warn("[NetworkStats] Missing bytes fields:", { eth, prevEth }); + } + } else { + console.log("[NetworkStats] Skipping - zero or negative time delta"); } + } else { + console.log("[NetworkStats] No previous check yet, setting as prev"); } prev = check; } + console.log(`[NetworkStats] Generated ${series.length} time series data points`); + + // If we only have one check, create a single data point with absolute values + if (series.length === 0 && sorted.length === 1) { + const check = sorted[0]; + const eth = (check.net || []).find((iface) => iface.name === "en0"); + if (eth) { + console.log( + "[NetworkStats] Only one data point available, showing absolute values" + ); + series.push({ + _id: check._id, // Use _id instead of time to match AreaChartBoxes + bytesPerSec: eth.avgBytesSent || 0, // Show absolute value instead of rate + packetsPerSec: eth.avgPacketsSent || 0, // Show absolute value instead of rate + errors: (eth.avgErrIn ?? 0) + (eth.avgErrOut ?? 0), + drops: 0, + }); + } + } + + if (series.length > 0) { + console.log("[NetworkStats] Sample series data:", series[0]); + } return series; } - -function getXAxisFormatter(checks) { - if (!checks || checks.length === 0) return (val) => val; - const sorted = [...checks].sort((a, b) => new Date(a._id) - new Date(b._id)); - const first = new Date(sorted[0]._id); - const last = new Date(sorted[sorted.length - 1]._id); - const diffDays = (last - first) / (1000 * 60 * 60 * 24); - - return diffDays > 2 - ? (val) => - new Date(val).toLocaleDateString(undefined, { month: "short", day: "numeric" }) - : (val) => - new Date(val).toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - hour12: false, - }); -} From 765a1a24ed97825184a53a6c673ee006b5dff77e Mon Sep 17 00:00:00 2001 From: Owaise Date: Tue, 5 Aug 2025 21:04:15 +0530 Subject: [PATCH 004/131] Added en.json based values and console removal. --- .../Components/NetworkStats/NetworkCharts.jsx | 26 +++++----- .../NetworkStats/NetworkStatBoxes.jsx | 16 +++--- .../Details/Components/NetworkStats/index.jsx | 49 ++----------------- .../Pages/Infrastructure/Details/index.jsx | 5 +- client/src/locales/en.json | 13 +++++ 5 files changed, 43 insertions(+), 66 deletions(-) diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 343f7681b..7aad494ab 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -9,6 +9,7 @@ import { InfrastructureTooltip, } from "../../../../../Components/Charts/Utils/chartUtils"; import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; const getFormattedNetworkMetric = (value) => { if (typeof value !== "number" || isNaN(value)) return "0"; @@ -20,22 +21,23 @@ const getFormattedNetworkMetric = (value) => { const NetworkCharts = ({ eth0Data, dateRange }) => { const theme = useTheme(); + const { t } = useTranslation(); const configs = [ { type: "network-bytes", data: eth0Data, dataKeys: ["bytesPerSec"], - heading: "Bytes per second", + heading: t("bytesPerSecond"), strokeColor: theme.palette.info.main, gradientStartColor: theme.palette.info.main, - yLabel: "Bytes per second", + yLabel: t("bytesPerSecond"), xTick: , toolTip: ( @@ -45,16 +47,16 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { type: "network-packets", data: eth0Data, dataKeys: ["packetsPerSec"], - heading: "Packets per second", + heading: t("packetsPerSecond"), strokeColor: theme.palette.success.main, gradientStartColor: theme.palette.success.main, - yLabel: "Packets per second", + yLabel: t("packetsPerSecond"), xTick: , toolTip: ( Math.round(value).toLocaleString()} /> @@ -64,16 +66,16 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { type: "network-errors", data: eth0Data, dataKeys: ["errors"], - heading: "Errors", + heading: t("errors"), strokeColor: theme.palette.error.main, gradientStartColor: theme.palette.error.main, - yLabel: "Errors", + yLabel: t("errors"), xTick: , toolTip: ( Math.round(value).toLocaleString()} /> @@ -83,16 +85,16 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { type: "network-drops", data: eth0Data, dataKeys: ["drops"], - heading: "Drops", + heading: t("drops"), strokeColor: theme.palette.warning.main, gradientStartColor: theme.palette.warning.main, - yLabel: "Drops", + yLabel: t("drops"), xTick: , toolTip: ( Math.round(value).toLocaleString()} /> diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx index b2f2f7841..3125e86a9 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -3,6 +3,7 @@ import PropTypes from "prop-types"; import StatusBoxes from "../../../../../Components/StatusBoxes"; import StatBox from "../../../../../Components/StatBox"; import { Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; function formatBytes(bytes) { if (bytes === 0 || bytes == null) return "0 B"; @@ -18,11 +19,12 @@ function formatNumber(num) { } const NetworkStatBoxes = ({ shouldRender, net }) => { + const { t } = useTranslation(); const filtered = net?.filter((iface) => iface.name === "en0" || iface.name === "wlan0") || []; if (!net?.length) { - return No network stats available.; + return {t("noNetworkStatsAvailable")}; } return ( @@ -34,32 +36,32 @@ const NetworkStatBoxes = ({ shouldRender, net }) => { .map((iface) => [ , , , , , , ]) diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index 6b8ccb85a..03e0c019d 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -37,101 +37,62 @@ export default Network; /* ---------- Helper functions ---------- */ function getEth0TimeSeries(checks) { - console.log(`[NetworkStats] Processing ${checks?.length || 0} checks`); - if (checks && checks.length > 0) { - console.log("[NetworkStats] First check _id:", checks[0]._id); - console.log("[NetworkStats] Last check _id:", checks[checks.length - 1]._id); - console.log( - "[NetworkStats] Sample check structure:", - JSON.stringify(checks[0], null, 2) - ); - } - const sorted = [...(checks || [])].sort((a, b) => new Date(a._id) - new Date(b._id)); const series = []; let prev = null; for (const check of sorted) { - console.log(`[NetworkStats] Processing check: ${check._id}`); const eth = (check.net || []).find((iface) => iface.name === "en0"); if (!eth) { - console.log("[NetworkStats] No en0 interface found in check:", check._id); prev = check; continue; } - console.log(`[NetworkStats] Found en0 interface in check ${check._id}:`, eth); - if (prev) { - console.log(`[NetworkStats] Have previous check: ${prev._id}`); const prevEth = (prev.net || []).find((iface) => iface.name === "en0"); const t1 = new Date(check._id); const t0 = new Date(prev._id); - console.log(`[NetworkStats] Time difference: ${t1 - t0}ms`); if (!prevEth || isNaN(t1) || isNaN(t0)) { - console.log("[NetworkStats] Skipping - invalid prev data or time"); prev = check; continue; } const dt = (t1 - t0) / 1000; - console.log(`[NetworkStats] Delta time: ${dt}s`); if (dt > 0) { const bytesField = eth.avgBytesSent; const prevBytesField = prevEth.avgBytesSent; - console.log(`[NetworkStats] Bytes comparison:`, { - current: bytesField, - previous: prevBytesField, - diff: bytesField - prevBytesField, - }); - if (bytesField !== undefined && prevBytesField !== undefined) { const dataPoint = { - _id: check._id, // Use _id instead of time to match AreaChartBoxes + _id: check._id, bytesPerSec: (bytesField - prevBytesField) / dt, packetsPerSec: (eth.avgPacketsSent - prevEth.avgPacketsSent) / dt, errors: (eth.avgErrIn ?? 0) + (eth.avgErrOut ?? 0), - drops: 0, // Skip drops for now since we don't have avgDropIn/Out + drops: 0, }; - console.log(`[NetworkStats] Adding data point:`, dataPoint); series.push(dataPoint); - } else { - console.warn("[NetworkStats] Missing bytes fields:", { eth, prevEth }); } - } else { - console.log("[NetworkStats] Skipping - zero or negative time delta"); } - } else { - console.log("[NetworkStats] No previous check yet, setting as prev"); } prev = check; } - console.log(`[NetworkStats] Generated ${series.length} time series data points`); - // If we only have one check, create a single data point with absolute values if (series.length === 0 && sorted.length === 1) { const check = sorted[0]; const eth = (check.net || []).find((iface) => iface.name === "en0"); if (eth) { - console.log( - "[NetworkStats] Only one data point available, showing absolute values" - ); series.push({ - _id: check._id, // Use _id instead of time to match AreaChartBoxes - bytesPerSec: eth.avgBytesSent || 0, // Show absolute value instead of rate - packetsPerSec: eth.avgPacketsSent || 0, // Show absolute value instead of rate + _id: check._id, + bytesPerSec: eth.avgBytesSent || 0, + packetsPerSec: eth.avgPacketsSent || 0, errors: (eth.avgErrIn ?? 0) + (eth.avgErrOut ?? 0), drops: 0, }); } } - if (series.length > 0) { - console.log("[NetworkStats] Sample series data:", series[0]); - } return series; } diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx index 78b78fa78..72f384101 100644 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ b/client/src/Pages/Infrastructure/Details/index.jsx @@ -25,7 +25,6 @@ const BREADCRUMBS = [ { name: "details", path: "" }, ]; const InfrastructureDetails = () => { - // Local state const [dateRange, setDateRange] = useState("recent"); const [trigger, setTrigger] = useState(false); @@ -96,11 +95,11 @@ const InfrastructureDetails = () => { onChange={(e, v) => setTab(v)} > diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 230637474..bf949ea45 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -201,6 +201,9 @@ "avgCpuTemperature": "Average CPU Temperature", "bar": "Bar", "basicInformation": "Basic Information", + "bytesPerSecond": "Bytes per second", + "bytesReceived": "Bytes Received", + "bytesSent": "Bytes Sent", "bulkImport": { "fallbackPage": "Import a file to upload a list of servers in bulk", "invalidFileType": "Invalid file type", @@ -332,6 +335,11 @@ }, "disk": "Disk", "diskUsage": "Disk Usage", + "drops": "Drops", + "errors": "Errors", + "errorsIn": "Errors In", + "errorsOut": "Errors Out", + "details": "Details", "displayName": "Display name", "distributedRightCategoryTitle": "Monitor", "distributedStatusHeaderText": "Real-time, real-device coverage", @@ -632,7 +640,9 @@ }, "ms": "ms", "navControls": "Controls", + "network": "Network", "nextWindow": "Next window", + "noNetworkStatsAvailable": "No network stats available.", "notFoundButton": "Go to the main dashboard", "notificationConfig": { "description": "Select the notifications channels you want to use", @@ -744,6 +754,9 @@ "passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.", "saving": "Saving..." }, + "packetsPerSecond": "Packets per second", + "packetsReceived": "Packets Received", + "packetsSent": "Packets Sent", "pause": "Pause", "pingMonitoring": "Ping monitoring", "pingMonitoringDescription": "Check whether your server is available or not.", From f5d0b74ccf67e4412e3bcfdbc5997d034aad543d Mon Sep 17 00:00:00 2001 From: Owaise Date: Sat, 9 Aug 2025 12:14:25 +0530 Subject: [PATCH 005/131] Fixed the processing to be done in backend now. --- .../Components/Charts/Utils/chartUtils.jsx | 43 +++++++++ .../Components/NetworkStats/NetworkCharts.jsx | 3 + .../Details/Components/NetworkStats/index.jsx | 89 ++++--------------- server/src/db/mongo/modules/monitorModule.js | 55 +++++++++++- .../db/mongo/modules/monitorModuleQueries.js | 34 +++++-- 5 files changed, 144 insertions(+), 80 deletions(-) diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/Charts/Utils/chartUtils.jsx index af5f27c66..e2560aa2e 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/Charts/Utils/chartUtils.jsx @@ -91,6 +91,49 @@ const getFormattedPercentage = (value) => { return `${(value * 100).toFixed(2)}%`; }; +/** + * Custom tick component for rendering network bytes per second. + * + * @param {Object} props - The properties object. + * @param {number} props.x - The x-coordinate for the tick. + * @param {number} props.y - The y-coordinate for the tick. + * @param {Object} props.payload - The payload object containing tick data. + * @param {number} props.index - The index of the tick. + * @returns {JSX.Element|null} The rendered tick component or null for the first tick. + */ +export const NetworkTick = ({ x, y, payload, index }) => { + const theme = useTheme(); + if (index === 0) return null; + + const formatBytes = (bytes) => { + if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(1)} GB/s`; + if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB/s`; + if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)} KB/s`; + return `${bytes} B/s`; + }; + + return ( + + {formatBytes(payload?.value)} + + ); +}; + +NetworkTick.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + index: PropTypes.number, +}; + + /** * Custom tooltip component for displaying infrastructure data. * diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 7aad494ab..74ddb6bae 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -7,6 +7,7 @@ import InfraAreaChart from "../../../../../Pages/Infrastructure/Details/Componen import { TzTick, InfrastructureTooltip, + NetworkTick, } from "../../../../../Components/Charts/Utils/chartUtils"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; @@ -23,6 +24,7 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { const theme = useTheme(); const { t } = useTranslation(); + const configs = [ { type: "network-bytes", @@ -33,6 +35,7 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { gradientStartColor: theme.palette.info.main, yLabel: t("bytesPerSecond"), xTick: , + yTick: , toolTip: ( { - const eth0Data = getEth0TimeSeries(checks); + const eth0Data = (checks || []) + .map((check) => { + const en0 = (check.net || []).find((iface) => iface.name === "en0"); + if (!en0) return null; + + return { + _id: check._id, + bytesPerSec: en0.avgBytesRecv, + packetsPerSec: en0.avgPacketsRecv, + errors: (en0.avgErrOut ?? 0), + drops: (en0.avgDropOut ?? 0) + }; + }) + .filter(Boolean); + + console.log(eth0Data); return ( <> - + - + ); }; @@ -34,65 +43,3 @@ Network.propTypes = { }; export default Network; - -/* ---------- Helper functions ---------- */ -function getEth0TimeSeries(checks) { - const sorted = [...(checks || [])].sort((a, b) => new Date(a._id) - new Date(b._id)); - const series = []; - let prev = null; - - for (const check of sorted) { - const eth = (check.net || []).find((iface) => iface.name === "en0"); - if (!eth) { - prev = check; - continue; - } - - if (prev) { - const prevEth = (prev.net || []).find((iface) => iface.name === "en0"); - const t1 = new Date(check._id); - const t0 = new Date(prev._id); - - if (!prevEth || isNaN(t1) || isNaN(t0)) { - prev = check; - continue; - } - - const dt = (t1 - t0) / 1000; - - if (dt > 0) { - const bytesField = eth.avgBytesSent; - const prevBytesField = prevEth.avgBytesSent; - - if (bytesField !== undefined && prevBytesField !== undefined) { - const dataPoint = { - _id: check._id, - bytesPerSec: (bytesField - prevBytesField) / dt, - packetsPerSec: (eth.avgPacketsSent - prevEth.avgPacketsSent) / dt, - errors: (eth.avgErrIn ?? 0) + (eth.avgErrOut ?? 0), - drops: 0, - }; - series.push(dataPoint); - } - } - } - prev = check; - } - - // If we only have one check, create a single data point with absolute values - if (series.length === 0 && sorted.length === 1) { - const check = sorted[0]; - const eth = (check.net || []).find((iface) => iface.name === "en0"); - if (eth) { - series.push({ - _id: check._id, - bytesPerSec: eth.avgBytesSent || 0, - packetsPerSec: eth.avgPacketsSent || 0, - errors: (eth.avgErrIn ?? 0) + (eth.avgErrOut ?? 0), - drops: 0, - }); - } - } - - return series; -} diff --git a/server/src/db/mongo/modules/monitorModule.js b/server/src/db/mongo/modules/monitorModule.js index 76f055f94..4cd28ea9d 100755 --- a/server/src/db/mongo/modules/monitorModule.js +++ b/server/src/db/mongo/modules/monitorModule.js @@ -326,10 +326,53 @@ class MonitorModule { } }; + processNetworkRates = (checks) => { + if (!Array.isArray(checks) || checks.length === 0) return []; + + const sorted = [...checks].sort((a, b) => new Date(a._id) - new Date(b._id)); + const lastSeen = {}; + + for (const check of sorted) { + if (!Array.isArray(check.net)) { + check.net = []; + continue; + } + + const newNet = []; + + for (const iface of check.net) { + const prev = lastSeen[iface.name]; + const t1 = new Date(check._id); + + if (prev) { + const t0 = new Date(prev._id); + const dt = (t1 - t0) / 1000; + + if (dt > 0) { + newNet.push({ + name: iface.name, + avgBytesRecv: (iface.avgBytesRecv - prev.avgBytesRecv), + avgPacketsRecv: (iface.avgPacketsRecv - prev.avgPacketsRecv), + avgErrOut: iface.avgErrOut - prev.avgErrOut, + avgDropOut: iface.avgDropOut, + }); + } + } + + lastSeen[iface.name] = { ...iface, _id: check._id }; + } + + check.net = newNet; + } + + return sorted; + }; + getHardwareDetailsById = async ({ monitorId, dateRange }) => { try { const monitor = await this.Monitor.findById(monitorId); const dates = this.getDateRange(dateRange); + const formatLookup = { recent: "%Y-%m-%dT%H:%M:00Z", day: "%Y-%m-%dT%H:00:00Z", @@ -337,13 +380,19 @@ class MonitorModule { month: "%Y-%m-%dT00:00:00Z", }; const dateString = formatLookup[dateRange]; + const hardwareStats = await this.HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); - const monitorStats = { + const stats = hardwareStats[0]; + if (stats && stats.checks) { + // Replace net with per-second rates + stats.checks = this.processNetworkRates(stats.checks); + } + + return { ...monitor.toObject(), - stats: hardwareStats[0], + stats, }; - return monitorStats; } catch (error) { error.service = SERVICE_NAME; error.method = "getHardwareDetailsById"; diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js index df15593e2..79804f183 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -393,7 +393,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { 0, ], }, - bytesSent: { + avgBytesSent: { $avg: { $map: { input: "$net", @@ -404,7 +404,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, - bytesRecv: { + avgBytesRecv: { $avg: { $map: { input: "$net", @@ -415,7 +415,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, - packetsSent: { + avgPacketsSent: { $avg: { $map: { input: "$net", @@ -426,7 +426,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, - packetsRecv: { + avgPacketsRecv: { $avg: { $map: { input: "$net", @@ -437,7 +437,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, - errIn: { + avgErrIn: { $avg: { $map: { input: "$net", @@ -448,7 +448,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, - errOut: { + avgErrOut: { $avg: { $map: { input: "$net", @@ -459,6 +459,28 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, }, }, + avgDropIn: { + $avg: { + $map: { + input: "$net", + as: "netArray", + in: { + $arrayElemAt: ["$$netArray.drop_in", "$$netIndex"], + }, + }, + }, + }, + avgDropOut: { + $avg: { + $map: { + input: "$net", + as: "netArray", + in: { + $arrayElemAt: ["$$netArray.drop_out", "$$netIndex"], + }, + }, + }, + }, }, }, }, From 47c231a2141675df72cf68efca55b7340d942a89 Mon Sep 17 00:00:00 2001 From: Owaise Date: Sat, 9 Aug 2025 12:17:25 +0530 Subject: [PATCH 006/131] Formating done. --- .../src/Components/Charts/Utils/chartUtils.jsx | 1 - .../Components/NetworkStats/NetworkCharts.jsx | 1 - .../Details/Components/NetworkStats/index.jsx | 16 +++++++++++----- server/src/db/mongo/modules/monitorModule.js | 4 ++-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/Charts/Utils/chartUtils.jsx index e2560aa2e..40eb5b539 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/Charts/Utils/chartUtils.jsx @@ -133,7 +133,6 @@ NetworkTick.propTypes = { index: PropTypes.number, }; - /** * Custom tooltip component for displaying infrastructure data. * diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 74ddb6bae..5dd9feaad 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -24,7 +24,6 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { const theme = useTheme(); const { t } = useTranslation(); - const configs = [ { type: "network-bytes", diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index 06cb25d17..f8bdf5e8a 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -10,11 +10,11 @@ const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { if (!en0) return null; return { - _id: check._id, + _id: check._id, bytesPerSec: en0.avgBytesRecv, packetsPerSec: en0.avgPacketsRecv, - errors: (en0.avgErrOut ?? 0), - drops: (en0.avgDropOut ?? 0) + errors: en0.avgErrOut ?? 0, + drops: en0.avgDropOut ?? 0, }; }) .filter(Boolean); @@ -23,13 +23,19 @@ const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { return ( <> - + - + ); }; diff --git a/server/src/db/mongo/modules/monitorModule.js b/server/src/db/mongo/modules/monitorModule.js index 4cd28ea9d..f0b7d190c 100755 --- a/server/src/db/mongo/modules/monitorModule.js +++ b/server/src/db/mongo/modules/monitorModule.js @@ -351,8 +351,8 @@ class MonitorModule { if (dt > 0) { newNet.push({ name: iface.name, - avgBytesRecv: (iface.avgBytesRecv - prev.avgBytesRecv), - avgPacketsRecv: (iface.avgPacketsRecv - prev.avgPacketsRecv), + avgBytesRecv: iface.avgBytesRecv - prev.avgBytesRecv, + avgPacketsRecv: iface.avgPacketsRecv - prev.avgPacketsRecv, avgErrOut: iface.avgErrOut - prev.avgErrOut, avgDropOut: iface.avgDropOut, }); From ed1b88f1fdcfe65cdf9e314753aeea0ca766206f Mon Sep 17 00:00:00 2001 From: karenvicent Date: Mon, 11 Aug 2025 18:21:23 -0400 Subject: [PATCH 007/131] refactor login page --- client/src/Pages/Auth/Login/index.jsx | 83 ++++--------------- .../src/Pages/Auth/hooks/useLoadingSubmit.jsx | 22 +++++ client/src/Pages/Auth/hooks/useLoginForm.jsx | 19 +++++ .../src/Pages/Auth/hooks/useLoginSubmit.jsx | 41 +++++++++ .../Pages/Auth/hooks/useValidateLoginForm.jsx | 31 +++++++ 5 files changed, 130 insertions(+), 66 deletions(-) create mode 100644 client/src/Pages/Auth/hooks/useLoadingSubmit.jsx create mode 100644 client/src/Pages/Auth/hooks/useLoginForm.jsx create mode 100644 client/src/Pages/Auth/hooks/useLoginSubmit.jsx create mode 100644 client/src/Pages/Auth/hooks/useValidateLoginForm.jsx diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx index d6b281c27..ed9f2e0ff 100644 --- a/client/src/Pages/Auth/Login/index.jsx +++ b/client/src/Pages/Auth/Login/index.jsx @@ -3,84 +3,35 @@ import Stack from "@mui/material/Stack"; import Button from "@mui/material/Button"; import TextInput from "../../../Components/Inputs/TextInput"; import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import { loginCredentials } from "../../../Validation/validation"; import TextLink from "../../../Components/TextLink"; import AuthPageWrapper from "../components/AuthPageWrapper"; // Utils -import { login } from "../../../Features/Auth/authSlice"; -import { useNavigate } from "react-router-dom"; -import { useDispatch } from "react-redux"; -import { useState } from "react"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import { createToast } from "../../../Utils/toastUtils"; +import useLoginForm from "../hooks/useLoginForm"; +import useValidateLoginForm from "../hooks/useValidateLoginForm"; +import useLoginSubmit from "../hooks/useLoginSubmit"; +import useLoadingSubmit from "../hooks/useLoadingSubmit"; const Login = () => { - // Local state - const [form, setForm] = useState({ - email: "", - password: "", - }); - const [errors, setErrors] = useState({ - email: "", - password: "", - }); - // Hooks const { t } = useTranslation(); const theme = useTheme(); - const dispatch = useDispatch(); - const navigate = useNavigate(); + const [form, onChange] = useLoginForm(); + const [errors, setErrors, validateField, validateForm] = useValidateLoginForm(); + const [handleLoginSubmit] = useLoginSubmit(); + const { submitting, executeSubmit } = useLoadingSubmit(); // Handlers - const onChange = (e) => { - let { name, value } = e.target; - if (name === "email") { - value = value.toLowerCase(); - } - const updatedForm = { ...form, [name]: value }; - const { error } = loginCredentials.validate({ [name]: value }); - setForm(updatedForm); - setErrors((prev) => ({ - ...prev, - [name]: error?.details?.[0]?.message || "", - })); + const handleChange = (e) => { + onChange(e); + validateField(e.target.name, e.target.value); }; const onSubmit = async (e) => { e.preventDefault(); - const toSubmit = { ...form }; - const { error } = loginCredentials.validate(toSubmit, { abortEarly: false }); - if (error) { - const formErrors = {}; - for (const err of error.details) { - formErrors[err.path[0]] = err.message; - } - setErrors(formErrors); - return; - } - const action = await dispatch(login(form)); - if (action.payload.success) { - navigate("/uptime"); - createToast({ - body: t("auth.login.toasts.success"), - }); - } else { - if (action.payload) { - if (action.payload.msg === "Incorrect password") - setErrors({ - password: t("auth.login.errors.password.incorrect"), - }); - // dispatch errors - createToast({ - body: t("auth.login.toasts.incorrectPassword"), - }); - } else { - // unknown errors - createToast({ - body: t("common.toasts.unknownError"), - }); - } - } + const isValid = validateForm(form); + if (!isValid) return; + await executeSubmit(() => handleLoginSubmit(form, setErrors)); }; return ( { placeholder={t("auth.common.inputs.email.placeholder")} autoComplete="email" value={form.email} - onChange={onChange} + onChange={handleChange} error={errors.email ? true : false} helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js /> @@ -120,7 +71,7 @@ const Login = () => { placeholder="••••••••••" autoComplete="current-password" value={form.password} - onChange={onChange} + onChange={handleChange} error={errors.password ? true : false} helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js endAdornment={} @@ -130,6 +81,7 @@ const Login = () => { color="accent" type="submit" sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }} + loading={submitting} > Login @@ -147,5 +99,4 @@ const Login = () => { ); }; - export default Login; diff --git a/client/src/Pages/Auth/hooks/useLoadingSubmit.jsx b/client/src/Pages/Auth/hooks/useLoadingSubmit.jsx new file mode 100644 index 000000000..a779165b6 --- /dev/null +++ b/client/src/Pages/Auth/hooks/useLoadingSubmit.jsx @@ -0,0 +1,22 @@ +// Hook to avoid double submits and manage loading state +import { useState, useCallback } from "react"; + +const useLoadingSubmit = () => { + const [submitting, setSubmitting] = useState(false); + const executeSubmit = useCallback( + async (submitFunction) => { + if (submitting) return; + setSubmitting(true); + try { + return await submitFunction(); + } finally { + setSubmitting(false); + } + }, + [submitting] + ); + + return { submitting, executeSubmit }; +}; + +export default useLoadingSubmit; diff --git a/client/src/Pages/Auth/hooks/useLoginForm.jsx b/client/src/Pages/Auth/hooks/useLoginForm.jsx new file mode 100644 index 000000000..61814d723 --- /dev/null +++ b/client/src/Pages/Auth/hooks/useLoginForm.jsx @@ -0,0 +1,19 @@ +import { useState } from "react"; + +const useLoginForm = () => { + const [form, setForm] = useState({ + email: "", + password: "", + }); + const onChange = (e) => { + let { name, value } = e.target; + if (name === "email") { + value = value.toLowerCase(); + } + const updatedForm = { ...form, [name]: value }; + setForm(updatedForm); + }; + return [form, onChange]; +}; + +export default useLoginForm; diff --git a/client/src/Pages/Auth/hooks/useLoginSubmit.jsx b/client/src/Pages/Auth/hooks/useLoginSubmit.jsx new file mode 100644 index 000000000..e15cd98a0 --- /dev/null +++ b/client/src/Pages/Auth/hooks/useLoginSubmit.jsx @@ -0,0 +1,41 @@ +import { useDispatch } from "react-redux"; +import { login } from "../../../Features/Auth/authSlice"; +import { useNavigate } from "react-router-dom"; +import { createToast } from "../../../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +const useLoginSubmit = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { t } = useTranslation(); + + const handleLoginSubmit = async (form, setErrors) => { + const action = await dispatch(login(form)); + if (action.payload.success) { + navigate("/uptime"); + createToast({ + body: t("auth.login.toasts.success"), + }); + } else { + if (action.payload) { + console.log(action.payload); + if (action.payload.msg === "Incorrect password") + setErrors({ + password: t("auth.login.errors.password.incorrect"), + }); + // dispatch errors + createToast({ + body: t("auth.login.toasts.incorrectPassword"), + }); + } else { + // unknown errors + createToast({ + body: t("common.toasts.unknownError"), + }); + } + } + }; + return [handleLoginSubmit]; +}; + +export default useLoginSubmit; diff --git a/client/src/Pages/Auth/hooks/useValidateLoginForm.jsx b/client/src/Pages/Auth/hooks/useValidateLoginForm.jsx new file mode 100644 index 000000000..f50c777f3 --- /dev/null +++ b/client/src/Pages/Auth/hooks/useValidateLoginForm.jsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { loginCredentials } from "../../../Validation/validation"; + +const useValidateLoginForm = () => { + const [errors, setErrors] = useState({ + email: "", + password: "", + }); + const validateField = (name, value) => { + const { error } = loginCredentials.validate({ [name]: value }); + setErrors((prev) => ({ + ...prev, + [name]: error?.details?.[0]?.message || "", + })); + }; + const validateForm = (form) => { + const { error } = loginCredentials.validate(form, { abortEarly: false }); + if (error) { + const formErrors = {}; + for (const err of error.details) { + formErrors[err.path[0]] = err.message; + } + setErrors(formErrors); + return false; + } + return true; + }; + return [errors, setErrors, validateField, validateForm]; +}; + +export default useValidateLoginForm; From 781854d85c6faf299e9a05d0c6786c4373047d43 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Mon, 11 Aug 2025 18:42:33 -0400 Subject: [PATCH 008/131] delete console.log --- client/src/Pages/Auth/hooks/useLoginSubmit.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/Pages/Auth/hooks/useLoginSubmit.jsx b/client/src/Pages/Auth/hooks/useLoginSubmit.jsx index e15cd98a0..ed3bbec67 100644 --- a/client/src/Pages/Auth/hooks/useLoginSubmit.jsx +++ b/client/src/Pages/Auth/hooks/useLoginSubmit.jsx @@ -18,7 +18,6 @@ const useLoginSubmit = () => { }); } else { if (action.payload) { - console.log(action.payload); if (action.payload.msg === "Incorrect password") setErrors({ password: t("auth.login.errors.password.incorrect"), From 355a445ead81c2e647022c20c7fad408164946de Mon Sep 17 00:00:00 2001 From: Owaise Date: Tue, 12 Aug 2025 10:05:58 +0530 Subject: [PATCH 009/131] Fixed the code for linus, windows and mac based. Also made changes to pipeline. --- .../Details/Components/NetworkStats/index.jsx | 28 +-- server/src/db/mongo/modules/monitorModule.js | 46 ----- .../db/mongo/modules/monitorModuleQueries.js | 162 +++++++++--------- 3 files changed, 100 insertions(+), 136 deletions(-) diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index f8bdf5e8a..22a6c3112 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -3,24 +3,32 @@ import NetworkStatBoxes from "./NetworkStatBoxes"; import NetworkCharts from "./NetworkCharts"; import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; -const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { - const eth0Data = (checks || []) +const getNetworkInterfaceData = (checks) => { + const interfaceNames = ["eth0", "Ethernet", "en0"]; + + return (checks || []) .map((check) => { - const en0 = (check.net || []).find((iface) => iface.name === "en0"); - if (!en0) return null; + const networkInterface = (check.net || []).find((iface) => + interfaceNames.includes(iface.name) + ); + + if (!networkInterface) { + return null; + } return { _id: check._id, - bytesPerSec: en0.avgBytesRecv, - packetsPerSec: en0.avgPacketsRecv, - errors: en0.avgErrOut ?? 0, - drops: en0.avgDropOut ?? 0, + bytesPerSec: networkInterface.avgBytesRecv, + packetsPerSec: networkInterface.avgPacketsRecv, + errors: networkInterface.avgErrOut ?? 0, + drops: networkInterface.avgDropOut ?? 0, }; }) .filter(Boolean); +}; - console.log(eth0Data); - +const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { + const eth0Data = getNetworkInterfaceData(checks); return ( <> { - if (!Array.isArray(checks) || checks.length === 0) return []; - - const sorted = [...checks].sort((a, b) => new Date(a._id) - new Date(b._id)); - const lastSeen = {}; - - for (const check of sorted) { - if (!Array.isArray(check.net)) { - check.net = []; - continue; - } - - const newNet = []; - - for (const iface of check.net) { - const prev = lastSeen[iface.name]; - const t1 = new Date(check._id); - - if (prev) { - const t0 = new Date(prev._id); - const dt = (t1 - t0) / 1000; - - if (dt > 0) { - newNet.push({ - name: iface.name, - avgBytesRecv: iface.avgBytesRecv - prev.avgBytesRecv, - avgPacketsRecv: iface.avgPacketsRecv - prev.avgPacketsRecv, - avgErrOut: iface.avgErrOut - prev.avgErrOut, - avgDropOut: iface.avgDropOut, - }); - } - } - - lastSeen[iface.name] = { ...iface, _id: check._id }; - } - - check.net = newNet; - } - - return sorted; - }; - getHardwareDetailsById = async ({ monitorId, dateRange }) => { try { const monitor = await this.Monitor.findById(monitorId); @@ -384,10 +342,6 @@ class MonitorModule { const hardwareStats = await this.HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); const stats = hardwareStats[0]; - if (stats && stats.checks) { - // Replace net with per-second rates - stats.checks = this.processNetworkRates(stats.checks); - } return { ...monitor.toObject(), diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js index b3417d05a..c6812b1bc 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -378,108 +378,110 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, net: { $map: { - input: { $range: [0, "$$netCount"] }, + input: { + $range: [0, { $size: { $arrayElemAt: ["$net", 0] } }], + }, as: "netIndex", in: { name: { $arrayElemAt: [ { $map: { - input: "$net", - as: "netArray", - in: { $arrayElemAt: ["$$netArray.name", "$$netIndex"] }, + input: { $arrayElemAt: ["$net", 0] }, + as: "iface", + in: "$$iface.name", }, }, - 0, + "$$netIndex", ], }, avgBytesSent: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.bytes_sent", "$$netIndex"], - }, + $subtract: [ + { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.bytes_sent", + }, + }, + "$$netIndex", + ], }, - }, + { + $arrayElemAt: [ + { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_sent" } }, + "$$netIndex", + ], + }, + ], }, avgBytesRecv: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.bytes_recv", "$$netIndex"], - }, + $subtract: [ + { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.bytes_recv", + }, + }, + "$$netIndex", + ], }, - }, + { + $arrayElemAt: [ + { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_recv" } }, + "$$netIndex", + ], + }, + ], }, avgPacketsSent: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.packets_sent", "$$netIndex"], - }, + $subtract: [ + { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.packets_sent", + }, + }, + "$$netIndex", + ], }, - }, + { + $arrayElemAt: [ + { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.packets_sent" } }, + "$$netIndex", + ], + }, + ], }, avgPacketsRecv: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.packets_recv", "$$netIndex"], - }, + $subtract: [ + { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.packets_recv", + }, + }, + "$$netIndex", + ], }, - }, - }, - avgErrIn: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.err_in", "$$netIndex"], - }, + { + $arrayElemAt: [ + { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.packets_recv" } }, + "$$netIndex", + ], }, - }, - }, - avgErrOut: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.err_out", "$$netIndex"], - }, - }, - }, - }, - avgDropIn: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.drop_in", "$$netIndex"], - }, - }, - }, - }, - avgDropOut: { - $avg: { - $map: { - input: "$net", - as: "netArray", - in: { - $arrayElemAt: ["$$netArray.drop_out", "$$netIndex"], - }, - }, - }, + ], }, }, }, From e27cc8a12babcbe8f64b072403b5dfb9a14cdb08 Mon Sep 17 00:00:00 2001 From: Owaise Date: Tue, 12 Aug 2025 23:48:47 +0530 Subject: [PATCH 010/131] Added delta instead of avg, and formatted the code to re-use already existing code. --- .../Components/Charts/Utils/chartUtils.jsx | 18 +-- .../Components/NetworkStats/NetworkCharts.jsx | 35 +++-- .../NetworkStats/NetworkStatBoxes.jsx | 19 +-- .../Details/Components/NetworkStats/index.jsx | 31 +++-- .../Details/Hooks/useHardwareUtils.jsx | 25 +++- server/src/db/mongo/modules/monitorModule.js | 11 ++ .../db/mongo/modules/monitorModuleQueries.js | 122 +++++++++++++++++- 7 files changed, 201 insertions(+), 60 deletions(-) diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/Charts/Utils/chartUtils.jsx index 40eb5b539..a7db3ea93 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/Charts/Utils/chartUtils.jsx @@ -101,16 +101,17 @@ const getFormattedPercentage = (value) => { * @param {number} props.index - The index of the tick. * @returns {JSX.Element|null} The rendered tick component or null for the first tick. */ -export const NetworkTick = ({ x, y, payload, index }) => { +export const NetworkTick = ({ x, y, payload, index, formatter}) => { const theme = useTheme(); if (index === 0) return null; - const formatBytes = (bytes) => { - if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(1)} GB/s`; - if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB/s`; - if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)} KB/s`; - return `${bytes} B/s`; - }; + if (formatter === undefined) { + formatter = (value, space=false) => { + if (typeof value !== "number") return value; + // need to add space between value and unit + return `${(value / 1024).toFixed(2)}${space ? " " : ""}Kbps`; + }; + } return ( { fontSize={11} fontWeight={400} > - {formatBytes(payload?.value)} + {formatter(payload?.value, true)} ); }; @@ -131,6 +132,7 @@ NetworkTick.propTypes = { y: PropTypes.number, payload: PropTypes.object, index: PropTypes.number, + formatter: PropTypes.func, }; /** diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 5dd9feaad..84325c29e 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -1,9 +1,7 @@ -// NetworkCharts.jsx import PropTypes from "prop-types"; -import { Stack } from "@mui/material"; +import { Stack, Typography } from "@mui/material"; import InfraAreaChart from "../../../../../Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart"; -// Utils import { TzTick, InfrastructureTooltip, @@ -11,43 +9,42 @@ import { } from "../../../../../Components/Charts/Utils/chartUtils"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; -const getFormattedNetworkMetric = (value) => { - if (typeof value !== "number" || isNaN(value)) return "0"; - if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(1)} GB/s`; - if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(1)} MB/s`; - if (value >= 1024) return `${(value / 1024).toFixed(1)} KB/s`; - return `${Math.round(value)} B/s`; -}; - -const NetworkCharts = ({ eth0Data, dateRange }) => { +const NetworkCharts = ({ ethernetData, dateRange }) => { const theme = useTheme(); const { t } = useTranslation(); + const {formatBytesString} = useHardwareUtils(); + + if (!ethernetData?.length) { + return {t("noNetworkStatsAvailable")}; + } + const configs = [ { type: "network-bytes", - data: eth0Data, + data: ethernetData, dataKeys: ["bytesPerSec"], heading: t("bytesPerSecond"), strokeColor: theme.palette.info.main, gradientStartColor: theme.palette.info.main, yLabel: t("bytesPerSecond"), xTick: , - yTick: , + yTick: , toolTip: ( ), }, { type: "network-packets", - data: eth0Data, + data: ethernetData, dataKeys: ["packetsPerSec"], heading: t("packetsPerSecond"), strokeColor: theme.palette.success.main, @@ -66,7 +63,7 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { }, { type: "network-errors", - data: eth0Data, + data: ethernetData, dataKeys: ["errors"], heading: t("errors"), strokeColor: theme.palette.error.main, @@ -85,7 +82,7 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { }, { type: "network-drops", - data: eth0Data, + data: ethernetData, dataKeys: ["drops"], heading: t("drops"), strokeColor: theme.palette.warning.main, @@ -127,7 +124,7 @@ const NetworkCharts = ({ eth0Data, dateRange }) => { }; NetworkCharts.propTypes = { - eth0Data: PropTypes.array.isRequired, + ethernetData: PropTypes.array.isRequired, dateRange: PropTypes.string.isRequired, }; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx index 3125e86a9..98ba90dae 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -1,27 +1,19 @@ -// NetworkStatBoxes.jsx import PropTypes from "prop-types"; import StatusBoxes from "../../../../../Components/StatusBoxes"; import StatBox from "../../../../../Components/StatBox"; import { Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; -function formatBytes(bytes) { - if (bytes === 0 || bytes == null) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB", "TB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; -} - -// Format numbers with commas function formatNumber(num) { return num != null ? num.toLocaleString() : "0"; } -const NetworkStatBoxes = ({ shouldRender, net }) => { +const NetworkStatBoxes = ({ shouldRender, net, ifaceName }) => { const { t } = useTranslation(); - const filtered = - net?.filter((iface) => iface.name === "en0" || iface.name === "wlan0") || []; + const { formatBytes } = useHardwareUtils(); + + const filtered = net?.filter((iface) => iface.name === ifaceName) || []; if (!net?.length) { return {t("noNetworkStatsAvailable")}; @@ -83,6 +75,7 @@ NetworkStatBoxes.propTypes = { err_out: PropTypes.number, }) ), + ifaceName: PropTypes.string.isRequired, }; export default NetworkStatBoxes; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index 22a6c3112..623720aba 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -3,37 +3,40 @@ import NetworkStatBoxes from "./NetworkStatBoxes"; import NetworkCharts from "./NetworkCharts"; import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; -const getNetworkInterfaceData = (checks) => { +const getInterfaceName = (net) => { const interfaceNames = ["eth0", "Ethernet", "en0"]; + const found = (net || []).find((iface) => interfaceNames.includes(iface.name)); + return found ? found.name : null; +}; +const getNetworkInterfaceData = (checks, ifaceName) => { return (checks || []) .map((check) => { - const networkInterface = (check.net || []).find((iface) => - interfaceNames.includes(iface.name) + const networkInterface = (check.net || []).find( + (iface) => iface.name === ifaceName ); - - if (!networkInterface) { - return null; - } - + if (!networkInterface) return null; return { _id: check._id, - bytesPerSec: networkInterface.avgBytesRecv, - packetsPerSec: networkInterface.avgPacketsRecv, - errors: networkInterface.avgErrOut ?? 0, - drops: networkInterface.avgDropOut ?? 0, + bytesPerSec: networkInterface.deltaBytesRecv, + packetsPerSec: networkInterface.deltaPacketsRecv, + errors: networkInterface.deltaErrOut ?? 0, + drops: networkInterface.deltaDropOut ?? 0, }; }) .filter(Boolean); }; const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { - const eth0Data = getNetworkInterfaceData(checks); + const ifaceName = getInterfaceName(net); + const ethernetData = getNetworkInterfaceData(checks, ifaceName); + return ( <> { setDateRange={setDateRange} /> diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx index a2e4c5153..930ca7ba3 100644 --- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx +++ b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx @@ -57,7 +57,7 @@ const useHardwareUtils = () => { if (GB >= 1) { return ( <> - {Number(GB.toFixed(0))} + {Number(GB.toFixed(2))} {space ? " " : ""} {t("gb")} @@ -65,7 +65,7 @@ const useHardwareUtils = () => { } else { return ( <> - {Number(MB.toFixed(0))} + {Number(MB.toFixed(2))} {space ? " " : ""} {t("mb")} @@ -73,6 +73,26 @@ const useHardwareUtils = () => { } }; + const formatBytesString = (bytes, space = false) => { + if ( + bytes === undefined || + bytes === null || + typeof bytes !== "number" || + bytes === 0 + ) { + return `0${space ? " " : ""}${t("gb")}`; + } + + const GB = bytes / (1024 * 1024 * 1024); + const MB = bytes / (1024 * 1024); + + if (GB >= 1) { + return `${Number(GB.toFixed(2))}${space ? " " : ""}${t("gb")}`; + } else { + return `${Number(MB.toFixed(2))}${space ? " " : ""}${t("mb")}`; + } + }; + /** * Converts a decimal value to a percentage * @@ -134,6 +154,7 @@ const useHardwareUtils = () => { decimalToPercentage, buildTemps, getDimensions, + formatBytesString, }; }; diff --git a/server/src/db/mongo/modules/monitorModule.js b/server/src/db/mongo/modules/monitorModule.js index 0e301cdcf..3afe664f6 100755 --- a/server/src/db/mongo/modules/monitorModule.js +++ b/server/src/db/mongo/modules/monitorModule.js @@ -343,6 +343,17 @@ class MonitorModule { const stats = hardwareStats[0]; + if (stats?.net?.length) { + const elapsedSeconds = (new Date(dates.end).getTime() - new Date(dates.start).getTime()) / 1000; + stats.net = stats.net.map((iface) => ({ + ...iface, + bytesSentPerSec: iface.deltaBytesSent / elapsedSeconds, + bytesRecvPerSec: iface.deltaBytesRecv / elapsedSeconds, + packetsSentPerSec: iface.deltaPacketsSent / elapsedSeconds, + packetsRecvPerSec: iface.deltaPacketsRecv / elapsedSeconds, + })); + } + return { ...monitor.toObject(), stats, diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js index c6812b1bc..91fb83444 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -395,7 +395,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { "$$netIndex", ], }, - avgBytesSent: { + deltaBytesSent: { $subtract: [ { $arrayElemAt: [ @@ -417,7 +417,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, ], }, - avgBytesRecv: { + deltaBytesRecv: { $subtract: [ { $arrayElemAt: [ @@ -439,7 +439,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, ], }, - avgPacketsSent: { + deltaPacketsSent: { $subtract: [ { $arrayElemAt: [ @@ -461,7 +461,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, ], }, - avgPacketsRecv: { + deltaPacketsRecv: { $subtract: [ { $arrayElemAt: [ @@ -483,6 +483,120 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, ], }, + deltaErrIn: { + $subtract: [ + { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.err_in", + }, + }, + "$$netIndex", + ], + }, + { + $arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.err_in" } }, "$$netIndex"], + }, + ], + }, + deltaErrOut: { + $subtract: [ + { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.err_out", + }, + }, + "$$netIndex", + ], + }, + { + $arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.err_out" } }, "$$netIndex"], + }, + ], + }, + deltaDropIn: { + $subtract: [ + { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.drop_in", + }, + }, + "$$netIndex", + ], + }, + { + $arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.drop_in" } }, "$$netIndex"], + }, + ], + }, + deltaDropOut: { + $subtract: [ + { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.drop_out", + }, + }, + "$$netIndex", + ], + }, + { + $arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.drop_out" } }, "$$netIndex"], + }, + ], + }, + deltaFifoIn: { + $subtract: [ + { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.fifo_in", + }, + }, + "$$netIndex", + ], + }, + { + $arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.fifo_in" } }, "$$netIndex"], + }, + ], + }, + deltaFifoOut: { + $subtract: [ + { + $arrayElemAt: [ + { + $map: { + input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + as: "iface", + in: "$$iface.fifo_out", + }, + }, + "$$netIndex", + ], + }, + { + $arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.fifo_out" } }, "$$netIndex"], + }, + ], + }, }, }, }, From f5aefb1f0b573fa4623f0512ae98d1bdc910e71c Mon Sep 17 00:00:00 2001 From: Owaise Date: Tue, 12 Aug 2025 23:49:45 +0530 Subject: [PATCH 011/131] Ran formattor. --- client/src/Components/Charts/Utils/chartUtils.jsx | 4 ++-- .../Details/Components/NetworkStats/NetworkCharts.jsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/Charts/Utils/chartUtils.jsx index a7db3ea93..ce15d27cd 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/Charts/Utils/chartUtils.jsx @@ -101,12 +101,12 @@ const getFormattedPercentage = (value) => { * @param {number} props.index - The index of the tick. * @returns {JSX.Element|null} The rendered tick component or null for the first tick. */ -export const NetworkTick = ({ x, y, payload, index, formatter}) => { +export const NetworkTick = ({ x, y, payload, index, formatter }) => { const theme = useTheme(); if (index === 0) return null; if (formatter === undefined) { - formatter = (value, space=false) => { + formatter = (value, space = false) => { if (typeof value !== "number") return value; // need to add space between value and unit return `${(value / 1024).toFixed(2)}${space ? " " : ""}Kbps`; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 84325c29e..98eced859 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -15,7 +15,7 @@ const NetworkCharts = ({ ethernetData, dateRange }) => { const theme = useTheme(); const { t } = useTranslation(); - const {formatBytesString} = useHardwareUtils(); + const { formatBytesString } = useHardwareUtils(); if (!ethernetData?.length) { return {t("noNetworkStatsAvailable")}; @@ -31,7 +31,7 @@ const NetworkCharts = ({ ethernetData, dateRange }) => { gradientStartColor: theme.palette.info.main, yLabel: t("bytesPerSecond"), xTick: , - yTick: , + yTick: , toolTip: ( Date: Tue, 12 Aug 2025 14:30:11 -0400 Subject: [PATCH 012/131] move hooks to login file --- .../src/Pages/Auth/{ => Login}/hooks/useLoadingSubmit.jsx | 0 client/src/Pages/Auth/{ => Login}/hooks/useLoginForm.jsx | 0 .../src/Pages/Auth/{ => Login}/hooks/useLoginSubmit.jsx | 4 ++-- .../Pages/Auth/{ => Login}/hooks/useValidateLoginForm.jsx | 2 +- client/src/Pages/Auth/Login/index.jsx | 8 ++++---- 5 files changed, 7 insertions(+), 7 deletions(-) rename client/src/Pages/Auth/{ => Login}/hooks/useLoadingSubmit.jsx (100%) rename client/src/Pages/Auth/{ => Login}/hooks/useLoginForm.jsx (100%) rename client/src/Pages/Auth/{ => Login}/hooks/useLoginSubmit.jsx (88%) rename client/src/Pages/Auth/{ => Login}/hooks/useValidateLoginForm.jsx (91%) diff --git a/client/src/Pages/Auth/hooks/useLoadingSubmit.jsx b/client/src/Pages/Auth/Login/hooks/useLoadingSubmit.jsx similarity index 100% rename from client/src/Pages/Auth/hooks/useLoadingSubmit.jsx rename to client/src/Pages/Auth/Login/hooks/useLoadingSubmit.jsx diff --git a/client/src/Pages/Auth/hooks/useLoginForm.jsx b/client/src/Pages/Auth/Login/hooks/useLoginForm.jsx similarity index 100% rename from client/src/Pages/Auth/hooks/useLoginForm.jsx rename to client/src/Pages/Auth/Login/hooks/useLoginForm.jsx diff --git a/client/src/Pages/Auth/hooks/useLoginSubmit.jsx b/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx similarity index 88% rename from client/src/Pages/Auth/hooks/useLoginSubmit.jsx rename to client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx index ed3bbec67..6bc5716ff 100644 --- a/client/src/Pages/Auth/hooks/useLoginSubmit.jsx +++ b/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx @@ -1,7 +1,7 @@ import { useDispatch } from "react-redux"; -import { login } from "../../../Features/Auth/authSlice"; +import { login } from "../../../../Features/Auth/authSlice"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../Utils/toastUtils"; +import { createToast } from "../../../../Utils/toastUtils"; import { useTranslation } from "react-i18next"; const useLoginSubmit = () => { diff --git a/client/src/Pages/Auth/hooks/useValidateLoginForm.jsx b/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx similarity index 91% rename from client/src/Pages/Auth/hooks/useValidateLoginForm.jsx rename to client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx index f50c777f3..e5d045be8 100644 --- a/client/src/Pages/Auth/hooks/useValidateLoginForm.jsx +++ b/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { loginCredentials } from "../../../Validation/validation"; +import { loginCredentials } from "../../../../Validation/validation"; const useValidateLoginForm = () => { const [errors, setErrors] = useState({ diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx index ed9f2e0ff..d06ecef6d 100644 --- a/client/src/Pages/Auth/Login/index.jsx +++ b/client/src/Pages/Auth/Login/index.jsx @@ -8,10 +8,10 @@ import AuthPageWrapper from "../components/AuthPageWrapper"; // Utils import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import useLoginForm from "../hooks/useLoginForm"; -import useValidateLoginForm from "../hooks/useValidateLoginForm"; -import useLoginSubmit from "../hooks/useLoginSubmit"; -import useLoadingSubmit from "../hooks/useLoadingSubmit"; +import useLoginForm from "./hooks/useLoginForm"; +import useValidateLoginForm from "./hooks/useValidateLoginForm"; +import useLoginSubmit from "./hooks/useLoginSubmit"; +import useLoadingSubmit from "./hooks/useLoadingSubmit"; const Login = () => { // Hooks From 480ba034cebab20aa1ec5c3e170a594cc56b705f Mon Sep 17 00:00:00 2001 From: singh-kanwarpreet Date: Wed, 13 Aug 2025 14:47:09 +0530 Subject: [PATCH 013/131] Made NavBar Still --- client/src/Pages/Logs/index.jsx | 47 ++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/client/src/Pages/Logs/index.jsx b/client/src/Pages/Logs/index.jsx index db421e312..1b2a7411d 100644 --- a/client/src/Pages/Logs/index.jsx +++ b/client/src/Pages/Logs/index.jsx @@ -23,21 +23,42 @@ const Logs = () => { }; const BREADCRUMBS = [{ name: t("logsPage.title"), path: "/logs" }]; + + // Height of the header (Breadcrumbs + Tabs) + const HEADER_HEIGHT = theme.spacing(25); + return ( - - - + - - - - - {value === 0 && } - {value === 1 && } - {value === 2 && } - + + + + + + + + + {/* Main content below fixed header */} + + {value === 0 && } + {value === 1 && } + {value === 2 && } + + ); }; From 7ab5f4ab09e0ebe6f62287023c2b8653eacca8f5 Mon Sep 17 00:00:00 2001 From: singh-kanwarpreet Date: Wed, 13 Aug 2025 22:41:13 +0530 Subject: [PATCH 014/131] Refactored code --- client/src/Pages/Logs/Logs/index.jsx | 20 ++++++++++++++++++-- client/src/Pages/Logs/index.jsx | 24 +++++++++--------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index b65f9c0fd..a5bc9757e 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -50,8 +50,17 @@ const Logs = () => { { _id: "debug", name: t("logsPage.logLevelSelect.values.debug") }, ]; return ( - - + + {t("logsPage.description")} @@ -61,6 +70,13 @@ const Logs = () => { alignItems="center" gap={theme.spacing(4)} mt={theme.spacing(10)} + sx={{ + position: "sticky", + top: 80, + zIndex: 900, + backgroundColor: theme.palette.tertiary.main, + paddingY: theme.spacing(1), + }} > {t("logsPage.logLevelSelect.title")} { - setLogLevel(e.target.value); - }} - /> + + + {t("logsPage.logLevelSelect.title")} + setLogLevel(e.target.value)} - /> - + + + + + {t("logsPage.logLevelSelect.title")} + setSelectedInterface(e.target.value)} + label={t("networkInterface")} + > + {availableInterfaces.map((interfaceName) => ( + + {interfaceName} + + ))} + + + )} + + { } }; - const formatBytesString = (bytes, space = false) => { + const formatBytesPerSecondString = (bytesPerSec, space = false) => { if ( - bytes === undefined || - bytes === null || - typeof bytes !== "number" || - bytes === 0 + bytesPerSec === undefined || + bytesPerSec === null || + typeof bytesPerSec !== "number" || + bytesPerSec === 0 ) { - return `0${space ? " " : ""}${t("gb")}`; + return `0${space ? " " : ""}B/s`; } - const GB = bytes / (1024 * 1024 * 1024); - const MB = bytes / (1024 * 1024); + const GB = bytesPerSec / (1024 * 1024 * 1024); + const MB = bytesPerSec / (1024 * 1024); + const KB = bytesPerSec / 1024; if (GB >= 1) { - return `${Number(GB.toFixed(2))}${space ? " " : ""}${t("gb")}`; + return `${Number(GB.toFixed(1))}${space ? " " : ""}GB/s`; + } else if (MB >= 1) { + return `${Number(MB.toFixed(1))}${space ? " " : ""}MB/s`; + } else if (KB >= 1) { + return `${Number(KB.toFixed(1))}${space ? " " : ""}KB/s`; } else { - return `${Number(MB.toFixed(2))}${space ? " " : ""}${t("mb")}`; + return `${Number(bytesPerSec.toFixed(1))}${space ? " " : ""}B/s`; + } + }; + + const formatPacketsPerSecondString = (packetsPerSec, space = false) => { + if ( + packetsPerSec === undefined || + packetsPerSec === null || + typeof packetsPerSec !== "number" || + packetsPerSec === 0 + ) { + return `0${space ? " " : ""}pps`; + } + + const M = packetsPerSec / (1000 * 1000); + const K = packetsPerSec / 1000; + + if (M >= 1) { + return `${Number(M.toFixed(1))}${space ? " " : ""}Mpps`; + } else if (K >= 1) { + return `${Number(K.toFixed(1))}${space ? " " : ""}Kpps`; + } else { + return `${Math.round(packetsPerSec)}${space ? " " : ""}pps`; } }; @@ -154,7 +181,8 @@ const useHardwareUtils = () => { decimalToPercentage, buildTemps, getDimensions, - formatBytesString, + formatBytesPerSecondString, + formatPacketsPerSecondString, }; }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 08c60c372..857825c9f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -205,6 +205,10 @@ "bytesPerSecond": "Bytes per second", "bytesReceived": "Bytes Received", "bytesSent": "Bytes Sent", + "dataReceived": "Data Received", + "dataSent": "Data Sent", + "dataRate": "Data Rate", + "rate": "Rate", "bulkImport": { "fallbackPage": "Import a file to upload a list of servers in bulk", "invalidFileType": "Invalid file type", @@ -341,6 +345,10 @@ "errors": "Errors", "errorsIn": "Errors In", "errorsOut": "Errors Out", + "networkErrors": "Network Errors", + "networkDrops": "Network Drops", + "networkInterface": "Network Interface", + "selectInterface": "Select Interface", "details": "Details", "displayName": "Display name", "distributedRightCategoryTitle": "Monitor", @@ -763,6 +771,7 @@ }, "packetsPerSecond": "Packets per second", "packetsReceived": "Packets Received", + "packetsReceivedRate": "Packets Received Rate", "packetsSent": "Packets Sent", "pause": "Pause", "pingMonitoring": "Ping monitoring", From 22e0507fbdac36eaf165ceaf4c82bb9965005949 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 14 Aug 2025 09:34:30 -0700 Subject: [PATCH 023/131] fix arm dist health check --- docker/dist-arm/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/dist-arm/docker-compose.yaml b/docker/dist-arm/docker-compose.yaml index 37a5f4636..96bbf8f3f 100644 --- a/docker/dist-arm/docker-compose.yaml +++ b/docker/dist-arm/docker-compose.yaml @@ -23,7 +23,7 @@ services: volumes: - ./mongo/data:/data/db healthcheck: - test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] + test: ["CMD", "mongo", "--eval", "db.runCommand({ ping: 1 })", "--quiet"] interval: 5s timeout: 30s start_period: 0s From 0e767d65811d72412069116e16d4d2633257d32f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 14 Aug 2025 09:58:41 -0700 Subject: [PATCH 024/131] remove redis from workflows --- .github/workflows/deploy-images.yml | 12 ------------ .github/workflows/staging-deploy.yml | 11 ----------- 2 files changed, 23 deletions(-) diff --git a/.github/workflows/deploy-images.yml b/.github/workflows/deploy-images.yml index fa39fed85..53d7ab368 100644 --- a/.github/workflows/deploy-images.yml +++ b/.github/workflows/deploy-images.yml @@ -74,18 +74,6 @@ jobs: run: | docker push ghcr.io/bluewave-labs/checkmate-mongo:latest - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-redis:latest \ - -f ./docker/dist/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Redis Docker image - run: | - docker push ghcr.io/bluewave-labs/checkmate-redis:latest - docker-build-and-push-server-mono-multiarch: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index ea5477815..27b7e8690 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -71,17 +71,6 @@ jobs: - name: Push MongoDB Docker image run: docker push ghcr.io/bluewave-labs/checkmate:mongo-staging - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:redis-staging \ - -f ./docker/staging/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Redis Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:redis-staging - deploy-to-staging: needs: docker-build-and-push-server runs-on: ubuntu-latest From 9ae7314f546ab16788cda422084be4d3c8c31ddc Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 14 Aug 2025 10:00:20 -0700 Subject: [PATCH 025/131] add release workflow --- .github/workflows/deploy-images-on-release.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/deploy-images-on-release.yml b/.github/workflows/deploy-images-on-release.yml index cf06f7840..a9b2dfdb8 100644 --- a/.github/workflows/deploy-images-on-release.yml +++ b/.github/workflows/deploy-images-on-release.yml @@ -79,18 +79,6 @@ jobs: run: | docker push ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }} \ - -f ./docker/dist/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Redis Docker image - run: | - docker push ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }} - docker-build-and-push-server-mono-multiarch: runs-on: ubuntu-latest steps: From 9e05d024a86e1c5aa46d419c90033ac7811d5ad3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 14 Aug 2025 10:32:10 -0700 Subject: [PATCH 026/131] remove redis from production workflow --- .github/workflows/production-deploy.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml index 7764c488b..532197710 100644 --- a/.github/workflows/production-deploy.yml +++ b/.github/workflows/production-deploy.yml @@ -71,17 +71,6 @@ jobs: - name: Push MongoDB Docker image run: docker push ghcr.io/bluewave-labs/checkmate:mongo-demo - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:redis-demo \ - -f ./docker/prod/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Redis Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:redis-demo - deploy-to-demo: needs: docker-build-and-push-server runs-on: ubuntu-latest From f2c26c0eed91b4e79127821a9e99892238d4c90d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 14 Aug 2025 11:26:46 -0700 Subject: [PATCH 027/131] bump versoin --- client/vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/vite.config.js b/client/vite.config.js index 44a5905c5..df686d751 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -5,7 +5,7 @@ import { execSync } from "child_process"; export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); - let version = 2.3; + let version = "3.0-beta"; return { base: "/", From 0241797e7990d5901e48fecf7f3c4b102e06c9db Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:48:11 -0400 Subject: [PATCH 028/131] Update README.md --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ca55e38ab..048cf0706 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This repository contains both the frontend and the backend of Checkmate, an open-source, self-hosted monitoring tool for tracking server hardware, uptime, response times, and incidents in real-time with beautiful visualizations. Checkmate regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time. -Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status. +Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status. Capture can run on Linux, Windows, Mac, Raspberry Pi, or any device that can run Go. Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks. @@ -90,20 +90,22 @@ Feel free to ask questions or share your ideas - we'd love to hear from you! - Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5) - Website monitoring - Page speed monitoring -- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent +- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent - Docker monitoring - Ping monitoring - SSL monitoring - Port monitoring +- Game server monitoring (3.0) - Incidents at a glance - Status pages -- E-mail, Webhooks, Discord, Telegram, Slack notifications +- E-mail, Webhooks, Discord and Slack notifications - Scheduled maintenance - JSON query monitoring - Multi-language support for English, German, Japanese, Portuguese (Brazil), Russian, Turkish, Ukrainian, Vietnamese, Chinese (Traditional, Taiwan) **Short term roadmap:** +- Plugins that will help Checkmate get any information from a remote service (e.g database, etc) - Better notifications - Network monitoring - ..and a few more features @@ -145,7 +147,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec ## Contributing -We are [Alex](http://github.com/ajhollid) (team lead), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) helping individuals and businesses monitor their infra and servers. +We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) helping individuals and businesses monitor their infra and servers. We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 7000+ stars and attracted 90+ contributors from around the globe. From ae9eb494aec5db1b1c050bf261d711f02b464782 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 14 Aug 2025 12:48:49 -0700 Subject: [PATCH 029/131] add capture to prod and staging docker-compose --- docker/prod/docker-compose.yaml | 11 +++++++++++ docker/staging/docker-compose.yaml | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/docker/prod/docker-compose.yaml b/docker/prod/docker-compose.yaml index 61c6c021d..6e29fd90a 100755 --- a/docker/prod/docker-compose.yaml +++ b/docker/prod/docker-compose.yaml @@ -49,3 +49,14 @@ services: start_period: 0s start_interval: 1s retries: 30 + capture: + image: ghcr.io/bluewave-labs/capture:latest + container_name: capture + restart: unless-stopped + ports: + - "59232:59232" + environment: + - API_SECRET=my_secret + - GIN_MODE=release + volumes: + - /etc/os-release:/etc/os-release:ro diff --git a/docker/staging/docker-compose.yaml b/docker/staging/docker-compose.yaml index 436c6327f..f4ed5ce6f 100755 --- a/docker/staging/docker-compose.yaml +++ b/docker/staging/docker-compose.yaml @@ -49,3 +49,14 @@ services: start_period: 0s start_interval: 1s retries: 30 + capture: + image: ghcr.io/bluewave-labs/capture:latest + container_name: capture + restart: unless-stopped + ports: + - "59232:59232" + environment: + - API_SECRET=my_secret + - GIN_MODE=release + volumes: + - /etc/os-release:/etc/os-release:ro From a6d445818f58fb626f90de3610bc9ffdbea22aca Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 14 Aug 2025 20:15:56 -0400 Subject: [PATCH 030/131] fix: improve vertical alignment of text and plus icons in filter headers Added flex alignment styles to MuiSelect-select class to ensure proper vertical alignment between text labels and plus icons in Type, Status, and State filter buttons on the Uptime page. --- client/src/Components/FilterHeader/index.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/Components/FilterHeader/index.jsx b/client/src/Components/FilterHeader/index.jsx index 36dd04081..5327dcd02 100644 --- a/client/src/Components/FilterHeader/index.jsx +++ b/client/src/Components/FilterHeader/index.jsx @@ -37,6 +37,12 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) => displayEmpty value={controlledValue} onChange={onChange} + sx={{ + "& .MuiSelect-select": { + display: "flex", + alignItems: "center", + }, + }} renderValue={(selected) => { if (!selected?.length) { return header; From f7b1ef9741554a24958c325bba3dca3bd81b561f Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 14 Aug 2025 22:11:47 -0400 Subject: [PATCH 031/131] fix: set Server Logs as default tab on Logs page Changed default tab from Diagnostics (index 2) to Server Logs (index 0) when users navigate to the Logs section from the sidebar. This makes Server Logs the active tab by default, which is more intuitive since it's the first tab in the list. --- client/src/Pages/Logs/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/Logs/index.jsx b/client/src/Pages/Logs/index.jsx index 52aa60199..79c89f282 100644 --- a/client/src/Pages/Logs/index.jsx +++ b/client/src/Pages/Logs/index.jsx @@ -15,7 +15,7 @@ const Logs = () => { const theme = useTheme(); // Local state - const [value, setValue] = useState(2); + const [value, setValue] = useState(0); // Handlers const handleChange = (event, newValue) => { From 43284ae62535845f4e39968c7671cb476f18a050 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 14 Aug 2025 22:19:54 -0400 Subject: [PATCH 032/131] fix: remove description text and dividers from Logs sections Removed description strings and divider lines from: - Server logs: 'System logs - last 1000 lines' and divider - Queue metrics: queue description and divider This cleans up the UI by removing redundant descriptive text that users can infer from the tab names and content context. --- client/src/Pages/Logs/Logs/index.jsx | 19 ------------------- client/src/Pages/Logs/Queue/index.jsx | 2 -- 2 files changed, 21 deletions(-) diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index d9ce2a25c..9264e2c4b 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -51,25 +51,6 @@ const Logs = () => { ]; return ( - - {t("logsPage.description")} - - { return ( - {t("queuePage.metricsTable.title")} - Date: Thu, 14 Aug 2025 22:22:05 -0400 Subject: [PATCH 033/131] fix: remove excess spacing after removing description text Removed top margin (mt={theme.spacing(10)}) and adjusted sticky position from the log level selector since it was creating unwanted space between the tab bar and the 'Log level' dropdown after removing the description text and divider. --- client/src/Pages/Logs/Logs/index.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index 9264e2c4b..b44b3fd1a 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -56,10 +56,9 @@ const Logs = () => { direction="row" alignItems="center" gap={theme.spacing(4)} - mt={theme.spacing(10)} sx={{ position: "sticky", - top: theme.spacing(34), + top: theme.spacing(17), backdropFilter: "blur(10px)", paddingTop: theme.spacing(4), paddingLeft: theme.spacing(6), From 1ecec115e4ed02b93e487df6da2c737b702d4f20 Mon Sep 17 00:00:00 2001 From: Owaise Date: Fri, 15 Aug 2025 11:28:22 +0530 Subject: [PATCH 034/131] Fixed the selector and also null points for networks. --- .../Details/Components/NetworkStats/index.jsx | 33 +++++------- .../db/mongo/modules/monitorModuleQueries.js | 54 +++++++++---------- 2 files changed, 39 insertions(+), 48 deletions(-) diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index 227523439..cf1ae436c 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -1,8 +1,9 @@ import PropTypes from "prop-types"; import { useState, useEffect } from "react"; -import { FormControl, InputLabel, Select, MenuItem, Box } from "@mui/material"; +import { Box } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; +import Select from "../../../../../Components/Inputs/Select"; import NetworkStatBoxes from "./NetworkStatBoxes"; import NetworkCharts from "./NetworkCharts"; import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; @@ -63,27 +64,17 @@ const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { gap={theme.spacing(4)} > {availableInterfaces.length > 0 && ( - setSelectedInterface(e.target.value)} + items={availableInterfaces.map((interfaceName) => ({ + _id: interfaceName, + name: interfaceName + }))} sx={{ minWidth: 200 }} - > - {t("networkInterface")} - - + /> )} { { $project: { diskCount: { - $size: "$disk", + $size: { $ifNull: ["$disk", []] }, }, - netCount: { $size: "$net" }, + netCount: { $size: { $ifNull: ["$net", []] } }, }, }, { @@ -381,7 +381,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, net: { $map: { - input: { $range: [0, { $size: { $arrayElemAt: ["$net", 0] } }] }, + input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$net", 0] }, []] } }] }, as: "netIndex", in: { name: { @@ -409,7 +409,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, as: "iface", in: "$$iface.bytes_sent", }, @@ -418,7 +418,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, }, in: { $cond: [ @@ -444,7 +444,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, as: "iface", in: "$$iface.bytes_recv", }, @@ -453,7 +453,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, }, in: { $cond: [ @@ -479,7 +479,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, as: "iface", in: "$$iface.packets_sent", }, @@ -488,7 +488,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, }, in: { $cond: [ @@ -522,9 +522,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $map: { input: { $arrayElemAt: [ - "$net", + { $ifNull: ["$net", []] }, { - $subtract: [{ $size: "$net" }, 1], + $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], }, ], }, @@ -538,9 +538,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tLast: { $arrayElemAt: [ - "$updatedAts", + { $ifNull: ["$updatedAts", []] }, { - $subtract: [{ $size: "$updatedAts" }, 1], + $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], }, ], }, @@ -566,7 +566,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, as: "iface", in: "$$iface.err_in", }, @@ -575,7 +575,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, }, in: { $cond: [ @@ -609,9 +609,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $map: { input: { $arrayElemAt: [ - "$net", + { $ifNull: ["$net", []] }, { - $subtract: [{ $size: "$net" }, 1], + $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], }, ], }, @@ -625,9 +625,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tLast: { $arrayElemAt: [ - "$updatedAts", + { $ifNull: ["$updatedAts", []] }, { - $subtract: [{ $size: "$updatedAts" }, 1], + $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], }, ], }, @@ -664,9 +664,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $map: { input: { $arrayElemAt: [ - "$net", + { $ifNull: ["$net", []] }, { - $subtract: [{ $size: "$net" }, 1], + $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], }, ], }, @@ -680,9 +680,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tLast: { $arrayElemAt: [ - "$updatedAts", + { $ifNull: ["$updatedAts", []] }, { - $subtract: [{ $size: "$updatedAts" }, 1], + $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], }, ], }, @@ -719,9 +719,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $map: { input: { $arrayElemAt: [ - "$net", + { $ifNull: ["$net", []] }, { - $subtract: [{ $size: "$net" }, 1], + $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], }, ], }, @@ -735,9 +735,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tLast: { $arrayElemAt: [ - "$updatedAts", + { $ifNull: ["$updatedAts", []] }, { - $subtract: [{ $size: "$updatedAts" }, 1], + $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], }, ], }, From 88c478c5339a035fcead1e6f75ba320119f3f695 Mon Sep 17 00:00:00 2001 From: Owaise Date: Fri, 15 Aug 2025 11:37:38 +0530 Subject: [PATCH 035/131] Formatting is done. --- .../Details/Components/NetworkStats/index.jsx | 2 +- .../db/mongo/modules/monitorModuleQueries.js | 32 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index cf1ae436c..41d2b0a87 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -71,7 +71,7 @@ const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { onChange={(e) => setSelectedInterface(e.target.value)} items={availableInterfaces.map((interfaceName) => ({ _id: interfaceName, - name: interfaceName + name: interfaceName, }))} sx={{ minWidth: 200 }} /> diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js index 13352dcd4..f6d20d855 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -409,7 +409,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, + input: { + $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], + }, as: "iface", in: "$$iface.bytes_sent", }, @@ -418,7 +420,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, + tLast: { + $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], + }, }, in: { $cond: [ @@ -444,7 +448,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, + input: { + $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], + }, as: "iface", in: "$$iface.bytes_recv", }, @@ -453,7 +459,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, + tLast: { + $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], + }, }, in: { $cond: [ @@ -479,7 +487,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, + input: { + $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], + }, as: "iface", in: "$$iface.packets_sent", }, @@ -488,7 +498,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, + tLast: { + $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], + }, }, in: { $cond: [ @@ -566,7 +578,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, + input: { + $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], + }, as: "iface", in: "$$iface.err_in", }, @@ -575,7 +589,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, + tLast: { + $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], + }, }, in: { $cond: [ From 5e5daef49ad3dc44bc7291f0dd375d513a7920e0 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 15 Aug 2025 03:38:22 -0400 Subject: [PATCH 036/131] Move vertical alignment styling from component to theme level - Move MuiSelect vertical alignment from FilterHeader component to globalTheme.js - Ensures consistent vertical alignment for all Select components application-wide - Removes component-specific styling in favor of theme-level consistency - Addresses reviewer feedback to make this a theme-level change Components already with custom Select styling will continue to work as local styles override theme styles. --- client/src/Components/FilterHeader/index.jsx | 6 ------ client/src/Utils/Theme/globalTheme.js | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/client/src/Components/FilterHeader/index.jsx b/client/src/Components/FilterHeader/index.jsx index 5327dcd02..36dd04081 100644 --- a/client/src/Components/FilterHeader/index.jsx +++ b/client/src/Components/FilterHeader/index.jsx @@ -37,12 +37,6 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) => displayEmpty value={controlledValue} onChange={onChange} - sx={{ - "& .MuiSelect-select": { - display: "flex", - alignItems: "center", - }, - }} renderValue={(selected) => { if (!selected?.length) { return header; diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index ce58153ec..bda415c90 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -564,6 +564,10 @@ const baseTheme = (palette) => ({ "& .MuiSelect-icon": { color: theme.palette.primary.contrastTextSecondary, // Dropdown + color }, + "& .MuiSelect-select": { + display: "flex", + alignItems: "center", + }, "&:hover": { backgroundColor: theme.palette.primary.main, // Background on hover }, From b8aa1ef76595f24933abc1018c7a13622718ac04 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 15 Aug 2025 09:17:33 -0700 Subject: [PATCH 037/131] fix path, remove redis form build script --- docker/dist-mono/build_images.sh | 2 +- server/src/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/dist-mono/build_images.sh b/docker/dist-mono/build_images.sh index fd3a163cc..e3a0233d4 100755 --- a/docker/dist-mono/build_images.sh +++ b/docker/dist-mono/build_images.sh @@ -5,7 +5,7 @@ cd "$(dirname "$0")" cd ../.. # Define service names and their corresponding Dockerfiles in parallel arrays -services=("mono_mongo" "mono_redis" "mono_server") +services=("mono_mongo" "mono_server") dockerfiles=( "./docker/dist-mono/mongoDB.Dockerfile" "./docker/dist-mono/server.Dockerfile" diff --git a/server/src/index.js b/server/src/index.js index 6b12eb25d..d1ff649eb 100755 --- a/server/src/index.js +++ b/server/src/index.js @@ -18,7 +18,7 @@ const startApp = async () => { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8")); - const frontendPath = path.join(__dirname, "public"); + const frontendPath = path.join(__dirname, "..", "public"); // Create services const settingsService = new SettingsService(AppSettings); From 8aee01e008e5cb0d66bde74124277022188d6ef6 Mon Sep 17 00:00:00 2001 From: Matt Stein Date: Fri, 15 Aug 2025 14:34:42 -0700 Subject: [PATCH 038/131] Fix PageSpeed settings validation. --- client/src/Pages/PageSpeed/Create/index.jsx | 2 +- client/src/Validation/validation.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx index 78709093a..72f282b83 100644 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ b/client/src/Pages/PageSpeed/Create/index.jsx @@ -152,7 +152,7 @@ const PageSpeedSetup = () => { }); const { error } = monitorValidation.validate( - { [name]: value }, + { ...monitor, [name]: value }, { abortEarly: false } ); setErrors((prev) => ({ diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index de248f87b..efb94ea2c 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -213,6 +213,7 @@ const monitorValidation = joi.object({ }), otherwise: joi.string().allow(null, ""), }), + notifications: joi.array().optional(), }); const imageValidation = joi.object({ From e3fde8d5646a9c2363ff1c450a6fac3583ecb20c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 15 Aug 2025 16:02:52 -0700 Subject: [PATCH 039/131] add got --- server/package-lock.json | 190 +++++++++++++++++++++++++++++++-------- server/package.json | 1 + 2 files changed, 156 insertions(+), 35 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 3084a5361..b761c4d79 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -21,6 +21,7 @@ "express": "^4.19.2", "express-rate-limit": "8.0.1", "gamedig": "^5.3.1", + "got": "14.4.7", "handlebars": "^4.7.8", "helmet": "^8.0.0", "ioredis": "^5.4.2", @@ -1137,6 +1138,12 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -1159,12 +1166,12 @@ "license": "BSD-3-Clause" }, "node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", + "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==", "license": "MIT", "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sindresorhus/is?sponsor=1" @@ -1821,21 +1828,21 @@ } }, "node_modules/cacheable-request": { - "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz", + "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==", "license": "MIT", "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", + "keyv": "^4.5.4", "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", + "normalize-url": "^8.0.1", "responselike": "^3.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" } }, "node_modules/call-bind-apply-helpers": { @@ -3750,12 +3757,12 @@ } }, "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", "license": "MIT", "engines": { - "node": ">= 14.17" + "node": ">= 18" } }, "node_modules/formdata-polyfill": { @@ -3840,6 +3847,82 @@ "node": ">=16.20.0" } }, + "node_modules/gamedig/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/gamedig/node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/gamedig/node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/gamedig/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gamedig/node_modules/got": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", + "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/gamedig/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -3852,6 +3935,15 @@ "node": ">=0.10.0" } }, + "node_modules/gamedig/node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/gaxios": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", @@ -3930,12 +4022,28 @@ } }, "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4024,28 +4132,40 @@ } }, "node_modules/got": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", - "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "version": "14.4.7", + "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz", + "integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==", "license": "MIT", "dependencies": { - "@sindresorhus/is": "^5.2.0", + "@sindresorhus/is": "^7.0.1", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", + "cacheable-request": "^12.0.1", "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" + "p-cancelable": "^4.0.1", + "responselike": "^3.0.0", + "type-fest": "^4.26.1" }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/handlebars": { @@ -6350,12 +6470,12 @@ } }, "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", "license": "MIT", "engines": { - "node": ">=12.20" + "node": ">=14.16" } }, "node_modules/p-limit": { diff --git a/server/package.json b/server/package.json index 929fab58a..98738aece 100755 --- a/server/package.json +++ b/server/package.json @@ -28,6 +28,7 @@ "express": "^4.19.2", "express-rate-limit": "8.0.1", "gamedig": "^5.3.1", + "got": "14.4.7", "handlebars": "^4.7.8", "helmet": "^8.0.0", "ioredis": "^5.4.2", From 9925e10ffc8c323f7f3a17ec6a7e174b1a1a57fd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 15 Aug 2025 16:03:12 -0700 Subject: [PATCH 040/131] add timings to model --- server/src/db/models/Check.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/db/models/Check.js b/server/src/db/models/Check.js index 8bb6cd028..0037ffadc 100755 --- a/server/src/db/models/Check.js +++ b/server/src/db/models/Check.js @@ -36,6 +36,15 @@ const BaseCheckSchema = mongoose.Schema({ responseTime: { type: Number, }, + /** + * Timings from Got. + * + * @type {Object} + */ + timings: { + type: Object, // Accepts any object + default: {}, + }, /** * HTTP status code received during the check. * From 45ac5899753d13b1fcf2c1fe7396ee3efa319e69 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 15 Aug 2025 16:04:43 -0700 Subject: [PATCH 041/131] refactor network service --- server/src/config/services.js | 19 +- .../SuperSimpleQueue/SuperSimpleQueue.js | 1 + .../SuperSimpleQueueHelper.js | 3 +- .../service/infrastructure/networkService.js | 491 +++++++----------- .../service/infrastructure/statusService.js | 2 + 5 files changed, 206 insertions(+), 310 deletions(-) mode change 100755 => 100644 server/src/service/infrastructure/networkService.js diff --git a/server/src/config/services.js b/server/src/config/services.js index 73220128f..515fe5cfe 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -19,8 +19,10 @@ import MaintenanceWindowService from "../service/business/maintenanceWindowServi import MonitorService from "../service/business/monitorService.js"; import papaparse from "papaparse"; import axios from "axios"; +import got from "got"; import ping from "ping"; import http from "http"; +import https from "https"; import Docker from "dockerode"; import net from "net"; import fs from "fs"; @@ -32,6 +34,8 @@ import mjml2html from "mjml"; import jwt from "jsonwebtoken"; import crypto from "crypto"; import { games } from "gamedig"; +import jmespath from "jmespath"; +import { GameDig } from "gamedig"; import { fileURLToPath } from "url"; import { ObjectId } from "mongodb"; @@ -125,7 +129,20 @@ export const initializeServices = async ({ logger, envSettings, settingsService await db.connect(); - const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService, settingsService); + const networkService = new NetworkService({ + axios, + got, + https, + jmespath, + GameDig, + ping, + logger, + http, + Docker, + net, + stringService, + settingsService, + }); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); const bufferService = new BufferService({ db, logger, envSettings }); const statusService = new StatusService({ db, logger, buffer: bufferService }); diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js index 60bcb0754..47d73c0df 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js @@ -54,6 +54,7 @@ class SuperSimpleQueue { id: monitorId.toString(), template: "monitor-job", repeat: monitor.interval, + active: monitor.isActive, data: monitor.toObject(), }); }; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index ca2e5f642..7659b1c71 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -31,7 +31,8 @@ class SuperSimpleQueueHelper { }); return; } - const networkResponse = await this.networkService.getStatus(monitor); + const networkResponse = await this.networkService.requestStatus(monitor); + if (!networkResponse) { throw new Error("No network response"); } diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.js old mode 100755 new mode 100644 index 8f904bf56..c624c961c --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.js @@ -1,23 +1,9 @@ -import jmespath from "jmespath"; -import https from "https"; -import { GameDig } from "gamedig"; - const SERVICE_NAME = "NetworkService"; -const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push"; -/** - * Constructs a new NetworkService instance. - * - * @param {Object} axios - The axios instance for HTTP requests. - * @param {Object} ping - The ping utility for network checks. - * @param {Object} logger - The logger instance for logging. - * @param {Object} http - The HTTP utility for network operations. - * @param {Object} net - The net utility for network operations. - */ class NetworkService { static SERVICE_NAME = SERVICE_NAME; - constructor(axios, ping, logger, http, Docker, net, stringService, settingsService) { + constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; @@ -29,6 +15,10 @@ class NetworkService { this.NETWORK_ERROR = 5000; this.PING_ERROR = 5001; this.axios = axios; + this.got = got; + this.https = https; + this.jmespath = jmespath; + this.GameDig = GameDig; this.ping = ping; this.logger = logger; this.http = http; @@ -38,151 +28,134 @@ class NetworkService { this.settingsService = settingsService; } - get serviceName() { - return NetworkService.SERVICE_NAME; - } - - /** - * Times the execution of an asynchronous operation. - * - * @param {Function} operation - The asynchronous operation to be timed. - * @returns {Promise} An object containing the response, response time, and optionally an error. - * @property {Object|null} response - The response from the operation, or null if an error occurred. - * @property {number} responseTime - The time taken for the operation to complete, in milliseconds. - * @property {Error} [error] - The error object if an error occurred during the operation. - */ + // Helper functions async timeRequest(operation) { - const startTime = Date.now(); + const start = process.hrtime.bigint(); try { const response = await operation(); - const endTime = Date.now(); - const responseTime = endTime - startTime; - return { response, responseTime }; + const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); + return { response, responseTime: elapsedMs }; } catch (error) { - const endTime = Date.now(); - const responseTime = endTime - startTime; - return { response: null, responseTime, error }; + const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); + return { response: null, responseTime: elapsedMs, error }; + } + } + + // Main entry point + async requestStatus(monitor) { + const type = monitor?.type || "unknown"; + switch (type) { + case this.TYPE_PING: + return await this.requestPing(monitor); + case this.TYPE_HTTP: + return await this.requestHttp(monitor); + case this.TYPE_PAGESPEED: + return await this.requestPageSpeed(monitor); + case this.TYPE_HARDWARE: + return await this.requestHardware(monitor); + case this.TYPE_DOCKER: + return await this.requestDocker(monitor); + case this.TYPE_PORT: + return await this.requestPort(monitor); + case this.TYPE_GAME: + return await this.requestGame(monitor); + default: + return await this.handleUnsupportedType(type); } } - /** - * Performs a ping check to a specified host to verify its availability. - * @async - * @param {Object} monitor - The monitor configuration object - * @param {string} monitor.url - The host URL to ping - * @param {string} monitor._id - The unique identifier of the monitor - * @returns {Promise} A promise that resolves to a ping response object - * @returns {string} pingResponse.monitorId - The ID of the monitor - * @returns {string} pingResponse.type - The type of monitor (always "ping") - * @returns {number} pingResponse.responseTime - The time taken for the ping - * @returns {Object} pingResponse.payload - The raw ping response data - * @returns {boolean} pingResponse.status - Whether the host is alive (true) or not (false) - * @returns {number} pingResponse.code - Status code (200 for success, PING_ERROR for failure) - * @returns {string} pingResponse.message - Success or failure message - * @throws {Error} If there's an error during the ping operation - */ async requestPing(monitor) { try { - const url = monitor.url; + if (!monitor?.url) { + throw new Error("Monitor URL is required"); + } + const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(url)); + if (!response) { + throw new Error("Ping failed - no result returned"); + } + const pingResponse = { monitorId: monitor._id, type: "ping", + status: response.alive, + code: 200, responseTime, + message: "Success", payload: response, }; + if (error) { pingResponse.status = false; - pingResponse.code = this.PING_ERROR; - pingResponse.message = "No response"; + pingResponse.code = 200; + pingResponse.message = "Ping failed"; return pingResponse; } - pingResponse.code = 200; - pingResponse.status = response.alive; - pingResponse.message = "Success"; return pingResponse; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestPing"; - throw error; + } catch (err) { + err.service = this.SERVICE_NAME; + err.method = "requestPing"; + throw err; } } - /** - * Performs an HTTP GET request to a specified URL with optional validation of response data. - * @async - * @param {Object} monitor - The monitor configuration object - * @param {string} monitor.url - The URL to make the HTTP request to - * @param {string} [monitor.secret] - Optional Bearer token for authentication - * @param {string} monitor._id - The unique identifier of the monitor - * @param {string} monitor.name - The name of the monitor - * @param {string} monitor.teamId - The team ID associated with the monitor - * @param {string} monitor.type - The type of monitor - * @param {boolean} [monitor.ignoreTlsErrors] - Whether to ignore TLS certificate errors - * @param {string} [monitor.jsonPath] - Optional JMESPath expression to extract data from JSON response - * @param {string} [monitor.matchMethod] - Method to match response data ('include', 'regex', or exact match) - * @param {string} [monitor.expectedValue] - Expected value to match against response data - * @returns {Promise} A promise that resolves to an HTTP response object - * @returns {string} httpResponse.monitorId - The ID of the monitor - * @returns {string} httpResponse.teamId - The team ID - * @returns {string} httpResponse.type - The type of monitor - * @returns {number} httpResponse.responseTime - The time taken for the request - * @returns {Object} httpResponse.payload - The response data - * @returns {boolean} httpResponse.status - Whether the request was successful and matched expected value (if specified) - * @returns {number} httpResponse.code - HTTP status code or NETWORK_ERROR - * @returns {string} httpResponse.message - Success or failure message - * @throws {Error} If there's an error during the HTTP request or data validation - */ async requestHttp(monitor) { + const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; + const httpResponse = { + monitorId: _id, + teamId: teamId, + type, + }; + try { - const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; - const config = {}; - - secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` }); - - if (ignoreTlsErrors === true) { - config.httpsAgent = new https.Agent({ - rejectUnauthorized: false, - }); + if (!url) { + throw new Error("Monitor URL is required"); } - - const { response, responseTime, error } = await this.timeRequest(() => this.axios.get(url, config)); - - const httpResponse = { - monitorId: _id, - teamId, - type, - responseTime, - payload: response?.data, + const config = { + headers: secret ? { Authorization: `Bearer ${secret}` } : undefined, }; - if (error) { - const code = error.response?.status || this.NETWORK_ERROR; - httpResponse.code = code; - httpResponse.status = false; - httpResponse.message = this.http.STATUS_CODES[code] || this.stringService.httpNetworkError; - return httpResponse; + if (ignoreTlsErrors) { + config.agent = { + https: new this.https.Agent({ + rejectUnauthorized: false, + }), + }; } - httpResponse.code = response.status; + const response = await this.got(url, config); + + let payload; + const contentType = response.headers["content-type"]; + + if (contentType && contentType.includes("application/json")) { + try { + payload = JSON.parse(response.body); + } catch { + payload = response.body; + } + } else { + payload = response.body; + } + + httpResponse.code = response.statusCode; + httpResponse.status = response.ok; + httpResponse.message = response.statusMessage; + httpResponse.responseTime = response.timings.phases.total || 0; + httpResponse.payload = payload; + httpResponse.timings = response.timings || {}; if (!expectedValue) { - // not configure expected value, return - httpResponse.status = true; - httpResponse.message = this.http.STATUS_CODES[response.status]; return httpResponse; } - // validate if response data match expected value - let result = response?.data; - this.logger.info({ service: this.SERVICE_NAME, method: "requestHttp", message: `Job: [${name}](${_id}) match result with expected value`, - details: { expectedValue, result, jsonPath, matchMethod }, + details: { expectedValue, payload, jsonPath, matchMethod }, }); if (jsonPath) { @@ -196,106 +169,75 @@ class NetworkService { } try { - result = jmespath.search(result, jsonPath); - } catch (error) { + this.jmespath.search(payload, jsonPath); + } catch { httpResponse.status = false; httpResponse.message = this.stringService.httpJsonPathError; return httpResponse; } } - - if (result === null || result === undefined) { + return httpResponse; + } catch (err) { + if (err.name === "HTTPError" || err.name === "RequestError") { + httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR; httpResponse.status = false; - httpResponse.message = this.stringService.httpEmptyResult; + httpResponse.message = err?.response?.statusCode || err.message; + httpResponse.responseTime = err?.timings?.phases?.total || 0; + httpResponse.payload = null; + httpResponse.timings = err?.timings || {}; return httpResponse; } - - let match; - result = typeof result === "object" ? JSON.stringify(result) : result.toString(); - if (matchMethod === "include") match = result.includes(expectedValue); - else if (matchMethod === "regex") match = new RegExp(expectedValue).test(result); - else match = result === expectedValue; - - httpResponse.status = match; - httpResponse.message = match ? this.stringService.httpMatchSuccess : this.stringService.httpMatchFail; - return httpResponse; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestHttp"; - throw error; + err.service = this.SERVICE_NAME; + err.method = "requestHttp"; + throw err; } } - /** - * Checks the performance of a webpage using Google's PageSpeed Insights API. - * @async - * @param {Object} monitor - The monitor configuration object - * @param {string} monitor.url - The URL of the webpage to analyze - * @returns {Promise} A promise that resolves to a pagespeed response object or undefined if API key is missing - * @returns {string} response.monitorId - The ID of the monitor - * @returns {string} response.type - The type of monitor - * @returns {number} response.responseTime - The time taken for the analysis - * @returns {boolean} response.status - Whether the analysis was successful - * @returns {number} response.code - HTTP status code from the PageSpeed API - * @returns {string} response.message - Success or failure message - * @returns {Object} response.payload - The PageSpeed analysis results - * @throws {Error} If there's an error during the PageSpeed analysis - */ - async requestPagespeed(monitor) { + async requestPageSpeed(monitor) { try { const url = monitor.url; - const updatedMonitor = { ...monitor }; - let pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`; - + if (!url) { + throw new Error("Monitor URL is required"); + } + let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`; const dbSettings = await this.settingsService.getDBSettings(); if (dbSettings?.pagespeedApiKey) { - pagespeedUrl += `&key=${dbSettings.pagespeedApiKey}`; + pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`; } else { this.logger.warn({ - message: "Pagespeed API key not found, job not executed", + message: "PageSpeed API key not found, job not executed", service: this.SERVICE_NAME, method: "requestPagespeed", details: { url }, }); - return; } - updatedMonitor.url = pagespeedUrl; - return await this.requestHttp(updatedMonitor); - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestPagespeed"; - throw error; + return await this.requestHttp({ + ...monitor, + url: pageSpeedUrl, + }); + } catch (err) { + err.service = this.SERVICE_NAME; + err.method = "requestPageSpeed"; + throw err; } } async requestHardware(monitor) { try { return await this.requestHttp(monitor); - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestHardware"; - throw error; + } catch (err) { + err.service = this.SERVICE_NAME; + err.method = "requestHardware"; + throw err; } } - /** - * Checks the status of a Docker container by its ID. - * @async - * @param {Object} monitor - The monitor configuration object - * @param {string} monitor.url - The Docker container ID to check - * @param {string} monitor._id - The unique identifier of the monitor - * @param {string} monitor.type - The type of monitor - * @returns {Promise} A promise that resolves to a docker response object - * @returns {string} dockerResponse.monitorId - The ID of the monitor - * @returns {string} dockerResponse.type - The type of monitor - * @returns {number} dockerResponse.responseTime - The time taken for the container inspection - * @returns {boolean} dockerResponse.status - Whether the container is running (true) or not (false) - * @returns {number} dockerResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure) - * @returns {string} dockerResponse.message - Success or failure message - * @throws {Error} If the container is not found or if there's an error inspecting the container - */ async requestDocker(monitor) { try { + if (!monitor.url) { + throw new Error("Monitor URL is required"); + } + const docker = new this.Docker({ socketPath: "/var/run/docker.sock", handleError: true, // Enable error handling @@ -306,14 +248,17 @@ class NetworkService { if (!containerExists) { throw new Error(this.stringService.dockerNotFound); } - const container = docker.getContainer(monitor.url); + const container = docker.getContainer(monitor.url); const { response, responseTime, error } = await this.timeRequest(() => container.inspect()); const dockerResponse = { monitorId: monitor._id, type: monitor.type, responseTime, + status: response?.State?.Status === "running" ? true : false, + code: 200, + message: "Docker container status fetched successfully", }; if (error) { @@ -322,34 +267,15 @@ class NetworkService { dockerResponse.message = error.reason || "Failed to fetch Docker container information"; return dockerResponse; } - dockerResponse.status = response?.State?.Status === "running" ? true : false; - dockerResponse.code = 200; - dockerResponse.message = "Docker container status fetched successfully"; + return dockerResponse; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestDocker"; - throw error; + } catch (err) { + err.service = this.SERVICE_NAME; + err.method = "requestDocker"; + throw err; } } - /** - * Attempts to establish a TCP connection to a specified host and port. - * @async - * @param {Object} monitor - The monitor configuration object - * @param {string} monitor.url - The host URL to connect to - * @param {number} monitor.port - The port number to connect to - * @param {string} monitor._id - The unique identifier of the monitor - * @param {string} monitor.type - The type of monitor - * @returns {Promise} A promise that resolves to a port response object - * @returns {string} portResponse.monitorId - The ID of the monitor - * @returns {string} portResponse.type - The type of monitor - * @returns {number} portResponse.responseTime - The time taken for the connection attempt - * @returns {boolean} portResponse.status - Whether the connection was successful - * @returns {number} portResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure) - * @returns {string} portResponse.message - Success or failure message - * @throws {Error} If the connection times out or encounters an error - */ async requestPort(monitor) { try { const { url, port } = monitor; @@ -381,21 +307,21 @@ class NetworkService { }); const portResponse = { + code: 200, + status: response.success, + message: this.stringService.portSuccess, monitorId: monitor._id, type: monitor.type, responseTime, }; if (error) { - portResponse.status = false; portResponse.code = this.NETWORK_ERROR; + portResponse.status = false; portResponse.message = this.stringService.portFail; return portResponse; } - portResponse.status = response.success; - portResponse.code = 200; - portResponse.message = this.stringService.portSuccess; return portResponse; } catch (error) { error.service = this.SERVICE_NAME; @@ -404,19 +330,55 @@ class NetworkService { } } - /** - * Handles unsupported job types by throwing an error with details. - * - * @param {string} type - The unsupported job type that was provided - * @throws {Error} An error with service name, method name and unsupported type message - */ - handleUnsupportedType(type) { + async requestGame(monitor) { + try { + const { url, port, gameId } = monitor; + + const gameResponse = { + code: 200, + status: true, + message: "Success", + monitorId: monitor._id, + type: "game", + }; + + const state = await this.GameDig.query({ + type: gameId, + host: url, + port: port, + }).catch((error) => { + this.logger.warn({ + message: error.message, + service: this.SERVICE_NAME, + method: "requestGame", + details: { url, port, gameId }, + }); + }); + + if (!state) { + gameResponse.code = this.NETWORK_ERROR; + gameResponse.status = false; + gameResponse.message = "No response"; + return gameResponse; + } + + gameResponse.responseTime = state.ping; + gameResponse.payload = state; + return gameResponse; + } catch (error) { + error.service = this.SERVICE_NAME; + error.method = "requestPing"; + throw error; + } + } + async handleUnsupportedType(type) { const err = new Error(`Unsupported type: ${type}`); err.service = this.SERVICE_NAME; err.method = "getStatus"; throw err; } + // Other network requests unrelated to monitoring: async requestWebhook(type, url, body) { try { const response = await this.axios.post(url, body, { @@ -471,93 +433,6 @@ class NetworkService { throw error; } } - - /** - * Requests the status of a game monitor. - * - * @param {Object} monitor - The monitor object to request the status for. - * @returns {Promise} The response from the game status request. - * @throws {Error} Throws an error if the request fails or if the monitor is not configured correctly. - * @property {string} monitorId - The ID of the monitor. - * @property {string} type - The type of the monitor (should be "game"). - * @property {number} responseTime - The time taken for the request. - * @property {Object|null} payload - The game state response or null if the request failed. - * @property {boolean} status - Indicates if the request was successful (true) or not (false). - * @property {number} code - The status code of the request (200 for success, NETWORK_ERROR for failure). - * @property {string} message - A message indicating the result of the request. - */ - async requestGame(monitor) { - try { - const { url, port, gameId } = monitor; - - const gameResponse = { - code: 200, - status: true, - message: "Success", - monitorId: monitor._id, - type: "game", - }; - - const state = await GameDig.query({ - type: gameId, - host: url, - port: port, - }).catch((error) => { - this.logger.warn({ - message: error.message, - service: this.SERVICE_NAME, - method: "requestGame", - details: { url, port, gameId }, - }); - }); - - if (!state) { - gameResponse.status = false; - gameResponse.code = this.NETWORK_ERROR; - gameResponse.message = "No response"; - return gameResponse; - } - - gameResponse.responseTime = state.ping; - gameResponse.payload = state; - return gameResponse; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestPing"; - throw error; - } - } - - /** - * Gets the status of a job based on its type and returns the appropriate response. - * - * @param {Object} job - The job object containing the data for the status request. - * @param {Object} job.data - The data object within the job. - * @param {string} job.data.type - The type of the job (e.g., "ping", "http", "pagespeed", "hardware"). - * @returns {Promise} The response object from the appropriate request method. - * @throws {Error} Throws an error if the job type is unsupported. - */ - async getStatus(monitor) { - const type = monitor.type ?? "unknown"; - switch (type) { - case this.TYPE_PING: - return await this.requestPing(monitor); - case this.TYPE_HTTP: - return await this.requestHttp(monitor); - case this.TYPE_PAGESPEED: - return await this.requestPagespeed(monitor); - case this.TYPE_HARDWARE: - return await this.requestHardware(monitor); - case this.TYPE_DOCKER: - return await this.requestDocker(monitor); - case this.TYPE_PORT: - return await this.requestPort(monitor); - case this.TYPE_GAME: - return await this.requestGame(monitor); - default: - return this.handleUnsupportedType(type); - } - } } export default NetworkService; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 72916a0ce..4a98996fb 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -192,6 +192,7 @@ class StatusService { conn_took, connect_took, tls_took, + timings, } = networkResponse; const check = { @@ -200,6 +201,7 @@ class StatusService { status, statusCode: code, responseTime, + timings: timings || {}, message, first_byte_took, body_read_took, From 7178f537a86400b484d59aaa4d7e3ebf5f01f261 Mon Sep 17 00:00:00 2001 From: Matt Stein Date: Fri, 15 Aug 2025 16:40:21 -0700 Subject: [PATCH 042/131] Update with changes from code review. --- client/src/Pages/PageSpeed/Create/index.jsx | 2 +- client/src/Validation/validation.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx index 72f282b83..46bc7f977 100644 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ b/client/src/Pages/PageSpeed/Create/index.jsx @@ -152,7 +152,7 @@ const PageSpeedSetup = () => { }); const { error } = monitorValidation.validate( - { ...monitor, [name]: value }, + { [name]: value, type: monitor.type }, { abortEarly: false } ); setErrors((prev) => ({ diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index efb94ea2c..de248f87b 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -213,7 +213,6 @@ const monitorValidation = joi.object({ }), otherwise: joi.string().allow(null, ""), }), - notifications: joi.array().optional(), }); const imageValidation = joi.object({ From 56025f6d5669c14cfb86818ea3f36c10afa8ca79 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Sun, 17 Aug 2025 15:28:06 -0400 Subject: [PATCH 043/131] refactor create infraestructure monitor page --- .../Create/Components/CustomAlertsSection.jsx | 87 ++++ .../Components/MonitorActionButtons.jsx | 78 ++++ .../Create/Components/MonitorStatusHeader.jsx | 73 ++++ .../hooks/useInfrastructureMonitorForm.jsx | 90 ++++ .../Create/hooks/useInfrastructureSubmit.jsx | 80 ++++ .../hooks/useValidateInfrastructureForm.jsx | 37 ++ .../src/Pages/Infrastructure/Create/index.jsx | 392 ++---------------- 7 files changed, 486 insertions(+), 351 deletions(-) create mode 100644 client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx create mode 100644 client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx create mode 100644 client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx create mode 100644 client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx create mode 100644 client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx create mode 100644 client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx new file mode 100644 index 000000000..5a6f5e981 --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx @@ -0,0 +1,87 @@ +import ConfigBox from "../../../../Components/ConfigBox"; +import { Box, Stack, Typography } from "@mui/material"; +import { CustomThreshold } from "../Components/CustomThreshold"; +import { capitalizeFirstLetter } from "../../../../Utils/stringUtils"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import PropTypes from "prop-types"; +const CustomAlertsSection = ({ + errors, + onChange, + infrastructureMonitor, + handleCheckboxChange, +}) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const METRICS = ["cpu", "memory", "disk", "temperature"]; + const METRIC_PREFIX = "usage_"; + const hasAlertError = (errors) => { + return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0; + }; + const getAlertError = (errors) => { + const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX)); + return errorKey ? errors[errorKey] : null; + }; + return ( + + + + {t("infrastructureCustomizeAlerts")} + + + {t("infrastructureAlertNotificationDescription")} + + + + {METRICS.map((metric) => { + return ( + + ); + })} + + {hasAlertError(errors) && ( + + {getAlertError(errors)} + + )} + + + ); +}; + +CustomAlertsSection.propTypes = { + errors: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, + infrastructureMonitor: PropTypes.object.isRequired, + handleCheckboxChange: PropTypes.func.isRequired, +}; + +export default CustomAlertsSection; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx new file mode 100644 index 000000000..becda3e3a --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx @@ -0,0 +1,78 @@ +import { useState } from "react"; +import { Box, Button } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; +import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; +import Dialog from "../../../../Components/Dialog"; +import PropTypes from "prop-types"; + +const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + + return ( + + + + setIsOpen(false)} + confirmationButtonLabel={t("delete")} + onConfirm={handleRemove} + /> + + ); +}; + +MonitorActionButtons.propTypes = { + monitor: PropTypes.object.isRequired, + isBusy: PropTypes.bool.isRequired, + handlePause: PropTypes.func.isRequired, + handleRemove: PropTypes.func.isRequired, +}; + +export default MonitorActionButtons; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx new file mode 100644 index 000000000..2ed48bf64 --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx @@ -0,0 +1,73 @@ +import { Box, Stack, Tooltip, Typography } from "@mui/material"; +import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import PulseDot from "../../../../Components/Animated/PulseDot"; +import PropTypes from "prop-types"; +const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); + return ( + + + + + + + + {infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."} + + + {t("editing")} + + + ); +}; + +MonitorStatusHeader.propTypes = { + monitor: PropTypes.object.isRequired, + infrastructureMonitor: PropTypes.object.isRequired, +}; + +export default MonitorStatusHeader; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx new file mode 100644 index 000000000..fa8a6ebe9 --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx @@ -0,0 +1,90 @@ +import { useState } from "react"; +const useInfrastructureMonitorForm = () => { + const [infrastructureMonitor, setInfrastructureMonitor] = useState({ + url: "", + name: "", + notifications: [], + notify_email: false, + interval: 0.25, + cpu: false, + usage_cpu: "", + memory: false, + usage_memory: "", + disk: false, + usage_disk: "", + temperature: false, + usage_temperature: "", + secret: "", + }); + + const onChangeForm = (name, value) => { + setInfrastructureMonitor({ + ...infrastructureMonitor, + [name]: value, + }); + }; + const handleCheckboxChange = (event) => { + setInfrastructureMonitor({ + ...infrastructureMonitor, + [event.target.name]: event.target.checked, + }); + }; + const initializeInfrastructureMonitorForCreate = (globalSettings) => { + const gt = globalSettings?.data?.settings?.globalThresholds || {}; + setInfrastructureMonitor({ + url: "", + name: "", + notifications: [], + interval: 0.25, + cpu: gt.cpu !== undefined, + usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "", + memory: gt.memory !== undefined, + usage_memory: gt.memory !== undefined ? gt.memory.toString() : "", + disk: gt.disk !== undefined, + usage_disk: gt.disk !== undefined ? gt.disk.toString() : "", + temperature: gt.temperature !== undefined, + usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", + secret: "", + }); + }; + + const initializeInfrastructureMonitorForUpdate = (monitor) => { + const MS_PER_MINUTE = 60000; + const { thresholds = {} } = monitor; + setInfrastructureMonitor({ + url: monitor.url.replace(/^https?:\/\//, ""), + name: monitor.name || "", + notifications: monitor.notifications || [], + interval: monitor.interval / MS_PER_MINUTE, + cpu: thresholds.usage_cpu !== undefined, + usage_cpu: + thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "", + memory: thresholds.usage_memory !== undefined, + usage_memory: + thresholds.usage_memory !== undefined + ? (thresholds.usage_memory * 100).toString() + : "", + disk: thresholds.usage_disk !== undefined, + usage_disk: + thresholds.usage_disk !== undefined + ? (thresholds.usage_disk * 100).toString() + : "", + temperature: thresholds.usage_temperature !== undefined, + usage_temperature: + thresholds.usage_temperature !== undefined + ? (thresholds.usage_temperature * 100).toString() + : "", + secret: monitor.secret || "", + }); + }; + return { + infrastructureMonitor, + setInfrastructureMonitor, + onChangeForm, + handleCheckboxChange, + initializeInfrastructureMonitorForCreate, + initializeInfrastructureMonitorForUpdate, + }; +}; + +export default useInfrastructureMonitorForm; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx new file mode 100644 index 000000000..da564ae7c --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx @@ -0,0 +1,80 @@ +import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks"; +const useInfrastructureSubmit = () => { + const [createMonitor, isCreating] = useCreateMonitor(); + const [updateMonitor, isUpdating] = useUpdateMonitor(); + const buildForm = (infrastructureMonitor, https) => { + const MS_PER_MINUTE = 60000; + + let form = { + url: `http${https ? "s" : ""}://` + infrastructureMonitor.url, + name: + infrastructureMonitor.name === "" + ? infrastructureMonitor.url + : infrastructureMonitor.name, + interval: infrastructureMonitor.interval * MS_PER_MINUTE, + cpu: infrastructureMonitor.cpu, + ...(infrastructureMonitor.cpu + ? { usage_cpu: infrastructureMonitor.usage_cpu } + : {}), + memory: infrastructureMonitor.memory, + ...(infrastructureMonitor.memory + ? { usage_memory: infrastructureMonitor.usage_memory } + : {}), + disk: infrastructureMonitor.disk, + ...(infrastructureMonitor.disk + ? { usage_disk: infrastructureMonitor.usage_disk } + : {}), + temperature: infrastructureMonitor.temperature, + ...(infrastructureMonitor.temperature + ? { usage_temperature: infrastructureMonitor.usage_temperature } + : {}), + secret: infrastructureMonitor.secret, + }; + return form; + }; + const submitInfrastructureForm = async ( + infrastructureMonitor, + form, + isCreate, + monitorId + ) => { + const { + cpu, + usage_cpu, + memory, + usage_memory, + disk, + usage_disk, + temperature, + usage_temperature, + ...rest + } = form; + + const thresholds = { + ...(cpu ? { usage_cpu: usage_cpu / 100 } : {}), + ...(memory ? { usage_memory: usage_memory / 100 } : {}), + ...(disk ? { usage_disk: usage_disk / 100 } : {}), + ...(temperature ? { usage_temperature: usage_temperature / 100 } : {}), + }; + + const finalForm = { + ...(isCreate ? {} : { _id: monitorId }), + ...rest, + description: form.name, + type: "hardware", + notifications: infrastructureMonitor.notifications, + thresholds, + }; + // Handle create or update + isCreate + ? await createMonitor({ monitor: finalForm, redirect: "/infrastructure" }) + : await updateMonitor({ monitor: finalForm, redirect: "/infrastructure" }); + }; + return { + buildForm, + submitInfrastructureForm, + isCreating, + isUpdating, + }; +}; +export default useInfrastructureSubmit; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx new file mode 100644 index 000000000..080f441c4 --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { infrastructureMonitorValidation } from "../../../../Validation/validation"; +import { createToast } from "../../../../Utils/toastUtils"; +const useValidateInfrastructureForm = () => { + const [errors, setErrors] = useState({}); + + const validateField = (name, value) => { + const { error } = infrastructureMonitorValidation.validate( + { [name]: value }, + { abortEarly: false } + ); + setErrors((prev) => ({ + ...prev, + ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), + })); + }; + + const validateForm = (form) => { + const { error } = infrastructureMonitorValidation.validate(form, { + abortEarly: false, + }); + + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + console.log(newErrors); + setErrors(newErrors); + createToast({ body: "Please check the form for errors." }); + return error; + } + return null; + }; + return { errors, validateField, validateForm }; +}; +export default useValidateInfrastructureForm; diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index ae95cce33..28007c538 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -1,40 +1,33 @@ //Components import Breadcrumbs from "../../../Components/Breadcrumbs"; import ConfigBox from "../../../Components/ConfigBox"; -import Dialog from "../../../Components/Dialog"; import FieldWrapper from "../../../Components/Inputs/FieldWrapper"; import Link from "../../../Components/Link"; -import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; -import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; -import PulseDot from "../../../Components/Animated/PulseDot"; import Select from "../../../Components/Inputs/Select"; import TextInput from "../../../Components/Inputs/TextInput"; -import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material"; -import { CustomThreshold } from "./Components/CustomThreshold"; +import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import { createToast } from "../../../Utils/toastUtils"; +import MonitorStatusHeader from "./Components/MonitorStatusHeader"; +import MonitorActionButtons from "./Components/MonitorActionButtons"; +import CustomAlertsSection from "./Components/CustomAlertsSection"; // Utils import NotificationsConfig from "../../../Components/NotificationConfig"; -import { capitalizeFirstLetter } from "../../../Utils/stringUtils"; -import { infrastructureMonitorValidation } from "../../../Validation/validation"; import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; import { useParams } from "react-router-dom"; -import { useSelector } from "react-redux"; import { useState, useEffect } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { - useCreateMonitor, useDeleteMonitor, useFetchGlobalSettings, useFetchHardwareMonitorById, usePauseMonitor, - useUpdateMonitor, } from "../../../Hooks/monitorHooks"; +import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm"; +import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm"; +import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit"; const CreateInfrastructureMonitor = () => { - const { user } = useSelector((state) => state.auth); const { monitorId } = useParams(); const isCreate = typeof monitorId === "undefined"; @@ -42,39 +35,29 @@ const CreateInfrastructureMonitor = () => { const { t } = useTranslation(); // State - const [errors, setErrors] = useState({}); const [https, setHttps] = useState(false); - const [isOpen, setIsOpen] = useState(false); const [updateTrigger, setUpdateTrigger] = useState(false); - const [infrastructureMonitor, setInfrastructureMonitor] = useState({ - url: "", - name: "", - notifications: [], - notify_email: false, - interval: 0.25, - cpu: false, - usage_cpu: "", - memory: false, - usage_memory: "", - disk: false, - usage_disk: "", - temperature: false, - usage_temperature: "", - secret: "", - }); // Fetch monitor details if editing - const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); const [monitor, isLoading] = useFetchHardwareMonitorById({ monitorId, updateTrigger, }); - const [createMonitor, isCreating] = useCreateMonitor(); const [deleteMonitor, isDeleting] = useDeleteMonitor(); const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings(); const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId(); const [pauseMonitor, isPausing] = usePauseMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); + const { + infrastructureMonitor, + setInfrastructureMonitor, + onChangeForm, + handleCheckboxChange, + initializeInfrastructureMonitorForCreate, + initializeInfrastructureMonitorForUpdate, + } = useInfrastructureMonitorForm(); + const { errors, validateField, validateForm } = useValidateInfrastructureForm(); + const { buildForm, submitInfrastructureForm, isCreating, isUpdating } = + useInfrastructureSubmit(); const FREQUENCIES = [ { _id: 0.25, name: t("time.fifteenSeconds") }, @@ -93,157 +76,27 @@ const CreateInfrastructureMonitor = () => { { name: "Configure", path: `/infrastructure/configure/${monitorId}` }, ]), ]; - const METRICS = ["cpu", "memory", "disk", "temperature"]; - const METRIC_PREFIX = "usage_"; - const MS_PER_MINUTE = 60000; - - const hasAlertError = (errors) => { - return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0; - }; - - const getAlertError = (errors) => { - const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX)); - return errorKey ? errors[errorKey] : null; - }; - // Populate form fields if editing useEffect(() => { if (isCreate) { if (globalSettingsLoading) return; - - const gt = globalSettings?.data?.settings?.globalThresholds || {}; - setHttps(false); - - setInfrastructureMonitor({ - url: "", - name: "", - notifications: [], - interval: 0.25, - cpu: gt.cpu !== undefined, - usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "", - memory: gt.memory !== undefined, - usage_memory: gt.memory !== undefined ? gt.memory.toString() : "", - disk: gt.disk !== undefined, - usage_disk: gt.disk !== undefined ? gt.disk.toString() : "", - temperature: gt.temperature !== undefined, - usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", - secret: "", - }); + initializeInfrastructureMonitorForCreate(globalSettings); } else if (monitor) { - const { thresholds = {} } = monitor; - setHttps(monitor.url.startsWith("https")); - - setInfrastructureMonitor({ - url: monitor.url.replace(/^https?:\/\//, ""), - name: monitor.name || "", - notifications: monitor.notifications || [], - interval: monitor.interval / MS_PER_MINUTE, - cpu: thresholds.usage_cpu !== undefined, - usage_cpu: - thresholds.usage_cpu !== undefined - ? (thresholds.usage_cpu * 100).toString() - : "", - memory: thresholds.usage_memory !== undefined, - usage_memory: - thresholds.usage_memory !== undefined - ? (thresholds.usage_memory * 100).toString() - : "", - disk: thresholds.usage_disk !== undefined, - usage_disk: - thresholds.usage_disk !== undefined - ? (thresholds.usage_disk * 100).toString() - : "", - temperature: thresholds.usage_temperature !== undefined, - usage_temperature: - thresholds.usage_temperature !== undefined - ? (thresholds.usage_temperature * 100).toString() - : "", - secret: monitor.secret || "", - }); + initializeInfrastructureMonitorForUpdate(monitor); } }, [isCreate, monitor, globalSettings, globalSettingsLoading]); // Handlers const onSubmit = async (event) => { event.preventDefault(); - - // Build the form - let form = { - url: `http${https ? "s" : ""}://` + infrastructureMonitor.url, - name: - infrastructureMonitor.name === "" - ? infrastructureMonitor.url - : infrastructureMonitor.name, - interval: infrastructureMonitor.interval * MS_PER_MINUTE, - cpu: infrastructureMonitor.cpu, - ...(infrastructureMonitor.cpu - ? { usage_cpu: infrastructureMonitor.usage_cpu } - : {}), - memory: infrastructureMonitor.memory, - ...(infrastructureMonitor.memory - ? { usage_memory: infrastructureMonitor.usage_memory } - : {}), - disk: infrastructureMonitor.disk, - ...(infrastructureMonitor.disk - ? { usage_disk: infrastructureMonitor.usage_disk } - : {}), - temperature: infrastructureMonitor.temperature, - ...(infrastructureMonitor.temperature - ? { usage_temperature: infrastructureMonitor.usage_temperature } - : {}), - secret: infrastructureMonitor.secret, - }; - - const { error } = infrastructureMonitorValidation.validate(form, { - abortEarly: false, - }); - + const form = buildForm(infrastructureMonitor, https); + const error = validateForm(form); if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - console.log(newErrors); - setErrors(newErrors); - createToast({ body: "Please check the form for errors." }); return; } - - // Build the thresholds for the form - const { - cpu, - usage_cpu, - memory, - usage_memory, - disk, - usage_disk, - temperature, - usage_temperature, - ...rest - } = form; - - const thresholds = { - ...(cpu ? { usage_cpu: usage_cpu / 100 } : {}), - ...(memory ? { usage_memory: usage_memory / 100 } : {}), - ...(disk ? { usage_disk: usage_disk / 100 } : {}), - ...(temperature ? { usage_temperature: usage_temperature / 100 } : {}), - }; - - form = { - ...(isCreate ? {} : { _id: monitorId }), - ...rest, - description: form.name, - type: "hardware", - notifications: infrastructureMonitor.notifications, - thresholds, - }; - - // Handle create or update - isCreate - ? await createMonitor({ monitor: form, redirect: "/infrastructure" }) - : await updateMonitor({ monitor: form, redirect: "/infrastructure" }); + submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId); }; const triggerUpdate = () => { @@ -252,28 +105,8 @@ const CreateInfrastructureMonitor = () => { const onChange = (event) => { const { value, name } = event.target; - setInfrastructureMonitor({ - ...infrastructureMonitor, - [name]: value, - }); - - const { error } = infrastructureMonitorValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - setErrors((prev) => ({ - ...prev, - ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), - })); - }; - - const handleCheckboxChange = (event) => { - const { name } = event.target; - const { checked } = event.target; - setInfrastructureMonitor({ - ...infrastructureMonitor, - [name]: checked, - }); + onChangeForm(name, value); + validateField(name, value); }; const handlePause = async () => { @@ -336,105 +169,19 @@ const CreateInfrastructureMonitor = () => { )} {!isCreate && ( - - - - - - - - {infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - + )} {!isCreate && ( - - - - + )} @@ -534,58 +281,12 @@ const CreateInfrastructureMonitor = () => { setNotifications={infrastructureMonitor.notifications} /> - - - - {t("infrastructureCustomizeAlerts")} - - - {t("infrastructureAlertNotificationDescription")} - - - - {METRICS.map((metric) => { - return ( - - ); - })} - {/* Error text */} - {hasAlertError(errors) && ( - - {getAlertError(errors)} - - )} - - + { - {!isCreate && ( - setIsOpen(false)} - confirmationButtonLabel={t("delete")} - onConfirm={handleRemove} - /> - )} ); }; From 9f9a2be1d8137a36474198b63f6c91fe9c5fad3e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 17 Aug 2025 15:08:12 -0700 Subject: [PATCH 044/131] fix missing URL --- server/src/service/infrastructure/networkService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.js index c624c961c..c90096d7c 100644 --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.js @@ -70,7 +70,7 @@ class NetworkService { throw new Error("Monitor URL is required"); } - const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(url)); + const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(monitor.url)); if (!response) { throw new Error("Ping failed - no result returned"); From 993339d0d1659d362dc7d5419d998d20862d3223 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 17 Aug 2025 20:21:18 -0700 Subject: [PATCH 045/131] update pager_duty validation --- .../src/Pages/Notifications/create/index.jsx | 2 - client/src/Pages/Notifications/utils.js | 8 ++-- client/src/Validation/validation.js | 44 ++++++++++++------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx index 10c280efb..6d8afe665 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -89,8 +89,6 @@ const CreateNotifications = () => { error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); - console.log(JSON.stringify(newErrors)); - console.log(JSON.stringify(form, null, 2)); createToast({ body: Object.values(newErrors)[0] }); setErrors(newErrors); return; diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js index 7ef6e5a60..0813f6868 100644 --- a/client/src/Pages/Notifications/utils.js +++ b/client/src/Pages/Notifications/utils.js @@ -9,7 +9,7 @@ export const NOTIFICATION_TYPES = [ export const TITLE_MAP = { email: "createNotifications.emailSettings.title", slack: "createNotifications.slackSettings.title", - pager_duty: "createNotifications.pagerDutySettings.title", + pager_duty: "createNotifications.pagerdutySettings.title", webhook: "createNotifications.webhookSettings.title", discord: "createNotifications.discordSettings.title", }; @@ -17,7 +17,7 @@ export const TITLE_MAP = { export const DESCRIPTION_MAP = { email: "createNotifications.emailSettings.description", slack: "createNotifications.slackSettings.description", - pager_duty: "createNotifications.pagerDutySettings.description", + pager_duty: "createNotifications.pagerdutySettings.description", webhook: "createNotifications.webhookSettings.description", discord: "createNotifications.discordSettings.description", }; @@ -25,7 +25,7 @@ export const DESCRIPTION_MAP = { export const LABEL_MAP = { email: "createNotifications.emailSettings.emailLabel", slack: "createNotifications.slackSettings.webhookLabel", - pager_duty: "createNotifications.pagerDutySettings.integrationKeyLabel", + pager_duty: "createNotifications.pagerdutySettings.integrationKeyLabel", webhook: "createNotifications.webhookSettings.webhookLabel", discord: "createNotifications.discordSettings.webhookLabel", }; @@ -33,7 +33,7 @@ export const LABEL_MAP = { export const PLACEHOLDER_MAP = { email: "createNotifications.emailSettings.emailPlaceholder", slack: "createNotifications.slackSettings.webhookPlaceholder", - pager_duty: "createNotifications.pagerDutySettings.integrationKeyPlaceholder", + pager_duty: "createNotifications.pagerdutySettings.integrationKeyPlaceholder", webhook: "createNotifications.webhookSettings.webhookPlaceholder", discord: "createNotifications.discordSettings.webhookPlaceholder", }; diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index de248f87b..c778efc9e 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -439,21 +439,35 @@ const notificationValidation = joi.object({ }), address: joi.when("type", { - is: "email", - then: joi - .string() - .email({ tlds: { allow: false } }) - .required() - .messages({ - "string.empty": "E-mail address cannot be empty", - "any.required": "E-mail address is required", - "string.email": "Please enter a valid e-mail address", - }), - otherwise: joi.string().uri().required().messages({ - "string.empty": "Webhook URL cannot be empty", - "any.required": "Webhook URL is required", - "string.uri": "Please enter a valid Webhook URL", - }), + switch: [ + { + is: "email", + then: joi + .string() + .email({ tlds: { allow: false } }) + .required() + .messages({ + "string.empty": "E-mail address cannot be empty", + "any.required": "E-mail address is required", + "string.email": "Please enter a valid e-mail address", + }), + }, + { + is: "pager_duty", + then: joi.string().required().messages({ + "string.empty": "PagerDuty routing key cannot be empty", + "any.required": "PagerDuty routing key is required", + }), + }, + { + is: joi.valid("webhook", "slack", "discord"), + then: joi.string().uri().required().messages({ + "string.empty": "Webhook URL cannot be empty", + "any.required": "Webhook URL is required", + "string.uri": "Please enter a valid Webhook URL", + }), + }, + ], }), }); From 54635425327c1fec92082ec09409e44457238266 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 10:52:00 -0700 Subject: [PATCH 046/131] unify check models --- client/src/Components/Fallback/FallbackBackground.jsx | 3 +-- .../PageSpeed/Details/Components/Charts/PieChartLegend.jsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/Components/Fallback/FallbackBackground.jsx b/client/src/Components/Fallback/FallbackBackground.jsx index 00ecafb4e..fe46f4064 100644 --- a/client/src/Components/Fallback/FallbackBackground.jsx +++ b/client/src/Components/Fallback/FallbackBackground.jsx @@ -1,7 +1,6 @@ import { useTheme } from "@emotion/react"; import Box from "@mui/material/Box"; import Background from "../../assets/Images/background-grid.svg?react"; -import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; import OutputAnimation from "../../assets/Animations/output.gif"; import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif"; import { useSelector } from "react-redux"; @@ -13,7 +12,7 @@ const FallbackBackground = () => { { : theme.palette.tertiary.main; // Find the position where the number ends and the unit begins - const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); + const match = audit?.displayValue?.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); let value; let unit; if (match) { From 155adf549e1a3e5cecb355c9306b8b34d7b5a3cf Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 10:55:12 -0700 Subject: [PATCH 047/131] add backend changes --- server/src/config/services.js | 16 +- server/src/db/models/Check.js | 264 +++++++++++------- server/src/db/models/HardwareCheck.js | 100 ------- server/src/db/models/Monitor.js | 18 +- server/src/db/models/MonitorStats.js | 4 - server/src/db/models/NetworkCheck.js | 46 --- server/src/db/models/PageSpeedCheck.js | 83 ------ server/src/db/mongo/modules/checkModule.js | 28 +- .../db/mongo/modules/hardwareCheckModule.js | 22 -- server/src/db/mongo/modules/monitorModule.js | 46 +-- .../db/mongo/modules/monitorModuleQueries.js | 68 +---- .../db/mongo/modules/networkCheckModule.js | 30 -- .../db/mongo/modules/pageSpeedCheckModule.js | 31 -- server/src/service/business/checkService.js | 3 +- .../SuperSimpleQueueHelper.js | 10 + .../service/infrastructure/bufferService.js | 66 ++--- .../service/infrastructure/statusService.js | 36 +-- 17 files changed, 228 insertions(+), 643 deletions(-) delete mode 100755 server/src/db/models/HardwareCheck.js delete mode 100644 server/src/db/models/NetworkCheck.js delete mode 100755 server/src/db/models/PageSpeedCheck.js delete mode 100755 server/src/db/mongo/modules/hardwareCheckModule.js delete mode 100644 server/src/db/mongo/modules/networkCheckModule.js delete mode 100755 server/src/db/mongo/modules/pageSpeedCheckModule.js diff --git a/server/src/config/services.js b/server/src/config/services.js index 515fe5cfe..08c2525f3 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -47,8 +47,6 @@ import { ParseBoolean } from "../utils/utils.js"; // Models import Check from "../db/models/Check.js"; -import HardwareCheck from "../db/models/HardwareCheck.js"; -import PageSpeedCheck from "../db/models/PageSpeedCheck.js"; import Monitor from "../db/models/Monitor.js"; import User from "../db/models/User.js"; import InviteToken from "../db/models/InviteToken.js"; @@ -56,7 +54,6 @@ import StatusPage from "../db/models/StatusPage.js"; import Team from "../db/models/Team.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; import MonitorStats from "../db/models/MonitorStats.js"; -import NetworkCheck from "../db/models/NetworkCheck.js"; import Notification from "../db/models/Notification.js"; import RecoveryToken from "../db/models/RecoveryToken.js"; import AppSettings from "../db/models/AppSettings.js"; @@ -65,12 +62,9 @@ import InviteModule from "../db/mongo/modules/inviteModule.js"; import CheckModule from "../db/mongo/modules/checkModule.js"; import StatusPageModule from "../db/mongo/modules/statusPageModule.js"; import UserModule from "../db/mongo/modules/userModule.js"; -import HardwareCheckModule from "../db/mongo/modules/hardwareCheckModule.js"; import MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js"; import MonitorModule from "../db/mongo/modules/monitorModule.js"; -import NetworkCheckModule from "../db/mongo/modules/networkCheckModule.js"; import NotificationModule from "../db/mongo/modules/notificationModule.js"; -import PageSpeedCheckModule from "../db/mongo/modules/pageSpeedCheckModule.js"; import RecoveryModule from "../db/mongo/modules/recoveryModule.js"; import SettingsModule from "../db/mongo/modules/settingsModule.js"; @@ -84,18 +78,15 @@ export const initializeServices = async ({ logger, envSettings, settingsService const stringService = new StringService(translationService); // Create DB - const checkModule = new CheckModule({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User }); + const checkModule = new CheckModule({ logger, Check, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); - const hardwareCheckModule = new HardwareCheckModule({ HardwareCheck, Monitor, logger }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const monitorModule = new MonitorModule({ Monitor, MonitorStats, Check, - PageSpeedCheck, - HardwareCheck, stringService, fs, path, @@ -104,9 +95,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService NormalizeData, NormalizeDataUptimeDetails, }); - const networkCheckModule = new NetworkCheckModule({ NetworkCheck }); const notificationModule = new NotificationModule({ Notification, Monitor }); - const pageSpeedCheckModule = new PageSpeedCheckModule({ PageSpeedCheck }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); const settingsModule = new SettingsModule({ AppSettings }); @@ -117,12 +106,9 @@ export const initializeServices = async ({ logger, envSettings, settingsService inviteModule, statusPageModule, userModule, - hardwareCheckModule, maintenanceWindowModule, monitorModule, - networkCheckModule, notificationModule, - pageSpeedCheckModule, recoveryModule, settingsModule, }); diff --git a/server/src/db/models/Check.js b/server/src/db/models/Check.js index 0037ffadc..42a718bd0 100755 --- a/server/src/db/models/Check.js +++ b/server/src/db/models/Check.js @@ -1,108 +1,176 @@ import mongoose from "mongoose"; -const BaseCheckSchema = mongoose.Schema({ - /** - * Reference to the associated Monitor document. - * - * @type {mongoose.Schema.Types.ObjectId} - */ - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - index: true, - }, - /** - * Status of the check (true for up, false for down). - * - * @type {Boolean} - */ - status: { - type: Boolean, - index: true, - }, - /** - * Response time of the check in milliseconds. - * - * @type {Number} - */ - responseTime: { - type: Number, - }, - /** - * Timings from Got. - * - * @type {Object} - */ - timings: { - type: Object, // Accepts any object - default: {}, - }, - /** - * HTTP status code received during the check. - * - * @type {Number} - */ - statusCode: { - type: Number, - index: true, - }, - /** - * Message or description of the check result. - * - * @type {String} - */ - message: { - type: String, - }, - /** - * Expiry date of the check, auto-calculated to expire after 30 days. - * - * @type {Date} - */ - - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24 * 30, // 30 days - }, - /** - * Acknowledgment of the check. - * - * @type {Boolean} - */ - ack: { - type: Boolean, - default: false, - }, - /** - * Resolution date of the check (when the check was resolved). - * - * @type {Date} - */ - ackAt: { - type: Date, - }, +const cpuSchema = mongoose.Schema({ + physical_core: { type: Number, default: 0 }, + logical_core: { type: Number, default: 0 }, + frequency: { type: Number, default: 0 }, + temperature: { type: [Number], default: [] }, + free_percent: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, }); -/** - * Check Schema for MongoDB collection. - * - * Represents a check associated with a monitor, storing information - * about the status and response of a particular check event. - */ -const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: true }); +const memorySchema = mongoose.Schema({ + total_bytes: { type: Number, default: 0 }, + available_bytes: { type: Number, default: 0 }, + used_bytes: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, +}); + +const diskSchema = mongoose.Schema({ + 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 }, + usage_percent: { type: Number, default: 0 }, +}); + +const hostSchema = mongoose.Schema({ + os: { type: String, default: "" }, + platform: { type: String, default: "" }, + kernel_version: { type: String, default: "" }, +}); + +const errorSchema = mongoose.Schema({ + metric: { type: [String], default: [] }, + err: { type: String, default: "" }, +}); + +const captureSchema = mongoose.Schema({ + version: { type: String, default: "" }, + mode: { type: String, default: "" }, +}); + +const networkInterfaceSchema = mongoose.Schema({ + name: { type: String }, + bytes_sent: { type: Number, default: 0 }, + bytes_recv: { type: Number, default: 0 }, + packets_sent: { type: Number, default: 0 }, + packets_recv: { type: Number, default: 0 }, + err_in: { type: Number, default: 0 }, + err_out: { type: Number, default: 0 }, + drop_in: { type: Number, default: 0 }, + drop_out: { type: Number, default: 0 }, + fifo_in: { type: Number, default: 0 }, + fifo_out: { type: Number, default: 0 }, +}); + +const CheckSchema = new mongoose.Schema( + { + // Common fields + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + index: true, + }, + + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + index: true, + }, + type: { + type: String, + enum: ["http", "hardware", "pagespeed", "distributed"], + required: true, + index: true, + }, + + status: { + type: Boolean, + index: true, + }, + + responseTime: { + type: Number, + }, + + timings: { + type: Object, + default: {}, + }, + + statusCode: { + type: Number, + index: true, + }, + + message: { + type: String, + }, + + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24 * 30, // 30 days + }, + + ack: { + type: Boolean, + default: false, + }, + + ackAt: { + type: Date, + }, + + // Hardware fields + cpu: { + type: cpuSchema, + default: () => ({}), + }, + memory: { + type: memorySchema, + default: () => ({}), + }, + disk: { + type: [diskSchema], + default: () => [], + }, + host: { + type: hostSchema, + default: () => ({}), + }, + + errors: { + type: [errorSchema], + default: () => [], + }, + + capture: { + type: captureSchema, + default: () => ({}), + }, + + net: { + type: [networkInterfaceSchema], + default: () => [], + }, + + // PageSpeed fields + accessibility: { + type: Number, + }, + bestPractices: { + type: Number, + }, + seo: { + type: Number, + }, + performance: { + type: Number, + }, + audits: { + type: Object, + }, + }, + { timestamps: true } +); + CheckSchema.index({ updatedAt: 1 }); CheckSchema.index({ monitorId: 1, updatedAt: 1 }); CheckSchema.index({ monitorId: 1, updatedAt: -1 }); CheckSchema.index({ teamId: 1, updatedAt: -1 }); export default mongoose.model("Check", CheckSchema); -export { BaseCheckSchema }; diff --git a/server/src/db/models/HardwareCheck.js b/server/src/db/models/HardwareCheck.js deleted file mode 100755 index 5b7d56213..000000000 --- a/server/src/db/models/HardwareCheck.js +++ /dev/null @@ -1,100 +0,0 @@ -import mongoose from "mongoose"; -import { BaseCheckSchema } from "./Check.js"; -const cpuSchema = mongoose.Schema({ - physical_core: { type: Number, default: 0 }, - logical_core: { type: Number, default: 0 }, - frequency: { type: Number, default: 0 }, - temperature: { type: [Number], default: [] }, - free_percent: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const memorySchema = mongoose.Schema({ - total_bytes: { type: Number, default: 0 }, - available_bytes: { type: Number, default: 0 }, - used_bytes: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const diskSchema = mongoose.Schema({ - 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 }, - usage_percent: { type: Number, default: 0 }, -}); - -const hostSchema = mongoose.Schema({ - os: { type: String, default: "" }, - platform: { type: String, default: "" }, - kernel_version: { type: String, default: "" }, -}); - -const errorSchema = mongoose.Schema({ - metric: { type: [String], default: [] }, - err: { type: String, default: "" }, -}); - -const captureSchema = mongoose.Schema({ - version: { type: String, default: "" }, - mode: { type: String, default: "" }, -}); - -const networkInterfaceSchema = mongoose.Schema({ - name: { type: String }, - bytes_sent: { type: Number, default: 0 }, - bytes_recv: { type: Number, default: 0 }, - packets_sent: { type: Number, default: 0 }, - packets_recv: { type: Number, default: 0 }, - err_in: { type: Number, default: 0 }, - err_out: { type: Number, default: 0 }, - drop_in: { type: Number, default: 0 }, - drop_out: { type: Number, default: 0 }, - fifo_in: { type: Number, default: 0 }, - fifo_out: { type: Number, default: 0 }, -}); - -const HardwareCheckSchema = mongoose.Schema( - { - ...BaseCheckSchema.obj, - cpu: { - type: cpuSchema, - default: () => ({}), - }, - memory: { - type: memorySchema, - default: () => ({}), - }, - disk: { - type: [diskSchema], - default: () => [], - }, - host: { - type: hostSchema, - default: () => ({}), - }, - - errors: { - type: [errorSchema], - default: () => [], - }, - - capture: { - type: captureSchema, - default: () => ({}), - }, - - net: { - type: [networkInterfaceSchema], - default: () => [], - required: false, - }, - }, - { timestamps: true } -); - -HardwareCheckSchema.index({ createdAt: 1 }); -HardwareCheckSchema.index({ monitorId: 1, createdAt: 1 }); -HardwareCheckSchema.index({ monitorId: 1, createdAt: -1 }); - -export default mongoose.model("HardwareCheck", HardwareCheckSchema); diff --git a/server/src/db/models/Monitor.js b/server/src/db/models/Monitor.js index 1ee467999..8d4ff9468 100755 --- a/server/src/db/models/Monitor.js +++ b/server/src/db/models/Monitor.js @@ -1,6 +1,4 @@ import mongoose from "mongoose"; -import HardwareCheck from "./HardwareCheck.js"; -import PageSpeedCheck from "./PageSpeedCheck.js"; import Check from "./Check.js"; import MonitorStats from "./MonitorStats.js"; import StatusPage from "./StatusPage.js"; @@ -133,13 +131,7 @@ MonitorSchema.pre("findOneAndDelete", async function (next) { throw new Error("Monitor not found"); } - if (doc?.type === "pagespeed") { - await PageSpeedCheck.deleteMany({ monitorId: doc._id }); - } else if (doc?.type === "hardware") { - await HardwareCheck.deleteMany({ monitorId: doc._id }); - } else { - await Check.deleteMany({ monitorId: doc._id }); - } + await Check.deleteMany({ monitorId: doc._id }); // Deal with status pages await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } }); @@ -156,13 +148,7 @@ MonitorSchema.pre("deleteMany", async function (next) { const monitors = await this.model.find(filter).select(["_id", "type"]).lean(); for (const monitor of monitors) { - if (monitor.type === "pagespeed") { - await PageSpeedCheck.deleteMany({ monitorId: monitor._id }); - } else if (monitor.type === "hardware") { - await HardwareCheck.deleteMany({ monitorId: monitor._id }); - } else { - await Check.deleteMany({ monitorId: monitor._id }); - } + await Check.deleteMany({ monitorId: monitor._id }); await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } }); await MonitorStats.deleteMany({ monitorId: monitor._id.toString() }); } diff --git a/server/src/db/models/MonitorStats.js b/server/src/db/models/MonitorStats.js index c28811b33..c081c8a4f 100755 --- a/server/src/db/models/MonitorStats.js +++ b/server/src/db/models/MonitorStats.js @@ -40,10 +40,6 @@ const MonitorStatsSchema = new mongoose.Schema( type: Number, default: 0, }, - uptBurnt: { - type: mongoose.Schema.Types.Decimal128, - required: false, - }, }, { timestamps: true } ); diff --git a/server/src/db/models/NetworkCheck.js b/server/src/db/models/NetworkCheck.js deleted file mode 100644 index eca69eef9..000000000 --- a/server/src/db/models/NetworkCheck.js +++ /dev/null @@ -1,46 +0,0 @@ -import mongoose from "mongoose"; -import { BaseCheckSchema } from "./Check.js"; - -const networkInterfaceSchema = mongoose.Schema({ - name: { type: String, required: true }, - bytes_sent: { type: Number, default: 0 }, - bytes_recv: { type: Number, default: 0 }, - packets_sent: { type: Number, default: 0 }, - packets_recv: { type: Number, default: 0 }, - err_in: { type: Number, default: 0 }, - err_out: { type: Number, default: 0 }, - drop_in: { type: Number, default: 0 }, - drop_out: { type: Number, default: 0 }, - fifo_in: { type: Number, default: 0 }, - fifo_out: { type: Number, default: 0 }, -}); - -const captureSchema = mongoose.Schema({ - version: { type: String, default: "" }, - mode: { type: String, default: "" }, -}); - -const NetworkCheckSchema = mongoose.Schema( - { - ...BaseCheckSchema.obj, - data: { - type: [networkInterfaceSchema], - default: () => [], - }, - capture: { - type: captureSchema, - default: () => ({}), - }, - errors: { - type: mongoose.Schema.Types.Mixed, - default: null, - }, - }, - { timestamps: true } -); - -NetworkCheckSchema.index({ createdAt: 1 }); -NetworkCheckSchema.index({ monitorId: 1, createdAt: 1 }); -NetworkCheckSchema.index({ monitorId: 1, createdAt: -1 }); - -export default mongoose.model("NetworkCheck", NetworkCheckSchema); diff --git a/server/src/db/models/PageSpeedCheck.js b/server/src/db/models/PageSpeedCheck.js deleted file mode 100755 index 67c7c375e..000000000 --- a/server/src/db/models/PageSpeedCheck.js +++ /dev/null @@ -1,83 +0,0 @@ -import mongoose from "mongoose"; -import { BaseCheckSchema } from "./Check.js"; -const AuditSchema = mongoose.Schema({ - id: { type: String, required: true }, - title: { type: String, required: true }, - description: { type: String, required: true }, - score: { type: Number, required: true }, - scoreDisplayMode: { type: String, required: true }, - displayValue: { type: String, required: true }, - numericValue: { type: Number, required: true }, - numericUnit: { type: String, required: true }, -}); - -const AuditsSchema = mongoose.Schema({ - cls: { - type: AuditSchema, - required: true, - }, - si: { - type: AuditSchema, - required: true, - }, - fcp: { - type: AuditSchema, - required: true, - }, - lcp: { - type: AuditSchema, - required: true, - }, - tbt: { - type: AuditSchema, - required: true, - }, -}); - -/** - * Mongoose schema for storing metrics from Google Lighthouse. - * @typedef {Object} PageSpeedCheck - * @property {mongoose.Schema.Types.ObjectId} monitorId - Reference to the Monitor model. - * @property {number} accessibility - Accessibility score. - * @property {number} bestPractices - Best practices score. - * @property {number} seo - SEO score. - * @property {number} performance - Performance score. - */ - -const PageSpeedCheck = mongoose.Schema( - { - ...BaseCheckSchema.obj, - accessibility: { - type: Number, - required: true, - }, - bestPractices: { - type: Number, - required: true, - }, - seo: { - type: Number, - required: true, - }, - performance: { - type: Number, - required: true, - }, - audits: { - type: AuditsSchema, - required: true, - }, - }, - { timestamps: true } -); - -/** - * Mongoose model for storing metrics from Google Lighthouse. - * @typedef {mongoose.Model} LighthouseMetricsModel - */ - -PageSpeedCheck.index({ createdAt: 1 }); -PageSpeedCheck.index({ monitorId: 1, createdAt: 1 }); -PageSpeedCheck.index({ monitorId: 1, createdAt: -1 }); - -export default mongoose.model("PageSpeedCheck", PageSpeedCheck); diff --git a/server/src/db/mongo/modules/checkModule.js b/server/src/db/mongo/modules/checkModule.js index a606a593b..dfca02bed 100755 --- a/server/src/db/mongo/modules/checkModule.js +++ b/server/src/db/mongo/modules/checkModule.js @@ -31,7 +31,7 @@ class CheckModule { } }; - getChecksByMonitor = async ({ monitorId, type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => { + getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => { try { status = status === "true" ? true : status === "false" ? false : undefined; page = parseInt(page); @@ -79,19 +79,7 @@ class CheckModule { skip = page * rowsPerPage; } - const checkModels = { - http: this.Check, - ping: this.Check, - docker: this.Check, - port: this.Check, - pagespeed: this.PageSpeedCheck, - hardware: this.HardwareCheck, - game: this.Check, - }; - - const Model = checkModels[type]; - - const checks = await Model.aggregate([ + const checks = await this.Check.aggregate([ { $match: matchStage }, { $sort: { createdAt: sortOrder } }, { @@ -166,18 +154,6 @@ class CheckModule { const aggregatePipeline = [ { $match: matchStage }, - { - $unionWith: { - coll: "hardwarechecks", - pipeline: [{ $match: matchStage }], - }, - }, - { - $unionWith: { - coll: "pagespeedchecks", - pipeline: [{ $match: matchStage }], - }, - }, { $sort: { createdAt: sortOrder } }, { diff --git a/server/src/db/mongo/modules/hardwareCheckModule.js b/server/src/db/mongo/modules/hardwareCheckModule.js deleted file mode 100755 index 448e72fed..000000000 --- a/server/src/db/mongo/modules/hardwareCheckModule.js +++ /dev/null @@ -1,22 +0,0 @@ -const SERVICE_NAME = "hardwareCheckModule"; - -class HardwareCheckModule { - constructor({ HardwareCheck, Monitor, logger }) { - this.HardwareCheck = HardwareCheck; - this.Monitor = Monitor; - this.logger = logger; - } - - createHardwareChecks = async (hardwareChecks) => { - try { - await this.HardwareCheck.insertMany(hardwareChecks, { ordered: false }); - return true; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createHardwareChecks"; - throw error; - } - }; -} - -export default HardwareCheckModule; diff --git a/server/src/db/mongo/modules/monitorModule.js b/server/src/db/mongo/modules/monitorModule.js index 0e301cdcf..8beb9316c 100755 --- a/server/src/db/mongo/modules/monitorModule.js +++ b/server/src/db/mongo/modules/monitorModule.js @@ -11,25 +11,10 @@ import { const SERVICE_NAME = "monitorModule"; class MonitorModule { - constructor({ - Monitor, - MonitorStats, - Check, - PageSpeedCheck, - HardwareCheck, - stringService, - fs, - path, - fileURLToPath, - ObjectId, - NormalizeData, - NormalizeDataUptimeDetails, - }) { + constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) { this.Monitor = Monitor; this.MonitorStats = MonitorStats; this.Check = Check; - this.PageSpeedCheck = PageSpeedCheck; - this.HardwareCheck = HardwareCheck; this.stringService = stringService; this.fs = fs; this.path = path; @@ -38,15 +23,6 @@ class MonitorModule { this.NormalizeData = NormalizeData; this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails; - this.CHECK_MODEL_LOOKUP = { - http: Check, - ping: Check, - docker: Check, - port: Check, - pagespeed: PageSpeedCheck, - hardware: HardwareCheck, - }; - const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -146,19 +122,18 @@ class MonitorModule { }; //Helper - getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => { + getMonitorChecks = async (monitorId, dateRange, sortOrder) => { const indexSpec = { monitorId: 1, - createdAt: sortOrder, // This will be 1 or -1 + updatedAt: sortOrder, // This will be 1 or -1 }; const [checksAll, checksForDateRange] = await Promise.all([ - model.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), - model - .find({ - monitorId, - createdAt: { $gte: dateRange.start, $lte: dateRange.end }, - }) + this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), + this.Check.find({ + monitorId, + createdAt: { $gte: dateRange.start, $lte: dateRange.end }, + }) .hint(indexSpec) .lean(), ]); @@ -295,9 +270,8 @@ class MonitorModule { const sort = sortOrder === "asc" ? 1 : -1; // Get Checks for monitor in date range requested - const model = this.CHECK_MODEL_LOOKUP[monitor.type]; const dates = this.getDateRange(dateRange); - const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, model, dates, sort); + const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort); // Build monitor stats const monitorStats = { @@ -339,7 +313,7 @@ class MonitorModule { }; const dateString = formatLookup[dateRange]; - const hardwareStats = await this.HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); + const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); const stats = hardwareStats[0]; diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js index f6d20d855..0a8f807e8 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -5,12 +5,12 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { { $match: { monitorId: new ObjectId(monitorId), - createdAt: { $gte: dates.start, $lte: dates.end }, + updatedAt: { $gte: dates.start, $lte: dates.end }, }, }, { $sort: { - createdAt: 1, + updatedAt: 1, }, }, { @@ -173,6 +173,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { { $match: { monitorId: monitor._id, + type: "hardware", createdAt: { $gte: dates.start, $lte: dates.end }, }, }, @@ -225,7 +226,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, { $lookup: { - from: "hardwarechecks", + from: "checks", let: { diskCount: "$diskCount", netCount: "$netCount", @@ -987,11 +988,6 @@ const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, row // Add checks if (limit) { let checksCollection = "checks"; - if (type === "pagespeed") { - checksCollection = "pagespeedchecks"; - } else if (type === "hardware") { - checksCollection = "hardwarechecks"; - } monitorsPipeline.push({ $lookup: { from: checksCollection, @@ -1057,11 +1053,7 @@ const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsP // Add checks if (limit) { let checksCollection = "checks"; - if (type === "pagespeed") { - checksCollection = "pagespeedchecks"; - } else if (type === "hardware") { - checksCollection = "hardwarechecks"; - } + pipeline.push({ $lookup: { from: checksCollection, @@ -1175,46 +1167,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => { }, ] : []), - ...(limit - ? [ - { - $lookup: { - from: "pagespeedchecks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "pagespeedchecks", - }, - }, - ] - : []), - ...(limit - ? [ - { - $lookup: { - from: "hardwarechecks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "hardwarechecks", - }, - }, - ] - : []), { $addFields: { @@ -1225,14 +1177,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => { case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] }, then: "$standardchecks", }, - { - case: { $eq: ["$type", "pagespeed"] }, - then: "$pagespeedchecks", - }, - { - case: { $eq: ["$type", "hardware"] }, - then: "$hardwarechecks", - }, ], default: [], }, @@ -1242,8 +1186,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => { { $project: { standardchecks: 0, - pagespeedchecks: 0, - hardwarechecks: 0, }, }, ], diff --git a/server/src/db/mongo/modules/networkCheckModule.js b/server/src/db/mongo/modules/networkCheckModule.js deleted file mode 100644 index 917e68440..000000000 --- a/server/src/db/mongo/modules/networkCheckModule.js +++ /dev/null @@ -1,30 +0,0 @@ -const SERVICE_NAME = "networkCheckModule"; - -class NetworkCheckModule { - constructor({ NetworkCheck }) { - this.NetworkCheck = NetworkCheck; - } - createNetworkCheck = async (networkCheckData) => { - try { - const networkCheck = await new this.NetworkCheck(networkCheckData); - await networkCheck.save(); - return networkCheck; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createNetworkCheck"; - throw error; - } - }; - getNetworkChecksByMonitorId = async (monitorId, limit = 100) => { - try { - const networkChecks = await this.NetworkCheck.find({ monitorId }).sort({ createdAt: -1 }).limit(limit); - return networkChecks; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNetworkChecksByMonitorId"; - throw error; - } - }; -} - -export default NetworkCheckModule; diff --git a/server/src/db/mongo/modules/pageSpeedCheckModule.js b/server/src/db/mongo/modules/pageSpeedCheckModule.js deleted file mode 100755 index b2a3eb71d..000000000 --- a/server/src/db/mongo/modules/pageSpeedCheckModule.js +++ /dev/null @@ -1,31 +0,0 @@ -// import PageSpeedCheck from "../../models/PageSpeedCheck.js"; -const SERVICE_NAME = "pageSpeedCheckModule"; - -class PageSpeedCheckModule { - constructor({ PageSpeedCheck }) { - this.PageSpeedCheck = PageSpeedCheck; - } - - createPageSpeedChecks = async (pageSpeedChecks) => { - try { - await this.PageSpeedCheck.insertMany(pageSpeedChecks, { ordered: false }); - return true; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createPageSpeedCheck"; - throw error; - } - }; - - deletePageSpeedChecksByMonitorId = async (monitorId) => { - try { - const result = await this.PageSpeedCheck.deleteMany({ monitorId }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deletePageSpeedChecksByMonitorId"; - throw error; - } - }; -} -export default PageSpeedCheckModule; diff --git a/server/src/service/business/checkService.js b/server/src/service/business/checkService.js index 7966fdbb4..9beb43326 100644 --- a/server/src/service/business/checkService.js +++ b/server/src/service/business/checkService.js @@ -33,10 +33,9 @@ class CheckService { throw this.errorService.createAuthorizationError(); } - let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; + let { sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; const result = await this.db.checkModule.getChecksByMonitor({ monitorId, - type, sortOrder, dateRange, filter, diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index 7659b1c71..ac8da9169 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -2,6 +2,16 @@ const SERVICE_NAME = "JobQueueHelper"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; + + /** + * @param {{ + * db: import("../database").Database, + * logger: import("../logger").Logger, + * networkService: import("../networkService").NetworkService, + * statusService: import("../statusService").StatusService, + * notificationService: import("../notificationService").NotificationService + * }} + */ constructor({ db, logger, networkService, statusService, notificationService }) { this.db = db; this.logger = logger; diff --git a/server/src/service/infrastructure/bufferService.js b/server/src/service/infrastructure/bufferService.js index dc86597b2..f55e25037 100755 --- a/server/src/service/infrastructure/bufferService.js +++ b/server/src/service/infrastructure/bufferService.js @@ -1,13 +1,4 @@ const SERVICE_NAME = "BufferService"; -const TYPE_MAP = { - http: "checks", - ping: "checks", - port: "checks", - docker: "checks", - pagespeed: "pagespeedChecks", - hardware: "hardwareChecks", - game: "checks", -}; class BufferService { static SERVICE_NAME = SERVICE_NAME; @@ -16,17 +7,7 @@ class BufferService { this.db = db; this.logger = logger; this.SERVICE_NAME = SERVICE_NAME; - this.buffers = { - checks: [], - pagespeedChecks: [], - hardwareChecks: [], - }; - this.OPERATION_MAP = { - checks: this.db.checkModule.createChecks, - pagespeedChecks: this.db.pageSpeedCheckModule.createPageSpeedChecks, - hardwareChecks: this.db.hardwareCheckModule.createHardwareChecks, - }; - + this.buffer = []; this.scheduleNextFlush(); this.logger.info({ message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`, @@ -39,9 +20,9 @@ class BufferService { return BufferService.SERVICE_NAME; } - addToBuffer({ check, type }) { + addToBuffer({ check }) { try { - this.buffers[TYPE_MAP[type]].push(check); + this.buffer.push(check); } catch (error) { this.logger.error({ message: error.message, @@ -55,7 +36,7 @@ class BufferService { scheduleNextFlush() { this.bufferTimer = setTimeout(async () => { try { - await this.flushBuffers(); + await this.flushBuffer(); } catch (error) { this.logger.error({ message: `Error in flush cycle: ${error.message}`, @@ -69,35 +50,24 @@ class BufferService { } }, this.BUFFER_TIMEOUT); } - async flushBuffers() { - let items = 0; - for (const [bufferName, buffer] of Object.entries(this.buffers)) { - items += buffer.length; - const operation = this.OPERATION_MAP[bufferName]; - if (!operation) { - this.logger.error({ - message: `No operation found for ${bufferName}`, - service: this.SERVICE_NAME, - method: "flushBuffers", - }); - continue; - } - try { - await operation(buffer); - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "flushBuffers", - stack: error.stack, - }); - } - this.buffers[bufferName] = []; + async flushBuffer() { + let items = this.buffer.length; + + try { + await this.db.checkModule.createChecks(this.buffer); + } catch (error) { + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "flushBuffer", + stack: error.stack, + }); } + this.buffer = []; this.logger.debug({ message: `Flushed ${items} items`, service: this.SERVICE_NAME, - method: "flushBuffers", + method: "flushBuffer", }); items = 0; } diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 4a98996fb..af2c82fa9 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -1,17 +1,14 @@ import MonitorStats from "../../db/models/MonitorStats.js"; -import { safelyParseFloat } from "../../utils/dataUtils.js"; const SERVICE_NAME = "StatusService"; class StatusService { static SERVICE_NAME = SERVICE_NAME; /** - * Creates an instance of StatusService. - * - * @param {Object} db - The database instance. - * @param {Object} logger - The logger instance. - */ - constructor({ db, logger, buffer }) { + * @param {{ + * buffer: import("./bufferService.js").BufferService + * }} + */ constructor({ db, logger, buffer }) { this.db = db; this.logger = logger; this.buffer = buffer; @@ -24,7 +21,7 @@ class StatusService { async updateRunningStats({ monitor, networkResponse }) { try { const monitorId = monitor._id; - const { responseTime, status, upt_burnt } = networkResponse; + const { responseTime, status } = networkResponse; // Get stats let stats = await MonitorStats.findOne({ monitorId }); if (!stats) { @@ -36,8 +33,6 @@ class StatusService { totalDownChecks: 0, uptimePercentage: 0, lastCheck: null, - timeSInceLastCheck: 0, - uptBurnt: 0, }); } @@ -82,12 +77,6 @@ class StatusService { // latest check stats.lastCheckTimestamp = new Date().getTime(); - // UPT burned - if (typeof upt_burnt !== "undefined" && upt_burnt !== null) { - const currentUptBurnt = safelyParseFloat(stats.uptBurnt); - const newUptBurnt = safelyParseFloat(upt_burnt); - stats.uptBurnt = currentUptBurnt + newUptBurnt; - } await stats.save(); return true; } catch (error) { @@ -119,7 +108,8 @@ class StatusService { * @returns {boolean} returnObject.prevStatus - The previous status of the monitor */ updateStatus = async (networkResponse) => { - this.insertCheck(networkResponse); + const check = this.buildCheck(networkResponse); + await this.insertCheck(check); try { const { monitorId, status, code } = networkResponse; const monitor = await this.db.monitorModule.getMonitorById(monitorId); @@ -198,6 +188,7 @@ class StatusService { const check = { monitorId, teamId, + type, status, statusCode: code, responseTime, @@ -264,25 +255,24 @@ class StatusService { * @param {Object} networkResponse.payload - The payload of the response. * @returns {Promise} A promise that resolves when the check is inserted. */ - insertCheck = async (networkResponse) => { + insertCheck = async (check) => { try { - const check = this.buildCheck(networkResponse); if (typeof check === "undefined") { this.logger.warn({ message: "Failed to build check", service: this.SERVICE_NAME, method: "insertCheck", - details: networkResponse, }); - return; + return false; } - this.buffer.addToBuffer({ check, type: networkResponse.type }); + this.buffer.addToBuffer({ check }); + return true; } catch (error) { this.logger.error({ message: error.message, service: error.service || this.SERVICE_NAME, method: error.method || "insertCheck", - details: error.details || `Error inserting check for monitor: ${networkResponse?.monitorId}`, + details: error.details || `Error inserting check for monitor: ${check?.monitorId}`, stack: error.stack, }); } From 1788cda5c2816011db318860a222f833bd9a1e84 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 11:27:41 -0700 Subject: [PATCH 048/131] let -> const --- server/src/db/mongo/modules/monitorModuleQueries.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js index 0a8f807e8..32aed1529 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -987,7 +987,7 @@ const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, row // Add checks if (limit) { - let checksCollection = "checks"; + const checksCollection = "checks"; monitorsPipeline.push({ $lookup: { from: checksCollection, @@ -1052,7 +1052,7 @@ const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsP // Add checks if (limit) { - let checksCollection = "checks"; + const checksCollection = "checks"; pipeline.push({ $lookup: { From 613fbf8c395a357c044471437e1ddbcf55a6f0ea Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Tue, 19 Aug 2025 01:11:56 +0530 Subject: [PATCH 049/131] Fix: Minor Changes --- client/src/Components/StarPrompt/index.jsx | 2 +- client/src/Pages/Uptime/Create/index.jsx | 16 +--------------- client/src/Validation/validation.js | 6 ++++++ 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/client/src/Components/StarPrompt/index.jsx b/client/src/Components/StarPrompt/index.jsx index 64fce1d3f..f5f130387 100644 --- a/client/src/Components/StarPrompt/index.jsx +++ b/client/src/Components/StarPrompt/index.jsx @@ -74,7 +74,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" }) { - try { - return new URL(url); - } catch (error) { - return null; - } -}; - /** * Create page renders monitor creation or configuration views. * @component @@ -299,7 +285,7 @@ const UptimeCreate = ({ isClone = false }) => { const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; const displayInterval = monitor?.interval / MS_PER_MINUTE || 1; - const parsedUrl = parseUrl(monitor?.url); + const parsedUrl = monitor?.url; const protocol = parsedUrl?.protocol?.replace(":", "") || ""; useEffect(() => { diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index c778efc9e..902356ae2 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -150,6 +150,11 @@ const monitorValidation = joi.object({ // can be replaced by a shortest alternative // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ "(?:" + + // Single hostname without dots (like localhost) + "[a-z0-9\\u00a1-\\uffff][a-z0-9\\u00a1-\\uffff_-]{0,62}" + + "|" + + // Domain with dots + "(?:" + "(?:" + "[a-z0-9\\u00a1-\\uffff]" + "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + @@ -159,6 +164,7 @@ const monitorValidation = joi.object({ // TLD identifier name, may end with dot "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + ")" + + ")" + // port number (optional) "(?::\\d{2,5})?" + // resource path (optional) From 74e80f086a3e34f08a1a36e5fabad7012ac1840b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 13:40:22 -0700 Subject: [PATCH 050/131] udpate monitor model --- server/src/db/models/Monitor.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/src/db/models/Monitor.js b/server/src/db/models/Monitor.js index 8d4ff9468..0ad0e0a78 100755 --- a/server/src/db/models/Monitor.js +++ b/server/src/db/models/Monitor.js @@ -28,6 +28,18 @@ const MonitorSchema = mongoose.Schema( type: Boolean, default: undefined, }, + statusWindow: { + type: [Boolean], + default: [], + }, + statusWindowSize: { + type: Number, + default: 5, + }, + statusWindowThreshold: { + type: Number, + default: 0.6, + }, type: { type: String, required: true, From d5778ec1737bb740e15eb6111e9593b5d6124ed1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 13:40:56 -0700 Subject: [PATCH 051/131] update stauts service to use n of m status changes --- .../service/infrastructure/bufferService.js | 2 +- .../service/infrastructure/statusService.js | 59 ++++++++++++++----- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/server/src/service/infrastructure/bufferService.js b/server/src/service/infrastructure/bufferService.js index f55e25037..ed48cc654 100755 --- a/server/src/service/infrastructure/bufferService.js +++ b/server/src/service/infrastructure/bufferService.js @@ -3,7 +3,7 @@ const SERVICE_NAME = "BufferService"; class BufferService { static SERVICE_NAME = SERVICE_NAME; constructor({ db, logger, envSettings }) { - this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 5000 : 1000 * 60 * 1; // 1 minute + this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 1000 : 1000 * 60 * 1; // 1 minute this.db = db; this.logger = logger; this.SERVICE_NAME = SERVICE_NAME; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index af2c82fa9..0c3434df0 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -117,31 +117,62 @@ class StatusService { // Update running stats this.updateRunningStats({ monitor, networkResponse }); - // No change in monitor status, return early - if (monitor.status === status) + // Update status sliding window + monitor.statusWindow.push(status); + if (monitor.statusWindow.length > monitor.statusWindowSize) { + monitor.statusWindow.shift(); + } + + if (!monitor.status) { + monitor.status = status; + } + + let newStatus = monitor.status; + let statusChanged = false; + const prevStatus = monitor.status; + + // Return early if not enough data points + if (monitor.statusWindow.length < monitor.statusWindowSize) { + await monitor.save(); return { monitor, statusChanged: false, - prevStatus: monitor.status, + prevStatus, code, - timestamp: new Date().getTime(), + timestamp: Date.now(), }; + } - // Monitor status changed, save prev status and update monitor - this.logger.info({ - service: this.SERVICE_NAME, - message: `${monitor.name} went from ${this.getStatusString(monitor.status)} to ${this.getStatusString(status)}`, - prevStatus: monitor.status, - newStatus: status, - }); + // Check if threshold has been met + const failures = monitor.statusWindow.filter((s) => s === false).length; + const failureRate = failures / monitor.statusWindow.length; - const prevStatus = monitor.status; - monitor.status = status; + // If threshold has been met and the monitor is not already down, mark down: + if (failureRate > monitor.statusWindowThreshold && monitor.status !== false) { + newStatus = false; + statusChanged = true; + } + // If the failure rate is below the threshold and the monitor is down, recover: + else if (failureRate <= monitor.statusWindowThreshold && monitor.status === false) { + newStatus = true; + statusChanged = true; + } + + if (statusChanged) { + this.logger.info({ + service: this.SERVICE_NAME, + message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`, + prevStatus, + newStatus, + }); + } + + monitor.status = newStatus; await monitor.save(); return { monitor, - statusChanged: true, + statusChanged, prevStatus, code, timestamp: new Date().getTime(), From 27415aeca624ec6b90d713614fdd69b8cf26d282 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 15:34:15 -0700 Subject: [PATCH 052/131] fix undefined bug, updated server validation --- .../src/service/infrastructure/statusService.js | 17 ++++++++++++++--- server/src/validation/joi.js | 4 ++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 0c3434df0..eac2d34bd 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -117,13 +117,18 @@ class StatusService { // Update running stats this.updateRunningStats({ monitor, networkResponse }); + // If the status window size has changed, empty + while (monitor.statusWindow.length > monitor.statusWindowSize) { + monitor.statusWindow.shift(); + } + // Update status sliding window monitor.statusWindow.push(status); if (monitor.statusWindow.length > monitor.statusWindowSize) { monitor.statusWindow.shift(); } - if (!monitor.status) { + if (monitor.status === undefined || monitor.status === null) { monitor.status = status; } @@ -145,10 +150,16 @@ class StatusService { // Check if threshold has been met const failures = monitor.statusWindow.filter((s) => s === false).length; - const failureRate = failures / monitor.statusWindow.length; + const failureRate = (failures / monitor.statusWindow.length) * 100; + + console.log({ + failureRate, + status: monitor.status, + statusWindow: monitor.statusWindow, + }); // If threshold has been met and the monitor is not already down, mark down: - if (failureRate > monitor.statusWindowThreshold && monitor.status !== false) { + if (failureRate >= monitor.statusWindowThreshold && monitor.status !== false) { newStatus = false; statusChanged = true; } diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index 31f8e97c0..725004afa 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -155,6 +155,8 @@ const createMonitorBodyValidation = joi.object({ name: joi.string().required(), description: joi.string().required(), type: joi.string().required(), + statusWindowSize: joi.number().min(1).max(20).default(5), + statusWindowThreshold: joi.number().min(1).max(100).default(60), url: joi.string().required(), ignoreTlsErrors: joi.boolean().default(false), port: joi.number(), @@ -183,6 +185,8 @@ const createMonitorsBodyValidation = joi.array().items( const editMonitorBodyValidation = joi.object({ name: joi.string(), + statusWindowSize: joi.number().min(1).max(20).default(5), + statusWindowThreshold: joi.number().min(1).max(100).default(60), description: joi.string(), interval: joi.number(), notifications: joi.array().items(joi.string()), From ced2417c986dd0a56927903c194cd3295749fb8f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 15:34:30 -0700 Subject: [PATCH 053/131] add incident config --- client/src/Hooks/monitorHooks.js | 2 ++ client/src/Pages/Uptime/Create/index.jsx | 44 ++++++++++++++++++++++++ client/src/Validation/validation.js | 10 ++++++ 3 files changed, 56 insertions(+) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 970b5d97f..eb1e7acdc 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -369,6 +369,8 @@ const useUpdateMonitor = () => { setIsLoading(true); const updatedFields = { name: monitor.name, + statusWindowSize: monitor.statusWindowSize, + statusWindowThreshold: monitor.statusWindowThreshold, description: monitor.description, interval: monitor.interval, notifications: monitor.notifications, diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 668d840af..c763071da 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -55,6 +55,8 @@ const UptimeCreate = ({ isClone = false }) => { // States const [monitor, setMonitor] = useState({ type: "http", + statusWindowSize: 5, + statusWindowThreshold: 60, matchMethod: "equal", expectedValue: "", jsonPath: "", @@ -177,7 +179,10 @@ const UptimeCreate = ({ isClone = false }) => { ? `http${https ? "s" : ""}://` + monitor.url : monitor.url, name: monitor.name || monitor.url.substring(0, 50), + statusWindowSize: monitor.statusWindowSize, + statusWindowThreshold: monitor.statusWindowThreshold, type: monitor.type, + port: monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, interval: monitor.interval, @@ -192,6 +197,8 @@ const UptimeCreate = ({ isClone = false }) => { _id: monitor._id, url: monitor.url, name: monitor.name || monitor.url.substring(0, 50), + statusWindowSize: monitor.statusWindowSize, + statusWindowThreshold: monitor.statusWindowThreshold, type: monitor.type, matchMethod: monitor.matchMethod, expectedValue: monitor.expectedValue, @@ -602,6 +609,43 @@ const UptimeCreate = ({ isClone = false }) => { /> + + + + Incidents + + + { + "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value." + } + + + + + + + Date: Mon, 18 Aug 2025 15:46:41 -0700 Subject: [PATCH 054/131] extract strings, sort json --- client/src/Pages/Uptime/Create/index.jsx | 12 +-- client/src/locales/en.json | 104 ++++++++++++----------- 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index c763071da..3f905fa11 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -615,18 +615,16 @@ const UptimeCreate = ({ isClone = false }) => { component="h2" variant="h2" > - Incidents + {t("createMonitorPage.incidentConfigTitle")} - { - "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value." - } + {t("createMonitorPage.incidentConfigDescription")} { /> to get the full container ID.", + "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", - "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.", - "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type." + "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard." }, "uptimeMonitor": { "fallback": { @@ -1066,5 +1071,6 @@ "websiteMonitoring": "Website monitoring", "websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.", "whenNewIncident": "When there is a new incident,", - "window": "window" + "window": "window", + "YourPhoto": "Profile photo" } From 4a116381826efc1084357da13e0b53c0f40dff6f Mon Sep 17 00:00:00 2001 From: karenvicent Date: Wed, 20 Aug 2025 12:51:24 -0400 Subject: [PATCH 055/131] refactor create maintenance window page --- .../Components/ConfigSelect/index.jsx | 38 +++ .../hooks/useMaintenanceActions.jsx | 64 +++++ .../hooks/useMaintenanceData.jsx | 74 ++++++ .../Maintenance/CreateMaintenance/index.jsx | 246 +++--------------- 4 files changed, 219 insertions(+), 203 deletions(-) create mode 100644 client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx create mode 100644 client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx create mode 100644 client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx new file mode 100644 index 000000000..1627ef3d5 --- /dev/null +++ b/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx @@ -0,0 +1,38 @@ +import Select from "../../../../../Components/Inputs/Select"; +import PropTypes from "prop-types"; +const ConfigSelect = ({ configSelection, valueSelect, onChange, ...props }) => { + const getValueById = (config, id) => { + const item = config.find((config) => config._id === id); + return item ? (item.value ? item.value : item.name) : null; + }; + + const getIdByValue = (config, name) => { + const item = config.find((config) => { + if (config.value) { + return config.value === name; + } else { + return config.name === name; + } + }); + return item ? item._id : null; + }; + + return ( + { - handleFormChange( - "repeat", - getValueById(repeatConfig, event.target.value) - ); - }} - items={repeatConfig} + valueSelect={form.repeat} + configSelection={repeatConfig} + onChange={(value) => handleFormChange("repeat", value)} /> @@ -437,7 +280,7 @@ const CreateMaintenance = () => { }} sx={{}} onChange={(newDate) => { - handleTimeChange("startDate", newDate); + handleFormChange("startDate", newDate); }} error={errors["startDate"]} /> @@ -453,7 +296,7 @@ const CreateMaintenance = () => { > {t("startTime")} - {t("timeZoneInfo")} + {t("timeZoneInfo")} @@ -462,7 +305,7 @@ const CreateMaintenance = () => { label={t("startTime")} value={form.startTime} onChange={(newTime) => { - handleTimeChange("startTime", newTime); + handleFormChange("startTime", newTime); }} slotProps={{ nextIconButton: { sx: { ml: theme.spacing(2) } }, @@ -508,16 +351,11 @@ const CreateMaintenance = () => { error={errors["duration"] ? true : false} helperText={errors["duration"]} /> - Date: Thu, 21 Aug 2025 23:05:50 -0400 Subject: [PATCH 064/131] Address PR Comments --- client/src/Pages/Settings/SettingsURL.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/Settings/SettingsURL.jsx b/client/src/Pages/Settings/SettingsURL.jsx index c12878adc..dd9168cac 100644 --- a/client/src/Pages/Settings/SettingsURL.jsx +++ b/client/src/Pages/Settings/SettingsURL.jsx @@ -29,7 +29,7 @@ const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => { Date: Thu, 21 Aug 2025 22:37:59 -0700 Subject: [PATCH 066/131] update arm dist docker compose --- docker/dist-arm/docker-compose.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker/dist-arm/docker-compose.yaml b/docker/dist-arm/docker-compose.yaml index 96bbf8f3f..842d4678a 100644 --- a/docker/dist-arm/docker-compose.yaml +++ b/docker/dist-arm/docker-compose.yaml @@ -15,9 +15,8 @@ services: - mongodb mongodb: image: mongo:4.4.18 - container_name: checkmate-mongodb restart: always - command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] + command: ["mongod", "--quiet", --bind_ip_all"] ports: - "27017:27017" volumes: From dabff74946f56c4ccbd04428939d0545ad8bbb2b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 22 Aug 2025 11:33:14 -0700 Subject: [PATCH 067/131] fix notification validation --- server/src/validation/joi.js | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index 725004afa..fa8dccde2 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -580,17 +580,31 @@ const createNotificationBodyValidation = joi.object({ }), address: joi.when("type", { - is: "email", - then: joi.string().email().required().messages({ - "string.empty": "E-mail address cannot be empty", - "any.required": "E-mail address is required", - "string.email": "Please enter a valid e-mail address", - }), - otherwise: joi.string().uri().required().messages({ - "string.empty": "Webhook URL cannot be empty", - "any.required": "Webhook URL is required", - "string.uri": "Please enter a valid Webhook URL", - }), + switch: [ + { + is: "email", + then: joi.string().email().required().messages({ + "string.empty": "E-mail address cannot be empty", + "any.required": "E-mail address is required", + "string.email": "Please enter a valid e-mail address", + }), + }, + { + is: "pager_duty", + then: joi.string().required().messages({ + "string.empty": "PagerDuty integration key cannot be empty", + "any.required": "PagerDuty integration key is required", + }), + }, + { + is: joi.string().valid("webhook", "slack", "discord"), + then: joi.string().uri().required().messages({ + "string.empty": "Webhook URL cannot be empty", + "any.required": "Webhook URL is required", + "string.uri": "Please enter a valid Webhook URL", + }), + }, + ], }), }); From 637bc841366e0ccdd87af632c56fb10eb2c6f96e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 25 Aug 2025 14:30:41 -0700 Subject: [PATCH 068/131] fix string service path --- server/src/service/system/translationService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/service/system/translationService.js b/server/src/service/system/translationService.js index 20b3431e0..0d3893852 100755 --- a/server/src/service/system/translationService.js +++ b/server/src/service/system/translationService.js @@ -7,7 +7,7 @@ class TranslationService { this.logger = logger; this.translations = {}; this._language = "en"; - this.localesDir = path.join(process.cwd(), "locales"); + this.localesDir = path.join(process.cwd(), "src/locales"); } get serviceName() { From 9a5ddb22b3bc48b2da59d6580c422ae5a9df8380 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 25 Aug 2025 14:37:00 -0700 Subject: [PATCH 069/131] bump version --- client/vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/vite.config.js b/client/vite.config.js index 434cc7e96..70215550e 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -5,7 +5,7 @@ import { execSync } from "child_process"; export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); - let version = "3.1.1"; + let version = "3.1.2"; return { base: "/", From 7a929777b4a4edac4cba145636659c915afe8434 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 25 Aug 2025 15:03:31 -0700 Subject: [PATCH 070/131] fix model types --- server/src/db/models/Check.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/db/models/Check.js b/server/src/db/models/Check.js index 42a718bd0..789cebaa5 100755 --- a/server/src/db/models/Check.js +++ b/server/src/db/models/Check.js @@ -72,7 +72,7 @@ const CheckSchema = new mongoose.Schema( }, type: { type: String, - enum: ["http", "hardware", "pagespeed", "distributed"], + enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"], required: true, index: true, }, From 9d0d1329b6f688be69ea6ca744926f7f02fe77c4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 25 Aug 2025 15:04:44 -0700 Subject: [PATCH 071/131] bump version --- client/vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/vite.config.js b/client/vite.config.js index 434cc7e96..1159fc42e 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -5,7 +5,7 @@ import { execSync } from "child_process"; export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); - let version = "3.1.1"; + let version = "3.1.3"; return { base: "/", From 2318082bb8ac0106523edb8cd8159252c2fe976a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 26 Aug 2025 14:54:24 -0700 Subject: [PATCH 072/131] fix advanced matching --- .../service/infrastructure/networkService.js | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.js index c90096d7c..72a117e74 100644 --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.js @@ -1,3 +1,5 @@ +import { match } from "assert"; + const SERVICE_NAME = "NetworkService"; class NetworkService { @@ -147,16 +149,24 @@ class NetworkService { httpResponse.payload = payload; httpResponse.timings = response.timings || {}; - if (!expectedValue) { + if (!expectedValue && !jsonPath) { return httpResponse; } - this.logger.info({ - service: this.SERVICE_NAME, - method: "requestHttp", - message: `Job: [${name}](${_id}) match result with expected value`, - details: { expectedValue, payload, jsonPath, matchMethod }, - }); + if (expectedValue) { + let ok = false; + if (matchMethod === "equal") ok = payload === expectedValue; + if (matchMethod === "include") ok = payload.includes(expectedValue); + if (matchMethod === "regex") ok = new RegExp(expectedValue).test(payload); + + if (ok === true) { + return httpResponse; + } else { + httpResponse.code = 500; + httpResponse.status = false; + return httpResponse; + } + } if (jsonPath) { const contentType = response.headers["content-type"]; From d3d20b29c3d51f6215efa3f646a719b2a3f6d674 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 27 Aug 2025 17:47:11 -0400 Subject: [PATCH 073/131] Fix status page deletion error - missing module reference - Fix deleteStatusPage method call to use statusPageModule - Change this.db.deleteStatusPage to this.db.statusPageModule.deleteStatusPage - Ensures consistency with other controller methods - Resolves "this.db.deleteStatusPage is not a function" error Fixes bug where status page deletion failed due to incorrect database module reference. --- server/src/controllers/statusPageController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/controllers/statusPageController.js b/server/src/controllers/statusPageController.js index 1cbcb49a2..a46688590 100755 --- a/server/src/controllers/statusPageController.js +++ b/server/src/controllers/statusPageController.js @@ -95,7 +95,7 @@ class StatusPageController extends BaseController { deleteStatusPage = this.asyncHandler( async (req, res) => { - await this.db.deleteStatusPage(req.params.url); + await this.db.statusPageModule.deleteStatusPage(req.params.url); return res.success({ msg: this.stringService.statusPageDelete, }); From d401eefd6be233aec92b48ce02956b0ecd7e2f8e Mon Sep 17 00:00:00 2001 From: karenvicent Date: Thu, 28 Aug 2025 12:58:38 -0400 Subject: [PATCH 074/131] Add direct user registration --- .../src/Components/Dialog/genericDialog.jsx | 6 +- client/src/Hooks/inviteHooks.js | 1 + .../components/AddMemberMenu/index.jsx | 68 +++++ .../components/AddTeamMember/index.jsx | 238 ++++++++++++++++++ .../Pages/Account/components/TeamPanel.jsx | 32 ++- .../Pages/Auth/components/PasswordTooltip.jsx | 13 +- .../Pages/Auth/hooks/usePasswordFeedback.jsx | 71 ++++++ client/src/Routes/index.jsx | 2 +- client/src/locales/en.json | 39 +++ 9 files changed, 456 insertions(+), 14 deletions(-) create mode 100644 client/src/Pages/Account/components/AddMemberMenu/index.jsx create mode 100644 client/src/Pages/Account/components/AddTeamMember/index.jsx create mode 100644 client/src/Pages/Auth/hooks/usePasswordFeedback.jsx diff --git a/client/src/Components/Dialog/genericDialog.jsx b/client/src/Components/Dialog/genericDialog.jsx index b2cbe1299..a06ef2732 100644 --- a/client/src/Components/Dialog/genericDialog.jsx +++ b/client/src/Components/Dialog/genericDialog.jsx @@ -2,7 +2,7 @@ import { useId } from "react"; import PropTypes from "prop-types"; import { Modal, Stack, Typography } from "@mui/material"; -const GenericDialog = ({ title, description, open, onClose, theme, children }) => { +const GenericDialog = ({ title, description, open, onClose, theme, children, width }) => { const titleId = useId(); const descriptionId = useId(); const ariaDescribedBy = description?.length > 0 ? descriptionId : ""; @@ -16,6 +16,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) = > {title} @@ -46,6 +48,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) = {description} @@ -64,6 +67,7 @@ GenericDialog.propTypes = { theme: PropTypes.object.isRequired, children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]) .isRequired, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; export { GenericDialog }; diff --git a/client/src/Hooks/inviteHooks.js b/client/src/Hooks/inviteHooks.js index 705906efa..810534454 100644 --- a/client/src/Hooks/inviteHooks.js +++ b/client/src/Hooks/inviteHooks.js @@ -33,6 +33,7 @@ const useGetInviteToken = () => { } setToken(inviteLink); + return token; } catch (error) { setError(error); } finally { diff --git a/client/src/Pages/Account/components/AddMemberMenu/index.jsx b/client/src/Pages/Account/components/AddMemberMenu/index.jsx new file mode 100644 index 000000000..5ed2ce96c --- /dev/null +++ b/client/src/Pages/Account/components/AddMemberMenu/index.jsx @@ -0,0 +1,68 @@ +import { useState } from "react"; +import Button from "@mui/material/Button"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { useTheme } from "@emotion/react"; +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import { useTranslation } from "react-i18next"; +import Proptypes from "prop-types"; + +const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const { t } = useTranslation(); + const theme = useTheme(); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + + + { + handleClose(); + handleInviteOpen(); + }} + > + {t("teamPanel.inviteTeamMember")} + + { + handleClose(); + handleIsRegisterOpen(true); + }} + > + {t("teamPanel.register")} + + + + ); +}; + +AddMemberMenu.propTypes = { + handleInviteOpen: Proptypes.func.isRequired, + handleIsRegisterOpen: Proptypes.func.isRequired, +}; + +export default AddMemberMenu; diff --git a/client/src/Pages/Account/components/AddTeamMember/index.jsx b/client/src/Pages/Account/components/AddTeamMember/index.jsx new file mode 100644 index 000000000..1d345cb3d --- /dev/null +++ b/client/src/Pages/Account/components/AddTeamMember/index.jsx @@ -0,0 +1,238 @@ +import { Button, Stack } from "@mui/material"; +import { GenericDialog } from "../../../../Components/Dialog/genericDialog"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import Select from "../../../../Components/Inputs/Select"; +import { newOrChangedCredentials } from "../../../../Validation/validation"; +import { useGetInviteToken } from "../../../../Hooks/inviteHooks"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useState } from "react"; +import { networkService } from "../../../../main"; +import PasswordTooltip from "../../../Auth/components/PasswordTooltip"; +import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback"; +import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; +import PropTypes from "prop-types"; +const INITIAL_FORM_STATE = { + firstName: "", + lastName: "", + email: "", + password: "", + confirm: "", + teamId: "", +}; + +const INITIAL_ROLE_STATE = ["user"]; +const AddTeamMember = ({ handleIsRegisterOpen, isRegisterOpen, onMemberAdded }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { feedback, handlePasswordFeedback } = usePasswordFeedback(); + const [getInviteToken, clearToken] = useGetInviteToken(); + const [form, setForm] = useState(INITIAL_FORM_STATE); + const [role, setRole] = useState(INITIAL_ROLE_STATE); + const [errors, setErrors] = useState({}); + const closeAddMemberModal = () => { + handleIsRegisterOpen(false); + setForm(INITIAL_FORM_STATE); + setRole(INITIAL_ROLE_STATE); + setErrors({}); + clearToken(); + }; + + const onChange = (e) => { + let { name, value } = e.target; + if (name === "email") value = value.toLowerCase(); + const updatedForm = { ...form, [name]: value }; + + const { error } = newOrChangedCredentials.validate( + { [name]: value }, + { abortEarly: false, context: { password: form.password } } + ); + + setErrors((prev) => ({ + ...prev, + [name]: error?.details?.[0]?.message || "", + })); + + setForm(updatedForm); + + if (name === "password" || name === "confirm") { + handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors); + } + }; + + const onsubmitAddMember = async (event) => { + event.preventDefault(); + + const { error } = newOrChangedCredentials.validate(form, { + abortEarly: false, + context: { password: form.password }, + }); + if (error) { + const formErrors = {}; + for (const err of error.details) { + formErrors[err.path[0]] = err.message; + } + setErrors(formErrors); + return; + } + + const token = await getInviteToken({ email: form.email, role: role }); + + const toSubmit = { + ...form, + inviteToken: token, + }; + delete toSubmit.confirm; + + try { + await networkService.registerUser(toSubmit); + + createToast({ + body: t("teamPanel.registerToast.success"), + }); + closeAddMemberModal(); + onMemberAdded(); + } catch (error) { + const errorMsg = error.response?.data?.msg || error.message || "unknownError"; + createToast({ + type: "error", + body: t(`teamPanel.registerToast.${errorMsg}`), + }); + } + }; + const tErr = (key) => (key ? t([`teamPanel.registerTeamMember.${key}`, key]) : ""); + return ( + <> + + + + + + + +