Compare commits

...
19 changed files with 176 additions and 13 deletions

No files matched your search

@@ -1,12 +1,13 @@
import type { Monitor } from "@/Types/Monitor"; import type { Monitor } from "@/Types/Monitor";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography"; 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 { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import prettyMilliseconds from "pretty-ms"; import prettyMilliseconds from "pretty-ms";
import { typographyLevels } from "@/Utils/Theme/Palette"; import { typographyLevels } from "@/Utils/Theme/Palette";
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from "@mui/material/useMediaQuery";
import { LAYOUT } from "@/Utils/Theme/constants";
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
const theme = useTheme(); const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isSmall = useMediaQuery(theme.breakpoints.down("md"));
@@ -28,7 +29,7 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
<Stack <Stack
direction="row" direction="row"
alignItems={"center"} alignItems={"center"}
gap={theme.spacing(4)} gap={theme.spacing(LAYOUT.XS)}
> >
<PulseDot color={getStatusColor(monitor.status, theme)} /> <PulseDot color={getStatusColor(monitor.status, theme)} />
<Typography <Typography
@@ -41,6 +42,12 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
> >
{formatUrl(monitor?.url)} {formatUrl(monitor?.url)}
</Typography> </Typography>
{monitor.type === "pagespeed" && monitor.strategy && (
<>
<Dot />
<StrategyBadge strategy={monitor.strategy} />
</>
)}
{!isSmall && ( {!isSmall && (
<> <>
<Dot /> <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>
);
};
@@ -21,3 +21,4 @@ export * from "./SplitBox";
export * from "./TextLink"; export * from "./TextLink";
export * from "./OfflineBanner"; export * from "./OfflineBanner";
export * from "./Avatar"; export * from "./Avatar";
export * from "./StrategyBadge";
+2
View File
@@ -1,5 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor"; import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
import { DefaultPageSpeedStrategy } from "@/Types/Monitor";
import type { Monitor, MonitorType } from "@/Types/Monitor"; import type { Monitor, MonitorType } from "@/Types/Monitor";
interface UseMonitorFormOptions { interface UseMonitorFormOptions {
@@ -88,6 +89,7 @@ export const useMonitorForm = ({
...base, ...base,
type: "pagespeed", type: "pagespeed",
url: data?.url || "", url: data?.url || "",
strategy: data?.strategy ?? DefaultPageSpeedStrategy,
}; };
break; break;
case "hardware": case "hardware":
+36
View File
@@ -37,6 +37,7 @@ import {
type MonitorType, type MonitorType,
type GamesMap, type GamesMap,
supportsGeoCheck, supportsGeoCheck,
DefaultPageSpeedStrategy,
} from "@/Types/Monitor"; } from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification"; import type { Notification } from "@/Types/Notification";
import type { MonitorFormData } from "@/Validation/monitor"; import type { MonitorFormData } from "@/Validation/monitor";
@@ -51,6 +52,7 @@ interface GeneralSettingsConfig {
showSecret: boolean; showSecret: boolean;
showGrpcServiceName: boolean; showGrpcServiceName: boolean;
showIgnoreTls: boolean; showIgnoreTls: boolean;
showStrategy: boolean;
} }
const getGeneralSettingsConfig = ( const getGeneralSettingsConfig = (
@@ -68,6 +70,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
ping: { ping: {
urlLabel: t("pages.createMonitor.form.general.option.host.label"), urlLabel: t("pages.createMonitor.form.general.option.host.label"),
@@ -79,6 +82,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
docker: { docker: {
urlLabel: t("pages.createMonitor.form.general.option.container.label"), urlLabel: t("pages.createMonitor.form.general.option.container.label"),
@@ -90,6 +94,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
port: { port: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"), urlLabel: t("pages.createMonitor.form.general.option.url.label"),
@@ -101,6 +106,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
game: { game: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"), urlLabel: t("pages.createMonitor.form.general.option.url.label"),
@@ -112,6 +118,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
grpc: { grpc: {
urlLabel: t("pages.createMonitor.form.general.option.host.label"), urlLabel: t("pages.createMonitor.form.general.option.host.label"),
@@ -123,6 +130,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: true, showGrpcServiceName: true,
showIgnoreTls: true, showIgnoreTls: true,
showStrategy: false,
}, },
pagespeed: { pagespeed: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"), urlLabel: t("pages.createMonitor.form.general.option.url.label"),
@@ -134,6 +142,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: true,
}, },
hardware: { hardware: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"), urlLabel: t("pages.createMonitor.form.general.option.url.label"),
@@ -145,6 +154,7 @@ const getGeneralSettingsConfig = (
showSecret: true, showSecret: true,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: false, showIgnoreTls: false,
showStrategy: false,
}, },
websocket: { websocket: {
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"), urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
@@ -156,6 +166,7 @@ const getGeneralSettingsConfig = (
showSecret: false, showSecret: false,
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: true, showIgnoreTls: true,
showStrategy: false,
}, },
}; };
return configs[type] || configs.http; return configs[type] || configs.http;
@@ -382,6 +393,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 */} {/* Port field - only for port and game types */}
{generalSettingsConfig.showPort && ( {generalSettingsConfig.showPort && (
<Controller <Controller
@@ -1,7 +1,13 @@
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; 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 { HistogramPageSpeed } from "@/Components/monitors";
import { ArrowDown, ArrowUp } from "lucide-react"; import { ArrowDown, ArrowUp } from "lucide-react";
import type { Header } from "@/Components/design-elements/Table"; import type { Header } from "@/Components/design-elements/Table";
@@ -179,7 +185,19 @@ export const PageSpeedMonitorsTable = ({
</Stack> </Stack>
), ),
render: (row) => { 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
@@ -38,6 +38,10 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; 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 { export interface Monitor {
id: string; id: string;
userId: string; userId: string;
@@ -72,6 +76,7 @@ export interface Monitor {
selectedDisks: string[]; selectedDisks: string[];
gameId?: string; gameId?: string;
grpcServiceName?: string; grpcServiceName?: string;
strategy?: PageSpeedStrategy;
group: string | null; group: string | null;
geoCheckEnabled?: boolean; geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[]; geoCheckLocations?: GeoContinent[];
+2
View File
@@ -1,5 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { GeoContinents } from "@/Types/GeoCheck"; import { GeoContinents } from "@/Types/GeoCheck";
import { PageSpeedStrategies } from "@/Types/Monitor";
// URL schema with custom error message // URL schema with custom error message
const urlSchema = z.url({ message: "Please enter a valid URL" }); const urlSchema = z.url({ message: "Please enter a valid URL" });
@@ -89,6 +90,7 @@ const grpcSchema = baseSchema.extend({
const pagespeedSchema = baseSchema.extend({ const pagespeedSchema = baseSchema.extend({
type: z.literal("pagespeed"), type: z.literal("pagespeed"),
url: urlSchema, url: urlSchema,
strategy: z.enum(PageSpeedStrategies),
}); });
// Hardware/Infrastructure monitor schema // Hardware/Infrastructure monitor schema
+9
View File
@@ -503,6 +503,11 @@
"wsUrl": { "wsUrl": {
"label": "WebSocket URL", "label": "WebSocket URL",
"placeholder": "wss://example.com/socket" "placeholder": "wss://example.com/socket"
},
"strategy": {
"label": "Strategy",
"desktop": "Desktop",
"mobile": "Mobile"
} }
}, },
"title": "General settings", "title": "General settings",
@@ -968,6 +973,10 @@
} }
}, },
"pageSpeed": { "pageSpeed": {
"strategy": {
"desktop": "Desktop",
"mobile": "Mobile"
},
"charts": { "charts": {
"common": { "common": {
"cls": "Cumulative Layout Shift (CLS)", "cls": "Cumulative Layout Shift (CLS)",
+11
View File
@@ -3631,6 +3631,12 @@
"type": "string", "type": "string",
"default": "" "default": ""
}, },
"strategy": {
"type": "string",
"enum": ["desktop", "mobile"],
"default": "desktop",
"description": "PageSpeed analysis strategy"
},
"selectedDisks": { "selectedDisks": {
"type": "array", "type": "array",
"items": { "items": {
@@ -3737,6 +3743,11 @@
"grpcServiceName": { "grpcServiceName": {
"type": "string" "type": "string"
}, },
"strategy": {
"type": "string",
"enum": ["desktop", "mobile"],
"description": "PageSpeed analysis strategy"
},
"selectedDisks": { "selectedDisks": {
"type": "array", "type": "array",
"items": { "items": {
@@ -0,0 +1,15 @@
import type { Pool } from "pg";
export const addStrategyToMonitors = async (pool: Pool) => {
await pool.query(`
ALTER TABLE monitors
ADD COLUMN IF NOT EXISTS strategy TEXT DEFAULT NULL;
`);
};
export const dropStrategyFromMonitors = async (pool: Pool) => {
await pool.query(`
ALTER TABLE monitors
DROP COLUMN IF EXISTS strategy;
`);
};
@@ -21,6 +21,7 @@ import { createStatusPages, dropStatusPages } from "./0018_create_status_pages.j
import { createAppSettings, dropAppSettings } from "./0019_create_app_settings.js"; import { createAppSettings, dropAppSettings } from "./0019_create_app_settings.js";
import { createContinuousAggregates, dropContinuousAggregates } from "./0020_create_continuous_aggregates.js"; import { createContinuousAggregates, dropContinuousAggregates } from "./0020_create_continuous_aggregates.js";
import { createRetentionCompression, dropRetentionCompression } from "./0021_create_retention_compression.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"; const SERVICE_NAME = "TimescaleDB Migrations";
@@ -52,6 +53,7 @@ const migrations: MigrationEntry[] = [
{ name: "0019_create_app_settings", up: createAppSettings, down: dropAppSettings }, { name: "0019_create_app_settings", up: createAppSettings, down: dropAppSettings },
{ name: "0020_create_continuous_aggregates", up: createContinuousAggregates, down: dropContinuousAggregates }, { name: "0020_create_continuous_aggregates", up: createContinuousAggregates, down: dropContinuousAggregates },
{ name: "0021_create_retention_compression", up: createRetentionCompression, down: dropRetentionCompression }, { name: "0021_create_retention_compression", up: createRetentionCompression, down: dropRetentionCompression },
{ name: "0022_add_strategy_to_monitors", up: addStrategyToMonitors, down: dropStrategyFromMonitors },
]; ];
const ensureMigrationsTable = async (pool: Pool) => { const ensureMigrationsTable = async (pool: Pool) => {
+5 -1
View File
@@ -1,6 +1,6 @@
import { Schema, model, Types } from "mongoose"; import { Schema, model, Types } from "mongoose";
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js"; import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js"; import { MonitorTypes, MonitorStatuses, PageSpeedStrategies } from "@/types/monitor.js";
import type { import type {
CheckAudits, CheckAudits,
CheckCaptureInfo, CheckCaptureInfo,
@@ -330,6 +330,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
type: String, type: String,
default: "", default: "",
}, },
strategy: {
type: String,
enum: PageSpeedStrategies,
},
group: { group: {
type: String, type: String,
trim: true, trim: true,
@@ -386,6 +386,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
selectedDisks: doc.selectedDisks ?? [], selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined, gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined,
strategy: doc.strategy ?? undefined,
group: doc.group ?? null, group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckEnabled: doc.geoCheckEnabled ?? false,
@@ -445,6 +446,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
selectedDisks: doc.selectedDisks ?? [], selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined, gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined,
strategy: doc.strategy ?? undefined,
group: doc.group ?? null, group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckEnabled: doc.geoCheckEnabled ?? false,
@@ -1,5 +1,6 @@
import type { Pool } from "pg"; import type { Pool } from "pg";
import type { Monitor, MonitorsSummary, MonitorStatus, MonitorType, MonitorMatchMethod, GeoContinent } from "@/types/monitor.js"; import type { Monitor, MonitorsSummary, MonitorStatus, MonitorType, MonitorMatchMethod, GeoContinent, PageSpeedStrategy } from "@/types/monitor.js";
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
@@ -36,6 +37,7 @@ interface MonitorRow {
selected_disks: string[] | null; selected_disks: string[] | null;
game_id: string | null; game_id: string | null;
grpc_service_name: string | null; grpc_service_name: string | null;
strategy: PageSpeedStrategy | null;
monitor_group: string | null; monitor_group: string | null;
geo_check_enabled: boolean; geo_check_enabled: boolean;
geo_check_locations: GeoContinent[] | null; 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, 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, 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, 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`; created_at, updated_at`;
export class TimescaleMonitorsRepository implements IMonitorsRepository { 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, interval_ms, is_active, status_window, status_window_size, status_window_threshold,
cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter, 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, 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)
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) 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}`, RETURNING ${MONITOR_COLUMNS}`,
[ [
userId, userId,
@@ -96,6 +98,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
monitor.selectedDisks ?? [], monitor.selectedDisks ?? [],
monitor.gameId ?? null, monitor.gameId ?? null,
monitor.grpcServiceName ?? null, monitor.grpcServiceName ?? null,
monitor.strategy ?? null,
monitor.group ?? null, monitor.group ?? null,
monitor.geoCheckEnabled ?? false, monitor.geoCheckEnabled ?? false,
monitor.geoCheckLocations ?? [], monitor.geoCheckLocations ?? [],
@@ -594,6 +597,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
["selectedDisks", "selected_disks"], ["selectedDisks", "selected_disks"],
["gameId", "game_id"], ["gameId", "game_id"],
["grpcServiceName", "grpc_service_name"], ["grpcServiceName", "grpc_service_name"],
["strategy", "strategy"],
["group", "monitor_group"], ["group", "monitor_group"],
["geoCheckEnabled", "geo_check_enabled"], ["geoCheckEnabled", "geo_check_enabled"],
["geoCheckLocations", "geo_check_locations"], ["geoCheckLocations", "geo_check_locations"],
@@ -1033,6 +1037,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
selectedDisks: row.selected_disks ?? [], selectedDisks: row.selected_disks ?? [],
gameId: row.game_id ?? undefined, gameId: row.game_id ?? undefined,
grpcServiceName: row.grpc_service_name ?? undefined, grpcServiceName: row.grpc_service_name ?? undefined,
strategy: row.strategy ?? undefined,
group: row.monitor_group, group: row.monitor_group,
geoCheckEnabled: row.geo_check_enabled, geoCheckEnabled: row.geo_check_enabled,
geoCheckLocations: row.geo_check_locations ?? [], geoCheckLocations: row.geo_check_locations ?? [],
@@ -1,6 +1,6 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.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 { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { ISettingsService } from "@/service/system/settingsService.js"; import { ISettingsService } from "@/service/system/settingsService.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
@@ -19,14 +19,15 @@ export class PageSpeedProvider implements IStatusProvider<PageSpeedStatusPayload
} }
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> { async handle(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
const { url } = monitor; const { url, strategy } = monitor;
try { try {
if (!url) throw new Error("URL is required for PageSpeed monitor"); if (!url) throw new Error("URL is required for PageSpeed monitor");
const dbSettings = await this.settingsService.getDBSettings(); const dbSettings = await this.settingsService.getDBSettings();
const apiKey = dbSettings?.pagespeedApiKey; const apiKey = dbSettings?.pagespeedApiKey;
const resolvedStrategy = strategy ?? DefaultPageSpeedStrategy;
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent( let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(
url url
)}&category=seo&category=accessibility&category=best-practices&category=performance`; )}&strategy=${encodeURIComponent(resolvedStrategy)}&category=seo&category=accessibility&category=best-practices&category=performance`;
if (apiKey) { if (apiKey) {
pageSpeedUrl += `&key=${apiKey}`; pageSpeedUrl += `&key=${apiKey}`;
@@ -249,6 +249,9 @@ export class StatusService implements IStatusService {
}; };
} }
// With a full window, a single raw check must not change monitor.status; only the sliding-window threshold can trigger a transition.
newStatus = monitor.status;
// Check if threshold has been met // Check if threshold has been met
const failures = monitor.statusWindow.filter((s) => s === false).length; const failures = monitor.statusWindow.filter((s) => s === false).length;
const failureRate = (failures / monitor.statusWindow.length) * 100; const failureRate = (failures / monitor.statusWindow.length) * 100;
+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", "unknown"] as const; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const;
export type MonitorType = (typeof MonitorTypes)[number]; 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 GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type); export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type);
@@ -49,6 +53,7 @@ export interface Monitor {
selectedDisks: string[]; selectedDisks: string[];
gameId?: string; gameId?: string;
grpcServiceName?: string; grpcServiceName?: string;
strategy?: PageSpeedStrategy;
group: string | null; group: string | null;
geoCheckEnabled?: boolean; geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[]; geoCheckLocations?: GeoContinent[];
+4 -1
View File
@@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { booleanCoercion } from "./shared.js"; import { booleanCoercion } from "./shared.js";
import { GeoContinents } from "@/types/geoCheck.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({ export const getMonitorByIdParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"), 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(), matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
gameId: z.union([z.string(), z.literal("")]).optional(), gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""), grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
strategy: z.enum(PageSpeedStrategies).optional(),
selectedDisks: z.array(z.string()).optional(), selectedDisks: z.array(z.string()).optional(),
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(), group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
geoCheckEnabled: z.boolean().optional(), geoCheckEnabled: z.boolean().optional(),
@@ -102,6 +103,7 @@ export const editMonitorBodyValidation = z.object({
tempAlertThreshold: z.number().optional(), tempAlertThreshold: z.number().optional(),
gameId: z.union([z.string(), z.literal("")]).optional(), gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: 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(), selectedDisks: z.array(z.string()).optional(),
group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(), group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(),
geoCheckEnabled: z.boolean().optional(), geoCheckEnabled: z.boolean().optional(),
@@ -156,6 +158,7 @@ const importedMonitorSchema = z.object({
selectedDisks: z.array(z.string()).default([]), selectedDisks: z.array(z.string()).default([]),
gameId: z.union([z.string(), z.literal("")]).optional(), gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""), 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), group: z.union([z.string().max(50).trim(), z.null()]).default(null),
geoCheckEnabled: z.boolean().default(false), geoCheckEnabled: z.boolean().default(false),
geoCheckLocations: z.array(z.enum(GeoContinents)).default([]), geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),