Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d0373989f | ||
|
|
ac51dfeab7 | ||
|
|
99a5aa1722 | ||
|
|
5791bdd5e7 | ||
|
|
f7514bc938 | ||
|
|
2bbaf7d1eb | ||
|
|
4222d39a8b | ||
|
|
fb69e29adf | ||
|
|
0d90083e63 | ||
|
|
8ab049aa6c | ||
|
|
69b6955ced | ||
|
|
04dfff4ebd | ||
|
|
13a7ae82e3 | ||
|
|
4c94a7c21e | ||
|
|
40968c370b | ||
|
|
a864bc0c05 | ||
|
|
fe242b1977 | ||
|
|
d1f1d31ea2 | ||
|
|
eacbb227c3 | ||
|
|
ec129e3604 |
No files matched your search
@@ -48,6 +48,10 @@ You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.
|
|||||||
|
|
||||||
Usage instructions can be found [here](https://checkmate.so/docs).
|
Usage instructions can be found [here](https://checkmate.so/docs).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
- [Docker](https://www.docker.com/) installed
|
||||||
|
- [Git](https://git-scm.com/) installed
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
|
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
|
||||||
@@ -80,7 +84,7 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
|
|||||||
If you have any questions, suggestions or comments, you have several options:
|
If you have any questions, suggestions or comments, you have several options:
|
||||||
|
|
||||||
- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred)
|
- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred)
|
||||||
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) (we check here from time to time)
|
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (we check here from time to time)
|
||||||
|
|
||||||
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||||
|
|
||||||
@@ -167,7 +171,7 @@ Here's how you can contribute:
|
|||||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
|
[](https://star-history.com/#bluewave-labs/Checkmate&Date)
|
||||||
|
|
||||||
## Our sponsors
|
## Our sponsors
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -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":
|
||||||
|
|||||||
@@ -1,28 +1,76 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { notificationSchema } from "@/Validation/notifications";
|
import { notificationSchema } from "@/Validation/notifications";
|
||||||
|
import type { NotificationFormData } from "@/Validation/notifications";
|
||||||
import type { Notification } from "@/Types/Notification";
|
import type { Notification } from "@/Types/Notification";
|
||||||
|
|
||||||
interface UseNotificationFormOptions {
|
interface UseNotificationFormOptions {
|
||||||
data?: Notification | null;
|
data?: Notification | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDefaults(data: Notification | null): NotificationFormData {
|
||||||
|
if (data?.type === "matrix") {
|
||||||
|
return {
|
||||||
|
type: "matrix",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
homeserverUrl: data.homeserverUrl || "",
|
||||||
|
roomId: data.roomId || "",
|
||||||
|
accessToken: data.accessToken || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "telegram") {
|
||||||
|
return {
|
||||||
|
type: "telegram",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
accessToken: data.accessToken || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "slack") {
|
||||||
|
return {
|
||||||
|
type: "slack",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "discord") {
|
||||||
|
return {
|
||||||
|
type: "discord",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "webhook") {
|
||||||
|
return {
|
||||||
|
type: "webhook",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "pager_duty") {
|
||||||
|
return {
|
||||||
|
type: "pager_duty",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "teams") {
|
||||||
|
return {
|
||||||
|
type: "teams",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Default: email (covers both data === null and data.type === "email")
|
||||||
|
return {
|
||||||
|
type: "email",
|
||||||
|
notificationName: data?.notificationName || "",
|
||||||
|
address: data?.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const useNotificationForm = ({ data = null }: UseNotificationFormOptions = {}) => {
|
export const useNotificationForm = ({ data = null }: UseNotificationFormOptions = {}) => {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const defaults =
|
const defaults = buildDefaults(data);
|
||||||
data?.type === "matrix"
|
|
||||||
? {
|
|
||||||
type: "matrix" as const,
|
|
||||||
notificationName: data.notificationName || "",
|
|
||||||
homeserverUrl: data.homeserverUrl || "",
|
|
||||||
roomId: data.roomId || "",
|
|
||||||
accessToken: data.accessToken || "",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: (data?.type || "email") as Exclude<Notification["type"], "matrix">,
|
|
||||||
notificationName: data?.notificationName || "",
|
|
||||||
address: data?.address || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
return { schema: notificationSchema, defaults };
|
return { schema: notificationSchema, defaults };
|
||||||
}, [data]);
|
}, [data]);
|
||||||
};
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ const NotificationsCreatePage = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{watchedType !== "matrix" && (
|
{watchedType !== "matrix" && watchedType !== "telegram" && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={addressConfig.title}
|
title={addressConfig.title}
|
||||||
subtitle={addressConfig.description}
|
subtitle={addressConfig.description}
|
||||||
@@ -155,7 +155,7 @@ const NotificationsCreatePage = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="address"
|
name="address"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaults.address}
|
defaultValue={"address" in defaults ? defaults.address : ""}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
@@ -171,6 +171,49 @@ const NotificationsCreatePage = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{watchedType === "telegram" && (
|
||||||
|
<ConfigBox
|
||||||
|
title={t("pages.notifications.form.telegram.title")}
|
||||||
|
subtitle={t("pages.notifications.form.telegram.description")}
|
||||||
|
rightContent={
|
||||||
|
<Stack spacing={theme.spacing(8)}>
|
||||||
|
<Controller
|
||||||
|
name="accessToken"
|
||||||
|
control={control}
|
||||||
|
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
fieldLabel={t("pages.notifications.form.telegram.optionBotToken")}
|
||||||
|
placeholder={t(
|
||||||
|
"pages.notifications.form.telegram.placeholderBotToken"
|
||||||
|
)}
|
||||||
|
fullWidth
|
||||||
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message ?? ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="address"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
fieldLabel={t("pages.notifications.form.telegram.optionChatId")}
|
||||||
|
placeholder={t("pages.notifications.form.telegram.placeholderChatId")}
|
||||||
|
fullWidth
|
||||||
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message ?? ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{watchedType === "matrix" && (
|
{watchedType === "matrix" && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.notifications.form.matrix.title")}
|
title={t("pages.notifications.form.matrix.title")}
|
||||||
@@ -180,7 +223,7 @@ const NotificationsCreatePage = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="homeserverUrl"
|
name="homeserverUrl"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaults.homeserverUrl}
|
defaultValue={"homeserverUrl" in defaults ? defaults.homeserverUrl : ""}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
@@ -196,7 +239,7 @@ const NotificationsCreatePage = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="roomId"
|
name="roomId"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaults.roomId}
|
defaultValue={"roomId" in defaults ? defaults.roomId : ""}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
@@ -212,7 +255,7 @@ const NotificationsCreatePage = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="accessToken"
|
name="accessToken"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaults.accessToken}
|
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const NotificationChannels = [
|
|||||||
"pager_duty",
|
"pager_duty",
|
||||||
"matrix",
|
"matrix",
|
||||||
"teams",
|
"teams",
|
||||||
|
"telegram",
|
||||||
] as const;
|
] as const;
|
||||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ const teamsSchema = baseSchema.extend({
|
|||||||
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
|
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const telegramSchema = baseSchema.extend({
|
||||||
|
type: z.literal("telegram"),
|
||||||
|
address: z.string().min(1, "Chat ID is required"),
|
||||||
|
accessToken: z.string().min(1, "Bot token is required"),
|
||||||
|
});
|
||||||
|
|
||||||
export const notificationSchema = z.discriminatedUnion("type", [
|
export const notificationSchema = z.discriminatedUnion("type", [
|
||||||
emailSchema,
|
emailSchema,
|
||||||
slackSchema,
|
slackSchema,
|
||||||
@@ -58,6 +64,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
|||||||
pagerDutySchema,
|
pagerDutySchema,
|
||||||
matrixSchema,
|
matrixSchema,
|
||||||
teamsSchema,
|
teamsSchema,
|
||||||
|
telegramSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
||||||
@@ -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",
|
||||||
@@ -951,6 +956,14 @@
|
|||||||
"description": "Select the type of notification channel to create.",
|
"description": "Select the type of notification channel to create.",
|
||||||
"optionType": "Type",
|
"optionType": "Type",
|
||||||
"title": "Channel type"
|
"title": "Channel type"
|
||||||
|
},
|
||||||
|
"telegram": {
|
||||||
|
"title": "Telegram configuration",
|
||||||
|
"description": "Configure your Telegram bot to send notifications.",
|
||||||
|
"optionBotToken": "Bot token",
|
||||||
|
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||||
|
"optionChatId": "Chat ID",
|
||||||
|
"placeholderChatId": "-1001234567890"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
@@ -960,6 +973,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pageSpeed": {
|
"pageSpeed": {
|
||||||
|
"strategy": {
|
||||||
|
"desktop": "Desktop",
|
||||||
|
"mobile": "Mobile"
|
||||||
|
},
|
||||||
"charts": {
|
"charts": {
|
||||||
"common": {
|
"common": {
|
||||||
"cls": "Cumulative Layout Shift (CLS)",
|
"cls": "Cumulative Layout Shift (CLS)",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
PagerDutyProvider,
|
PagerDutyProvider,
|
||||||
MatrixProvider,
|
MatrixProvider,
|
||||||
TeamsProvider,
|
TeamsProvider,
|
||||||
|
TelegramProvider,
|
||||||
// Interfaces
|
// Interfaces
|
||||||
INetworkService,
|
INetworkService,
|
||||||
IEmailService,
|
IEmailService,
|
||||||
@@ -296,6 +297,7 @@ export const initializeServices = async ({
|
|||||||
const pagerDutyProvider = new PagerDutyProvider(logger);
|
const pagerDutyProvider = new PagerDutyProvider(logger);
|
||||||
const matrixProvider = new MatrixProvider(logger);
|
const matrixProvider = new MatrixProvider(logger);
|
||||||
const teamsProvider = new TeamsProvider(logger);
|
const teamsProvider = new TeamsProvider(logger);
|
||||||
|
const telegramProvider = new TelegramProvider(logger);
|
||||||
|
|
||||||
const notificationsService = new NotificationsService(
|
const notificationsService = new NotificationsService(
|
||||||
notificationsRepository,
|
notificationsRepository,
|
||||||
@@ -307,6 +309,7 @@ export const initializeServices = async ({
|
|||||||
pagerDutyProvider,
|
pagerDutyProvider,
|
||||||
matrixProvider,
|
matrixProvider,
|
||||||
teamsProvider,
|
teamsProvider,
|
||||||
|
telegramProvider,
|
||||||
settingsService,
|
settingsService,
|
||||||
logger,
|
logger,
|
||||||
notificationMessageBuilder
|
notificationMessageBuilder
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
|
|||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as NotificationChannel[],
|
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram"] as NotificationChannel[],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
notificationName: {
|
notificationName: {
|
||||||
|
|||||||
@@ -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 ?? [],
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
|
|||||||
export * from "@/service/infrastructure/notificationProviders/slack.js";
|
export * from "@/service/infrastructure/notificationProviders/slack.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/teams.js";
|
export * from "@/service/infrastructure/notificationProviders/teams.js";
|
||||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
||||||
|
export * from "@/service/infrastructure/notificationProviders/telegram.js";
|
||||||
|
|
||||||
// System services
|
// System services
|
||||||
export * from "@/service/system/settingsService.js";
|
export * from "@/service/system/settingsService.js";
|
||||||
@@ -146,7 +146,8 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
|
|
||||||
deleteJob = async (monitor: Monitor) => {
|
deleteJob = async (monitor: Monitor) => {
|
||||||
this.scheduler.removeJob(monitor.id);
|
this.scheduler.removeJob(monitor.id);
|
||||||
this.scheduler.removeJob(`${monitor.id}-geo`);
|
const geoJob = await this.scheduler.getJob(`${monitor.id}-geo`);
|
||||||
|
if (geoJob) await this.scheduler.removeJob(`${monitor.id}-geo`);
|
||||||
};
|
};
|
||||||
|
|
||||||
pauseJob = async (monitor: Monitor) => {
|
pauseJob = async (monitor: Monitor) => {
|
||||||
@@ -154,7 +155,9 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
if (result === false) {
|
if (result === false) {
|
||||||
throw new Error("Failed to pause monitor");
|
throw new Error("Failed to pause monitor");
|
||||||
}
|
}
|
||||||
await this.scheduler.pauseJob(`${monitor.id}-geo`);
|
const geoJob = await this.scheduler.getJob(`${monitor.id}-geo`);
|
||||||
|
if (geoJob) await this.scheduler.removeJob(`${monitor.id}-geo`);
|
||||||
|
|
||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
message: `Paused monitor ${monitor.id}`,
|
message: `Paused monitor ${monitor.id}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
@@ -177,31 +180,35 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private syncGeoJob = async (monitor: Monitor) => {
|
||||||
|
const geoJobId = `${monitor.id}-geo`;
|
||||||
|
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
||||||
|
|
||||||
|
// If geoChecks have been disabled, or the monitor type doesn't support them, remove
|
||||||
|
if (!monitor.geoCheckEnabled || !supportsGeoCheck(monitor.type)) {
|
||||||
|
if (existingGeoJob) this.scheduler.removeJob(geoJobId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the job exists, update it
|
||||||
|
if (existingGeoJob) {
|
||||||
|
this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, active: monitor.isActive, data: monitor });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, create it
|
||||||
|
this.scheduler.addJob({
|
||||||
|
id: geoJobId,
|
||||||
|
template: "geo-check-job",
|
||||||
|
repeat: monitor.geoCheckInterval,
|
||||||
|
active: monitor.isActive,
|
||||||
|
data: monitor,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
updateJob = async (monitor: Monitor) => {
|
updateJob = async (monitor: Monitor) => {
|
||||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||||
|
await this.syncGeoJob(monitor);
|
||||||
// Handle geo check job lifecycle
|
|
||||||
const geoJobId = `${monitor.id}-geo`;
|
|
||||||
if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) {
|
|
||||||
// Check if geo job exists
|
|
||||||
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
|
||||||
if (existingGeoJob) {
|
|
||||||
// Update existing geo job
|
|
||||||
this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, active: monitor.isActive, data: monitor });
|
|
||||||
} else {
|
|
||||||
// Create new geo job
|
|
||||||
this.scheduler.addJob({
|
|
||||||
id: geoJobId,
|
|
||||||
template: "geo-check-job",
|
|
||||||
repeat: monitor.geoCheckInterval,
|
|
||||||
active: monitor.isActive,
|
|
||||||
data: monitor,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Remove geo job if disabled or monitor type changed
|
|
||||||
this.scheduler.removeJob(geoJobId);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
shutdown = async () => {
|
shutdown = async () => {
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
const SERVICE_NAME = "TelegramProvider";
|
||||||
|
import type { Notification } from "@/types/index.js";
|
||||||
|
import { INotificationProvider } from "@/service/index.js";
|
||||||
|
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||||
|
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||||
|
import got from "got";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
|
export class TelegramProvider implements INotificationProvider {
|
||||||
|
private logger: ILogger;
|
||||||
|
|
||||||
|
constructor(logger: ILogger) {
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||||
|
if (!notification.address || !notification.accessToken) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await got.post(`https://api.telegram.org/bot${notification.accessToken}/sendMessage`, {
|
||||||
|
json: {
|
||||||
|
chat_id: notification.address,
|
||||||
|
text: getTestMessage(),
|
||||||
|
parse_mode: "HTML",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const errMsg = error instanceof Error ? error.message : "unknown error";
|
||||||
|
const errStack = error instanceof Error ? error.stack : undefined;
|
||||||
|
this.logger.warn({
|
||||||
|
message: "Telegram test alert failed",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendTestAlert",
|
||||||
|
stack: errStack,
|
||||||
|
details: { error: errMsg },
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||||
|
if (!notification.address || !notification.accessToken) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = this.buildTelegramText(message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await got.post(`https://api.telegram.org/bot${notification.accessToken}/sendMessage`, {
|
||||||
|
json: {
|
||||||
|
chat_id: notification.address,
|
||||||
|
text,
|
||||||
|
parse_mode: "HTML",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.info({
|
||||||
|
message: "Telegram notification sent",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendMessage",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const errMsg = error instanceof Error ? error.message : "unknown error";
|
||||||
|
const errStack = error instanceof Error ? error.stack : undefined;
|
||||||
|
this.logger.warn({
|
||||||
|
message: "Telegram alert failed",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendMessage",
|
||||||
|
stack: errStack,
|
||||||
|
details: { error: errMsg },
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildTelegramText(message: NotificationMessage): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
lines.push(`<b>${message.content.title}</b>`);
|
||||||
|
lines.push(message.content.summary);
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
lines.push("<b>Monitor Details:</b>");
|
||||||
|
lines.push(`• Name: ${message.monitor.name}`);
|
||||||
|
lines.push(`• URL: ${message.monitor.url}`);
|
||||||
|
lines.push(`• Type: ${message.monitor.type}`);
|
||||||
|
lines.push(`• Status: ${message.monitor.status}`);
|
||||||
|
|
||||||
|
if (message.content.details && message.content.details.length > 0) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push("<b>Additional Information:</b>");
|
||||||
|
message.content.details.forEach((detail) => lines.push(`• ${detail}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push("<b>Threshold Breaches:</b>");
|
||||||
|
message.content.thresholds.forEach((breach) => {
|
||||||
|
lines.push(`• ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.content.incident) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`<a href="${message.clientHost}/incidents/${message.monitor.id}">View Incident</a>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ export class NotificationsService implements INotificationsService {
|
|||||||
private pagerDutyProvider: INotificationProvider;
|
private pagerDutyProvider: INotificationProvider;
|
||||||
private matrixProvider: INotificationProvider;
|
private matrixProvider: INotificationProvider;
|
||||||
private teamsProvider: INotificationProvider;
|
private teamsProvider: INotificationProvider;
|
||||||
|
private telegramProvider: INotificationProvider;
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
private settingsService: ISettingsService;
|
private settingsService: ISettingsService;
|
||||||
private notificationMessageBuilder: INotificationMessageBuilder;
|
private notificationMessageBuilder: INotificationMessageBuilder;
|
||||||
@@ -47,6 +48,7 @@ export class NotificationsService implements INotificationsService {
|
|||||||
pagerDutyProvider: INotificationProvider,
|
pagerDutyProvider: INotificationProvider,
|
||||||
matrixProvider: INotificationProvider,
|
matrixProvider: INotificationProvider,
|
||||||
teamsProvider: INotificationProvider,
|
teamsProvider: INotificationProvider,
|
||||||
|
telegramProvider: INotificationProvider,
|
||||||
settingsService: ISettingsService,
|
settingsService: ISettingsService,
|
||||||
logger: ILogger,
|
logger: ILogger,
|
||||||
notificationMessageBuilder: INotificationMessageBuilder
|
notificationMessageBuilder: INotificationMessageBuilder
|
||||||
@@ -60,6 +62,7 @@ export class NotificationsService implements INotificationsService {
|
|||||||
this.pagerDutyProvider = pagerDutyProvider;
|
this.pagerDutyProvider = pagerDutyProvider;
|
||||||
this.matrixProvider = matrixProvider;
|
this.matrixProvider = matrixProvider;
|
||||||
this.teamsProvider = teamsProvider;
|
this.teamsProvider = teamsProvider;
|
||||||
|
this.telegramProvider = telegramProvider;
|
||||||
this.settingsService = settingsService;
|
this.settingsService = settingsService;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.notificationMessageBuilder = notificationMessageBuilder;
|
this.notificationMessageBuilder = notificationMessageBuilder;
|
||||||
@@ -97,6 +100,8 @@ export class NotificationsService implements INotificationsService {
|
|||||||
return await this.emailProvider.sendMessage!(notification, notificationMessage);
|
return await this.emailProvider.sendMessage!(notification, notificationMessage);
|
||||||
case "teams":
|
case "teams":
|
||||||
return await this.teamsProvider.sendMessage!(notification, notificationMessage);
|
return await this.teamsProvider.sendMessage!(notification, notificationMessage);
|
||||||
|
case "telegram":
|
||||||
|
return await this.telegramProvider.sendMessage!(notification, notificationMessage);
|
||||||
default:
|
default:
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: `Unknown notification type: ${notification.type}`,
|
message: `Unknown notification type: ${notification.type}`,
|
||||||
@@ -157,6 +162,8 @@ export class NotificationsService implements INotificationsService {
|
|||||||
return await this.webhookProvider.sendTestAlert(notification);
|
return await this.webhookProvider.sendTestAlert(notification);
|
||||||
case "teams":
|
case "teams":
|
||||||
return await this.teamsProvider.sendTestAlert(notification);
|
return await this.teamsProvider.sendTestAlert(notification);
|
||||||
|
case "telegram":
|
||||||
|
return await this.telegramProvider.sendTestAlert(notification);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as const;
|
export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram"] as const;
|
||||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
|
|||||||
@@ -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([]),
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [
|
|||||||
type: z.literal("teams"),
|
type: z.literal("teams"),
|
||||||
address: z.url({ message: "Please enter a valid Webhook URL" }),
|
address: z.url({ message: "Please enter a valid Webhook URL" }),
|
||||||
}),
|
}),
|
||||||
|
// Telegram notification
|
||||||
|
z.object({
|
||||||
|
notificationName: z.string().min(1, "Notification name is required"),
|
||||||
|
type: z.literal("telegram"),
|
||||||
|
address: z.string().min(1, "Chat ID is required"),
|
||||||
|
accessToken: z.string().min(1, "Bot token is required"),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const testNotificationBodyValidation = createNotificationBodyValidation;
|
export const testNotificationBodyValidation = createNotificationBodyValidation;
|
||||||
|
|||||||
Reference in new issue
Block a user