From 2bbaf7d1eb110cfb6f66175f19f0549212e4cf1f Mon Sep 17 00:00:00 2001 From: mannilakash Date: Tue, 7 Apr 2026 07:02:44 -0400 Subject: [PATCH] feat: adding mobile/desktop pagespeed strategy --- .../design-elements/MonitorStatus.tsx | 11 ++++- .../design-elements/StrategyBadge.tsx | 38 +++++++++++++++++ .../src/Components/design-elements/index.tsx | 1 + client/src/Hooks/useMonitorForm.ts | 2 + client/src/Pages/CreateMonitor/index.tsx | 41 +++++++++++++++++++ .../Components/PageSpeedMonitorsTable.tsx | 17 +++++++- client/src/Types/Monitor.ts | 5 +++ client/src/Validation/monitor.ts | 2 + client/src/locales/en.json | 9 ++++ server/openapi.json | 11 +++++ server/src/db/migration/timescaledb/index.ts | 2 + server/src/db/models/Monitor.ts | 7 +++- .../monitors/MongoMonitorsRepository.ts | 2 + .../monitors/TimescaleMonitorsRepository.ts | 11 +++-- .../network/PageSpeedProvider.ts | 7 ++-- server/src/types/monitor.ts | 5 +++ server/src/validation/monitorValidation.ts | 5 ++- 17 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 client/src/Components/design-elements/StrategyBadge.tsx diff --git a/client/src/Components/design-elements/MonitorStatus.tsx b/client/src/Components/design-elements/MonitorStatus.tsx index 04c62de47..4520356a4 100644 --- a/client/src/Components/design-elements/MonitorStatus.tsx +++ b/client/src/Components/design-elements/MonitorStatus.tsx @@ -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 }) => { { > {formatUrl(monitor?.url)} + {monitor.type === "pagespeed" && monitor.strategy && ( + <> + + + + )} {!isSmall && ( <> diff --git a/client/src/Components/design-elements/StrategyBadge.tsx b/client/src/Components/design-elements/StrategyBadge.tsx new file mode 100644 index 000000000..1414f8b6c --- /dev/null +++ b/client/src/Components/design-elements/StrategyBadge.tsx @@ -0,0 +1,38 @@ +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 ( + + {t(`pages.pageSpeed.strategy.${strategy}`)} + + ); +}; diff --git a/client/src/Components/design-elements/index.tsx b/client/src/Components/design-elements/index.tsx index ed6a885f6..45114116b 100644 --- a/client/src/Components/design-elements/index.tsx +++ b/client/src/Components/design-elements/index.tsx @@ -21,3 +21,4 @@ export * from "./SplitBox"; export * from "./TextLink"; export * from "./OfflineBanner"; export * from "./Avatar"; +export * from "./StrategyBadge"; diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 963409fc8..ac13854b3 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -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": diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 15b76eab3..911e37189 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -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; } const getGeneralSettingsConfig = ( @@ -68,6 +70,7 @@ const getGeneralSettingsConfig = ( showSecret: false, showGrpcServiceName: false, showIgnoreTls: false, + showStrategy: false, }, ping: { urlLabel: t("pages.createMonitor.form.general.option.host.label"), @@ -79,6 +82,7 @@ const getGeneralSettingsConfig = ( showSecret: false, showGrpcServiceName: false, showIgnoreTls: false, + showStrategy: false, }, docker: { urlLabel: t("pages.createMonitor.form.general.option.container.label"), @@ -90,6 +94,7 @@ const getGeneralSettingsConfig = ( showSecret: false, showGrpcServiceName: false, showIgnoreTls: false, + showStrategy: false, }, port: { urlLabel: t("pages.createMonitor.form.general.option.url.label"), @@ -101,6 +106,7 @@ const getGeneralSettingsConfig = ( showSecret: false, showGrpcServiceName: false, showIgnoreTls: false, + showStrategy: false, }, game: { urlLabel: t("pages.createMonitor.form.general.option.url.label"), @@ -112,6 +118,7 @@ const getGeneralSettingsConfig = ( showSecret: false, showGrpcServiceName: false, showIgnoreTls: false, + showStrategy: false, }, grpc: { urlLabel: t("pages.createMonitor.form.general.option.host.label"), @@ -123,6 +130,7 @@ const getGeneralSettingsConfig = ( showSecret: false, showGrpcServiceName: true, showIgnoreTls: true, + showStrategy: false, }, pagespeed: { urlLabel: t("pages.createMonitor.form.general.option.url.label"), @@ -134,6 +142,7 @@ const getGeneralSettingsConfig = ( showSecret: false, showGrpcServiceName: false, showIgnoreTls: false, + showStrategy: true, }, hardware: { urlLabel: t("pages.createMonitor.form.general.option.url.label"), @@ -145,6 +154,7 @@ const getGeneralSettingsConfig = ( showSecret: true, showGrpcServiceName: false, showIgnoreTls: false, + showStrategy: false, }, websocket: { urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"), @@ -156,6 +166,7 @@ const getGeneralSettingsConfig = ( showSecret: false, showGrpcServiceName: false, showIgnoreTls: true, + showStrategy: false, }, }; return configs[type] || configs.http; @@ -382,6 +393,36 @@ const CreateMonitorPage = () => { /> )} + {/* Strategy field - only for pagespeed type */} + {generalSettingsConfig.showStrategy && ( + ( + + )} + /> + )} + {/* Port field - only for port and game types */} {generalSettingsConfig.showPort && ( ), render: (row) => { - return row.name; + return ( + + {row.name} + + + ); }, }, { diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 053b517d1..dc9aabaf4 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -38,6 +38,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; @@ -72,6 +76,7 @@ export interface Monitor { selectedDisks: string[]; gameId?: string; grpcServiceName?: string; + strategy?: PageSpeedStrategy; group: string | null; geoCheckEnabled?: boolean; geoCheckLocations?: GeoContinent[]; diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index 9acffe6fe..88df71101 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { GeoContinents } from "@/Types/GeoCheck"; +import { PageSpeedStrategies } from "@/Types/Monitor"; // URL schema with custom error message const urlSchema = z.url({ message: "Please enter a valid URL" }); @@ -89,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 diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 83ca4fa33..013858adc 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -503,6 +503,11 @@ "wsUrl": { "label": "WebSocket URL", "placeholder": "wss://example.com/socket" + }, + "strategy": { + "label": "Strategy", + "desktop": "Desktop", + "mobile": "Mobile" } }, "title": "General settings", @@ -968,6 +973,10 @@ } }, "pageSpeed": { + "strategy": { + "desktop": "Desktop", + "mobile": "Mobile" + }, "charts": { "common": { "cls": "Cumulative Layout Shift (CLS)", diff --git a/server/openapi.json b/server/openapi.json index 35d109693..6a44c19f2 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -3631,6 +3631,12 @@ "type": "string", "default": "" }, + "strategy": { + "type": "string", + "enum": ["desktop", "mobile"], + "default": "desktop", + "description": "PageSpeed analysis strategy" + }, "selectedDisks": { "type": "array", "items": { @@ -3737,6 +3743,11 @@ "grpcServiceName": { "type": "string" }, + "strategy": { + "type": "string", + "enum": ["desktop", "mobile"], + "description": "PageSpeed analysis strategy" + }, "selectedDisks": { "type": "array", "items": { diff --git a/server/src/db/migration/timescaledb/index.ts b/server/src/db/migration/timescaledb/index.ts index 14084ebf3..9a76d093a 100644 --- a/server/src/db/migration/timescaledb/index.ts +++ b/server/src/db/migration/timescaledb/index.ts @@ -21,6 +21,7 @@ import { createStatusPages, dropStatusPages } from "./0018_create_status_pages.j import { createAppSettings, dropAppSettings } from "./0019_create_app_settings.js"; import { createContinuousAggregates, dropContinuousAggregates } from "./0020_create_continuous_aggregates.js"; import { createRetentionCompression, dropRetentionCompression } from "./0021_create_retention_compression.js"; +import { addStrategyToMonitors, dropStrategyFromMonitors } from "./0022_add_strategy_to_monitors.js"; const SERVICE_NAME = "TimescaleDB Migrations"; @@ -52,6 +53,7 @@ const migrations: MigrationEntry[] = [ { name: "0019_create_app_settings", up: createAppSettings, down: dropAppSettings }, { name: "0020_create_continuous_aggregates", up: createContinuousAggregates, down: dropContinuousAggregates }, { name: "0021_create_retention_compression", up: createRetentionCompression, down: dropRetentionCompression }, + { name: "0022_add_strategy_to_monitors", up: addStrategyToMonitors, down: dropStrategyFromMonitors }, ]; const ensureMigrationsTable = async (pool: Pool) => { diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index 036aeadad..a20c34c09 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -1,6 +1,6 @@ import { Schema, model, Types } from "mongoose"; import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js"; -import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js"; +import { MonitorTypes, MonitorStatuses, PageSpeedStrategies, DefaultPageSpeedStrategy } from "@/types/monitor.js"; import type { CheckAudits, CheckCaptureInfo, @@ -330,6 +330,11 @@ const MonitorSchema = new Schema( type: String, default: "", }, + strategy: { + type: String, + enum: PageSpeedStrategies, + default: DefaultPageSpeedStrategy, + }, group: { type: String, trim: true, diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index b2d759448..aed881006 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -386,6 +386,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, @@ -445,6 +446,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, diff --git a/server/src/repositories/monitors/TimescaleMonitorsRepository.ts b/server/src/repositories/monitors/TimescaleMonitorsRepository.ts index a94d38504..af663680a 100644 --- a/server/src/repositories/monitors/TimescaleMonitorsRepository.ts +++ b/server/src/repositories/monitors/TimescaleMonitorsRepository.ts @@ -1,5 +1,6 @@ import type { Pool } from "pg"; import type { Monitor, MonitorsSummary, MonitorStatus, MonitorType, MonitorMatchMethod, GeoContinent } from "@/types/monitor.js"; +import { DefaultPageSpeedStrategy } from "@/types/monitor.js"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; @@ -36,6 +37,7 @@ interface MonitorRow { selected_disks: string[] | null; game_id: string | null; grpc_service_name: string | null; + strategy: string | null; monitor_group: string | null; geo_check_enabled: boolean; geo_check_locations: GeoContinent[] | null; @@ -49,7 +51,7 @@ const MONITOR_COLUMNS = `id, user_id, team_id, name, description, type, status, interval_ms, is_active, status_window, status_window_size, status_window_threshold, uptime_percentage, cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter, disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks, - game_id, grpc_service_name, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms, + game_id, grpc_service_name, strategy, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms, created_at, updated_at`; export class TimescaleMonitorsRepository implements IMonitorsRepository { @@ -62,8 +64,8 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository { interval_ms, is_active, status_window, status_window_size, status_window_threshold, cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter, disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks, - game_id, grpc_service_name, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34) + game_id, grpc_service_name, strategy, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35) RETURNING ${MONITOR_COLUMNS}`, [ userId, @@ -96,6 +98,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository { monitor.selectedDisks ?? [], monitor.gameId ?? null, monitor.grpcServiceName ?? null, + monitor.strategy ?? DefaultPageSpeedStrategy, monitor.group ?? null, monitor.geoCheckEnabled ?? false, monitor.geoCheckLocations ?? [], @@ -594,6 +597,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository { ["selectedDisks", "selected_disks"], ["gameId", "game_id"], ["grpcServiceName", "grpc_service_name"], + ["strategy", "strategy"], ["group", "monitor_group"], ["geoCheckEnabled", "geo_check_enabled"], ["geoCheckLocations", "geo_check_locations"], @@ -1033,6 +1037,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository { selectedDisks: row.selected_disks ?? [], gameId: row.game_id ?? undefined, grpcServiceName: row.grpc_service_name ?? undefined, + strategy: (row.strategy as Monitor["strategy"]) ?? undefined, group: row.monitor_group, geoCheckEnabled: row.geo_check_enabled, geoCheckLocations: row.geo_check_locations ?? [], diff --git a/server/src/service/infrastructure/network/PageSpeedProvider.ts b/server/src/service/infrastructure/network/PageSpeedProvider.ts index 121fe9bf8..0d8560868 100644 --- a/server/src/service/infrastructure/network/PageSpeedProvider.ts +++ b/server/src/service/infrastructure/network/PageSpeedProvider.ts @@ -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> { - 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}`; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index f29ce75d7..4e4309fe7 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -6,6 +6,10 @@ export type { GeoContinent } from "@/types/geoCheck.js"; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "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); @@ -49,6 +53,7 @@ export interface Monitor { selectedDisks: string[]; gameId?: string; grpcServiceName?: string; + strategy?: PageSpeedStrategy; group: string | null; geoCheckEnabled?: boolean; geoCheckLocations?: GeoContinent[]; diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index df000ecef..2685c03f2 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; import { GeoContinents } from "@/types/geoCheck.js"; -import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js"; +import { MonitorMatchMethods, MonitorTypes, PageSpeedStrategies, DefaultPageSpeedStrategy } from "@/types/monitor.js"; export const getMonitorByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), @@ -73,6 +73,7 @@ export const createMonitorBodyValidation = z.object({ 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).default(DefaultPageSpeedStrategy), selectedDisks: z.array(z.string()).optional(), group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(), geoCheckEnabled: z.boolean().optional(), @@ -102,6 +103,7 @@ export const editMonitorBodyValidation = z.object({ 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(), @@ -156,6 +158,7 @@ const importedMonitorSchema = z.object({ 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).default(DefaultPageSpeedStrategy), group: z.union([z.string().max(50).trim(), z.null()]).default(null), geoCheckEnabled: z.boolean().default(false), geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),