Merge pull request #3470 from bluewave-labs/feat/pagespeed-mobile-strat

feat: adding mobile/desktop pagespeed strategy
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-05-11 11:34:11 -07:00
commit 01da5847f0
18 files changed
+371 -78

No files matched your search

@@ -1,12 +1,13 @@
import type { Monitor } from "@/Types/Monitor";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { PulseDot, Dot } from "@/Components/design-elements";
import { PulseDot, Dot, StrategyBadge } from "@/Components/design-elements";
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
import prettyMilliseconds from "pretty-ms";
import { typographyLevels } from "@/Utils/Theme/Palette";
import useMediaQuery from "@mui/material/useMediaQuery";
import { LAYOUT } from "@/Utils/Theme/constants";
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
@@ -28,7 +29,7 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
gap={theme.spacing(LAYOUT.XS)}
>
<PulseDot color={getStatusColor(monitor.status, theme)} />
<Typography
@@ -41,6 +42,12 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
>
{formatUrl(monitor?.url)}
</Typography>
{monitor.type === "pagespeed" && monitor.strategy && (
<>
<Dot />
<StrategyBadge strategy={monitor.strategy} />
</>
)}
{!isSmall && (
<>
<Dot />
@@ -0,0 +1,32 @@
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import type { PageSpeedStrategy } from "@/Types/Monitor";
import { SPACING } from "@/Utils/Theme/constants";
interface StrategyBadgeProps {
strategy: PageSpeedStrategy | undefined;
variant?: "caption" | "body2";
}
export const StrategyBadge = ({ strategy, variant = "body2" }: StrategyBadgeProps) => {
const theme = useTheme();
const { t } = useTranslation();
if (!strategy) {
return null;
}
return (
<Typography
variant={variant}
px={SPACING.XXL}
py={SPACING.SM}
borderRadius={theme.shape.borderRadius}
bgcolor={theme.palette.secondary.light}
color={theme.palette.text.primary}
>
{t(`pages.pageSpeed.strategy.${strategy}`)}
</Typography>
);
};
@@ -26,3 +26,4 @@ export * from "./NoticeBanner";
export * from "./OfflineBanner";
export * from "./Avatar";
export * from "./StatusCodeLabel";
export * from "./StrategyBadge";
+2
View File
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
import { DefaultPageSpeedStrategy } from "@/Types/Monitor";
import type { Monitor, MonitorType } from "@/Types/Monitor";
interface UseMonitorFormOptions {
@@ -88,6 +89,7 @@ export const useMonitorForm = ({
...base,
type: "pagespeed",
url: data?.url || "",
strategy: data?.strategy ?? DefaultPageSpeedStrategy,
};
break;
case "hardware":
+37
View File
@@ -37,6 +37,7 @@ import {
type MonitorType,
type GamesMap,
supportsGeoCheck,
DefaultPageSpeedStrategy,
} from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification";
import type { MonitorFormData } from "@/Validation/monitor";
@@ -51,6 +52,7 @@ interface GeneralSettingsConfig {
showSecret: boolean;
showGrpcServiceName: boolean;
showIgnoreTls: boolean;
showStrategy: boolean;
showDnsServer: boolean;
showDnsRecordType: boolean;
}
@@ -70,6 +72,7 @@ const getGeneralSettingsConfig = (
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
@@ -83,6 +86,7 @@ const getGeneralSettingsConfig = (
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
@@ -96,6 +100,7 @@ const getGeneralSettingsConfig = (
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
@@ -109,6 +114,7 @@ const getGeneralSettingsConfig = (
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
@@ -122,6 +128,7 @@ const getGeneralSettingsConfig = (
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
@@ -135,6 +142,7 @@ const getGeneralSettingsConfig = (
showSecret: false,
showGrpcServiceName: true,
showIgnoreTls: true,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
@@ -148,6 +156,7 @@ const getGeneralSettingsConfig = (
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: true,
showDnsServer: false,
showDnsRecordType: false,
},
@@ -161,6 +170,7 @@ const getGeneralSettingsConfig = (
showSecret: true,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
@@ -174,6 +184,7 @@ const getGeneralSettingsConfig = (
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: true,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
@@ -187,6 +198,7 @@ const getGeneralSettingsConfig = (
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: true,
showDnsRecordType: true,
},
@@ -435,6 +447,31 @@ const CreateMonitorPage = () => {
/>
)}
{/* Strategy field - only for pagespeed type */}
{generalSettingsConfig.showStrategy && (
<Controller
name="strategy"
control={control}
render={({ field, fieldState }) => (
<Select
{...field}
value={field.value ?? DefaultPageSpeedStrategy}
fieldLabel={t(
"pages.createMonitor.form.general.option.strategy.label"
)}
error={!!fieldState.error}
>
<MenuItem value="desktop">
{t("pages.createMonitor.form.general.option.strategy.desktop")}
</MenuItem>
<MenuItem value="mobile">
{t("pages.createMonitor.form.general.option.strategy.mobile")}
</MenuItem>
</Select>
)}
/>
)}
{/* Port field - only for port and game types */}
{generalSettingsConfig.showPort && (
<Controller
@@ -1,7 +1,13 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { Table, Pagination, StatusLabel } from "@/Components/design-elements";
import {
Table,
Pagination,
StatusLabel,
StrategyBadge,
} from "@/Components/design-elements";
import { LAYOUT } from "@/Utils/Theme/constants";
import { HistogramPageSpeed } from "@/Components/monitors";
import { ArrowDown, ArrowUp } from "lucide-react";
import type { Header } from "@/Components/design-elements/Table";
@@ -179,7 +185,19 @@ export const PageSpeedMonitorsTable = ({
</Stack>
),
render: (row) => {
return row.name;
return (
<Stack
direction="row"
alignItems="center"
gap={LAYOUT.XS}
>
<Typography>{row.name}</Typography>
<StrategyBadge
strategy={row.strategy}
variant="caption"
/>
</Stack>
);
},
},
{
+5
View File
@@ -44,6 +44,10 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
export const PageSpeedStrategies = ["desktop", "mobile"] as const;
export type PageSpeedStrategy = (typeof PageSpeedStrategies)[number];
export const DefaultPageSpeedStrategy: PageSpeedStrategy = "desktop";
export interface Monitor {
id: string;
userId: string;
@@ -78,6 +82,7 @@ export interface Monitor {
selectedDisks: string[];
gameId?: string;
grpcServiceName?: string;
strategy?: PageSpeedStrategy;
group: string | null;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[];
+2 -1
View File
@@ -1,6 +1,6 @@
import { z } from "zod";
import { GeoContinents } from "@/Types/GeoCheck";
import { DnsRecordTypes } from "@/Types/Monitor";
import { DnsRecordTypes, PageSpeedStrategies } from "@/Types/Monitor";
// URL schema with custom error message
const urlSchema = z.url({ message: "Please enter a valid URL" });
@@ -90,6 +90,7 @@ const grpcSchema = baseSchema.extend({
const pagespeedSchema = baseSchema.extend({
type: z.literal("pagespeed"),
url: urlSchema,
strategy: z.enum(PageSpeedStrategies),
});
// Hardware/Infrastructure monitor schema
+9
View File
@@ -563,6 +563,11 @@
"label": "WebSocket URL",
"placeholder": "wss://example.com/socket"
},
"strategy": {
"label": "Strategy",
"desktop": "Desktop",
"mobile": "Mobile"
},
"captureUrl": {
"label": "Capture endpoint",
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
@@ -1102,6 +1107,10 @@
}
},
"pageSpeed": {
"strategy": {
"desktop": "Desktop",
"mobile": "Mobile"
},
"charts": {
"common": {
"cls": "Cumulative Layout Shift (CLS)",
+57 -12
View File
@@ -346,7 +346,7 @@
},
"type": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"]
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
},
"url": {
"type": "string"
@@ -436,6 +436,13 @@
"geoCheckInterval": {
"type": "number"
},
"dnsServer": {
"type": "string"
},
"dnsRecordType": {
"type": "string",
"enum": ["A", "AAAA", "CNAME", "MX", "TXT", "NS"]
},
"teamId": {
"type": "string"
},
@@ -2981,7 +2988,7 @@
{
"schema": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"]
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
},
"required": false,
"name": "type",
@@ -3416,7 +3423,7 @@
{
"schema": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"]
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
},
"required": false,
"name": "type",
@@ -4664,7 +4671,8 @@
"format": "date-time"
}
},
"required": ["monitors", "name", "duration", "durationUnit", "start", "end", "repeat"]
"required": ["monitors", "name", "duration", "durationUnit", "start", "end", "repeat"],
"additionalProperties": false
}
}
}
@@ -5208,7 +5216,8 @@
"type": "string",
"enum": ["seconds", "minutes", "hours", "days"]
}
}
},
"additionalProperties": false
}
}
}
@@ -5439,13 +5448,13 @@
"anyOf": [
{
"type": "string",
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
},
{
"type": "array",
"items": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
}
}
]
@@ -5648,13 +5657,13 @@
"anyOf": [
{
"type": "string",
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
},
{
"type": "array",
"items": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
}
}
]
@@ -6862,7 +6871,7 @@
},
"type": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"]
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
},
"statusWindowSize": {
"type": "number",
@@ -6975,6 +6984,10 @@
],
"default": ""
},
"strategy": {
"type": "string",
"enum": ["desktop", "mobile"]
},
"selectedDisks": {
"type": "array",
"items": {
@@ -7009,6 +7022,14 @@
"geoCheckInterval": {
"type": "number",
"minimum": 300000
},
"dnsServer": {
"type": "string",
"minLength": 1
},
"dnsRecordType": {
"type": "string",
"enum": ["A", "AAAA", "CNAME", "MX", "TXT", "NS"]
}
},
"required": ["name", "type", "url"]
@@ -7496,7 +7517,7 @@
},
"type": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"]
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
},
"ignoreTlsErrors": {
"type": "boolean",
@@ -7630,6 +7651,10 @@
],
"default": ""
},
"strategy": {
"type": "string",
"enum": ["desktop", "mobile"]
},
"group": {
"anyOf": [
{
@@ -7659,6 +7684,14 @@
"minimum": 300000,
"default": 300000
},
"dnsServer": {
"type": "string",
"minLength": 1
},
"dnsRecordType": {
"type": "string",
"enum": ["A", "AAAA", "CNAME", "MX", "TXT", "NS"]
},
"createdAt": {
"type": "string"
},
@@ -8034,7 +8067,7 @@
},
"type": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"]
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
},
"url": {
"type": "string"
@@ -8153,6 +8186,10 @@
}
]
},
"strategy": {
"type": "string",
"enum": ["desktop", "mobile"]
},
"selectedDisks": {
"type": "array",
"items": {
@@ -8187,6 +8224,14 @@
"geoCheckInterval": {
"type": "number",
"minimum": 300000
},
"dnsServer": {
"type": "string",
"minLength": 1
},
"dnsRecordType": {
"type": "string",
"enum": ["A", "AAAA", "CNAME", "MX", "TXT", "NS"]
}
}
}
+67 -52
View File
@@ -21,7 +21,7 @@
"dockerode": "4.0.6",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "8.3.2",
"express-rate-limit": "^8.5.1",
"gamedig": "5.2.0",
"got": "14.4.7",
"handlebars": "^4.7.8",
@@ -811,14 +811,15 @@
}
},
"node_modules/@aws-sdk/xml-builder": {
"version": "3.972.16",
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz",
"integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==",
"version": "3.972.22",
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz",
"integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.1",
"fast-xml-parser": "5.5.8",
"@nodable/entities": "2.1.0",
"@smithy/types": "^4.14.1",
"fast-xml-parser": "5.7.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -865,7 +866,6 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -1485,7 +1485,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -1530,7 +1529,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -3193,6 +3191,19 @@
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@nodable/entities": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nodable"
}
],
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3757,9 +3768,9 @@
}
},
"node_modules/@smithy/types": {
"version": "4.13.1",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz",
"integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==",
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz",
"integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -4244,7 +4255,6 @@
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
@@ -4405,7 +4415,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -4643,7 +4652,6 @@
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/types": "8.56.1",
@@ -5257,7 +5265,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5836,7 +5843,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -6730,7 +6736,6 @@
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"cssnano-preset-default": "^7.0.9",
"lilconfig": "^3.1.3"
@@ -7495,7 +7500,6 @@
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7857,7 +7861,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -7900,12 +7903,12 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz",
"integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==",
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
"ip-address": "^10.2.0"
},
"engines": {
"node": ">= 16"
@@ -7990,9 +7993,9 @@
"license": "MIT"
},
"node_modules/fast-xml-builder": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
"integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
"dev": true,
"funding": [
{
@@ -8002,13 +8005,14 @@
],
"license": "MIT",
"dependencies": {
"path-expression-matcher": "^1.1.3"
"path-expression-matcher": "^1.5.0",
"xml-naming": "^0.1.0"
}
},
"node_modules/fast-xml-parser": {
"version": "5.5.8",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz",
"integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz",
"integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==",
"dev": true,
"funding": [
{
@@ -8018,9 +8022,10 @@
],
"license": "MIT",
"dependencies": {
"fast-xml-builder": "^1.1.4",
"path-expression-matcher": "^1.2.0",
"strnum": "^2.2.0"
"@nodable/entities": "^2.1.0",
"fast-xml-builder": "^1.1.5",
"path-expression-matcher": "^1.5.0",
"strnum": "^2.2.3"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -9097,9 +9102,9 @@
}
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -9526,7 +9531,6 @@
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "30.2.0",
"@jest/types": "30.2.0",
@@ -11543,9 +11547,9 @@
}
},
"node_modules/mongoose": {
"version": "8.19.2",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.2.tgz",
"integrity": "sha512-ww2T4dBV+suCbOfG5YPwj9pLCfUVyj8FEA1D3Ux1HHqutpLxGyOYEPU06iPRBW4cKr3PJfOSYsIpHWPTkz5zig==",
"version": "8.23.1",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.1.tgz",
"integrity": "sha512-gHSPD8qEwRmiXapK17hEnFWZdcFENMegHTcw5XIIg2+7R8eXQvdwSiMpD/A2oG8tKzFLLHyRXd8/eaDPAVwZgQ==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.4",
@@ -12154,9 +12158,9 @@
}
},
"node_modules/path-expression-matcher": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.1.tgz",
"integrity": "sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
"dev": true,
"funding": [
{
@@ -12371,7 +12375,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -14210,9 +14213,9 @@
}
},
"node_modules/strnum": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
"integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==",
"dev": true,
"funding": [
{
@@ -14394,7 +14397,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14563,7 +14565,6 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -14727,7 +14728,6 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -15390,6 +15390,22 @@
"node": ">=18"
}
},
"node_modules/xml-naming": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
"integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
@@ -15562,7 +15578,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+1 -1
View File
@@ -41,7 +41,7 @@
"dockerode": "4.0.6",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "8.3.2",
"express-rate-limit": "^8.5.1",
"gamedig": "5.2.0",
"got": "14.4.7",
"handlebars": "^4.7.8",
+5 -1
View File
@@ -1,6 +1,6 @@
import { Schema, model, Types } from "mongoose";
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
import { DnsRecordTypes, MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
import { DnsRecordTypes, MonitorTypes, MonitorStatuses, PageSpeedStrategies } from "@/types/monitor.js";
import type {
CheckAudits,
CheckCaptureInfo,
@@ -330,6 +330,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
type: String,
default: "",
},
strategy: {
type: String,
enum: PageSpeedStrategies,
},
group: {
type: String,
trim: true,
@@ -441,6 +441,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined,
strategy: doc.strategy ?? undefined,
group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false,
@@ -502,6 +503,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined,
strategy: doc.strategy ?? undefined,
group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false,
@@ -1,6 +1,6 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { Monitor, MonitorType, DefaultPageSpeedStrategy } from "@/types/monitor.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { ISettingsService } from "@/service/system/settingsService.js";
import { ILogger } from "@/utils/logger.js";
@@ -19,14 +19,15 @@ export class PageSpeedProvider implements IStatusProvider<PageSpeedStatusPayload
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
const { url } = monitor;
const { url, strategy } = monitor;
try {
if (!url) throw new Error("URL is required for PageSpeed monitor");
const dbSettings = await this.settingsService.getDBSettings();
const apiKey = dbSettings?.pagespeedApiKey;
const resolvedStrategy = strategy ?? DefaultPageSpeedStrategy;
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(
url
)}&category=seo&category=accessibility&category=best-practices&category=performance`;
)}&strategy=${encodeURIComponent(resolvedStrategy)}&category=seo&category=accessibility&category=best-practices&category=performance`;
if (apiKey) {
pageSpeedUrl += `&key=${apiKey}`;
+5
View File
@@ -6,6 +6,10 @@ export type { GeoContinent } from "@/types/geoCheck.js";
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export const PageSpeedStrategies = ["desktop", "mobile"] as const;
export type PageSpeedStrategy = (typeof PageSpeedStrategies)[number];
export const DefaultPageSpeedStrategy: PageSpeedStrategy = "desktop";
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type);
@@ -67,6 +71,7 @@ export interface Monitor {
selectedDisks: string[];
gameId?: string;
grpcServiceName?: string;
strategy?: PageSpeedStrategy;
group: string | null;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[];
+20 -4
View File
@@ -1,7 +1,7 @@
import { z } from "zod";
import { booleanCoercion, dnsHostnameRegex, dnsServerValidation } from "./shared.js";
import { GeoContinents } from "@/types/geoCheck.js";
import { DnsRecordTypes, MonitorMatchMethods, MonitorStatuses, MonitorTypes } from "@/types/monitor.js";
import { DnsRecordTypes, MonitorMatchMethods, MonitorStatuses, MonitorTypes, PageSpeedStrategies } from "@/types/monitor.js";
export const getMonitorByIdParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
@@ -49,6 +49,16 @@ const refineDnsHostname = (body: { type?: string; url?: string }, ctx: z.Refinem
}
};
const refineStrategyType = (body: { type?: string; strategy?: string }, ctx: z.RefinementCtx) => {
if (body.strategy !== undefined && body.type !== undefined && body.type !== "pagespeed") {
ctx.addIssue({
code: "custom",
path: ["strategy"],
message: "Strategy is only valid for pagespeed monitors",
});
}
};
export const createMonitorBodyValidation = z
.object({
_id: z.string().optional(),
@@ -74,6 +84,7 @@ export const createMonitorBodyValidation = z
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
strategy: z.enum(PageSpeedStrategies).optional(),
selectedDisks: z.array(z.string()).optional(),
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
geoCheckEnabled: z.boolean().optional(),
@@ -82,7 +93,8 @@ export const createMonitorBodyValidation = z
dnsServer: dnsServerValidation.optional(),
dnsRecordType: z.enum(DnsRecordTypes).optional(),
})
.superRefine(refineDnsHostname);
.superRefine(refineDnsHostname)
.superRefine(refineStrategyType);
export const editMonitorBodyValidation = z
.object({
@@ -107,6 +119,7 @@ export const editMonitorBodyValidation = z
tempAlertThreshold: z.number().optional(),
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).optional(),
strategy: z.enum(PageSpeedStrategies).optional(),
selectedDisks: z.array(z.string()).optional(),
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
geoCheckEnabled: z.boolean().optional(),
@@ -115,7 +128,8 @@ export const editMonitorBodyValidation = z
dnsServer: dnsServerValidation.optional(),
dnsRecordType: z.enum(DnsRecordTypes).optional(),
})
.superRefine(refineDnsHostname);
.superRefine(refineDnsHostname)
.superRefine(refineStrategyType);
export const pauseMonitorParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
@@ -173,6 +187,7 @@ const importedMonitorSchema = z
selectedDisks: z.array(z.string()).default([]),
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
strategy: z.enum(PageSpeedStrategies).optional(),
group: z.union([z.string().max(50).trim(), z.null()]).default(null),
geoCheckEnabled: z.boolean().default(false),
geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),
@@ -182,7 +197,8 @@ const importedMonitorSchema = z
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
})
.superRefine(refineDnsHostname);
.superRefine(refineDnsHostname)
.superRefine(refineStrategyType);
export const importMonitorsBodyValidation = z.object({
monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"),
@@ -129,3 +129,96 @@ describe("monitorValidation — DNS fields", () => {
});
});
});
describe("monitorValidation — strategy gating", () => {
describe("createMonitorBodyValidation", () => {
it("accepts strategy on pagespeed monitors", () => {
const parsed = createMonitorBodyValidation.parse({
name: "PS check",
type: "pagespeed",
url: "https://example.com",
strategy: "mobile",
});
expect(parsed.strategy).toBe("mobile");
});
it("accepts pagespeed monitors without a strategy", () => {
const parsed = createMonitorBodyValidation.parse({
name: "PS check",
type: "pagespeed",
url: "https://example.com",
});
expect(parsed.strategy).toBeUndefined();
});
it("rejects strategy on non-pagespeed monitor types", () => {
for (const type of ["http", "ping", "docker", "port", "game", "grpc", "websocket", "dns", "hardware"] as const) {
expect(() =>
createMonitorBodyValidation.parse({
name: "wrong-type check",
type,
url: type === "dns" ? "example.com" : "https://example.com",
strategy: "desktop",
})
).toThrow();
}
});
it("allows non-pagespeed monitors without a strategy", () => {
const parsed = createMonitorBodyValidation.parse({
name: "HTTP check",
type: "http",
url: "https://example.com",
});
expect(parsed.strategy).toBeUndefined();
});
});
describe("editMonitorBodyValidation", () => {
it("rejects strategy when type is explicitly non-pagespeed", () => {
expect(() =>
editMonitorBodyValidation.parse({
type: "http",
strategy: "mobile",
})
).toThrow();
});
it("allows strategy when type is omitted (partial edit)", () => {
const parsed = editMonitorBodyValidation.parse({
strategy: "mobile",
});
expect(parsed.strategy).toBe("mobile");
});
});
describe("importMonitorsBodyValidation", () => {
it("does not inject a default strategy on imported HTTP monitors", () => {
const parsed = importMonitorsBodyValidation.parse({
monitors: [
{
name: "Imported HTTP",
type: "http",
url: "https://example.com",
},
],
});
expect(parsed.monitors[0].strategy).toBeUndefined();
});
it("rejects strategy on imported non-pagespeed monitors", () => {
expect(() =>
importMonitorsBodyValidation.parse({
monitors: [
{
name: "Imported HTTP",
type: "http",
url: "https://example.com",
strategy: "desktop",
},
],
})
).toThrow();
});
});
});