Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a23e4de1d3 | ||
|
|
3b858de2b5 | ||
|
|
d25ce9e498 | ||
|
|
b7c9e2710a | ||
|
|
bfdf28a2cc | ||
|
|
4ca24c794e | ||
|
|
7f9d2f275e | ||
|
|
1966134647 | ||
|
|
7646d11ea0 | ||
|
|
c10d9be0f0 | ||
|
|
27c3a9e12b | ||
|
|
134c2ebbf4 | ||
|
|
e39b44db6c | ||
|
|
1c1474cd6f | ||
|
|
db800e2259 | ||
|
|
b6765830a5 | ||
|
|
8cbde4a705 | ||
|
|
6fcaabbdb6 | ||
|
|
7cfbc85d9b | ||
|
|
ab834d400e | ||
|
|
1fcf06a415 | ||
|
|
dab465b8c5 | ||
|
|
59d245b424 | ||
|
|
e921b20fe3 | ||
|
|
0e6680366d | ||
|
|
7f7a9f40df | ||
|
|
31698bd002 | ||
|
|
c7ccdafec2 | ||
|
|
c920191984 | ||
|
|
374b6aa6ae | ||
|
|
3d5c81fe2f | ||
|
|
3bd6ec5d28 | ||
|
|
7bb47ef432 | ||
|
|
7856317e8f | ||
|
|
edbd8f2abb | ||
|
|
cddf8b32e6 | ||
|
|
0032765bd1 | ||
|
|
6bc9194e8f | ||
|
|
ef15e5d6e0 | ||
|
|
97750da889 | ||
|
|
af8083bdaa | ||
|
|
54b33ec835 | ||
|
|
847904a274 | ||
|
|
7bc03c976b | ||
|
|
73f05ab78e | ||
|
|
584e4633d3 | ||
|
|
433c57b42e | ||
|
|
a2cf2afc09 | ||
|
|
232be7a405 | ||
|
|
7d66bb5668 | ||
|
|
566fded55c | ||
|
|
fbb0e940c6 | ||
|
|
f983863c72 | ||
|
|
e0a75c78db | ||
|
|
2cd2a6d917 | ||
|
|
6f55e4934f | ||
|
|
0e1dc3e3fe | ||
|
|
e18681606c | ||
|
|
d2d75cd035 | ||
|
|
a65515fc08 | ||
|
|
302ac6f4f1 | ||
|
|
b51d40fe77 | ||
|
|
27f3def9bd | ||
|
|
e7a7c0e603 | ||
|
|
55885ea007 | ||
|
|
475801e91c | ||
|
|
cd501e46cd | ||
|
|
93c15588a4 | ||
|
|
a8ecd44abf | ||
|
|
412a3b7cc9 | ||
|
|
be8580ee99 | ||
|
|
8020bfde21 | ||
|
|
7061eb8eb0 | ||
|
|
70ae15725e | ||
|
|
32a7bd6543 | ||
|
|
68db4b8701 | ||
|
|
b75c1a4fcb | ||
|
|
e50240885d | ||
|
|
22645d86f7 | ||
|
|
62778886e1 | ||
|
|
9c467c763d | ||
|
|
97ace1e058 | ||
|
|
2fb6cb0b6f | ||
|
|
a7b0ce4d8f | ||
|
|
4a73170583 | ||
|
|
41310cb552 | ||
|
|
111b68b8df | ||
|
|
2430bbeebb | ||
|
|
f694917dd4 | ||
|
|
47b18e1d9d | ||
|
|
0d863aefe8 | ||
|
|
c319738048 | ||
|
|
002a82bb9a | ||
|
|
8719efab5d | ||
|
|
86fd535eaf | ||
|
|
2f6dbe9a3a | ||
|
|
649eaae712 | ||
|
|
1888ee0478 | ||
|
|
fd2c54d3d3 | ||
|
|
bd941db771 | ||
|
|
88fceddbbf | ||
|
|
13251ca7c5 | ||
|
|
6d6b597f99 | ||
|
|
dfc1733820 | ||
|
|
39a449e594 | ||
|
|
974d33b3de | ||
|
|
afe2b9d3bb | ||
|
|
5ab5cbe622 | ||
|
|
b6b1b8464c | ||
|
|
f337ca5ff0 | ||
|
|
f5bc8e78e4 | ||
|
|
8089e71f96 | ||
|
|
6195650dc1 | ||
|
|
f59d8bf1bf | ||
|
|
0941f3ce1a |
No files matched your search
@@ -3,4 +3,6 @@
|
||||
.VSCodeCounter
|
||||
*.sh
|
||||
mongo
|
||||
node_modules/
|
||||
node_modules/
|
||||
docs/architecture
|
||||
docs/reviews
|
||||
@@ -147,7 +147,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec
|
||||
|
||||
## Contributing
|
||||
|
||||
We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) helping individuals and businesses monitor their infra and servers.
|
||||
We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) helping individuals and businesses monitor their infra and servers.
|
||||
|
||||
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 7000+ stars and attracted 90+ contributors from around the globe.
|
||||
|
||||
|
||||
@@ -2472,11 +2472,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.4",
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,102 +1,155 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { ResponsiveStyleValue } from "@mui/system";
|
||||
import type { CheckSnapshot } from "@/Types/Check";
|
||||
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface HistogramResponseTimeProps {
|
||||
checks: CheckSnapshot[];
|
||||
height?: ResponsiveStyleValue<number | string>;
|
||||
gap?: ResponsiveStyleValue<number | string>;
|
||||
showStats?: boolean;
|
||||
statsPosition?: "left" | "right";
|
||||
}
|
||||
|
||||
interface ResponseTimeStats {
|
||||
max: number | string;
|
||||
avg: number | string;
|
||||
}
|
||||
|
||||
const DEFAULT_HEIGHT = 50;
|
||||
|
||||
const calculateResponseTimeStats = (checks: CheckSnapshot[]): ResponseTimeStats => {
|
||||
if (!Array.isArray(checks) || checks.length === 0) {
|
||||
return { max: "-", avg: "-" };
|
||||
}
|
||||
|
||||
const validChecks = checks.filter((check) => check.originalResponseTime != null);
|
||||
if (validChecks.length === 0) {
|
||||
return { max: "-", avg: "-" };
|
||||
}
|
||||
|
||||
const responseTimes = validChecks.map((check) => check.originalResponseTime);
|
||||
const max = Math.max(...responseTimes);
|
||||
const avg = Math.round(
|
||||
responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
|
||||
);
|
||||
|
||||
return { max, avg };
|
||||
};
|
||||
|
||||
export const HistogramResponseTime = ({
|
||||
checks,
|
||||
height = DEFAULT_HEIGHT,
|
||||
gap,
|
||||
showStats = true,
|
||||
statsPosition = "right",
|
||||
}: HistogramResponseTimeProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const stats = useMemo(() => calculateResponseTimeStats(checks), [checks]);
|
||||
|
||||
if (!Array.isArray(checks) || checks.length === 0) return null;
|
||||
|
||||
let data = Array<any>();
|
||||
const data =
|
||||
checks.length !== 25
|
||||
? [...checks, ...Array(25 - checks.length).fill({ status: "placeholder" })]
|
||||
: checks;
|
||||
|
||||
if (!checks || checks.length === 0) {
|
||||
data = [];
|
||||
}
|
||||
if (checks.length !== 25) {
|
||||
const placeholders = Array(25 - checks.length).fill({
|
||||
status: "placeholder",
|
||||
});
|
||||
data = [...checks, ...placeholders];
|
||||
} else {
|
||||
data = checks;
|
||||
}
|
||||
const chartHeight = typeof height === "number" ? `${height}px` : height;
|
||||
const gridGap = gap ?? theme.spacing(0.5);
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(25, 1fr)",
|
||||
gap: gridGap,
|
||||
alignItems: "end",
|
||||
height: chartHeight,
|
||||
}}
|
||||
|
||||
const statsContent = showStats &&
|
||||
(typeof stats.max === "number" || typeof stats.avg === "number") && (
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems={statsPosition === "left" ? "flex-end" : "flex-start"}
|
||||
minWidth={70}
|
||||
pr={statsPosition === "left" ? theme.spacing(8) : 0}
|
||||
pl={statsPosition === "right" ? theme.spacing(8) : 0}
|
||||
>
|
||||
{data.map((check, index) => {
|
||||
const isPlaceholder = (check as any).status === "placeholder";
|
||||
const heightPct = `${Math.max(0, Math.min(100, (check as any).responseTime ?? 0))}%`;
|
||||
const barColor =
|
||||
check.status === true
|
||||
? theme.palette.success.light
|
||||
: theme.palette.error.light;
|
||||
const bar = (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: theme.spacing(1),
|
||||
bgcolor: theme.palette.action.hover,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
{t("common.charts.histogram.avg", { value: stats.avg })}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{t("common.charts.histogram.max", { value: stats.max })}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction={statsPosition === "left" ? "row-reverse" : "row"}
|
||||
alignItems="center"
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(25, 1fr)",
|
||||
gap: gridGap,
|
||||
alignItems: "end",
|
||||
height: chartHeight,
|
||||
}}
|
||||
>
|
||||
{data.map((check, index) => {
|
||||
const isPlaceholder = (check as any).status === "placeholder";
|
||||
const heightPct = `${Math.max(0, Math.min(100, (check as any).responseTime ?? 0))}%`;
|
||||
const barColor =
|
||||
check.status === true
|
||||
? theme.palette.success.light
|
||||
: theme.palette.error.light;
|
||||
const bar = (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: isPlaceholder ? 0 : heightPct,
|
||||
bgcolor: barColor,
|
||||
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
height: "100%",
|
||||
borderRadius: theme.spacing(1),
|
||||
bgcolor: theme.palette.action.hover,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: isPlaceholder ? 0 : heightPct,
|
||||
bgcolor: barColor,
|
||||
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return isPlaceholder ? (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
{bar}
|
||||
</Box>
|
||||
) : (
|
||||
<HeatmapResponseTimeTooltip
|
||||
key={index}
|
||||
check={check}
|
||||
>
|
||||
{bar}
|
||||
</HeatmapResponseTimeTooltip>
|
||||
);
|
||||
})}
|
||||
return isPlaceholder ? (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
{bar}
|
||||
</Box>
|
||||
) : (
|
||||
<HeatmapResponseTimeTooltip
|
||||
key={index}
|
||||
check={check}
|
||||
>
|
||||
{bar}
|
||||
</HeatmapResponseTimeTooltip>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
{statsContent}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import { BaseBox } from "@/Components/v2/design-elements";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SxProps } from "@mui/material";
|
||||
|
||||
@@ -18,6 +17,7 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => {
|
||||
return (
|
||||
<BaseBox
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.default,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
@@ -27,11 +27,23 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => {
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-10%"
|
||||
left="5%"
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
backgroundImage: `
|
||||
linear-gradient(${theme.palette.divider} 1px, transparent 1px),
|
||||
linear-gradient(90deg, ${theme.palette.divider} 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "24px 24px",
|
||||
maskImage:
|
||||
"linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)",
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</BaseBox>
|
||||
);
|
||||
@@ -127,6 +139,17 @@ export const InitializingStatusBox = ({ n }: { n: number }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const BreachedStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("pages.common.monitors.status.breached")}
|
||||
n={n}
|
||||
color={theme.palette.warning.main}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TotalChecksBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -21,6 +21,8 @@ export const StatusLabel = ({ status, sx }: { status: MonitorStatus; sx?: SxProp
|
||||
return t("pages.common.monitors.status.up");
|
||||
} else if (status === "down") {
|
||||
return t("pages.common.monitors.status.down");
|
||||
} else if (status === "breached") {
|
||||
return t("pages.common.monitors.status.breached");
|
||||
} else if (status === "maintenance") {
|
||||
return t("pages.common.monitors.status.maintenance");
|
||||
} else if (status === "paused") {
|
||||
|
||||
@@ -38,11 +38,11 @@ export const ImageUpload = ({
|
||||
const isValidType = accept.some((type) => file.type.includes(type));
|
||||
const isValidSize = file.size <= maxSize;
|
||||
if (!isValidType) {
|
||||
setLocalError(t("common.errors.invalidFileFormat"));
|
||||
setLocalError(t("components.imageUpload.errors.invalidFileFormat"));
|
||||
return;
|
||||
}
|
||||
if (!isValidSize) {
|
||||
setLocalError(t("common.errors.invalidFileSize"));
|
||||
setLocalError(t("components.imageUpload.errors.invalidFileSize"));
|
||||
return;
|
||||
}
|
||||
setLocalError(null);
|
||||
@@ -137,15 +137,15 @@ export const ImageUpload = ({
|
||||
color="primary"
|
||||
fontWeight={500}
|
||||
>
|
||||
{t("common.imageUpload.clickToUpload")}
|
||||
{t("components.imageUpload.clickToUpload")}
|
||||
</Typography>{" "}
|
||||
{t("common.imageUpload.orDragAndDrop")}
|
||||
{t("components.imageUpload.orDragAndDrop")}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.disabled"
|
||||
>
|
||||
{accept.join(", ").toUpperCase()} • {t("common.imageUpload.maxSize")}{" "}
|
||||
{accept.join(", ").toUpperCase()} • {t("components.imageUpload.maxSize")}{" "}
|
||||
{Math.round(maxSize / 1024 / 1024)}MB
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -41,7 +41,10 @@ export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>(
|
||||
"& .MuiSlider-thumb": {
|
||||
backgroundColor: "#fff",
|
||||
"&:hover, &.Mui-focusVisible": {
|
||||
boxShadow: `0 0 0 8px ${theme.palette.primary.main}20`,
|
||||
boxShadow: "none",
|
||||
},
|
||||
"&:active": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
},
|
||||
"& .MuiSlider-valueLabel": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
InitializingStatusBox,
|
||||
BreachedStatusBox,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import Stack from "@mui/material/Stack";
|
||||
|
||||
@@ -11,9 +12,13 @@ import { useTheme } from "@mui/material";
|
||||
|
||||
interface MonitorsSummaryProps {
|
||||
summary: MonitorsSummary | null;
|
||||
showBreached?: boolean;
|
||||
}
|
||||
|
||||
export const HeaderMonitorsSummary = ({ summary }: MonitorsSummaryProps) => {
|
||||
export const HeaderMonitorsSummary = ({
|
||||
summary,
|
||||
showBreached = false,
|
||||
}: MonitorsSummaryProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
@@ -22,6 +27,7 @@ export const HeaderMonitorsSummary = ({ summary }: MonitorsSummaryProps) => {
|
||||
>
|
||||
<UpStatusBox n={summary?.upMonitors || 0} />
|
||||
<DownStatusBox n={summary?.downMonitors || 0} />
|
||||
{showBreached && <BreachedStatusBox n={summary?.breachedMonitors || 0} />}
|
||||
<PausedStatusBox n={summary?.pausedMonitors || 0} />
|
||||
<InitializingStatusBox n={summary?.initializingMonitors || 0} />
|
||||
</Stack>
|
||||
|
||||
@@ -37,6 +37,7 @@ export const useMonitorForm = ({
|
||||
matchMethod: data?.matchMethod || "",
|
||||
expectedValue: data?.expectedValue || "",
|
||||
jsonPath: data?.jsonPath || "",
|
||||
globalpingEnabled: data?.globalpingEnabled || false,
|
||||
};
|
||||
break;
|
||||
case "ping":
|
||||
@@ -44,6 +45,7 @@ export const useMonitorForm = ({
|
||||
...base,
|
||||
type: "ping",
|
||||
url: data?.url || "",
|
||||
globalpingEnabled: data?.globalpingEnabled || false,
|
||||
};
|
||||
break;
|
||||
case "port":
|
||||
@@ -83,10 +85,10 @@ export const useMonitorForm = ({
|
||||
type: "hardware",
|
||||
url: data?.url || "",
|
||||
secret: data?.secret || "",
|
||||
cpuAlertThreshold: data?.cpuAlertThreshold ?? 80,
|
||||
memoryAlertThreshold: data?.memoryAlertThreshold ?? 80,
|
||||
diskAlertThreshold: data?.diskAlertThreshold ?? 80,
|
||||
tempAlertThreshold: data?.tempAlertThreshold ?? 80,
|
||||
cpuAlertThreshold: data?.cpuAlertThreshold ?? 100,
|
||||
memoryAlertThreshold: data?.memoryAlertThreshold ?? 100,
|
||||
diskAlertThreshold: data?.diskAlertThreshold ?? 100,
|
||||
tempAlertThreshold: data?.tempAlertThreshold ?? 100,
|
||||
selectedDisks: data?.selectedDisks || [],
|
||||
};
|
||||
break;
|
||||
@@ -100,6 +102,7 @@ export const useMonitorForm = ({
|
||||
matchMethod: "",
|
||||
expectedValue: "",
|
||||
jsonPath: "",
|
||||
globalpingEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useMemo } from "react";
|
||||
import { settingsSchema } from "@/Validation/settings";
|
||||
import type { Settings } from "@/Types/Settings";
|
||||
import type { SettingsFormData } from "@/Validation/settings";
|
||||
|
||||
interface UseSettingsFormOptions {
|
||||
data?: Settings | null;
|
||||
}
|
||||
export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => {
|
||||
return useMemo(() => {
|
||||
const defaults: SettingsFormData = {
|
||||
systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false,
|
||||
systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false,
|
||||
systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true,
|
||||
systemEmailSecure: data?.systemEmailSecure ?? false,
|
||||
systemEmailPool: data?.systemEmailPool ?? false,
|
||||
showURL: data?.showURL ?? false,
|
||||
systemEmailHost: data?.systemEmailHost || "",
|
||||
systemEmailUser: data?.systemEmailUser || "",
|
||||
systemEmailAddress: data?.systemEmailAddress || "",
|
||||
systemEmailConnectionHost: data?.systemEmailConnectionHost || "localhost",
|
||||
systemEmailTLSServername: data?.systemEmailTLSServername || "",
|
||||
systemEmailPort: data?.systemEmailPort,
|
||||
globalThresholds: {
|
||||
cpu: data?.globalThresholds?.cpu,
|
||||
memory: data?.globalThresholds?.memory,
|
||||
disk: data?.globalThresholds?.disk,
|
||||
temperature: data?.globalThresholds?.temperature,
|
||||
},
|
||||
checkTTL: data?.checkTTL ?? 30,
|
||||
pagespeedApiKey: "",
|
||||
globalpingApiKey: "",
|
||||
globalpingLocationsTier: data?.globalpingLocationsTier ?? 6,
|
||||
systemEmailPassword: "",
|
||||
};
|
||||
|
||||
return { schema: settingsSchema, defaults };
|
||||
}, [data]);
|
||||
};
|
||||
@@ -97,27 +97,27 @@ const SetNewPasswordPage = () => {
|
||||
/>
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<BulletPointCheck
|
||||
text={t("auth.common.passwordRules.length")}
|
||||
text={t("pages.auth.common.passwordRules.length")}
|
||||
variant={getVariant(hasLength)}
|
||||
/>
|
||||
<BulletPointCheck
|
||||
text={t("auth.common.passwordRules.special")}
|
||||
text={t("pages.auth.common.passwordRules.special")}
|
||||
variant={getVariant(hasSpecial)}
|
||||
/>
|
||||
<BulletPointCheck
|
||||
text={t("auth.common.passwordRules.number")}
|
||||
text={t("pages.auth.common.passwordRules.number")}
|
||||
variant={getVariant(hasNumber)}
|
||||
/>
|
||||
<BulletPointCheck
|
||||
text={t("auth.common.passwordRules.uppercase")}
|
||||
text={t("pages.auth.common.passwordRules.uppercase")}
|
||||
variant={getVariant(hasUppercase)}
|
||||
/>
|
||||
<BulletPointCheck
|
||||
text={t("auth.common.passwordRules.lowercase")}
|
||||
text={t("pages.auth.common.passwordRules.lowercase")}
|
||||
variant={getVariant(hasLowercase)}
|
||||
/>
|
||||
<BulletPointCheck
|
||||
text={t("auth.common.passwordRules.match")}
|
||||
text={t("pages.auth.common.passwordRules.match")}
|
||||
variant={getVariant(passwordsMatch)}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -127,7 +127,7 @@ const SetNewPasswordPage = () => {
|
||||
fullWidth
|
||||
loading={loading}
|
||||
>
|
||||
{t("auth.forgotPassword.buttons.resetPassword")}
|
||||
{t("common.buttons.resetPassword")}
|
||||
</Button>
|
||||
<TextLink
|
||||
alignSelf="center"
|
||||
|
||||
@@ -146,7 +146,6 @@ const CreateMonitorPage = () => {
|
||||
|
||||
const { data: notifications } = useGet<Notification[]>("/notifications/team");
|
||||
const { data: games } = useGet<GamesMap>("/monitors/games");
|
||||
|
||||
const { schema, defaults } = useMonitorForm({
|
||||
data: existingMonitor ?? null,
|
||||
defaultType,
|
||||
@@ -223,8 +222,8 @@ const CreateMonitorPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (errors: unknown) => {
|
||||
console.log(errors);
|
||||
const onError = (_errors: unknown) => {
|
||||
// Validation errors are shown inline via react-hook-form field errors
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -329,11 +328,16 @@ const CreateMonitorPage = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => field.onChange(Number(e.target.value) || 0)}
|
||||
value={field.value === 0 ? "" : field.value}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
field.onChange(val === "" ? 0 : Number(val));
|
||||
}}
|
||||
type="number"
|
||||
fieldLabel={t("portToMonitor")}
|
||||
placeholder="5173"
|
||||
fieldLabel={t("pages.createMonitor.form.general.option.port.label")}
|
||||
placeholder={t(
|
||||
"pages.createMonitor.form.general.option.port.placeholder"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
@@ -351,10 +355,12 @@ const CreateMonitorPage = () => {
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
fieldLabel={t("chooseGame")}
|
||||
fieldLabel={t("pages.createMonitor.form.general.option.game.label")}
|
||||
error={!!fieldState.error}
|
||||
>
|
||||
<MenuItem value="">Select a game</MenuItem>
|
||||
<MenuItem value="">
|
||||
{t("pages.createMonitor.form.general.option.game.placeholder")}{" "}
|
||||
</MenuItem>
|
||||
{games &&
|
||||
Object.entries(games).map(([key, game]) => (
|
||||
<MenuItem
|
||||
@@ -426,22 +432,167 @@ const CreateMonitorPage = () => {
|
||||
)}
|
||||
error={!!fieldState.error}
|
||||
>
|
||||
<MenuItem value={15000}>{t("time.fifteenSeconds")}</MenuItem>
|
||||
<MenuItem value={30000}>{t("time.thirtySeconds")}</MenuItem>
|
||||
<MenuItem value={60000}>{t("time.oneMinute")}</MenuItem>
|
||||
<MenuItem value={120000}>{t("time.twoMinutes")}</MenuItem>
|
||||
<MenuItem value={180000}>{t("time.threeMinutes")}</MenuItem>
|
||||
<MenuItem value={240000}>{t("time.fourMinutes")}</MenuItem>
|
||||
<MenuItem value={300000}>{t("time.fiveMinutes")}</MenuItem>
|
||||
<MenuItem value={600000}>{t("time.tenMinutes")}</MenuItem>
|
||||
<MenuItem value={900000}>{t("time.fifteenMinutes")}</MenuItem>
|
||||
<MenuItem value={1800000}>{t("time.thirtyMinutes")}</MenuItem>
|
||||
<MenuItem value={15000}>
|
||||
{t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.value.fifteenSeconds"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value={30000}>
|
||||
{t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.value.thirtySeconds"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value={60000}>
|
||||
{t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.value.oneMinute"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value={120000}>
|
||||
{t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.value.twoMinutes"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value={180000}>
|
||||
{t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.value.threeMinutes"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value={240000}>
|
||||
{t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.value.fourMinutes"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value={300000}>
|
||||
{t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.value.fiveMinutes"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value={600000}>
|
||||
{t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.value.tenMinutes"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value={900000}>
|
||||
{t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.value.fifteenMinutes"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value={1800000}>
|
||||
{t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.value.thirtyMinutes"
|
||||
)}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Global monitoring - only for http and ping types */}
|
||||
{(watchedType === "http" || watchedType === "ping") && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.globalping.title")}
|
||||
subtitle={t("pages.createMonitor.form.globalping.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="globalpingEnabled"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
id="globalpingEnabled"
|
||||
checked={field.value ?? false}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Alert Thresholds - only for hardware type */}
|
||||
{generalSettingsConfig.showSecret && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.thresholds.title")}
|
||||
subtitle={t("pages.createMonitor.form.thresholds.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="cpuAlertThreshold"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SliderWithLabel
|
||||
{...field}
|
||||
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
||||
fieldLabel={t(
|
||||
"pages.createMonitor.form.thresholds.option.cpuThreshold.label"
|
||||
)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
valueLabelDisplay="auto"
|
||||
valueLabelFormat={(value) => `${value}%`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="memoryAlertThreshold"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SliderWithLabel
|
||||
{...field}
|
||||
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
||||
fieldLabel={t(
|
||||
"pages.createMonitor.form.thresholds.option.memoryThreshold.label"
|
||||
)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
valueLabelDisplay="auto"
|
||||
valueLabelFormat={(value) => `${value}%`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="diskAlertThreshold"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SliderWithLabel
|
||||
{...field}
|
||||
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
||||
fieldLabel={t(
|
||||
"pages.createMonitor.form.thresholds.option.diskThreshold.label"
|
||||
)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
valueLabelDisplay="auto"
|
||||
valueLabelFormat={(value) => `${value}%`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="tempAlertThreshold"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SliderWithLabel
|
||||
{...field}
|
||||
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
||||
fieldLabel={t(
|
||||
"pages.createMonitor.form.thresholds.option.tempThreshold.label"
|
||||
)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
valueLabelDisplay="auto"
|
||||
valueLabelFormat={(value) => `${value}°C`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.incidents.title")}
|
||||
subtitle={t("pages.createMonitor.form.incidents.description")}
|
||||
@@ -617,11 +768,21 @@ const CreateMonitorPage = () => {
|
||||
"pages.createMonitor.form.advanced.option.matchMethod.label"
|
||||
)}
|
||||
>
|
||||
<MenuItem value="equal">{t("matchMethodOptions.equal")}</MenuItem>
|
||||
<MenuItem value="include">
|
||||
{t("matchMethodOptions.include")}
|
||||
<MenuItem value="equal">
|
||||
{t(
|
||||
"pages.createMonitor.form.advanced.option.matchMethod.equal"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value="include">
|
||||
{t(
|
||||
"pages.createMonitor.form.advanced.option.matchMethod.include"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value="regex">
|
||||
{t(
|
||||
"pages.createMonitor.form.advanced.option.matchMethod.regex"
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem value="regex">{t("matchMethodOptions.regex")}</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -46,10 +46,9 @@ export const DialogResolution = ({
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title={t("incidentsPage.resolveIncidentDialogTitle")}
|
||||
title={t("pages.incidents.dialog.resolveIncident.title")}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
confirmText={t("incidentsPage.resolveIncidentDialogConfirm")}
|
||||
confirmColor="error"
|
||||
cancelColor="primary"
|
||||
loading={isResolving}
|
||||
@@ -58,8 +57,10 @@ export const DialogResolution = ({
|
||||
>
|
||||
<Box sx={{ mt: theme.spacing(4) }}>
|
||||
<TextField
|
||||
fieldLabel={t("incidentsPage.resolveIncidentDialogCommentLabel")}
|
||||
placeholder={t("incidentsPage.resolveIncidentDialogCommentPlaceholder")}
|
||||
fieldLabel={t("pages.incidents.dialog.resolveIncident.option.comment.label")}
|
||||
placeholder={t(
|
||||
"pages.incidents.dialog.resolveIncident.option.comment.placeholder"
|
||||
)}
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
fullWidth
|
||||
|
||||
@@ -133,7 +133,10 @@ const InfrastructureMonitors = () => {
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<HeaderMonitorsSummary summary={summary} />
|
||||
<HeaderMonitorsSummary
|
||||
summary={summary}
|
||||
showBreached={true}
|
||||
/>
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
justifyContent={isSmall ? "flex-start" : "space-between"}
|
||||
|
||||
@@ -196,13 +196,13 @@ export const MaintenanceWindowTable = ({
|
||||
/>
|
||||
<DialogInput
|
||||
open={deleteDialogOpen}
|
||||
title={t("maintenanceTableActionMenuDialogTitle")}
|
||||
title={t("common.dialogs.delete.title")}
|
||||
content={t("common.dialogs.delete.description")}
|
||||
onCancel={() => {
|
||||
setDeleteDialogOpen(false);
|
||||
setSelectedWindow(null);
|
||||
}}
|
||||
onConfirm={handleDelete}
|
||||
confirmText={t("delete")}
|
||||
loading={deleteLoading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface NotFoundProps {
|
||||
title: string;
|
||||
desc: string;
|
||||
title?: string;
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
const NotFoundPage = ({ title, desc }: NotFoundProps) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ const SettingsAbout = () => {
|
||||
component="h1"
|
||||
variant="h2"
|
||||
>
|
||||
{t("settingsPage.aboutSettings.title")}
|
||||
{t("pages.settings.aboutSettings.title")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
@@ -24,7 +24,7 @@ const SettingsAbout = () => {
|
||||
{t("common.appName")} {__APP_VERSION__}
|
||||
</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
|
||||
{t("settingsPage.aboutSettings.labelDevelopedBy")}
|
||||
{t("pages.settings.aboutSettings.labelDevelopedBy")}
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
|
||||
@@ -8,8 +8,9 @@ import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Dialog from "@/Components/v1/Dialog/index.jsx";
|
||||
import { useState } from "react";
|
||||
import { useDelete, usePost } from "@/Hooks/UseApi";
|
||||
|
||||
const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) => {
|
||||
const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, isLoading }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
// Local state
|
||||
@@ -18,7 +19,8 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { post: postDemoMonitors } = usePost();
|
||||
const { deleteFn: deleteAllMonitorsFn } = useDelete();
|
||||
return (
|
||||
<>
|
||||
<ConfigBox>
|
||||
@@ -27,10 +29,10 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
component="h1"
|
||||
variant="h2"
|
||||
>
|
||||
{t("settingsPage.demoMonitorsSettings.title")}
|
||||
{t("pages.settings.demoMonitorsSettings.title")}
|
||||
</Typography>
|
||||
<Typography sx={HEADER_SX}>
|
||||
{t("settingsPage.demoMonitorsSettings.description")}
|
||||
{t("pages.settings.demoMonitorsSettings.description")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
@@ -38,17 +40,12 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isLoading}
|
||||
onClick={() => {
|
||||
const syntheticEvent = {
|
||||
target: {
|
||||
name: "demo",
|
||||
},
|
||||
};
|
||||
handleChange(syntheticEvent);
|
||||
onClick={async () => {
|
||||
await postDemoMonitors("/monitors/demo", {});
|
||||
}}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("settingsPage.demoMonitorsSettings.buttonAddMonitors")}
|
||||
{t("pages.settings.demoMonitorsSettings.buttonAddMonitors")}
|
||||
</Button>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
@@ -58,10 +55,10 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
component="h1"
|
||||
variant="h2"
|
||||
>
|
||||
{t("settingsPage.systemResetSettings.title")}
|
||||
{t("pages.settings.systemResetSettings.title")}
|
||||
</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsPage.systemResetSettings.description")}
|
||||
{t("pages.settings.systemResetSettings.description")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
@@ -72,22 +69,17 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("settingsPage.systemResetSettings.buttonRemoveAllMonitors")}
|
||||
{t("pages.settings.systemResetSettings.buttonRemoveAllMonitors")}
|
||||
</Button>
|
||||
</Box>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
theme={theme}
|
||||
title={t("settingsPage.systemResetSettings.dialogTitle")}
|
||||
title={t("pages.settings.systemResetSettings.dialogTitle")}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
confirmationButtonLabel={t("settingsPage.systemResetSettings.dialogConfirm")}
|
||||
onConfirm={() => {
|
||||
const syntheticEvent = {
|
||||
target: {
|
||||
name: "deleteMonitors",
|
||||
},
|
||||
};
|
||||
handleChange(syntheticEvent);
|
||||
confirmationButtonLabel={t("pages.settings.systemResetSettings.dialogConfirm")}
|
||||
onConfirm={async () => {
|
||||
await deleteAllMonitorsFn("/monitors/");
|
||||
setIsOpen(false);
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
@@ -99,7 +91,6 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
|
||||
SettingsDemoMonitors.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
handleChange: PropTypes.func,
|
||||
HEADER_SX: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,16 +15,18 @@ import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornment
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
import { useSelector } from "react-redux";
|
||||
import { createToast } from "@/Utils/toastUtils.jsx";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
const SettingsEmail = ({
|
||||
isAdmin,
|
||||
HEADER_SX,
|
||||
handleChange,
|
||||
settingsData,
|
||||
setSettingsData,
|
||||
isEmailPasswordSet,
|
||||
emailPasswordHasBeenReset,
|
||||
setEmailPasswordHasBeenReset,
|
||||
control,
|
||||
defaults,
|
||||
formValues,
|
||||
setValue,
|
||||
}) => {
|
||||
// Setup
|
||||
const { t } = useTranslation();
|
||||
@@ -44,22 +46,13 @@ const SettingsEmail = ({
|
||||
systemEmailIgnoreTLS = false,
|
||||
systemEmailRequireTLS = false,
|
||||
systemEmailRejectUnauthorized = true,
|
||||
} = settingsData?.settings || {};
|
||||
// Local state
|
||||
const [password, setPassword] = useState("");
|
||||
} = formValues || {};
|
||||
|
||||
// Network
|
||||
const { post: sendTestEmailFn, loading: isSending } = usePost();
|
||||
const user = useSelector((state) => state.auth.user);
|
||||
|
||||
// Handlers
|
||||
const handlePasswordChange = (e) => {
|
||||
setPassword(e.target.value);
|
||||
setSettingsData({
|
||||
...settingsData,
|
||||
settings: { ...settingsData.settings, systemEmailPassword: e.target.value },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle sending test email with current form values
|
||||
@@ -70,10 +63,10 @@ const SettingsEmail = ({
|
||||
!systemEmailHost ||
|
||||
!systemEmailPort ||
|
||||
!systemEmailAddress ||
|
||||
!(password || systemEmailPassword)
|
||||
!systemEmailPassword
|
||||
) {
|
||||
createToast({
|
||||
body: t("settingsPage.emailSettings.toastEmailRequiredFieldsError"),
|
||||
body: t("pages.settings.emailSettings.toastEmailRequiredFieldsError"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
@@ -85,7 +78,7 @@ const SettingsEmail = ({
|
||||
systemEmailHost,
|
||||
systemEmailPort,
|
||||
systemEmailAddress,
|
||||
systemEmailPassword: password || systemEmailPassword,
|
||||
systemEmailPassword,
|
||||
systemEmailSecure,
|
||||
systemEmailPool,
|
||||
systemEmailIgnoreTLS,
|
||||
@@ -108,229 +101,267 @@ const SettingsEmail = ({
|
||||
component="h1"
|
||||
variant="h2"
|
||||
>
|
||||
{t("settingsPage.emailSettings.title")}
|
||||
{t("pages.settings.emailSettings.title")}
|
||||
</Typography>
|
||||
<Typography sx={HEADER_SX}>
|
||||
{t("settingsPage.emailSettings.description")}
|
||||
{t("pages.settings.emailSettings.description")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Box>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelHost")}
|
||||
name="systemEmailHost"
|
||||
placeholder="smtp.gmail.com"
|
||||
value={systemEmailHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelPort")}
|
||||
name="systemEmailPort"
|
||||
placeholder="425"
|
||||
type="number"
|
||||
value={systemEmailPort}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelUser")}
|
||||
name="systemEmailUser"
|
||||
placeholder={t("settingsPage.emailSettings.placeholderUser")}
|
||||
value={systemEmailUser}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelAddress")}
|
||||
name="systemEmailAddress"
|
||||
placeholder="uptime@bluewavelabs.ca"
|
||||
value={systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Box>
|
||||
{(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && (
|
||||
<Box>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelPassword")}
|
||||
name="systemEmailPassword"
|
||||
type="password"
|
||||
placeholder="123 456 789 101112"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isEmailPasswordSet === true && emailPasswordHasBeenReset === false && (
|
||||
<Box>
|
||||
<Typography>{t("settingsPage.emailSettings.labelPasswordSet")}</Typography>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setPassword("");
|
||||
setSettingsData({
|
||||
...settingsData,
|
||||
settings: { ...settingsData.settings, systemEmailPassword: "" },
|
||||
});
|
||||
setEmailPasswordHasBeenReset(true);
|
||||
}}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Box>
|
||||
<Controller
|
||||
name="systemEmailHost"
|
||||
control={control}
|
||||
defaultValue={defaults.systemEmailHost}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("pages.settings.emailSettings.labelHost")}
|
||||
placeholder="smtp.gmail.com"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Controller
|
||||
name="systemEmailPort"
|
||||
control={control}
|
||||
defaultValue={defaults.systemEmailPort}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("pages.settings.emailSettings.labelPort")}
|
||||
placeholder="425"
|
||||
type="number"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Controller
|
||||
name="systemEmailUser"
|
||||
control={control}
|
||||
defaultValue={defaults.systemEmailUser}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("pages.settings.emailSettings.labelUser")}
|
||||
placeholder={t("pages.settings.emailSettings.placeholderUser")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Controller
|
||||
name="systemEmailAddress"
|
||||
control={control}
|
||||
defaultValue={defaults.systemEmailAddress}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("pages.settings.emailSettings.labelAddress")}
|
||||
placeholder="uptime@bluewavelabs.ca"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
{(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && (
|
||||
<Box>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelTLSServername")}
|
||||
name="systemEmailTLSServername"
|
||||
placeholder="bluewavelabs.ca"
|
||||
value={systemEmailTLSServername}
|
||||
onChange={handleChange}
|
||||
<Controller
|
||||
name="systemEmailPassword"
|
||||
control={control}
|
||||
defaultValue={defaults.systemEmailPassword}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("pages.settings.emailSettings.labelPassword")}
|
||||
type="password"
|
||||
placeholder="123 456 789 101112"
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isEmailPasswordSet === true && emailPasswordHasBeenReset === false && (
|
||||
<Box>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelConnectionHost")}
|
||||
name="systemEmailConnectionHost"
|
||||
placeholder="bluewavelabs.ca"
|
||||
value={systemEmailConnectionHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Typography>{t("pages.settings.emailSettings.labelPasswordSet")}</Typography>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEmailPasswordHasBeenReset(true);
|
||||
}}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Controller
|
||||
name="systemEmailTLSServername"
|
||||
control={control}
|
||||
defaultValue={defaults.systemEmailTLSServername}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("pages.settings.emailSettings.labelTLSServername")}
|
||||
placeholder="bluewavelabs.ca"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Controller
|
||||
name="systemEmailConnectionHost"
|
||||
control={control}
|
||||
defaultValue={defaults.systemEmailConnectionHost}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("pages.settings.emailSettings.labelConnectionHost")}
|
||||
placeholder="bluewavelabs.ca"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{[
|
||||
[
|
||||
"pages.settings.emailSettings.labelSecure",
|
||||
"systemEmailSecure",
|
||||
systemEmailSecure,
|
||||
],
|
||||
[
|
||||
"pages.settings.emailSettings.labelPool",
|
||||
"systemEmailPool",
|
||||
systemEmailPool,
|
||||
],
|
||||
[
|
||||
"pages.settings.emailSettings.labelIgnoreTLS",
|
||||
"systemEmailIgnoreTLS",
|
||||
systemEmailIgnoreTLS,
|
||||
],
|
||||
[
|
||||
"pages.settings.emailSettings.labelRequireTLS",
|
||||
"systemEmailRequireTLS",
|
||||
systemEmailRequireTLS,
|
||||
],
|
||||
[
|
||||
"pages.settings.emailSettings.labelRejectUnauthorized",
|
||||
"systemEmailRejectUnauthorized",
|
||||
systemEmailRejectUnauthorized,
|
||||
],
|
||||
].map(([labelKey, name, value]) => (
|
||||
<Controller
|
||||
key={name}
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={defaults[name]}
|
||||
render={({ field }) => (
|
||||
<Box
|
||||
key={name}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography>{t(labelKey)}</Typography>
|
||||
|
||||
<Switch
|
||||
{...field}
|
||||
checked={field.value}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<TextLink
|
||||
text={t("pages.settings.emailSettings.descriptionTransport")}
|
||||
linkText={t("pages.settings.emailSettings.linkTransport")}
|
||||
href="https://nodemailer.com/smtp"
|
||||
target="_blank"
|
||||
/>
|
||||
<Box
|
||||
component={"pre"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(4),
|
||||
fontFamily: "monospace",
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{[
|
||||
[
|
||||
"settingsPage.emailSettings.labelSecure",
|
||||
"systemEmailSecure",
|
||||
systemEmailSecure,
|
||||
],
|
||||
[
|
||||
"settingsPage.emailSettings.labelPool",
|
||||
"systemEmailPool",
|
||||
systemEmailPool,
|
||||
],
|
||||
[
|
||||
"settingsPage.emailSettings.labelIgnoreTLS",
|
||||
"systemEmailIgnoreTLS",
|
||||
systemEmailIgnoreTLS,
|
||||
],
|
||||
[
|
||||
"settingsPage.emailSettings.labelRequireTLS",
|
||||
"systemEmailRequireTLS",
|
||||
systemEmailRequireTLS,
|
||||
],
|
||||
[
|
||||
"settingsPage.emailSettings.labelRejectUnauthorized",
|
||||
"systemEmailRejectUnauthorized",
|
||||
systemEmailRejectUnauthorized,
|
||||
],
|
||||
].map(([labelKey, name, value]) => (
|
||||
<Box
|
||||
key={name}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography>{t(labelKey)}</Typography>
|
||||
<Switch
|
||||
name={name}
|
||||
checked={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<TextLink
|
||||
text={t("settingsPage.emailSettings.descriptionTransport")}
|
||||
linkText={t("settingsPage.emailSettings.linkTransport")}
|
||||
href="https://nodemailer.com/smtp"
|
||||
target="_blank"
|
||||
/>
|
||||
<Box
|
||||
component={"pre"}
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{JSON.stringify(
|
||||
{
|
||||
host: systemEmailHost,
|
||||
port: systemEmailPort,
|
||||
secure: systemEmailSecure,
|
||||
auth: {
|
||||
user: systemEmailUser || systemEmailAddress,
|
||||
pass: "<your_password>",
|
||||
},
|
||||
name: systemEmailConnectionHost || "localhost",
|
||||
pool: systemEmailPool,
|
||||
tls: {
|
||||
rejectUnauthorized: systemEmailRejectUnauthorized,
|
||||
ignoreTLS: systemEmailIgnoreTLS,
|
||||
requireTLS: systemEmailRequireTLS,
|
||||
servername: systemEmailTLSServername,
|
||||
},
|
||||
<code>
|
||||
{JSON.stringify(
|
||||
{
|
||||
host: systemEmailHost,
|
||||
port: systemEmailPort,
|
||||
secure: systemEmailSecure,
|
||||
auth: {
|
||||
user: systemEmailUser || systemEmailAddress,
|
||||
pass: "<your_password>",
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</code>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<pre>
|
||||
{JSON.stringify({
|
||||
systemEmailHost,
|
||||
systemEmailAddress,
|
||||
systemEmailPassword,
|
||||
})}
|
||||
</pre>
|
||||
|
||||
<Box>
|
||||
{systemEmailHost &&
|
||||
systemEmailPort &&
|
||||
systemEmailAddress &&
|
||||
systemEmailPassword && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isSending}
|
||||
onClick={handleSendTestEmail}
|
||||
>
|
||||
{t("settingsPage.emailSettings.buttonSendTestEmail")}
|
||||
</Button>
|
||||
name: systemEmailConnectionHost || "localhost",
|
||||
pool: systemEmailPool,
|
||||
tls: {
|
||||
rejectUnauthorized: systemEmailRejectUnauthorized,
|
||||
ignoreTLS: systemEmailIgnoreTLS,
|
||||
requireTLS: systemEmailRequireTLS,
|
||||
servername: systemEmailTLSServername,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</code>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
{systemEmailHost &&
|
||||
systemEmailPort &&
|
||||
systemEmailAddress &&
|
||||
systemEmailPassword && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isSending}
|
||||
onClick={handleSendTestEmail}
|
||||
>
|
||||
{t("pages.settings.emailSettings.buttonSendTestEmail")}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsEmail.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
settingsData: PropTypes.object,
|
||||
setSettingsData: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
HEADER_SX: PropTypes.object,
|
||||
isPasswordSet: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -8,8 +8,9 @@ import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Dialog from "@/Components/v1/Dialog/index.jsx";
|
||||
import { useState } from "react";
|
||||
import { useLazyGet } from "@/Hooks/UseApi";
|
||||
|
||||
const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) => {
|
||||
const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, isLoading }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
// Local state
|
||||
@@ -18,6 +19,29 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
const { get: fetchJson } = useLazyGet();
|
||||
|
||||
const handleExport = async () => {
|
||||
const res = await fetchJson("/monitors/export/json");
|
||||
const json = res?.data ?? [];
|
||||
if (!json || json.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(json, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "monitors.json";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
return;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -38,14 +62,7 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isLoading}
|
||||
onClick={() => {
|
||||
const syntheticEvent = {
|
||||
target: {
|
||||
name: "export",
|
||||
},
|
||||
};
|
||||
handleChange(syntheticEvent);
|
||||
}}
|
||||
onClick={handleExport}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
Export Monitors to JSON
|
||||
@@ -58,7 +75,6 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
|
||||
SettingsDemoMonitors.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
handleChange: PropTypes.func,
|
||||
HEADER_SX: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,39 +7,18 @@ import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
const SettingsGlobalThresholds = ({
|
||||
isAdmin,
|
||||
HEADING_SX,
|
||||
settingsData,
|
||||
setSettingsData,
|
||||
|
||||
control,
|
||||
defaults,
|
||||
}) => {
|
||||
const { t } = useTranslation(); // For language translation
|
||||
const theme = useTheme(); // MUI theme access
|
||||
|
||||
// Handles input change and updates parent state
|
||||
const handleChange = (e, min, max) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
const numValue = parseFloat(value);
|
||||
const isValidNumber =
|
||||
value === "" ||
|
||||
(!isNaN(numValue) && isFinite(numValue) && numValue >= min && numValue <= max);
|
||||
|
||||
if (isValidNumber) {
|
||||
setSettingsData((prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
globalThresholds: {
|
||||
...prev.settings?.globalThresholds,
|
||||
[name]: value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Only render this section for admins
|
||||
if (!isAdmin) return null;
|
||||
|
||||
@@ -51,11 +30,11 @@ const SettingsGlobalThresholds = ({
|
||||
component="h1"
|
||||
variant="h2"
|
||||
>
|
||||
{t("settingsPage.globalThresholds.title", "Global Thresholds")}
|
||||
{t("pages.settings.globalThresholds.title", "Global Thresholds")}
|
||||
</Typography>
|
||||
<Typography sx={HEADING_SX}>
|
||||
{t(
|
||||
"settingsPage.globalThresholds.description",
|
||||
"pages.settings.globalThresholds.description",
|
||||
"Configure global CPU, Memory, Disk, and Temperature thresholds."
|
||||
)}
|
||||
</Typography>
|
||||
@@ -69,14 +48,21 @@ const SettingsGlobalThresholds = ({
|
||||
["Disk Threshold (%)", "disk", 1, 100],
|
||||
["Temperature Threshold (°C)", "temperature", 1, 150],
|
||||
].map(([label, name, min, max]) => (
|
||||
<TextInput
|
||||
<Controller
|
||||
key={name}
|
||||
name={name}
|
||||
label={label}
|
||||
placeholder={`${min} - ${max}`}
|
||||
type="number"
|
||||
value={settingsData?.settings?.globalThresholds?.[name] || ""}
|
||||
onChange={(e) => handleChange(e, min, max)}
|
||||
name={`globalThresholds.${name}`}
|
||||
control={control}
|
||||
defaultValue={defaults.globalThresholds?.[name]}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={label}
|
||||
placeholder={`${min} - ${max}`}
|
||||
type="number"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
@@ -88,8 +74,6 @@ const SettingsGlobalThresholds = ({
|
||||
SettingsGlobalThresholds.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
HEADING_SX: PropTypes.object,
|
||||
settingsData: PropTypes.object,
|
||||
setSettingsData: PropTypes.func,
|
||||
};
|
||||
|
||||
export default SettingsGlobalThresholds;
|
||||
@@ -0,0 +1,197 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Radio from "@mui/material/Radio";
|
||||
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
|
||||
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
|
||||
import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
const SettingsGlobalping = ({
|
||||
isAdmin,
|
||||
HEADING_SX,
|
||||
isGlobalpingKeySet,
|
||||
globalpingKeyHasBeenReset,
|
||||
setGlobalpingKeyHasBeenReset,
|
||||
defaults,
|
||||
control,
|
||||
setValue,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h2"
|
||||
>
|
||||
{t("pages.settings.globalpingSettings.title")}
|
||||
</Typography>
|
||||
<Typography sx={HEADING_SX}>
|
||||
{t("pages.settings.globalpingSettings.description")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
{(isGlobalpingKeySet === false || globalpingKeyHasBeenReset === true) && (
|
||||
<Controller
|
||||
name="globalpingApiKey"
|
||||
control={control}
|
||||
defaultValue={defaults.globalpingApiKey}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("pages.settings.globalpingSettings.labelApiKey")}
|
||||
type={"password"}
|
||||
optionalLabel={t("pages.settings.globalpingSettings.optionalLabel")}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGlobalpingKeySet === true && globalpingKeyHasBeenReset === false && (
|
||||
<Box>
|
||||
<Typography>
|
||||
{t("pages.settings.globalpingSettings.labelApiKeySet")}
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setValue("globalpingApiKey", "");
|
||||
setGlobalpingKeyHasBeenReset(true);
|
||||
}}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{ mb: theme.spacing(2) }}
|
||||
>
|
||||
{t("pages.settings.globalpingSettings.locationsTierTitle")}
|
||||
</Typography>
|
||||
<Typography sx={{ mb: theme.spacing(8), color: theme.palette.text.secondary }}>
|
||||
{t("pages.settings.globalpingSettings.locationsTierDescription")}
|
||||
</Typography>
|
||||
<Controller
|
||||
name="globalpingLocationsTier"
|
||||
control={control}
|
||||
defaultValue={defaults.globalpingLocationsTier}
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
value={String(field.value ?? 6)}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
sx={{ gap: theme.spacing(4) }}
|
||||
>
|
||||
{[
|
||||
{ value: "3", labelKey: "tier3Label", descKey: "tier3Description" },
|
||||
{ value: "6", labelKey: "tier6Label", descKey: "tier6Description" },
|
||||
{ value: "15", labelKey: "tier15Label", descKey: "tier15Description" },
|
||||
].map((tier) => (
|
||||
<FormControlLabel
|
||||
key={tier.value}
|
||||
value={tier.value}
|
||||
control={<Radio sx={{ p: 0, mr: theme.spacing(4) }} />}
|
||||
label={
|
||||
<Box>
|
||||
<Typography
|
||||
fontWeight={500}
|
||||
lineHeight={1.5}
|
||||
>
|
||||
{t(`pages.settings.globalpingSettings.${tier.labelKey}`)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
{t(`pages.settings.globalpingSettings.${tier.descKey}`)}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
sx={{ alignItems: "flex-start", m: 0 }}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: theme.spacing(8),
|
||||
borderRadius: theme.spacing(2),
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
>
|
||||
{t("pages.settings.globalpingSettings.rateLimitsTitle")}
|
||||
</Typography>
|
||||
<Typography sx={{ mb: theme.spacing(4) }}>
|
||||
{t("pages.settings.globalpingSettings.rateLimitsDescription")}
|
||||
</Typography>
|
||||
<Box
|
||||
component="ul"
|
||||
sx={{ m: 0, pl: theme.spacing(8) }}
|
||||
>
|
||||
<Typography
|
||||
component="li"
|
||||
sx={{ mb: theme.spacing(2) }}
|
||||
>
|
||||
{t("pages.settings.globalpingSettings.rateLimitNoAuth")}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="li"
|
||||
sx={{ mb: theme.spacing(2) }}
|
||||
>
|
||||
{t("pages.settings.globalpingSettings.rateLimitAuth")}
|
||||
</Typography>
|
||||
<Typography component="li">
|
||||
{t("pages.settings.globalpingSettings.rateLimitSponsor")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
sx={{ mt: theme.spacing(4), fontStyle: "italic" }}
|
||||
>
|
||||
{t("pages.settings.globalpingSettings.rateLimitsNote")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsGlobalping.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
HEADING_SX: PropTypes.object,
|
||||
isGlobalpingKeySet: PropTypes.bool,
|
||||
globalpingKeyHasBeenReset: PropTypes.bool,
|
||||
setGlobalpingKeyHasBeenReset: PropTypes.func,
|
||||
defaults: PropTypes.object,
|
||||
control: PropTypes.object,
|
||||
setValue: PropTypes.func,
|
||||
};
|
||||
|
||||
export default SettingsGlobalping;
|
||||
@@ -10,15 +10,17 @@ import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
const SettingsPagespeed = ({
|
||||
isAdmin,
|
||||
HEADING_SX,
|
||||
settingsData,
|
||||
setSettingsData,
|
||||
isApiKeySet,
|
||||
apiKeyHasBeenReset,
|
||||
setApiKeyHasBeenReset,
|
||||
defaults,
|
||||
control,
|
||||
setValue,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@@ -26,15 +28,6 @@ const SettingsPagespeed = ({
|
||||
// Local state
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
|
||||
// Handler
|
||||
const handleChange = (e) => {
|
||||
setApiKey(e.target.value);
|
||||
setSettingsData({
|
||||
...settingsData,
|
||||
settings: { ...settingsData.settings, pagespeedApiKey: e.target.value },
|
||||
});
|
||||
};
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
@@ -46,35 +39,40 @@ const SettingsPagespeed = ({
|
||||
component="h1"
|
||||
variant="h2"
|
||||
>
|
||||
{t("settingsPage.pageSpeedSettings.title")}
|
||||
{t("pages.settings.pageSpeedSettings.title")}
|
||||
</Typography>
|
||||
<Typography sx={HEADING_SX}>
|
||||
{t("settingsPage.pageSpeedSettings.description")}
|
||||
{t("pages.settings.pageSpeedSettings.description")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
{(isApiKeySet === false || apiKeyHasBeenReset === true) && (
|
||||
<TextInput
|
||||
<Controller
|
||||
name="pagespeedApiKey"
|
||||
label={t("settingsPage.pageSpeedSettings.labelApiKey")}
|
||||
value={apiKey}
|
||||
type={"password"}
|
||||
onChange={handleChange}
|
||||
optionalLabel="(Optional)"
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
control={control}
|
||||
defaultValue={defaults.pagespeedApiKey}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("pages.settings.pageSpeedSettings.labelApiKey")}
|
||||
type={"password"}
|
||||
optionalLabel="(Optional)"
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isApiKeySet === true && apiKeyHasBeenReset === false && (
|
||||
<Box>
|
||||
<Typography>{t("settingsPage.pageSpeedSettings.labelApiKeySet")}</Typography>
|
||||
<Typography>
|
||||
{t("pages.settings.pageSpeedSettings.labelApiKeySet")}
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setApiKey("");
|
||||
setSettingsData({
|
||||
...settingsData,
|
||||
settings: { ...settingsData.settings, pagespeedApiKey: "" },
|
||||
});
|
||||
setValue("pagespeedApiKey", "");
|
||||
setApiKeyHasBeenReset(true);
|
||||
}}
|
||||
variant="contained"
|
||||
@@ -93,8 +91,6 @@ const SettingsPagespeed = ({
|
||||
SettingsPagespeed.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
HEADING_SX: PropTypes.object,
|
||||
settingsData: PropTypes.object,
|
||||
setSettingsData: PropTypes.func,
|
||||
isApiKeySet: PropTypes.bool,
|
||||
setIsApiKeySet: PropTypes.func,
|
||||
apiKeyHasBeenReset: PropTypes.bool,
|
||||
|
||||
@@ -11,8 +11,9 @@ import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { useDelete } from "@/Hooks/UseApi";
|
||||
|
||||
const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors }) => {
|
||||
const SettingsStats = ({ isAdmin, HEADING_SX, errors }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -20,6 +21,7 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
const { deleteFn: deleteMonitorStatsFn } = useDelete();
|
||||
|
||||
return (
|
||||
<ConfigBox>
|
||||
@@ -29,16 +31,16 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
|
||||
variant="h2"
|
||||
sx={HEADING_SX}
|
||||
>
|
||||
{t("settingsPage.statsSettings.title")}
|
||||
{t("pages.settings.statsSettings.title")}
|
||||
</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsPage.statsSettings.description")}
|
||||
{t("pages.settings.statsSettings.description")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Box>
|
||||
<Typography>
|
||||
{t("settingsPage.statsSettings.clearAllStatsDescription")}
|
||||
{t("pages.settings.statsSettings.clearAllStatsDescription")}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -46,26 +48,22 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("settingsPage.statsSettings.clearAllStatsButton")}
|
||||
{t("pages.settings.statsSettings.clearAllStatsButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
theme={theme}
|
||||
title={t("settingsPage.statsSettings.clearAllStatsDialogTitle")}
|
||||
description={t("settingsPage.statsSettings.clearAllStatsDialogDescription")}
|
||||
title={t("pages.settings.statsSettings.clearAllStatsDialogTitle")}
|
||||
description={t("pages.settings.statsSettings.clearAllStatsDialogDescription")}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
confirmationButtonLabel={t(
|
||||
"settingsPage.statsSettings.clearAllStatsDialogConfirm"
|
||||
"pages.settings.statsSettings.clearAllStatsDialogConfirm"
|
||||
)}
|
||||
onConfirm={() => {
|
||||
const syntheticEvent = {
|
||||
target: {
|
||||
name: "deleteStats",
|
||||
},
|
||||
};
|
||||
handleChange(syntheticEvent);
|
||||
onConfirm={async () => {
|
||||
await deleteMonitorStatsFn("/checks/team");
|
||||
|
||||
setIsOpen(false);
|
||||
}}
|
||||
isLoading={false}
|
||||
@@ -77,9 +75,6 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
|
||||
SettingsStats.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
HEADING_SX: PropTypes.object,
|
||||
handleChange: PropTypes.func,
|
||||
settingsData: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
};
|
||||
|
||||
export default SettingsStats;
|
||||
@@ -7,31 +7,28 @@ import timezones from "@/Utils/timezones.json";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { setTimezone } from "@/Features/UI/uiSlice.js";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
|
||||
const SettingsTimeZone = ({ HEADING_SX }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [rawInput, setRawInput] = useState("");
|
||||
const dispatch = useDispatch();
|
||||
const { timezone } = useSelector((state) => state.ui);
|
||||
|
||||
const selectedTimezone = useMemo(
|
||||
() => timezones.find((tz) => tz._id === timezone) ?? null,
|
||||
[timezone]
|
||||
);
|
||||
|
||||
const handleTimezoneChange = useCallback(
|
||||
(newValue) => {
|
||||
setRawInput("");
|
||||
handleChange({
|
||||
target: {
|
||||
name: "timezone",
|
||||
value: newValue?._id ?? "",
|
||||
},
|
||||
});
|
||||
},
|
||||
[handleChange]
|
||||
);
|
||||
const handleTimezoneChange = (newValue) => {
|
||||
setRawInput("");
|
||||
const newId = newValue?._id ?? "";
|
||||
dispatch(setTimezone({ timezone: newId }));
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigBox>
|
||||
@@ -40,11 +37,11 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
|
||||
component="h1"
|
||||
variant="h2"
|
||||
>
|
||||
{t("settingsPage.timezoneSettings.title")}
|
||||
{t("pages.settings.timezoneSettings.title")}
|
||||
</Typography>
|
||||
<Typography sx={HEADING_SX}>
|
||||
<Typography component="span">
|
||||
{t("settingsPage.timezoneSettings.description")}
|
||||
{t("pages.settings.timezoneSettings.description")}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -68,8 +65,6 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
|
||||
|
||||
SettingsTimeZone.propTypes = {
|
||||
HEADING_SX: PropTypes.object,
|
||||
handleChange: PropTypes.func,
|
||||
timezone: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SettingsTimeZone;
|
||||
@@ -11,11 +11,19 @@ import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { setMode, setLanguage, setChartType } from "@/Features/UI/uiSlice.js";
|
||||
|
||||
const SettingsUI = ({ HEADING_SX, handleChange, mode, language, chartType }) => {
|
||||
const SettingsUI = ({ HEADING_SX }) => {
|
||||
const {
|
||||
mode,
|
||||
language = "en",
|
||||
chartType = "histogram",
|
||||
} = useSelector((state) => state.ui);
|
||||
const { t, i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const languages = Object.keys(i18n.options.resources || {});
|
||||
const dispatch = useDispatch();
|
||||
const v2Theme = mode === "dark" ? darkTheme : lightTheme;
|
||||
return (
|
||||
<ConfigBox>
|
||||
@@ -24,18 +32,18 @@ const SettingsUI = ({ HEADING_SX, handleChange, mode, language, chartType }) =>
|
||||
component="h1"
|
||||
variant="h2"
|
||||
>
|
||||
{t("settingsPage.uiSettings.title")}
|
||||
{t("pages.settings.uiSettings.title")}
|
||||
</Typography>
|
||||
<Typography sx={HEADING_SX}>
|
||||
{t("settingsPage.uiSettings.description")}
|
||||
{t("pages.settings.uiSettings.description")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Select
|
||||
name="mode"
|
||||
label={t("settingsPage.uiSettings.labelTheme")}
|
||||
label={t("pages.settings.uiSettings.labelTheme")}
|
||||
value={mode}
|
||||
onChange={handleChange}
|
||||
onChange={(e) => dispatch(setMode(e.target.value))}
|
||||
items={[
|
||||
{ _id: "light", name: "Light" },
|
||||
{ _id: "dark", name: "Dark" },
|
||||
@@ -43,19 +51,19 @@ const SettingsUI = ({ HEADING_SX, handleChange, mode, language, chartType }) =>
|
||||
></Select>
|
||||
<Select
|
||||
name="language"
|
||||
label={t("settingsPage.uiSettings.labelLanguage")}
|
||||
label={t("pages.settings.uiSettings.labelLanguage")}
|
||||
value={language}
|
||||
onChange={handleChange}
|
||||
onChange={(e) => dispatch(setLanguage(e.target.value))}
|
||||
items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))}
|
||||
></Select>
|
||||
<Select
|
||||
name="chartType"
|
||||
label={t("settingsPage.uiSettings.labelChartType")}
|
||||
label={t("pages.settings.uiSettings.labelChartType")}
|
||||
value={chartType}
|
||||
onChange={handleChange}
|
||||
onChange={(e) => dispatch(setChartType(e.target.value))}
|
||||
items={[
|
||||
{ _id: "histogram", name: t("settingsPage.uiSettings.chartTypeHistogram") },
|
||||
{ _id: "heatmap", name: t("settingsPage.uiSettings.chartTypeHeatmap") },
|
||||
{ _id: "histogram", name: t("pages.settings.uiSettings.chartTypeHistogram") },
|
||||
{ _id: "heatmap", name: t("pages.settings.uiSettings.chartTypeHeatmap") },
|
||||
]}
|
||||
></Select>
|
||||
<ThemeProvider theme={v2Theme}>
|
||||
@@ -68,10 +76,6 @@ const SettingsUI = ({ HEADING_SX, handleChange, mode, language, chartType }) =>
|
||||
|
||||
SettingsUI.propTypes = {
|
||||
HEADING_SX: PropTypes.object,
|
||||
handleChange: PropTypes.func,
|
||||
mode: PropTypes.string,
|
||||
language: PropTypes.string,
|
||||
chartType: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SettingsUI;
|
||||
@@ -3,13 +3,14 @@ import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
|
||||
import Select from "@/Components/v1/Inputs/Select/index.jsx";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => {
|
||||
const SettingsURL = ({ HEADING_SX, control, defaults }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
return (
|
||||
@@ -19,23 +20,30 @@ const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => {
|
||||
component="h1"
|
||||
variant="h2"
|
||||
>
|
||||
{t("settingsPage.urlSettings.title")}
|
||||
{t("pages.settings.urlSettings.title")}
|
||||
</Typography>
|
||||
<Typography sx={HEADING_SX}>
|
||||
{t("settingsPage.urlSettings.description")}
|
||||
{t("pages.settings.urlSettings.description")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Select
|
||||
<Controller
|
||||
name="showURL"
|
||||
label={t("settingsPage.urlSettings.label")}
|
||||
value={showURL === true}
|
||||
onChange={handleChange}
|
||||
items={[
|
||||
{ _id: true, name: t("settingsPage.urlSettings.selectEnabled") },
|
||||
{ _id: false, name: t("settingsPage.urlSettings.selectDisabled") },
|
||||
]}
|
||||
></Select>
|
||||
control={control}
|
||||
defaultValue={defaults.showURL}
|
||||
render={({ field, fieldState }) => (
|
||||
<Select
|
||||
{...field}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
label={t("pages.settings.urlSettings.label")}
|
||||
items={[
|
||||
{ _id: true, name: t("pages.settings.urlSettings.selectEnabled") },
|
||||
{ _id: false, name: t("pages.settings.urlSettings.selectDisabled") },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
);
|
||||
@@ -43,8 +51,6 @@ const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => {
|
||||
|
||||
SettingsURL.propTypes = {
|
||||
HEADING_SX: PropTypes.object,
|
||||
handleChange: PropTypes.func,
|
||||
showURL: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SettingsURL;
|
||||
@@ -5,6 +5,7 @@ import SettingsTimeZone from "./SettingsTimeZone.jsx";
|
||||
import SettingsUI from "./SettingsUI.jsx";
|
||||
import SettingsURL from "./SettingsURL.jsx";
|
||||
import SettingsPagespeed from "./SettingsPagespeed.jsx";
|
||||
import SettingsGlobalping from "./SettingsGlobalping.jsx";
|
||||
import SettingsDemoMonitors from "./SettingsDemoMonitors.jsx";
|
||||
import SettingsAbout from "./SettingsAbout.jsx";
|
||||
import SettingsEmail from "./SettingsEmail.jsx";
|
||||
@@ -12,18 +13,14 @@ import SettingsGlobalThresholds from "./SettingsGlobalThresholds.jsx";
|
||||
import SettingsExport from "./SettingsExport.jsx";
|
||||
import Button from "@mui/material/Button";
|
||||
// Utils
|
||||
import { settingsValidation } from "@/Validation/validation.js";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSettingsForm } from "@/Hooks/useSettingsForm.js";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import {
|
||||
setTimezone,
|
||||
setMode,
|
||||
setLanguage,
|
||||
setShowURL,
|
||||
setChartType,
|
||||
} from "@/Features/UI/uiSlice.js";
|
||||
|
||||
import SettingsStats from "./SettingsStats.jsx";
|
||||
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
@@ -32,32 +29,46 @@ import { useGet, usePost, useDelete, useLazyGet, usePatch } from "@/Hooks/UseApi
|
||||
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
|
||||
|
||||
const Settings = () => {
|
||||
// Redux state
|
||||
const {
|
||||
mode,
|
||||
language = "en",
|
||||
timezone,
|
||||
showURL,
|
||||
chartType = "histogram",
|
||||
} = useSelector((state) => state.ui);
|
||||
|
||||
const { data: fetchedSettings, isLoading: isSettingsLoading } = useGet("/settings");
|
||||
// Local state
|
||||
const [settingsData, setSettingsData] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isApiKeySet, setIsApiKeySet] = useState(settingsData?.pagespeedKeySet ?? false);
|
||||
const [isApiKeySet, setIsApiKeySet] = useState(
|
||||
fetchedSettings?.pagespeedKeySet ?? false
|
||||
);
|
||||
const [apiKeyHasBeenReset, setApiKeyHasBeenReset] = useState(false);
|
||||
const [isGlobalpingKeySet, setIsGlobalpingKeySet] = useState(
|
||||
fetchedSettings?.globalpingKeySet ?? false
|
||||
);
|
||||
const [globalpingKeyHasBeenReset, setGlobalpingKeyHasBeenReset] = useState(false);
|
||||
const [isEmailPasswordSet, setIsEmailPasswordSet] = useState(
|
||||
settingsData?.emailPasswordSet ?? false
|
||||
fetchedSettings?.emailPasswordSet ?? false
|
||||
);
|
||||
const [emailPasswordHasBeenReset, setEmailPasswordHasBeenReset] = useState(false);
|
||||
|
||||
// Network
|
||||
const { data: fetchedSettings, isLoading: isSettingsLoading } = useGet("/settings");
|
||||
|
||||
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
|
||||
const form = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaults,
|
||||
});
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
reset,
|
||||
handleSubmit,
|
||||
clearErrors,
|
||||
trigger,
|
||||
getValues,
|
||||
setValue,
|
||||
} = form;
|
||||
useEffect(() => {
|
||||
reset(defaults, { keepDirtyValues: false });
|
||||
}, [defaults, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchedSettings) {
|
||||
setSettingsData(fetchedSettings);
|
||||
setIsApiKeySet(fetchedSettings?.pagespeedKeySet);
|
||||
setIsGlobalpingKeySet(fetchedSettings?.globalpingKeySet);
|
||||
setIsEmailPasswordSet(fetchedSettings?.emailPasswordSet);
|
||||
}
|
||||
}, [fetchedSettings]);
|
||||
@@ -69,21 +80,61 @@ const Settings = () => {
|
||||
if (response?.data) {
|
||||
const data = response.data;
|
||||
setIsApiKeySet(data.pagespeedKeySet);
|
||||
setIsGlobalpingKeySet(data.globalpingKeySet);
|
||||
setIsEmailPasswordSet(data.emailPasswordSet);
|
||||
if (data.pagespeedKeySet === true) {
|
||||
setApiKeyHasBeenReset(false);
|
||||
}
|
||||
if (data.globalpingKeySet === true) {
|
||||
setGlobalpingKeyHasBeenReset(false);
|
||||
}
|
||||
if (data.emailPasswordSet === true) {
|
||||
setEmailPasswordHasBeenReset(false);
|
||||
}
|
||||
setSettingsData(data);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
// Convert undefined to empty strings for proper unsetting on backend
|
||||
const toSubmit = Object.fromEntries(
|
||||
Object.entries(data).map(([key, value]) => {
|
||||
if (value === undefined) return [key, ""];
|
||||
if (typeof value === "object" && value !== null) {
|
||||
// Handle nested objects like globalThresholds
|
||||
return [
|
||||
key,
|
||||
Object.fromEntries(
|
||||
Object.entries(value).map(([k, v]) => [k, v === undefined ? "" : v])
|
||||
),
|
||||
];
|
||||
}
|
||||
return [key, value];
|
||||
})
|
||||
);
|
||||
|
||||
// Don't send empty sensitive fields when the key is already set and
|
||||
// hasn't been explicitly reset — otherwise the backend $unsets them
|
||||
if (isApiKeySet && !apiKeyHasBeenReset && !toSubmit.pagespeedApiKey) {
|
||||
delete toSubmit.pagespeedApiKey;
|
||||
}
|
||||
if (isGlobalpingKeySet && !globalpingKeyHasBeenReset && !toSubmit.globalpingApiKey) {
|
||||
delete toSubmit.globalpingApiKey;
|
||||
}
|
||||
if (
|
||||
isEmailPasswordSet &&
|
||||
!emailPasswordHasBeenReset &&
|
||||
!toSubmit.systemEmailPassword
|
||||
) {
|
||||
delete toSubmit.systemEmailPassword;
|
||||
}
|
||||
|
||||
saveSettings(toSubmit);
|
||||
};
|
||||
|
||||
// New API hooks to replace monitorHooks
|
||||
const { post: postDemoMonitors, loading: isAddingDemoMonitors } = usePost();
|
||||
const { deleteFn: deleteAllMonitorsFn, loading: isDeletingMonitors } = useDelete();
|
||||
const { deleteFn: deleteMonitorStatsFn, loading: isDeletingMonitorStats } = useDelete();
|
||||
const { loading: isAddingDemoMonitors } = usePost();
|
||||
const { loading: isDeletingMonitors } = useDelete();
|
||||
const { loading: isDeletingMonitorStats } = useDelete();
|
||||
const { get: fetchJson, loading: isFetchingJson } = useLazyGet();
|
||||
|
||||
// Setup
|
||||
@@ -92,184 +143,52 @@ const Settings = () => {
|
||||
const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) };
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
// Handlers
|
||||
const handleChange = async (e) => {
|
||||
const { name, value, checked } = e.target;
|
||||
|
||||
let newValue;
|
||||
if (
|
||||
name === "systemEmailIgnoreTLS" ||
|
||||
name === "systemEmailRequireTLS" ||
|
||||
name === "systemEmailRejectUnauthorized" ||
|
||||
name === "systemEmailSecure" ||
|
||||
name === "systemEmailPool"
|
||||
) {
|
||||
newValue = checked;
|
||||
}
|
||||
|
||||
// Ensure showURL is a proper boolean
|
||||
if (name === "showURL") {
|
||||
newValue = value === true || value === "true";
|
||||
}
|
||||
|
||||
// Build next state early
|
||||
const newSettingsData = {
|
||||
...settingsData,
|
||||
settings: { ...settingsData.settings, [name]: newValue ?? value },
|
||||
};
|
||||
|
||||
if (name === "timezone") {
|
||||
dispatch(setTimezone({ timezone: value }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "mode") {
|
||||
dispatch(setMode(value));
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "language") {
|
||||
dispatch(setLanguage(value));
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "chartType") {
|
||||
dispatch(setChartType(value));
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "deleteStats") {
|
||||
await deleteMonitorStatsFn("/checks/team");
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "demo") {
|
||||
await postDemoMonitors("/monitors/demo", {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "deleteMonitors") {
|
||||
await deleteAllMonitorsFn("/monitors/");
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "export") {
|
||||
const res = await fetchJson("/monitors/export/json");
|
||||
const json = res?.data ?? [];
|
||||
if (!json || json.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(json, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "monitors.json";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
return;
|
||||
}
|
||||
|
||||
setSettingsData(newSettingsData);
|
||||
|
||||
// Update Redux immediately for UI feedback
|
||||
if (name === "showURL") {
|
||||
dispatch(setShowURL(newValue));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// Validate]
|
||||
|
||||
const toSubmit = {
|
||||
checkTTL: settingsData.settings.checkTTL,
|
||||
pagespeedApiKey: settingsData.settings.pagespeedApiKey,
|
||||
language: settingsData.settings.language,
|
||||
timezone: settingsData.settings.timezone,
|
||||
systemEmailHost: settingsData.settings.systemEmailHost,
|
||||
systemEmailPort: settingsData.settings.systemEmailPort,
|
||||
systemEmailSecure: settingsData.settings.systemEmailSecure,
|
||||
systemEmailPool: settingsData.settings.systemEmailPool,
|
||||
systemEmailAddress: settingsData.settings.systemEmailAddress,
|
||||
systemEmailPassword: settingsData.settings.systemEmailPassword,
|
||||
systemEmailUser: settingsData.settings.systemEmailUser,
|
||||
systemEmailConnectionHost: settingsData.settings.systemEmailConnectionHost,
|
||||
systemEmailTLSServername: settingsData.settings.systemEmailTLSServername,
|
||||
systemEmailIgnoreTLS: settingsData.settings.systemEmailIgnoreTLS,
|
||||
systemEmailRequireTLS: settingsData.settings.systemEmailRequireTLS,
|
||||
systemEmailRejectUnauthorized: settingsData.settings.systemEmailRejectUnauthorized,
|
||||
showURL: settingsData.settings.showURL,
|
||||
globalThresholds: settingsData.settings.globalThresholds,
|
||||
};
|
||||
|
||||
const { error } = settingsValidation.validate(toSubmit, {
|
||||
abortEarly: false,
|
||||
});
|
||||
if (!error || error.details.length === 0) {
|
||||
setErrors({});
|
||||
saveSettings(toSubmit);
|
||||
} else {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Typography variant="h1">{t("settingsPage.title")}</Typography>
|
||||
<SettingsTimeZone
|
||||
HEADING_SX={HEADING_SX}
|
||||
handleChange={handleChange}
|
||||
timezone={timezone}
|
||||
/>
|
||||
<SettingsUI
|
||||
HEADING_SX={HEADING_SX}
|
||||
handleChange={handleChange}
|
||||
mode={mode}
|
||||
language={language}
|
||||
chartType={chartType}
|
||||
/>
|
||||
<Typography variant="h1">{t("pages.settings.title")}</Typography>
|
||||
<SettingsTimeZone HEADING_SX={HEADING_SX} />
|
||||
<SettingsUI HEADING_SX={HEADING_SX} />
|
||||
<SettingsPagespeed
|
||||
isAdmin={isAdmin}
|
||||
HEADING_SX={HEADING_SX}
|
||||
settingsData={settingsData}
|
||||
setSettingsData={setSettingsData}
|
||||
isApiKeySet={isApiKeySet}
|
||||
apiKeyHasBeenReset={apiKeyHasBeenReset}
|
||||
setApiKeyHasBeenReset={setApiKeyHasBeenReset}
|
||||
control={control}
|
||||
defaults={defaults}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<SettingsGlobalping
|
||||
isAdmin={isAdmin}
|
||||
HEADING_SX={HEADING_SX}
|
||||
isGlobalpingKeySet={isGlobalpingKeySet}
|
||||
globalpingKeyHasBeenReset={globalpingKeyHasBeenReset}
|
||||
setGlobalpingKeyHasBeenReset={setGlobalpingKeyHasBeenReset}
|
||||
control={control}
|
||||
defaults={defaults}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<SettingsURL
|
||||
HEADING_SX={HEADING_SX}
|
||||
handleChange={handleChange}
|
||||
showURL={showURL}
|
||||
control={control}
|
||||
defaults={defaults}
|
||||
/>
|
||||
<SettingsStats
|
||||
isAdmin={isAdmin}
|
||||
HEADING_SX={HEADING_SX}
|
||||
settingsData={settingsData}
|
||||
handleChange={handleChange}
|
||||
errors={errors}
|
||||
/>
|
||||
<SettingsGlobalThresholds
|
||||
isAdmin={isAdmin}
|
||||
HEADING_SX={HEADING_SX}
|
||||
settingsData={settingsData}
|
||||
setSettingsData={setSettingsData}
|
||||
control={control}
|
||||
defaults={defaults}
|
||||
/>
|
||||
|
||||
<SettingsDemoMonitors
|
||||
isAdmin={isAdmin}
|
||||
HEADER_SX={HEADING_SX}
|
||||
handleChange={handleChange}
|
||||
isLoading={
|
||||
isSettingsLoading || isSaving || isDeletingMonitorStats || isAddingDemoMonitors
|
||||
}
|
||||
@@ -277,18 +196,18 @@ const Settings = () => {
|
||||
<SettingsEmail
|
||||
isAdmin={isAdmin}
|
||||
HEADER_SX={HEADING_SX}
|
||||
handleChange={handleChange}
|
||||
settingsData={settingsData}
|
||||
setSettingsData={setSettingsData}
|
||||
isEmailPasswordSet={isEmailPasswordSet}
|
||||
emailPasswordHasBeenReset={emailPasswordHasBeenReset}
|
||||
setEmailPasswordHasBeenReset={setEmailPasswordHasBeenReset}
|
||||
control={control}
|
||||
defaults={defaults}
|
||||
formValues={watch()}
|
||||
setValue={setValue}
|
||||
/>
|
||||
|
||||
<SettingsExport
|
||||
isAdmin={isAdmin}
|
||||
HEADER_SX={HEADING_SX}
|
||||
handleChange={handleChange}
|
||||
isLoading={isSettingsLoading || isSaving || isFetchingJson}
|
||||
/>
|
||||
<SettingsAbout />
|
||||
@@ -318,13 +237,13 @@ const Settings = () => {
|
||||
loading={
|
||||
isSaving || isDeletingMonitorStats || isSettingsLoading || isDeletingMonitors
|
||||
}
|
||||
disabled={Object.keys(errors).length > 0}
|
||||
disabled={!form.formState.isValid}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ px: theme.spacing(12), py: theme.spacing(8) }}
|
||||
onClick={handleSave}
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
>
|
||||
{t("settingsPage.saveButtonLabel")}
|
||||
{t("pages.settings.saveButtonLabel")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -64,7 +64,7 @@ export const HeaderStatusPageControls = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("publicLink")}
|
||||
{t("components.headerStatusPageControls.publicLink")}
|
||||
</Typography>
|
||||
<Box>
|
||||
<ExternalLink size={14} />
|
||||
|
||||
@@ -96,7 +96,6 @@ const StatusPageView = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography>
|
||||
<StatusBar monitors={monitors} />
|
||||
<MonitorsList
|
||||
statusPage={statusPage}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { BasePage } from "@/Components/v2/design-elements";
|
||||
import { HeaderTimeRange } from "@/Components/v2/common";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import {
|
||||
HistogramStatus,
|
||||
RadialAvgResponse,
|
||||
@@ -13,11 +17,11 @@ import { MonitorStatBoxes } from "@/Components/v2/monitors";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import type { MonitorDetailsResponse } from "@/Types/Monitor";
|
||||
import type { MonitorDetailsResponse, LocationCheckData } from "@/Types/Monitor";
|
||||
import type { ChecksResponse } from "@/Types/Check";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
@@ -38,6 +42,7 @@ const UptimeDetailsPage = () => {
|
||||
const [page, setPage] = useState<number>(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState<number>(5);
|
||||
const [dateRange, setDateRange] = useState<string>("recent");
|
||||
const [selectedLocationTab, setSelectedLocationTab] = useState<number>(0);
|
||||
|
||||
const monitorDetailsUrl = useMemo(() => {
|
||||
if (!monitorId) {
|
||||
@@ -108,6 +113,30 @@ const UptimeDetailsPage = () => {
|
||||
const checks = checksData?.checks ?? [];
|
||||
const checksCount = checksData?.checksCount ?? 0;
|
||||
|
||||
const globalpingLocationsUrl = monitor?.globalpingEnabled
|
||||
? "/monitors/globalping/locations"
|
||||
: null;
|
||||
const { data: globalpingData } = useGet<{
|
||||
locations: { id: string; label: string }[];
|
||||
labels: Record<string, string>;
|
||||
}>(globalpingLocationsUrl);
|
||||
const locationLabels = globalpingData?.labels ?? {};
|
||||
|
||||
const locationChecks = monitorData?.locationChecks;
|
||||
const locationKeys = useMemo(
|
||||
() => (locationChecks ? Object.keys(locationChecks).sort() : []),
|
||||
[locationChecks]
|
||||
);
|
||||
|
||||
// Reset tab index when location keys change to prevent out-of-bounds
|
||||
useEffect(() => {
|
||||
setSelectedLocationTab(0);
|
||||
}, [locationKeys.length]);
|
||||
|
||||
const selectedLocation = locationKeys[selectedLocationTab] ?? null;
|
||||
const selectedLocationData: LocationCheckData | null =
|
||||
selectedLocation && locationChecks ? locationChecks[selectedLocation] : null;
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<HeaderMonitorControls
|
||||
@@ -152,6 +181,63 @@ const UptimeDetailsPage = () => {
|
||||
checks={monitorData?.groupedChecks || []}
|
||||
range={dateRange}
|
||||
/>
|
||||
{locationKeys.length > 0 && (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
>
|
||||
{t("pages.uptime.details.locations.title")}
|
||||
</Typography>
|
||||
<Tabs
|
||||
value={selectedLocationTab}
|
||||
onChange={(_e, newVal) => setSelectedLocationTab(newVal)}
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
>
|
||||
{locationKeys.map((loc) => (
|
||||
<Tab
|
||||
key={loc}
|
||||
label={locationLabels[loc] ?? loc}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
{selectedLocationData && (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
{t("pages.uptime.details.locations.avgResponseTime")}
|
||||
</Typography>
|
||||
<Typography variant="h3">
|
||||
{Math.round(selectedLocationData.avgResponseTime)} ms
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
{t("pages.uptime.details.locations.uptime")}
|
||||
</Typography>
|
||||
<Typography variant="h3">
|
||||
{(selectedLocationData.uptimePercentage * 100).toFixed(1)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<HistogramDetails
|
||||
checks={selectedLocationData.groupedChecks || []}
|
||||
range={dateRange}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<ChecksTable
|
||||
checks={checks}
|
||||
count={checksCount}
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
} from "@/Components/v2/design-elements";
|
||||
import { HeatmapResponseTime, HistogramResponseTime } from "@/Components/v2/common";
|
||||
import { ActionsMenu } from "@/Components/v2/actions-menu";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import { ArrowDown, ArrowUp, Globe } from "lucide-react";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
@@ -167,7 +168,23 @@ export const MonitorTable = ({
|
||||
),
|
||||
|
||||
render: (row) => {
|
||||
return row?.name;
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
{row?.name}
|
||||
{row.globalpingEnabled && (
|
||||
<Tooltip title={t("pages.uptime.table.headers.globalpingEnabled")}>
|
||||
<Globe
|
||||
size={14}
|
||||
aria-label={t("pages.uptime.table.headers.globalpingEnabled")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ThemeProvider } from "@mui/material";
|
||||
import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import type { RootState } from "@/store";
|
||||
import { Navigate, Route, Routes as LibRoutes } from "react-router";
|
||||
import RootLayout from "@/Components/v2/layout/RootLayout";
|
||||
import NotFound from "@/Pages/NotFound";
|
||||
@@ -59,7 +60,7 @@ import {
|
||||
import CreateMonitor from "@/Pages/CreateMonitor";
|
||||
|
||||
const Routes = () => {
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const mode = useSelector((state: RootState) => state.ui.mode);
|
||||
const v2theme = mode === "light" ? lightTheme : darkTheme;
|
||||
return (
|
||||
<LibRoutes>
|
||||
@@ -390,12 +391,11 @@ const Routes = () => {
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/register/:token"
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<AuthRegister superAdminExists={true} />
|
||||
<AuthRegister />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
@@ -18,16 +18,10 @@ export const MonitorStatuses = [
|
||||
"paused",
|
||||
"initializing",
|
||||
"maintenance",
|
||||
"breached",
|
||||
] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export interface MonitorThresholds {
|
||||
usage_cpu?: number;
|
||||
usage_memory?: number;
|
||||
usage_disk?: number;
|
||||
usage_temperature?: number;
|
||||
}
|
||||
|
||||
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
|
||||
|
||||
export interface Monitor {
|
||||
@@ -53,15 +47,18 @@ export interface Monitor {
|
||||
uptimePercentage?: number;
|
||||
notifications: string[];
|
||||
secret?: string;
|
||||
thresholds?: MonitorThresholds;
|
||||
alertThreshold: number;
|
||||
cpuAlertThreshold: number;
|
||||
cpuAlertCounter: number;
|
||||
memoryAlertThreshold: number;
|
||||
memoryAlertCounter: number;
|
||||
diskAlertThreshold: number;
|
||||
diskAlertCounter: number;
|
||||
tempAlertThreshold: number;
|
||||
tempAlertCounter: number;
|
||||
selectedDisks: string[];
|
||||
gameId?: string;
|
||||
group: string | null;
|
||||
globalpingEnabled: boolean;
|
||||
recentChecks: CheckSnapshot[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -76,6 +73,7 @@ export interface MonitorsSummary {
|
||||
pausedMonitors: number;
|
||||
initializingMonitors: number;
|
||||
maintenanceMonitors: number;
|
||||
breachedMonitors: number;
|
||||
}
|
||||
|
||||
export interface MonitorsWithChecksResponse {
|
||||
@@ -88,6 +86,7 @@ export interface MonitorStats {
|
||||
id: string;
|
||||
monitorId: string;
|
||||
avgResponseTime: number;
|
||||
maxResponseTime: number;
|
||||
totalChecks: number;
|
||||
totalUpChecks: number;
|
||||
totalDownChecks: number;
|
||||
@@ -99,6 +98,12 @@ export interface MonitorStats {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface LocationCheckData {
|
||||
groupedChecks: GroupedCheck[];
|
||||
uptimePercentage: number;
|
||||
avgResponseTime: number;
|
||||
}
|
||||
|
||||
export interface MonitorData {
|
||||
monitor: Monitor;
|
||||
groupedChecks: GroupedCheck[];
|
||||
@@ -106,6 +111,7 @@ export interface MonitorData {
|
||||
groupedDownChecks: GroupedCheck[];
|
||||
groupedAvgResponseTime: number;
|
||||
groupedUptimePercentage: number;
|
||||
locationChecks?: Record<string, LocationCheckData>;
|
||||
}
|
||||
|
||||
export interface MonitorDetailsResponse {
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface Settings {
|
||||
systemEmailRequireTLS: boolean;
|
||||
systemEmailRejectUnauthorized: boolean;
|
||||
showURL: boolean;
|
||||
globalpingLocationsTier?: number;
|
||||
singleton: boolean;
|
||||
globalThresholds?: SettingsThresholds;
|
||||
createdAt: string;
|
||||
@@ -29,6 +30,7 @@ export interface Settings {
|
||||
|
||||
export interface AppSettingsResponse {
|
||||
pagespeedKeySet: boolean;
|
||||
globalpingKeySet: boolean;
|
||||
emailPasswordSet: boolean;
|
||||
settings: Settings;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Monitor, MonitorStatus, MonitorType } from "@/Types/Monitor";
|
||||
import type { MonitorStatus, MonitorType } from "@/Types/Monitor";
|
||||
import type { PaletteKey } from "@/Utils/Theme/v2Theme";
|
||||
import type { ValueType } from "@/Components/v2/design-elements/StatusLabel";
|
||||
|
||||
@@ -23,6 +23,9 @@ export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
|
||||
if (status === "down") {
|
||||
return "error";
|
||||
}
|
||||
if (status === "breached") {
|
||||
return "error";
|
||||
}
|
||||
return "warning";
|
||||
};
|
||||
|
||||
@@ -81,40 +84,3 @@ export const formatUrl = (url: string, maxLength: number = 55) => {
|
||||
? `${strippedUrl.slice(0, maxLength)}…`
|
||||
: strippedUrl;
|
||||
};
|
||||
|
||||
export interface IStatusPageHeaderConfig {
|
||||
paletteKey: PaletteKey;
|
||||
message: string;
|
||||
}
|
||||
export const getStatusPageHeaderConfig = (
|
||||
monitors: Monitor[],
|
||||
t: any
|
||||
): IStatusPageHeaderConfig => {
|
||||
if (!monitors || monitors.length === 0) {
|
||||
return { paletteKey: "error", message: "No monitors available" };
|
||||
}
|
||||
|
||||
const allUp = monitors.every((monitor) => monitor.status === "up");
|
||||
const anyDown = monitors.some((monitor) => monitor.status === "down");
|
||||
const allDown = monitors.every((monitor) => monitor.status === "down");
|
||||
|
||||
if (allUp)
|
||||
return {
|
||||
paletteKey: "success",
|
||||
message: t("statusPage.details.statusHeader.allUp"),
|
||||
};
|
||||
if (allDown)
|
||||
return {
|
||||
paletteKey: "error",
|
||||
message: t("statusPage.details.statusHeader.allDown"),
|
||||
};
|
||||
if (anyDown)
|
||||
return {
|
||||
paletteKey: "warning",
|
||||
message: t("statusPage.details.statusHeader.anyDown"),
|
||||
};
|
||||
return {
|
||||
paletteKey: "warning",
|
||||
message: t("statusPage.details.statusHeader.anyDown"),
|
||||
};
|
||||
};
|
||||
@@ -2,11 +2,10 @@ import { z } from "zod";
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "auth.common.inputs.email.errors.empty")
|
||||
.email("auth.common.inputs.email.errors.invalid")
|
||||
.email("Please enter a valid email address")
|
||||
.min(1, "Please enter your email address")
|
||||
.transform((val) => val.toLowerCase().trim()),
|
||||
password: z.string().min(1, "auth.common.inputs.password.errors.empty"),
|
||||
password: z.string().min(1, "Please enter your password"),
|
||||
});
|
||||
|
||||
export type LoginFormData = z.infer<typeof loginSchema>;
|
||||
@@ -31,12 +31,14 @@ const httpSchema = baseSchema.extend({
|
||||
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
|
||||
expectedValue: z.string().optional(),
|
||||
jsonPath: z.string().optional(),
|
||||
globalpingEnabled: z.boolean(),
|
||||
});
|
||||
|
||||
// Ping monitor schema
|
||||
const pingSchema = baseSchema.extend({
|
||||
type: z.literal("ping"),
|
||||
url: z.string().min(1, "Host is required"),
|
||||
globalpingEnabled: z.boolean(),
|
||||
});
|
||||
|
||||
// Port monitor schema
|
||||
|
||||
@@ -2,9 +2,8 @@ import { z } from "zod";
|
||||
|
||||
export const recoverySchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "auth.common.inputs.email.errors.empty")
|
||||
.email("auth.common.inputs.email.errors.invalid")
|
||||
.email("Please enter a valid email address")
|
||||
.min(1, "Please enter your email address")
|
||||
.transform((val) => val.toLowerCase().trim()),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Helper to normalize empty strings to undefined
|
||||
const optionalString = () =>
|
||||
z.preprocess(
|
||||
(val) =>
|
||||
val === "" || val === null || val === undefined ? undefined : String(val).trim(),
|
||||
z.string().optional()
|
||||
);
|
||||
|
||||
// Helper for threshold fields
|
||||
const thresholdField = (min: number, max: number, unit: string) =>
|
||||
z.preprocess(
|
||||
(val) => (val === "" || val === null || val === undefined ? undefined : val),
|
||||
z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(min, `Min ${min}${unit}`)
|
||||
.max(max, `Max ${max}${unit}`)
|
||||
.optional()
|
||||
);
|
||||
|
||||
export const settingsSchema = z.object({
|
||||
systemEmailIgnoreTLS: z.boolean(),
|
||||
systemEmailRequireTLS: z.boolean(),
|
||||
systemEmailRejectUnauthorized: z.boolean(),
|
||||
systemEmailConnectionHost: optionalString(),
|
||||
systemEmailSecure: z.boolean().optional(),
|
||||
systemEmailPool: z.boolean().optional(),
|
||||
showURL: z.boolean().optional(),
|
||||
checkTTL: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1, "Please enter a value")
|
||||
.max(365, "Maximum 365 days"),
|
||||
pagespeedApiKey: optionalString(),
|
||||
globalpingApiKey: optionalString(),
|
||||
globalpingLocationsTier: z.coerce.number().optional(),
|
||||
systemEmailHost: z.preprocess(
|
||||
(val) =>
|
||||
val === "" || val === null || val === undefined ? undefined : String(val).trim(),
|
||||
z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9.-]+$/, "Invalid hostname or IP address")
|
||||
.optional()
|
||||
),
|
||||
systemEmailPort: z.preprocess(
|
||||
(val) => (val === "" || val === null || val === undefined ? undefined : val),
|
||||
z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1, "Port must be at least 1")
|
||||
.max(65535, "Port must be at most 65535")
|
||||
.optional()
|
||||
),
|
||||
systemEmailAddress: z.preprocess((val) => {
|
||||
if (val === "" || val === null || val === undefined) return undefined;
|
||||
return String(val).toLowerCase().trim();
|
||||
}, z.string().email("Please enter a valid email address").optional()),
|
||||
systemEmailUser: optionalString(),
|
||||
systemEmailPassword: optionalString(),
|
||||
systemEmailTLSServername: optionalString(),
|
||||
globalThresholds: z
|
||||
.object({
|
||||
cpu: thresholdField(0, 100, "%"),
|
||||
memory: thresholdField(0, 100, "%"),
|
||||
disk: thresholdField(0, 100, "%"),
|
||||
temperature: thresholdField(0, 150, "°C"),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
||||
|
Before Width: | Height: | Size: 853 B |
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -1,67 +0,0 @@
|
||||
<svg width="768" height="768" viewBox="0 0 768 768" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_14_2)">
|
||||
<mask id="mask0_14_2" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="768" height="768">
|
||||
<path d="M768 0H0V768H768V0Z" fill="url(#paint0_radial_14_2)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_14_2)">
|
||||
<mask id="mask1_14_2" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="768" height="768">
|
||||
<path d="M768 0H0V768H768V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_14_2)">
|
||||
<mask id="mask2_14_2" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="768" height="768">
|
||||
<path d="M768 0H0V768H768V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask2_14_2)">
|
||||
<path d="M0.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M48.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M96.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M144.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M192.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M240.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M288.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M336.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M384.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M432.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M480.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M528.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M576.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M624.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M672.5 0V768" stroke="#b8b9bd"/>
|
||||
<path d="M720.5 0V768" stroke="#b8b9bd"/>
|
||||
</g>
|
||||
<path d="M767.5 0.5H0.5V767.5H767.5V0.5Z" stroke="#b8b9bd"/>
|
||||
<mask id="mask3_14_2" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="768" height="768">
|
||||
<path d="M768 0H0V768H768V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask3_14_2)">
|
||||
<path d="M0 47.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 95.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 143.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 191.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 239.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 287.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 335.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 383.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 431.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 479.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 527.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 575.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 623.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 671.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 719.5H768" stroke="#b8b9bd"/>
|
||||
<path d="M0 767.5H768" stroke="#b8b9bd"/>
|
||||
</g>
|
||||
<path d="M767.5 0.5H0.5V767.5H767.5V0.5Z" stroke="#b8b9bd"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_14_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(384 384) rotate(90) scale(384)">
|
||||
<stop/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_14_2">
|
||||
<rect width="768" height="768" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,36 +0,0 @@
|
||||
<svg width="256" height="170" viewBox="0 0 256 170" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_24_2)">
|
||||
<path d="M248.196 0.390244H7.80489C3.7099 0.390244 0.390259 3.70989 0.390259 7.80487V161.561C0.390259 165.656 3.7099 168.975 7.80489 168.975H248.196C252.291 168.975 255.61 165.656 255.61 161.561V7.80487C255.61 3.70989 252.291 0.390244 248.196 0.390244Z" fill="#151518"/>
|
||||
<path d="M248.196 0.390244H7.80489C3.7099 0.390244 0.390259 3.70989 0.390259 7.80487V161.561C0.390259 165.656 3.7099 168.975 7.80489 168.975H248.196C252.291 168.975 255.61 165.656 255.61 161.561V7.80487C255.61 3.70989 252.291 0.390244 248.196 0.390244Z" stroke="#27272A" stroke-width="0.780488"/>
|
||||
<path d="M234.976 18H21.122C19.3977 18 18 19.3977 18 21.122V69.5122C18 71.2364 19.3977 72.6341 21.122 72.6341H234.976C236.7 72.6341 238.098 71.2364 238.098 69.5122V21.122C238.098 19.3977 236.7 18 234.976 18Z" fill="#27272A" fill-opacity="0.8"/>
|
||||
<path d="M99.9025 78.8293H21.0732C19.3489 78.8293 17.9512 80.227 17.9512 81.9512V85.0732C17.9512 86.7974 19.3489 88.1952 21.0732 88.1952H99.9025C101.627 88.1952 103.024 86.7974 103.024 85.0732V81.9512C103.024 80.227 101.627 78.8293 99.9025 78.8293Z" fill="#27272A" fill-opacity="0.6"/>
|
||||
<path d="M234.927 93.6584H21.0732C19.3489 93.6584 17.9512 95.0561 17.9512 96.7803V99.9023C17.9512 101.627 19.3489 103.024 21.0732 103.024H234.927C236.651 103.024 238.049 101.627 238.049 99.9023V96.7803C238.049 95.0561 236.651 93.6584 234.927 93.6584Z" fill="#27272A" fill-opacity="0.3"/>
|
||||
<path d="M234.927 108.488H21.0732C19.3489 108.488 17.9512 109.886 17.9512 111.61V114.732C17.9512 116.456 19.3489 117.854 21.0732 117.854H234.927C236.651 117.854 238.049 116.456 238.049 114.732V111.61C238.049 109.886 236.651 108.488 234.927 108.488Z" fill="#27272A" fill-opacity="0.3"/>
|
||||
<g filter="url(#filter0_d_24_2)">
|
||||
<mask id="mask0_24_2" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="174" y="129" width="65" height="21">
|
||||
<path d="M234.927 129.561H177.171C175.447 129.561 174.049 130.959 174.049 132.683V146.732C174.049 148.456 175.447 149.854 177.171 149.854H234.927C236.651 149.854 238.049 148.456 238.049 146.732V132.683C238.049 130.959 236.651 129.561 234.927 129.561Z" fill="white"/>
|
||||
<path d="M177.171 130.061H234.927C236.375 130.061 237.549 131.235 237.549 132.683V146.732C237.549 148.18 236.375 149.354 234.927 149.354H177.171C175.723 149.354 174.549 148.18 174.549 146.732V132.683C174.549 131.235 175.723 130.061 177.171 130.061Z" stroke="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_24_2)">
|
||||
<path d="M234.927 129.561H177.171C175.447 129.561 174.049 130.959 174.049 132.683V146.732C174.049 148.456 175.447 149.854 177.171 149.854H234.927C236.651 149.854 238.049 148.456 238.049 146.732V132.683C238.049 130.959 236.651 129.561 234.927 129.561Z" fill="#27272A" fill-opacity="0.2"/>
|
||||
<path d="M177.171 130.061H234.927C236.375 130.061 237.549 131.235 237.549 132.683V146.732C237.549 148.18 236.375 149.354 234.927 149.354H177.171C175.723 149.354 174.549 148.18 174.549 146.732V132.683C174.549 131.235 175.723 130.061 177.171 130.061Z" stroke="#27272A" stroke-opacity="0.8"/>
|
||||
</g>
|
||||
<path d="M234.927 129.951H177.171C175.662 129.951 174.439 131.174 174.439 132.683V146.731C174.439 148.24 175.662 149.463 177.171 149.463H234.927C236.435 149.463 237.658 148.24 237.658 146.731V132.683C237.658 131.174 236.435 129.951 234.927 129.951Z" stroke="#27272A" stroke-opacity="0.8" stroke-width="0.780488"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_24_2" x="172.049" y="128.561" width="68.0002" height="24.2929" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_24_2"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_24_2" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_24_2">
|
||||
<rect width="256" height="170" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,29 +0,0 @@
|
||||
<svg width="256" height="170" viewBox="0 0 256 170" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.390244" y="0.390244" width="255.22" height="168.585" rx="7.41463" fill="white"/>
|
||||
<rect x="0.390244" y="0.390244" width="255.22" height="168.585" rx="7.41463" stroke="#EBEBEB" stroke-width="0.780488"/>
|
||||
<rect x="17.9512" y="16.3901" width="220.098" height="54.6341" rx="3.12195" fill="#EFF2FF"/>
|
||||
<rect x="17.9512" y="78.8293" width="85.0732" height="9.36585" rx="3.12195" fill="#D9D9D9"/>
|
||||
<rect x="17.9512" y="93.6584" width="220.098" height="9.36585" rx="3.12195" fill="#F3F3F3"/>
|
||||
<rect x="17.9512" y="108.488" width="220.098" height="9.36585" rx="3.12195" fill="#F3F3F3"/>
|
||||
<g filter="url(#filter0_d_28_7483)">
|
||||
<g clip-path="url(#clip0_28_7483)">
|
||||
<rect x="174.049" y="129.561" width="64" height="20.2927" rx="3.12195" fill="#E0F2FE"/>
|
||||
</g>
|
||||
<rect x="174.439" y="129.951" width="63.2195" height="19.5122" rx="2.73171" stroke="#D5D9EB" stroke-width="0.780488"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_28_7483" x="172.049" y="128.561" width="68" height="24.2927" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_28_7483"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_28_7483" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_28_7483">
|
||||
<rect x="174.049" y="129.561" width="64" height="20.2927" rx="3.12195" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,19 +0,0 @@
|
||||
<svg width="250" height="176" viewBox="0 0 250 176" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M246 72.5H4C2.067 72.5 0.5 74.067 0.5 76V100C0.5 101.933 2.067 103.5 4 103.5H246C247.933 103.5 249.5 101.933 249.5 100V76C249.5 74.067 247.933 72.5 246 72.5Z" fill="white" stroke="#E1E1E1"/>
|
||||
<path d="M112.5 81H42.5C38.3579 81 35 84.3579 35 88.5C35 92.6421 38.3579 96 42.5 96H112.5C116.642 96 120 92.6421 120 88.5C120 84.3579 116.642 81 112.5 81Z" fill="#F1F1F1"/>
|
||||
<path d="M25 88.5C25 84.3579 21.6421 81 17.5 81C13.3579 81 10 84.3579 10 88.5C10 92.6421 13.3579 96 17.5 96C21.6421 96 25 92.6421 25 88.5Z" fill="#F1F1F1"/>
|
||||
<path d="M219 34H31C28.7909 34 27 35.7909 27 38V56C27 58.2091 28.7909 60 31 60H219C221.209 60 223 58.2091 223 56V38C223 35.7909 221.209 34 219 34Z" fill="white"/>
|
||||
<path d="M219 34.5H31C29.067 34.5 27.5 36.067 27.5 38V56C27.5 57.933 29.067 59.5 31 59.5H219C220.933 59.5 222.5 57.933 222.5 56V38C222.5 36.067 220.933 34.5 219 34.5Z" stroke="#E1E1E1" stroke-opacity="0.71"/>
|
||||
<path d="M156.5 42H60.5C57.4624 42 55 44.4624 55 47.5C55 50.5376 57.4624 53 60.5 53H156.5C159.538 53 162 50.5376 162 47.5C162 44.4624 159.538 42 156.5 42Z" fill="#F4F4F4"/>
|
||||
<path d="M41.5 42H40.5C37.4624 42 35 44.4624 35 47.5C35 50.5376 37.4624 53 40.5 53H41.5C44.5376 53 47 50.5376 47 47.5C47 44.4624 44.5376 42 41.5 42Z" fill="#F4F4F4"/>
|
||||
<path d="M219 116H31C28.7909 116 27 117.791 27 120V138C27 140.209 28.7909 142 31 142H219C221.209 142 223 140.209 223 138V120C223 117.791 221.209 116 219 116Z" fill="white"/>
|
||||
<path d="M219 116.5H31C29.067 116.5 27.5 118.067 27.5 120V138C27.5 139.933 29.067 141.5 31 141.5H219C220.933 141.5 222.5 139.933 222.5 138V120C222.5 118.067 220.933 116.5 219 116.5Z" stroke="#E1E1E1" stroke-opacity="0.71"/>
|
||||
<path d="M156.5 124H60.5C57.4624 124 55 126.462 55 129.5C55 132.538 57.4624 135 60.5 135H156.5C159.538 135 162 132.538 162 129.5C162 126.462 159.538 124 156.5 124Z" fill="#F4F4F4"/>
|
||||
<path d="M41.5 124H40.5C37.4624 124 35 126.462 35 129.5C35 132.538 37.4624 135 40.5 135H41.5C44.5376 135 47 132.538 47 129.5C47 126.462 44.5376 124 41.5 124Z" fill="#F4F4F4"/>
|
||||
<path d="M176 0.5H59C57.067 0.5 55.5 2.067 55.5 4V18C55.5 19.933 57.067 21.5 59 21.5H176C177.933 21.5 179.5 19.933 179.5 18V4C179.5 2.067 177.933 0.5 176 0.5Z" fill="white" stroke="#F6F6F6"/>
|
||||
<path d="M136.5 7H77.5C75.0147 7 73 9.01472 73 11.5C73 13.9853 75.0147 16 77.5 16H136.5C138.985 16 141 13.9853 141 11.5C141 9.01472 138.985 7 136.5 7Z" fill="#F6F6F6"/>
|
||||
<path d="M68 11C68 8.79086 66.2091 7 64 7C61.7909 7 60 8.79086 60 11V12C60 14.2091 61.7909 16 64 16C66.2091 16 68 14.2091 68 12V11Z" fill="#F6F6F6"/>
|
||||
<path d="M176 154.5H59C57.067 154.5 55.5 156.067 55.5 158V172C55.5 173.933 57.067 175.5 59 175.5H176C177.933 175.5 179.5 173.933 179.5 172V158C179.5 156.067 177.933 154.5 176 154.5Z" fill="white" stroke="#F6F6F6"/>
|
||||
<path d="M136.5 161H77.5C75.0147 161 73 163.015 73 165.5C73 167.985 75.0147 170 77.5 170H136.5C138.985 170 141 167.985 141 165.5C141 163.015 138.985 161 136.5 161Z" fill="#F6F6F6"/>
|
||||
<path d="M68 165C68 162.791 66.2091 161 64 161C61.7909 161 60 162.791 60 165V166C60 168.209 61.7909 170 64 170C66.2091 170 68 168.209 68 166V165Z" fill="#F6F6F6"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,19 +0,0 @@
|
||||
<svg width="250" height="176" viewBox="0 0 250 176" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M246 72.5H4C2.067 72.5 0.5 74.067 0.5 76V100C0.5 101.933 2.067 103.5 4 103.5H246C247.933 103.5 249.5 101.933 249.5 100V76C249.5 74.067 247.933 72.5 246 72.5Z" fill="#151518" stroke="#1F1F23"/>
|
||||
<path d="M112.5 81H42.5C38.3579 81 35 84.3579 35 88.5C35 92.6421 38.3579 96 42.5 96H112.5C116.642 96 120 92.6421 120 88.5C120 84.3579 116.642 81 112.5 81Z" fill="#1F1F23"/>
|
||||
<path d="M25 88.5C25 84.3579 21.6421 81 17.5 81C13.3579 81 10 84.3579 10 88.5C10 92.6421 13.3579 96 17.5 96C21.6421 96 25 92.6421 25 88.5Z" fill="#1F1F23"/>
|
||||
<path d="M219 34H31C28.7909 34 27 35.7909 27 38V56C27 58.2091 28.7909 60 31 60H219C221.209 60 223 58.2091 223 56V38C223 35.7909 221.209 34 219 34Z" fill="#151518"/>
|
||||
<path d="M219 34.5H31C29.067 34.5 27.5 36.067 27.5 38V56C27.5 57.933 29.067 59.5 31 59.5H219C220.933 59.5 222.5 57.933 222.5 56V38C222.5 36.067 220.933 34.5 219 34.5Z" stroke="#1F1F23" stroke-opacity="0.71"/>
|
||||
<path d="M156.5 42H60.5C57.4624 42 55 44.4624 55 47.5C55 50.5376 57.4624 53 60.5 53H156.5C159.538 53 162 50.5376 162 47.5C162 44.4624 159.538 42 156.5 42Z" fill="#252529"/>
|
||||
<path d="M41.5 42H40.5C37.4624 42 35 44.4624 35 47.5C35 50.5376 37.4624 53 40.5 53H41.5C44.5376 53 47 50.5376 47 47.5C47 44.4624 44.5376 42 41.5 42Z" fill="#252529"/>
|
||||
<path d="M219 116H31C28.7909 116 27 117.791 27 120V138C27 140.209 28.7909 142 31 142H219C221.209 142 223 140.209 223 138V120C223 117.791 221.209 116 219 116Z" fill="#151518"/>
|
||||
<path d="M219 116.5H31C29.067 116.5 27.5 118.067 27.5 120V138C27.5 139.933 29.067 141.5 31 141.5H219C220.933 141.5 222.5 139.933 222.5 138V120C222.5 118.067 220.933 116.5 219 116.5Z" stroke="#1F1F23" stroke-opacity="0.71"/>
|
||||
<path d="M156.5 124H60.5C57.4624 124 55 126.462 55 129.5C55 132.538 57.4624 135 60.5 135H156.5C159.538 135 162 132.538 162 129.5C162 126.462 159.538 124 156.5 124Z" fill="#252529"/>
|
||||
<path d="M41.5 124H40.5C37.4624 124 35 126.462 35 129.5C35 132.538 37.4624 135 40.5 135H41.5C44.5376 135 47 132.538 47 129.5C47 126.462 44.5376 124 41.5 124Z" fill="#252529"/>
|
||||
<path d="M176 0.5H59C57.067 0.5 55.5 2.067 55.5 4V18C55.5 19.933 57.067 21.5 59 21.5H176C177.933 21.5 179.5 19.933 179.5 18V4C179.5 2.067 177.933 0.5 176 0.5Z" fill="#151518" stroke="#202023"/>
|
||||
<path d="M136.5 7H77.5C75.0147 7 73 9.01472 73 11.5C73 13.9853 75.0147 16 77.5 16H136.5C138.985 16 141 13.9853 141 11.5C141 9.01472 138.985 7 136.5 7Z" fill="#202023"/>
|
||||
<path d="M68 11C68 8.79086 66.2091 7 64 7C61.7909 7 60 8.79086 60 11V12C60 14.2091 61.7909 16 64 16C66.2091 16 68 14.2091 68 12V11Z" fill="#202023"/>
|
||||
<path d="M176 154.5H59C57.067 154.5 55.5 156.067 55.5 158V172C55.5 173.933 57.067 175.5 59 175.5H176C177.933 175.5 179.5 173.933 179.5 172V158C179.5 156.067 177.933 154.5 176 154.5Z" fill="#151518" stroke="#202023"/>
|
||||
<path d="M136.5 161H77.5C75.0147 161 73 163.015 73 165.5C73 167.985 75.0147 170 77.5 170H136.5C138.985 170 141 167.985 141 165.5C141 163.015 138.985 161 136.5 161Z" fill="#202023"/>
|
||||
<path d="M68 165C68 162.791 66.2091 161 64 161C61.7909 161 60 162.791 60 165V166C60 168.209 61.7909 170 64 170C66.2091 170 68 168.209 68 166V165Z" fill="#202023"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,35 +0,0 @@
|
||||
<svg width="104" height="32" viewBox="0 0 104 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.09074 25.1666C4.44267 27.0471 6.17683 28.6205 8.1795 29.7838C10.1822 30.947 12.4081 31.6738 14.7114 31.9165C13.5264 30.1333 11.8039 28.4928 9.65354 27.2438C7.50318 25.9948 5.22591 25.3125 3.09074 25.1666Z" fill="url(#paint0_linear_13278_146906)"/>
|
||||
<path d="M12.543 22.2705C8.40015 19.8636 3.91612 19.2502 0.707662 20.3338C1.0174 21.3575 1.42589 22.3487 1.92738 23.2934C4.71498 23.2288 7.75856 23.9859 10.5906 25.6308C13.4227 27.2757 15.5888 29.5459 16.9143 32C17.9839 31.9672 19.0479 31.8309 20.0913 31.5932C19.4426 28.2698 16.6849 24.6779 12.543 22.2705Z" fill="url(#paint1_linear_13278_146906)"/>
|
||||
<path d="M32.2852 12.5009C31.7585 10.3584 30.8054 8.34403 29.4829 6.57804C28.1604 4.81205 26.4956 3.33067 24.5879 2.22235C22.6802 1.11403 20.5687 0.401504 18.3796 0.12731C16.1904 -0.146884 13.9684 0.02288 11.8463 0.626466C15.3915 1.06033 19.3267 2.39122 23.1859 4.63324C27.0452 6.87525 30.1533 9.63411 32.2852 12.5009Z" fill="url(#paint2_linear_13278_146906)"/>
|
||||
<path d="M27.1271 20.3583C25.3124 17.3446 22.2038 14.4588 18.3743 12.2342C14.5449 10.0095 10.4991 8.7388 6.98531 8.65474C3.894 8.58152 1.57389 9.48017 0.621548 11.1197C0.616124 11.1294 0.608531 11.1386 0.602566 11.1484C0.516877 11.4559 0.443119 11.7639 0.37587 12.0731C1.70568 11.5481 3.24645 11.2558 4.95969 11.2232C8.76959 11.1517 13.0334 12.3703 16.9681 14.6562C20.9027 16.9422 24.0759 20.0438 25.9003 23.3878C26.7182 24.8944 27.2285 26.3777 27.4308 27.7948C27.6662 27.5844 27.8972 27.3669 28.1212 27.1408C28.1272 27.1305 28.131 27.1196 28.1369 27.1088C29.0893 25.4677 28.721 23.0076 27.1271 20.3583Z" fill="url(#paint3_linear_13278_146906)"/>
|
||||
<path d="M15.4609 17.2485C9.59662 13.8416 3.11626 13.3079 -5.33621e-07 15.6855C0.00612042 16.4297 0.0630161 17.1726 0.170292 17.9091C1.08699 17.6312 2.03177 17.4562 2.98718 17.3874C6.46951 17.1254 10.3087 18.0957 13.7927 20.1207C17.2766 22.1458 20.023 25.0018 21.5209 28.1543C21.935 29.018 22.2508 29.9254 22.4624 30.8595C23.1555 30.5878 23.8294 30.2694 24.4794 29.9066C25.0011 26.0213 21.3268 20.656 15.4609 17.2485Z" fill="url(#paint4_linear_13278_146906)"/>
|
||||
<path d="M30.1434 15.3141C28.3082 12.3036 25.1724 9.40969 21.3158 7.17039C17.4593 4.93109 13.3977 3.64033 9.87257 3.53674C7.1853 3.45919 5.10382 4.11053 4.02457 5.34109C8.50588 4.58182 14.4168 5.85794 20.146 9.18625C25.8753 12.5146 29.9135 17.0181 31.4722 21.2868C32.0064 19.7406 31.5416 17.6098 30.1434 15.3141Z" fill="url(#paint5_linear_13278_146906)"/>
|
||||
<path d="M46.6645 9.90909L49.4003 9.90909L49.4003 19.0369C49.4003 19.8807 49.2107 20.6136 48.8314 21.2358C48.4564 21.858 47.9344 22.3374 47.2654 22.674C46.5963 23.0107 45.8186 23.179 44.9323 23.179C44.1439 23.179 43.428 23.0405 42.7845 22.7635C42.1453 22.4822 41.6382 22.0561 41.2632 21.4851C40.8882 20.9098 40.7029 20.1875 40.7071 19.3182L43.4621 19.3182C43.4706 19.6634 43.5409 19.9595 43.673 20.2067C43.8094 20.4496 43.9948 20.6371 44.2291 20.7692C44.4678 20.897 44.749 20.9609 45.0729 20.9609C45.4138 20.9609 45.7014 20.8885 45.9358 20.7436C46.1745 20.5945 46.3556 20.3771 46.4791 20.0916C46.6027 19.8061 46.6645 19.4545 46.6645 19.0369L46.6645 9.90909ZM57.9337 18.8196L57.9337 13.1818L60.6567 13.1818L60.6567 23L58.0423 23L58.0423 21.2166L57.9401 21.2166C57.7185 21.7919 57.3499 22.2543 56.8343 22.6037C56.3229 22.9531 55.6986 23.1278 54.9614 23.1278C54.3051 23.1278 53.7277 22.9787 53.2291 22.6804C52.7306 22.3821 52.3406 21.9581 52.0594 21.4084C51.7824 20.8587 51.6418 20.2003 51.6375 19.4332L51.6375 13.1818L54.3605 13.1818L54.3605 18.9474C54.3648 19.527 54.5203 19.9851 54.8272 20.3217C55.134 20.6584 55.5452 20.8267 56.0608 20.8267C56.3889 20.8267 56.6958 20.7521 56.9813 20.603C57.2668 20.4496 57.4969 20.2237 57.6716 19.9254C57.8506 19.6271 57.9379 19.2585 57.9337 18.8196ZM62.8348 26.6818L62.8348 13.1818L65.5194 13.1818L65.5194 14.831L65.6409 14.831C65.7602 14.5668 65.9328 14.2983 66.1586 14.0256C66.3888 13.7486 66.6871 13.5185 67.0535 13.3352C67.4243 13.1477 67.8845 13.054 68.4342 13.054C69.1501 13.054 69.8106 13.2415 70.4158 13.6165C71.0209 13.9872 71.5045 14.5476 71.8667 15.2976C72.229 16.0433 72.4101 16.9787 72.4101 18.1037C72.4101 19.1989 72.2332 20.1236 71.8795 20.8778C71.5301 21.6278 71.0528 22.1967 70.4477 22.5845C69.8469 22.968 69.1736 23.1598 68.4278 23.1598C67.8994 23.1598 67.4498 23.0724 67.0791 22.8977C66.7126 22.723 66.4122 22.5036 66.1778 22.2393C65.9435 21.9709 65.7645 21.7003 65.6409 21.4276L65.5578 21.4276L65.5578 26.6818L62.8348 26.6818ZM65.5003 18.0909C65.5003 18.6747 65.5812 19.1839 65.7432 19.6186C65.9051 20.0533 66.1395 20.392 66.4463 20.6349C66.7531 20.8736 67.126 20.9929 67.5649 20.9929C68.0081 20.9929 68.3831 20.8714 68.6899 20.6286C68.9967 20.3814 69.229 20.0405 69.3866 19.6058C69.5486 19.1669 69.6295 18.6619 69.6295 18.0909C69.6295 17.5241 69.5507 17.0256 69.393 16.5952C69.2354 16.1648 69.0031 15.8281 68.6963 15.5852C68.3895 15.3423 68.0123 15.2209 67.5649 15.2209C67.1217 15.2209 66.7467 15.3381 66.4399 15.5724C66.1373 15.8068 65.9051 16.1392 65.7432 16.5696C65.5812 17 65.5003 17.5071 65.5003 18.0909ZM74.2254 23L74.2254 13.1818L76.9484 13.1818L76.9484 23L74.2254 23ZM75.5933 11.9162C75.1885 11.9162 74.8412 11.782 74.5514 11.5135C74.2659 11.2408 74.1231 10.9148 74.1231 10.5355C74.1231 10.1605 74.2659 9.83878 74.5514 9.57031C74.8412 9.29759 75.1885 9.16122 75.5933 9.16122C75.9981 9.16122 76.3433 9.29759 76.6288 9.57031C76.9186 9.83878 77.0635 10.1605 77.0635 10.5355C77.0635 10.9148 76.9186 11.2408 76.6288 11.5135C76.3433 11.782 75.9981 11.9162 75.5933 11.9162ZM84.352 13.1818L84.352 15.2273L78.4394 15.2273L78.4394 13.1818L84.352 13.1818ZM79.7817 10.8295L82.5047 10.8295L82.5047 19.983C82.5047 20.2344 82.5431 20.4304 82.6198 20.571C82.6965 20.7074 82.803 20.8033 82.9394 20.8587C83.08 20.9141 83.2419 20.9418 83.4252 20.9418C83.553 20.9418 83.6808 20.9311 83.8087 20.9098C83.9365 20.8842 84.0345 20.8651 84.1027 20.8523L84.531 22.8786C84.3946 22.9212 84.2029 22.9702 83.9557 23.0256C83.7085 23.0852 83.4081 23.1215 83.0544 23.1342C82.3982 23.1598 81.8229 23.0724 81.3286 22.8722C80.8385 22.6719 80.4571 22.3608 80.1844 21.9389C79.9117 21.517 79.7774 20.9844 79.7817 20.3409L79.7817 10.8295ZM90.4948 23.1918C89.4848 23.1918 88.6155 22.9872 87.8868 22.5781C87.1624 22.1648 86.6041 21.581 86.2121 20.8267C85.82 20.0682 85.624 19.1712 85.624 18.1357C85.624 17.1257 85.82 16.2393 86.2121 15.4766C86.6041 14.7138 87.156 14.1193 87.8676 13.6932C88.5835 13.267 89.423 13.054 90.3861 13.054C91.0338 13.054 91.6368 13.1584 92.195 13.3672C92.7575 13.5717 93.2476 13.8807 93.6652 14.294C94.0871 14.7074 94.4152 15.2273 94.6496 15.8537C94.884 16.4759 95.0012 17.2045 95.0012 18.0398L95.0012 18.7876L86.7107 18.7876L86.7107 17.1001L92.4379 17.1001C92.4379 16.7081 92.3527 16.3608 92.1823 16.0582C92.0118 15.7557 91.7753 15.5192 91.4727 15.3487C91.1745 15.174 90.8272 15.0866 90.4308 15.0866C90.0175 15.0866 89.651 15.1825 89.3314 15.3743C89.0161 15.5618 88.7689 15.8153 88.5899 16.1349C88.411 16.4503 88.3193 16.8018 88.3151 17.1896L88.3151 18.794C88.3151 19.2798 88.4046 19.6996 88.5835 20.0533C88.7668 20.407 89.0246 20.6797 89.357 20.8715C89.6894 21.0632 90.0835 21.1591 90.5395 21.1591C90.8421 21.1591 91.1191 21.1165 91.3705 21.0313C91.6219 20.946 91.8371 20.8182 92.0161 20.6477C92.195 20.4773 92.3314 20.2685 92.4252 20.0213L94.9436 20.1875C94.8158 20.7926 94.5537 21.321 94.1574 21.7727C93.7654 22.2202 93.2583 22.5696 92.6361 22.821C92.0182 23.0682 91.3044 23.1918 90.4948 23.1918ZM96.7781 23L96.7781 13.1818L99.4181 13.1818L99.4181Line truncated
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_13278_146906" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#C7F284"/>
|
||||
<stop offset="1" stop-color="#00BEF0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_13278_146906" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#C7F284"/>
|
||||
<stop offset="1" stop-color="#00BEF0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_13278_146906" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#C7F284"/>
|
||||
<stop offset="1" stop-color="#00BEF0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_13278_146906" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#C7F284"/>
|
||||
<stop offset="1" stop-color="#00BEF0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_13278_146906" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#C7F284"/>
|
||||
<stop offset="1" stop-color="#00BEF0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_13278_146906" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#C7F284"/>
|
||||
<stop offset="1" stop-color="#00BEF0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.2 KiB |
@@ -1,35 +0,0 @@
|
||||
<svg width="104" height="32" viewBox="0 0 104 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.09074 25.1666C4.44267 27.0471 6.17683 28.6205 8.1795 29.7838C10.1822 30.947 12.4081 31.6738 14.7114 31.9165C13.5264 30.1333 11.8039 28.4928 9.65354 27.2438C7.50318 25.9948 5.22591 25.3125 3.09074 25.1666Z" fill="url(#paint0_linear_13278_146916)"/>
|
||||
<path d="M12.543 22.2705C8.40015 19.8636 3.91612 19.2502 0.707662 20.3338C1.0174 21.3575 1.42589 22.3487 1.92738 23.2934C4.71498 23.2288 7.75856 23.9859 10.5906 25.6308C13.4227 27.2757 15.5888 29.5459 16.9143 32C17.9839 31.9672 19.0479 31.8309 20.0913 31.5932C19.4426 28.2698 16.6849 24.6779 12.543 22.2705Z" fill="url(#paint1_linear_13278_146916)"/>
|
||||
<path d="M32.2852 12.5009C31.7585 10.3584 30.8054 8.34403 29.4829 6.57804C28.1604 4.81205 26.4956 3.33067 24.5879 2.22235C22.6802 1.11403 20.5687 0.401504 18.3796 0.12731C16.1904 -0.146884 13.9684 0.02288 11.8463 0.626466C15.3915 1.06033 19.3267 2.39122 23.1859 4.63324C27.0452 6.87525 30.1533 9.63411 32.2852 12.5009Z" fill="url(#paint2_linear_13278_146916)"/>
|
||||
<path d="M27.1271 20.3583C25.3124 17.3446 22.2038 14.4588 18.3743 12.2342C14.5449 10.0095 10.4991 8.7388 6.98531 8.65474C3.894 8.58152 1.57389 9.48017 0.621548 11.1197C0.616124 11.1294 0.608531 11.1386 0.602566 11.1484C0.516877 11.4559 0.443119 11.7639 0.37587 12.0731C1.70568 11.5481 3.24645 11.2558 4.95969 11.2232C8.76959 11.1517 13.0334 12.3703 16.9681 14.6562C20.9027 16.9422 24.0759 20.0438 25.9003 23.3878C26.7182 24.8944 27.2285 26.3777 27.4308 27.7948C27.6662 27.5844 27.8972 27.3669 28.1212 27.1408C28.1272 27.1305 28.131 27.1196 28.1369 27.1088C29.0893 25.4677 28.721 23.0076 27.1271 20.3583Z" fill="url(#paint3_linear_13278_146916)"/>
|
||||
<path d="M15.4609 17.2485C9.59662 13.8416 3.11626 13.3079 -5.33621e-07 15.6855C0.00612042 16.4297 0.0630161 17.1726 0.170292 17.9091C1.08699 17.6312 2.03177 17.4562 2.98718 17.3874C6.46951 17.1254 10.3087 18.0957 13.7927 20.1207C17.2766 22.1458 20.023 25.0018 21.5209 28.1543C21.935 29.018 22.2508 29.9254 22.4624 30.8595C23.1555 30.5878 23.8294 30.2694 24.4794 29.9066C25.0011 26.0213 21.3268 20.656 15.4609 17.2485Z" fill="url(#paint4_linear_13278_146916)"/>
|
||||
<path d="M30.1434 15.3141C28.3082 12.3036 25.1724 9.40969 21.3158 7.17039C17.4593 4.93109 13.3977 3.64033 9.87257 3.53674C7.1853 3.45919 5.10382 4.11053 4.02457 5.34109C8.50588 4.58182 14.4168 5.85794 20.146 9.18625C25.8753 12.5146 29.9135 17.0181 31.4722 21.2868C32.0064 19.7406 31.5416 17.6098 30.1434 15.3141Z" fill="url(#paint5_linear_13278_146916)"/>
|
||||
<path d="M46.6645 9.90909L49.4003 9.90909L49.4003 19.0369C49.4003 19.8807 49.2106 20.6136 48.8314 21.2358C48.4564 21.858 47.9344 22.3374 47.2653 22.674C46.5963 23.0107 45.8186 23.179 44.9322 23.179C44.1439 23.179 43.428 23.0405 42.7845 22.7635C42.1453 22.4822 41.6382 22.0561 41.2632 21.4851C40.8882 20.9098 40.7028 20.1875 40.7071 19.3182L43.4621 19.3182C43.4706 19.6634 43.5409 19.9595 43.673 20.2067C43.8094 20.4496 43.9947 20.6371 44.2291 20.7692C44.4677 20.897 44.749 20.9609 45.0729 20.9609C45.4138 20.9609 45.7014 20.8885 45.9358 20.7436C46.1744 20.5945 46.3555 20.3771 46.4791 20.0916C46.6027 19.8061 46.6645 19.4545 46.6645 19.0369L46.6645 9.90909ZM57.9337 18.8196L57.9337 13.1818L60.6567 13.1818L60.6567 23L58.0423 23L58.0423 21.2166L57.94 21.2166C57.7185 21.7919 57.3498 22.2543 56.8342 22.6037C56.3229 22.9531 55.6986 23.1278 54.9614 23.1278C54.3051 23.1278 53.7277 22.9787 53.2291 22.6804C52.7305 22.3821 52.3406 21.9581 52.0594 21.4084C51.7824 20.8587 51.6418 20.2003 51.6375 19.4332L51.6375 13.1818L54.3605 13.1818L54.3605 18.9474C54.3648 19.527 54.5203 19.9851 54.8271 20.3217C55.1339 20.6584 55.5452 20.8267 56.0608 20.8267C56.3889 20.8267 56.6957 20.7521 56.9812 20.603C57.2668 20.4496 57.4969 20.2237 57.6716 19.9254C57.8506 19.6271 57.9379 19.2585 57.9337 18.8196ZM62.8348 26.6818L62.8348 13.1818L65.5194 13.1818L65.5194 14.831L65.6409 14.831C65.7602 14.5668 65.9328 14.2983 66.1586 14.0256C66.3887 13.7486 66.687 13.5185 67.0535 13.3352C67.4242 13.1477 67.8845 13.054 68.4342 13.054C69.1501 13.054 69.8106 13.2415 70.4157 13.6165C71.0208 13.9872 71.5045 14.5476 71.8667 15.2976C72.2289 16.0433 72.41 16.9787 72.41 18.1037C72.41 19.1989 72.2332 20.1236 71.8795 20.8778C71.5301 21.6278 71.0528 22.1967 70.4477 22.5845C69.8468 22.968 69.1735 23.1598 68.4278 23.1598C67.8994 23.1598 67.4498 23.0724 67.0791 22.8977C66.7126 22.723 66.4122 22.5036 66.1778 22.2393C65.9434 21.9709 65.7644 21.7003 65.6409 21.4276L65.5578 21.4276L65.5578 26.6818L62.8348 26.6818ZM65.5002 18.0909C65.5002 18.6747 65.5812 19.1839 65.7431 19.6186C65.9051 20.0533 66.1394 20.392 66.4463 20.6349C66.7531 20.8736 67.1259 20.9929 67.5649 20.9929C68.0081 20.9929 68.3831 20.8714 68.6899 20.6286C68.9967 20.3814 69.2289 20.0405 69.3866 19.6058C69.5485 19.1669 69.6295 18.6619 69.6295 18.0909C69.6295 17.5241 69.5507 17.0256 69.393 16.5952C69.2353 16.1648 69.0031 15.8281 68.6963 15.5852C68.3894 15.3423 68.0123 15.2209 67.5649 15.2209C67.1217 15.2209 66.7467 15.3381 66.4399 15.5724C66.1373 15.8068 65.9051 16.1392 65.7431 16.5696C65.5812 17 65.5002 17.5071 65.5002 18.0909ZM74.2254 23L74.2254 13.1818L76.9484 13.1818L76.9484 23L74.2254 23ZM75.5933 11.9162C75.1884 11.9162 74.8411 11.782 74.5514 11.5135C74.2659 11.2408 74.1231 10.9148 74.1231 10.5355C74.1231 10.1605 74.2659 9.83878 74.5514 9.57031C74.8411 9.29759 75.1884 9.16122 75.5933 9.16122C75.9981 9.16122 76.3433 9.29759 76.6288 9.57031C76.9186 9.83878 77.0634 10.1605 77.0634 10.5355C77.0634 10.9148 76.9186 11.2408 76.6288 11.5135C76.3433 11.782 75.9981 11.9162 75.5933 11.9162ZM84.352 13.1818L84.352 15.2273L78.4393 15.2273L78.4393 13.1818L84.352 13.1818ZM79.7817 10.8295L82.5047 10.8295L82.5047 19.983C82.5047 20.2344 82.543 20.4304 82.6197 20.571C82.6964 20.7074 82.803 20.8033 82.9393 20.8587C83.08 20.9141 83.2419 20.9418 83.4251 20.9418C83.553 20.9418 83.6808 20.9311 83.8087 20.9098C83.9365 20.8842 84.0345 20.8651 84.1027 20.8523L84.531 22.8786C84.3946 22.9212 84.2028 22.9702 83.9557 23.0256C83.7085 23.0852 83.4081 23.1215 83.0544 23.1342C82.3981 23.1598 81.8229 23.0724 81.3285 22.8722C80.8385 22.6719 80.4571 22.3608 80.1844 21.9389C79.9116 21.517 79.7774 20.9844 79.7817 20.3409L79.7817 10.8295ZM90.4947 23.1918C89.4848 23.1918 88.6155 22.9872 87.8868 22.5781C87.1623 22.1648 86.6041 21.581 86.2121 20.8267C85.82 20.0682 85.624 19.1712 85.624 18.1357C85.624 17.1257 85.82 16.2393 86.2121 15.4766C86.6041 14.7138 87.156 14.1193 87.8676 13.6932C88.5835 13.267 89.423 13.054 90.3861 13.054C91.0338 13.054 91.6368 13.1584 92.195 13.3672C92.7575 13.5717 93.2476 13.8807 93.6652 14.294C94.0871 14.7074 94.4152 15.2273 94.6496 15.8537C94.8839 16.4759 95.0011 17.2045 95.0011 18.0398L95.0011 18.7876L86.7106 18.7876L86.7106 17.1001L92.4379 17.1001C92.4379 16.7081 92.3527 16.3608 92.1822 16.0582C92.0118 15.7557 91.7753 15.5192 91.4727 15.3487C91.1744 15.174 90.8271 15.0866 90.4308 15.0866C90.0175 15.0866 89.651 15.1825 89.3314 15.3743C89.016 15.5618 88.7689 15.8153 88.5899 16.1349C88.4109 16.4503 88.3193 16.8018 88.315 17.1896L88.315 18.794C88.315 19.2798 88.4045 19.6996 88.5835 20.0533C88.7668 20.407 89.0246 20.6797 89.3569 20.8715C89.6893 21.0632 90.0835 21.1591 90.5395 21.1591C90.842 21.1591 91.119 21.1165 91.3704 21.0313C91.6219 20.946 91.8371 20.8182 92.016 20.6477C92.195 20.4773 92.3314 20.2685 92.4251 20.0213L94.9436 20.1875C94.8158 20.7926 94.5537 21.321 94.1574 21.7727C93.7653 22.2202 93.2582 22.5696 92.6361 22.821C92.0182 23.0682 91.3044 23.1918 90.4947 23.1918ZM96.7781 23L96.7781 13.1818L99.418 13.1818L99.418 14.8949L99.Line truncated
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_13278_146916" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#A4D756"/>
|
||||
<stop offset="1" stop-color="#00B6E7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_13278_146916" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#A4D756"/>
|
||||
<stop offset="1" stop-color="#00B6E7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_13278_146916" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#A4D756"/>
|
||||
<stop offset="1" stop-color="#00B6E7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_13278_146916" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#A4D756"/>
|
||||
<stop offset="1" stop-color="#00B6E7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_13278_146916" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#A4D756"/>
|
||||
<stop offset="1" stop-color="#00B6E7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_13278_146916" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#A4D756"/>
|
||||
<stop offset="1" stop-color="#00B6E7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.2 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="100" viewBox="0 0 300 100">
|
||||
<rect width="100%" height="100%" fill="#DDDDDD" />
|
||||
<path fill="#999999"
|
||||
d="m76.795 37.635-7.38 12.4v7.84h-3.75v-7.84l-7.38-12.4h3.32q.49 0 .78.23.29.24.48.61l3.7 6.76q.32.6.57 1.13.24.53.44 1.07.18-.54.42-1.07.23-.53.54-1.13l3.68-6.76q.16-.31.47-.58.3-.26.78-.26zm7.88 5.65q1.6 0 2.91.52t2.24 1.47 1.43 2.32q.51 1.38.51 3.07 0 1.71-.51 3.08-.5 1.37-1.43 2.34-.93.96-2.24 1.48t-2.91.52q-1.61 0-2.92-.52-1.32-.52-2.25-1.48-.93-.97-1.44-2.34t-.51-3.08q0-1.69.51-3.07.51-1.37 1.44-2.32t2.25-1.47q1.31-.52 2.92-.52m0 12.14q1.8 0 2.66-1.2.86-1.21.86-3.53 0-2.33-.86-3.54-.86-1.22-2.66-1.22-1.82 0-2.69 1.22-.88 1.23-.88 3.54t.88 3.52q.87 1.21 2.69 1.21m18.65-11.91h3.46v14.36h-2.11q-.69 0-.87-.63l-.24-1.15q-.88.9-1.95 1.45-1.06.55-2.5.55-1.18 0-2.08-.4-.9-.39-1.52-1.12t-.93-1.73q-.32-1-.32-2.21v-9.12h3.46v9.12q0 1.32.61 2.04t1.83.72q.89 0 1.68-.4.78-.4 1.48-1.1zm9.93.86.21 1.63q.67-1.29 1.59-2.02.93-.74 2.19-.74.99 0 1.59.43l-.22 2.59q-.07.26-.2.36-.14.11-.36.11-.21 0-.62-.07-.42-.07-.81-.07-.57 0-1.02.16-.45.17-.8.49-.36.31-.63.76-.28.45-.52 1.02v8.85h-3.45v-14.36h2.03q.53 0 .74.18.21.19.28.68m14.24-7.3h3.45v20.8h-3.45zm13.27 6.21q1.59 0 2.9.52t2.24 1.47 1.44 2.32q.5 1.38.5 3.07 0 1.71-.5 3.08-.51 1.37-1.44 2.34-.93.96-2.24 1.48t-2.9.52q-1.61 0-2.93-.52-1.31-.52-2.24-1.48-.94-.97-1.45-2.34t-.51-3.08q0-1.69.51-3.07.51-1.37 1.45-2.32.93-.95 2.24-1.47 1.32-.52 2.93-.52m0 12.14q1.79 0 2.65-1.2.86-1.21.86-3.53 0-2.33-.86-3.54-.86-1.22-2.65-1.22-1.82 0-2.7 1.22-.87 1.23-.87 3.54t.87 3.52q.88 1.21 2.7 1.21m14.98-5.08q.64 0 1.12-.18.47-.17.79-.48.31-.31.48-.74.16-.44.16-.95 0-1.07-.64-1.69t-1.91-.62q-1.28 0-1.91.62-.64.62-.64 1.69 0 .5.16.93.16.44.48.75.31.32.8.49.48.18 1.11.18m3.9 8.17q0-.42-.25-.68-.25-.27-.68-.42-.44-.14-1.02-.21t-1.23-.11l-1.34-.06q-.7-.03-1.36-.11-.57.32-.93.75-.35.44-.35 1.01 0 .38.19.71.18.33.6.57.41.23 1.07.37.66.13 1.61.13.96 0 1.66-.15.7-.14 1.16-.4.45-.26.66-.62t.21-.78m-.68-14.51h4.13v1.28q0 .62-.74.76l-1.29.24q.29.74.29 1.62 0 1.07-.42 1.93-.43.86-1.19 1.46-.75.6-1.78.93t-2.22.33q-.42 0-.81-.04-.4-.04-.77-.11-.68.4-.68.91 0 .43.4.63.4.21 1.06.29t1.5.1q.84.03 1.72.1t1.72.24q.84.18 1.5.55.66.38 1.06 1.03t.4 1.68q0 .95-.47 1.84-.47.9-1.36 1.6t-2.18 1.13q-1.3.42-2.95.42-1.63 0-2.83-.31-1.2-.32-2-.84-.8-.53-1.19-1.21-.39-.69-.39-1.43 0-1.01.61-1.69.6-.68 1.67-1.08-.58-.3-.91-.79-.34-.49-.34-1.28 0-.33.12-.67t.35-.68q.23-.33.58-.64.35-.3.83-.53-1.09-.59-1.72-1.57-.62-.98-.62-2.3 0-1.06.43-1.92.42-.86 1.19-1.47.76-.61 1.8-.93 1.05-.33 2.28-.33.92 0 1.73.19.82.19 1.49.56m12.67-.72q1.59 0 2.9.52t2.24 1.47 1.44 2.32q.5 1.38.5 3.07 0 1.71-.5 3.08-.51 1.37-1.44 2.34-.93.96-2.24 1.48t-2.9.52q-1.61 0-2.93-.52-1.31-.52-2.24-1.48-.94-.97-1.45-2.34t-.51-3.08q0-1.69.51-3.07.51-1.37 1.45-2.32.93-.95 2.24-1.47 1.32-.52 2.93-.52m0 12.14q1.79 0 2.65-1.2.86-1.21.86-3.53 0-2.33-.86-3.54-.86-1.22-2.65-1.22-1.82 0-2.7 1.22-.87 1.23-.87 3.54t.87 3.52q.88 1.21 2.7 1.21m19.99-18.35v7.98q.84-.79 1.85-1.28t2.36-.49q1.18 0 2.09.4t1.52 1.12.92 1.72q.32 1 .32 2.21v9.14h-3.46v-9.14q0-1.32-.6-2.04t-1.84-.72q-.89 0-1.68.41-.78.4-1.48 1.1v10.39h-3.46v-20.8zm15.06 11.97h6.53q0-.68-.19-1.27-.19-.6-.57-1.04-.38-.45-.96-.71t-1.35-.26q-1.5 0-2.36.85-.86.86-1.1 2.43m8.82 2.08h-8.89q.09 1.11.4 1.91.3.81.81 1.33.5.53 1.2.79.69.26 1.53.26t1.45-.2 1.06-.43q.46-.24.8-.44.34-.19.66-.19.44 0 .65.32l.99 1.26q-.57.67-1.29 1.12-.71.46-1.49.73-.77.28-1.58.39-.8.11-1.56.11-1.5 0-2.79-.5-1.28-.49-2.24-1.47-.95-.97-1.49-2.41-.55-1.43-.55-3.32 0-1.47.48-2.77.47-1.29 1.36-2.25t2.17-1.52 2.89-.56q1.36 0 2.51.44 1.15.43 1.97 1.26.83.84 1.3 2.05t.47 2.76q0 .79-.17 1.06t-.65.27m6.46-6.75.21 1.63q.67-1.29 1.59-2.02.93-.74 2.19-.74.99 0 1.59.43l-.22 2.59q-.07.26-.2.36-.14.11-.36.11-.21 0-.62-.07-.42-.07-.81-.07-.57 0-1.02.16-.45.17-.8.49-.36.31-.63.76-.28.45-.52 1.02v8.85h-3.45v-14.36h2.03q.53 0 .74.18.21.19.28.68m10.12 4.67h6.52q0-.68-.18-1.27-.19-.6-.57-1.04-.38-.45-.96-.71t-1.35-.26q-1.5 0-2.36.85-.86.86-1.1 2.43m8.82 2.08h-8.89q.08 1.11.39 1.91.31.81.81 1.33.51.53 1.2.79t1.53.26 1.45-.2 1.07-.43q.45-.24.8-.44.34-.19.66-.19.43 0 .64.32l1 1.26q-.58.67-1.29 1.12-.71.46-1.49.73-.78.28-1.58.39-.81.11-1.56.11-1.5 0-2.79-.5-1.29-.49-2.24-1.47-.95-.97-1.5-2.41-.54-1.43-.54-3.32 0-1.47.47-2.77.48-1.29 1.37-2.25t2.17-1.52 2.89-.56q1.36 0 2.5.44 1.15.43 1.98 1.26.82.84 1.29 2.05t.47 2.76q0 .79-.17 1.06-.16.27-.64.27" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
@@ -1,24 +0,0 @@
|
||||
<svg width="646" height="96" viewBox="0 0 646 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1064_606)">
|
||||
<path d="M108.53 75.6899L90.81 94.6899C90.4267 95.1026 89.9626 95.432 89.4464 95.6573C88.9303 95.8827 88.3732 95.9994 87.81 95.9999H3.81C3.40937 95.9997 3.01749 95.8827 2.68235 95.6631C2.34722 95.4436 2.08338 95.1311 1.92313 94.7639C1.76288 94.3967 1.71318 93.9908 1.78012 93.5958C1.84706 93.2008 2.02772 92.8338 2.3 92.5399L20 73.5399C20.3833 73.1273 20.8474 72.7979 21.3636 72.5725C21.8797 72.3472 22.4368 72.2305 23 72.2299H107C107.404 72.2216 107.802 72.333 108.143 72.5502C108.484 72.7674 108.754 73.0806 108.917 73.4504C109.081 73.8203 109.131 74.2303 109.062 74.6288C108.993 75.0273 108.808 75.3965 108.53 75.6899ZM90.81 37.4199C90.4253 37.0091 89.9608 36.6811 89.445 36.4558C88.9292 36.2306 88.3728 36.1129 87.81 36.11H3.81C3.40937 36.1102 3.01749 36.2272 2.68235 36.4468C2.34722 36.6663 2.08338 36.9788 1.92313 37.346C1.76288 37.7132 1.71318 38.1191 1.78012 38.5141C1.84706 38.9091 2.02772 39.2761 2.3 39.57L20 58.58C20.3847 58.9908 20.8492 59.3188 21.365 59.5441C21.8808 59.7693 22.4372 59.887 23 59.8899H107C107.4 59.8878 107.79 59.7693 108.124 59.5491C108.458 59.3288 108.72 59.0162 108.879 58.6494C109.038 58.2826 109.087 57.8774 109.019 57.4833C108.952 57.0892 108.772 56.7232 108.5 56.43L90.81 37.4199ZM3.81 23.7699H87.81C88.3732 23.7694 88.9303 23.6527 89.4464 23.4273C89.9626 23.202 90.4267 22.8726 90.81 22.4599L108.53 3.45995C108.808 3.16647 108.993 2.79726 109.062 2.39877C109.131 2.00028 109.081 1.59031 108.917 1.22045C108.754 0.850591 108.484 0.537368 108.143 0.320195C107.802 0.103021 107.404 -0.0084012 107 -5.10783e-05H23C22.4368 0.000541762 21.8797 0.117167 21.3636 0.342553C20.8474 0.567938 20.3833 0.897249 20 1.30995L2.3 20.3099C2.02772 20.6038 1.84706 20.9708 1.78012 21.3658C1.71318 21.7608 1.76288 22.1667 1.92313 22.5339C2.08338 22.9011 2.34722 23.2136 2.68235 23.4331C3.01749 23.6527 3.40937 23.7697 3.81 23.7699Z" fill="url(#paint0_linear_1064_606)"/>
|
||||
<path d="M210.94 40.6002H166V25.8002H222.62V11.0002H165.85C163.91 10.9897 161.988 11.3613 160.192 12.0938C158.396 12.8264 156.761 13.9055 155.383 15.2696C154.004 16.6337 152.907 18.2561 152.155 20.044C151.403 21.832 151.01 23.7506 151 25.6902V40.6902C151.008 42.6315 151.398 44.5523 152.149 46.3425C152.9 48.1328 153.996 49.7575 155.375 51.1237C156.755 52.49 158.39 53.5709 160.187 54.3047C161.984 55.0385 163.909 55.4108 165.85 55.4002H210.85V70.2002H152.07V85.0002H210.94C212.88 85.0108 214.802 84.6391 216.598 83.9066C218.394 83.174 220.029 82.0949 221.407 80.7308C222.786 79.3667 223.883 77.7444 224.635 75.9564C225.387 74.1684 225.78 72.2498 225.79 70.3102V55.3102C225.782 53.3689 225.392 51.4482 224.641 49.6579C223.89 47.8676 222.794 46.2429 221.415 44.8767C220.035 43.5105 218.4 42.4296 216.603 41.6958C214.806 40.962 212.881 40.5897 210.94 40.6002Z" fill="white"/>
|
||||
<path d="M298 11H252.89C250.947 10.9842 249.02 11.3519 247.219 12.0821C245.419 12.8123 243.78 13.8905 242.397 15.2552C241.013 16.6198 239.913 18.2439 239.159 20.0345C238.404 21.8251 238.01 23.747 238 25.69V70.31C238.01 72.253 238.404 74.1749 239.159 75.9655C239.913 77.7561 241.013 79.3802 242.397 80.7448C243.78 82.1095 245.419 83.1877 247.219 83.9179C249.02 84.6481 250.947 85.0158 252.89 85H298C299.94 85.0105 301.862 84.6389 303.658 83.9064C305.454 83.1738 307.089 82.0947 308.467 80.7306C309.846 79.3665 310.943 77.7441 311.695 75.9562C312.447 74.1682 312.84 72.2496 312.85 70.31V25.69C312.84 23.7504 312.447 21.8318 311.695 20.0438C310.943 18.2559 309.846 16.6335 308.467 15.2694C307.089 13.9053 305.454 12.8262 303.658 12.0936C301.862 11.3611 299.94 10.9895 298 11ZM297.89 70.2H253V25.8H297.87L297.89 70.2Z" fill="white"/>
|
||||
<path d="M456 11.0001H412C410.06 10.9896 408.138 11.3612 406.342 12.0937C404.546 12.8263 402.911 13.9054 401.533 15.2695C400.154 16.6336 399.057 18.256 398.305 20.0439C397.553 21.8319 397.16 23.7505 397.15 25.6901V85.0001H412.15V60.6901H455.95V85.0001H470.95V25.6901C470.94 23.742 470.544 21.8152 469.786 20.0206C469.027 18.2261 467.922 16.5993 466.532 15.2338C465.143 13.8684 463.497 12.7914 461.689 12.0648C459.881 11.3382 457.948 10.9764 456 11.0001ZM455.89 45.8901H412.09V25.8001H455.89V45.8901Z" fill="white"/>
|
||||
<path d="M631.15 11.0002H587.15C585.21 10.9897 583.288 11.3613 581.492 12.0938C579.696 12.8264 578.062 13.9055 576.683 15.2696C575.304 16.6337 574.207 18.2561 573.455 20.044C572.703 21.832 572.31 23.7506 572.3 25.6902V85.0002H587.3V60.6902H631V85.0002H646V25.6902C645.99 23.7506 645.597 21.832 644.845 20.044C644.093 18.2561 642.996 16.6337 641.617 15.2696C640.238 13.9055 638.604 12.8264 636.808 12.0938C635.012 11.3613 633.09 10.9897 631.15 11.0002ZM631 45.8902H587.2V25.8002H631V45.8902Z" fill="white"/>
|
||||
<path d="M544 70.2001H538L516.55 17.2001C515.815 15.3716 514.55 13.8045 512.918 12.6999C511.286 11.5952 509.361 11.0033 507.39 11.0001H494.08C492.786 10.9935 491.504 11.2418 490.307 11.7307C489.109 12.2197 488.02 12.9397 487.1 13.8497C486.181 14.7598 485.45 15.8419 484.949 17.0345C484.448 18.227 484.187 19.5066 484.18 20.8001V85.0001H499.18V25.8001H505.18L526.62 78.8001C527.367 80.6251 528.642 82.1858 530.281 83.283C531.919 84.3803 533.848 84.9641 535.82 84.9601H549.13C550.424 84.9667 551.706 84.7185 552.903 84.2295C554.101 83.7406 555.19 83.0205 556.11 82.1105C557.029 81.2005 557.76 80.1183 558.261 78.9258C558.762 77.7332 559.023 76.4537 559.03 75.1601V11.0001H544V70.2001Z" fill="white"/>
|
||||
<path d="M341.1 11H326.1V70.31C326.11 72.2539 326.505 74.1766 327.26 75.9678C328.015 77.7591 329.116 79.3836 330.5 80.7484C331.884 82.1132 333.525 83.1912 335.326 83.9208C337.128 84.6504 339.056 85.0171 341 85H386V70.2H341.1V11Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1064_606" x1="10.81" y1="98.29" x2="98.89" y2="-1.01005" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#9945FF"/>
|
||||
<stop offset="0.3" stop-color="#8752F3"/>
|
||||
<stop offset="0.5" stop-color="#5497D5"/>
|
||||
<stop offset="0.6" stop-color="#43B4CA"/>
|
||||
<stop offset="0.72" stop-color="#28E0B9"/>
|
||||
<stop offset="0.97" stop-color="#19FB9B"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1064_606">
|
||||
<rect width="646" height="96" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.1 KiB |
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="646" height="96" viewBox="0 0 646 96" fill="none" version="1.1" id="svg36"
|
||||
sodipodi:docname="solana_logo_banner_dark.svg" inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview id="namedview38" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25"
|
||||
inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false" inkscape:zoom="2.0820433" inkscape:cx="324.68104" inkscape:cy="48.02974"
|
||||
inkscape:window-width="1854" inkscape:window-height="1131" inkscape:window-x="0" inkscape:window-y="0"
|
||||
inkscape:window-maximized="1" inkscape:current-layer="svg36" />
|
||||
<g clip-path="url(#clip0_1064_606)" id="g16">
|
||||
<path
|
||||
d="M108.53 75.6899L90.81 94.6899C90.4267 95.1026 89.9626 95.432 89.4464 95.6573C88.9303 95.8827 88.3732 95.9994 87.81 95.9999H3.81C3.40937 95.9997 3.01749 95.8827 2.68235 95.6631C2.34722 95.4436 2.08338 95.1311 1.92313 94.7639C1.76288 94.3967 1.71318 93.9908 1.78012 93.5958C1.84706 93.2008 2.02772 92.8338 2.3 92.5399L20 73.5399C20.3833 73.1273 20.8474 72.7979 21.3636 72.5725C21.8797 72.3472 22.4368 72.2305 23 72.2299H107C107.404 72.2216 107.802 72.333 108.143 72.5502C108.484 72.7674 108.754 73.0806 108.917 73.4504C109.081 73.8203 109.131 74.2303 109.062 74.6288C108.993 75.0273 108.808 75.3965 108.53 75.6899ZM90.81 37.4199C90.4253 37.0091 89.9608 36.6811 89.445 36.4558C88.9292 36.2306 88.3728 36.1129 87.81 36.11H3.81C3.40937 36.1102 3.01749 36.2272 2.68235 36.4468C2.34722 36.6663 2.08338 36.9788 1.92313 37.346C1.76288 37.7132 1.71318 38.1191 1.78012 38.5141C1.84706 38.9091 2.02772 39.2761 2.3 39.57L20 58.58C20.3847 58.9908 20.8492 59.3188 21.365 59.5441C21.8808 59.7693 22.4372 59.887 23 59.8899H107C107.4 59.8878 107.79 59.7693 108.124 59.5491C108.458 59.3288 108.72 59.0162 108.879 58.6494C109.038 58.2826 109.087 57.8774 109.019 57.4833C108.952 57.0892 108.772 56.7232 108.5 56.43L90.81 37.4199ZM3.81 23.7699H87.81C88.3732 23.7694 88.9303 23.6527 89.4464 23.4273C89.9626 23.202 90.4267 22.8726 90.81 22.4599L108.53 3.45995C108.808 3.16647 108.993 2.79726 109.062 2.39877C109.131 2.00028 109.081 1.59031 108.917 1.22045C108.754 0.850591 108.484 0.537368 108.143 0.320195C107.802 0.103021 107.404 -0.0084012 107 -5.10783e-05H23C22.4368 0.000541762 21.8797 0.117167 21.3636 0.342553C20.8474 0.567938 20.3833 0.897249 20 1.30995L2.3 20.3099C2.02772 20.6038 1.84706 20.9708 1.78012 21.3658C1.71318 21.7608 1.76288 22.1667 1.92313 22.5339C2.08338 22.9011 2.34722 23.2136 2.68235 23.4331C3.01749 23.6527 3.40937 23.7697 3.81 23.7699Z"
|
||||
fill="url(#paint0_linear_1064_606)" id="path2" />
|
||||
<path
|
||||
d="M210.94 40.6002H166V25.8002H222.62V11.0002H165.85C163.91 10.9897 161.988 11.3613 160.192 12.0938C158.396 12.8264 156.761 13.9055 155.383 15.2696C154.004 16.6337 152.907 18.2561 152.155 20.044C151.403 21.832 151.01 23.7506 151 25.6902V40.6902C151.008 42.6315 151.398 44.5523 152.149 46.3425C152.9 48.1328 153.996 49.7575 155.375 51.1237C156.755 52.49 158.39 53.5709 160.187 54.3047C161.984 55.0385 163.909 55.4108 165.85 55.4002H210.85V70.2002H152.07V85.0002H210.94C212.88 85.0108 214.802 84.6391 216.598 83.9066C218.394 83.174 220.029 82.0949 221.407 80.7308C222.786 79.3667 223.883 77.7444 224.635 75.9564C225.387 74.1684 225.78 72.2498 225.79 70.3102V55.3102C225.782 53.3689 225.392 51.4482 224.641 49.6579C223.89 47.8676 222.794 46.2429 221.415 44.8767C220.035 43.5105 218.4 42.4296 216.603 41.6958C214.806 40.962 212.881 40.5897 210.94 40.6002Z"
|
||||
fill="white" id="path4" style="fill:#000000;fill-opacity:1" />
|
||||
<path
|
||||
d="M298 11H252.89C250.947 10.9842 249.02 11.3519 247.219 12.0821C245.419 12.8123 243.78 13.8905 242.397 15.2552C241.013 16.6198 239.913 18.2439 239.159 20.0345C238.404 21.8251 238.01 23.747 238 25.69V70.31C238.01 72.253 238.404 74.1749 239.159 75.9655C239.913 77.7561 241.013 79.3802 242.397 80.7448C243.78 82.1095 245.419 83.1877 247.219 83.9179C249.02 84.6481 250.947 85.0158 252.89 85H298C299.94 85.0105 301.862 84.6389 303.658 83.9064C305.454 83.1738 307.089 82.0947 308.467 80.7306C309.846 79.3665 310.943 77.7441 311.695 75.9562C312.447 74.1682 312.84 72.2496 312.85 70.31V25.69C312.84 23.7504 312.447 21.8318 311.695 20.0438C310.943 18.2559 309.846 16.6335 308.467 15.2694C307.089 13.9053 305.454 12.8262 303.658 12.0936C301.862 11.3611 299.94 10.9895 298 11ZM297.89 70.2H253V25.8H297.87L297.89 70.2Z"
|
||||
fill="white" id="path6" style="fill:#000000" />
|
||||
<path
|
||||
d="M456 11.0001H412C410.06 10.9896 408.138 11.3612 406.342 12.0937C404.546 12.8263 402.911 13.9054 401.533 15.2695C400.154 16.6336 399.057 18.256 398.305 20.0439C397.553 21.8319 397.16 23.7505 397.15 25.6901V85.0001H412.15V60.6901H455.95V85.0001H470.95V25.6901C470.94 23.742 470.544 21.8152 469.786 20.0206C469.027 18.2261 467.922 16.5993 466.532 15.2338C465.143 13.8684 463.497 12.7914 461.689 12.0648C459.881 11.3382 457.948 10.9764 456 11.0001ZM455.89 45.8901H412.09V25.8001H455.89V45.8901Z"
|
||||
fill="white" id="path8" style="fill:#000000" />
|
||||
<path
|
||||
d="M631.15 11.0002H587.15C585.21 10.9897 583.288 11.3613 581.492 12.0938C579.696 12.8264 578.062 13.9055 576.683 15.2696C575.304 16.6337 574.207 18.2561 573.455 20.044C572.703 21.832 572.31 23.7506 572.3 25.6902V85.0002H587.3V60.6902H631V85.0002H646V25.6902C645.99 23.7506 645.597 21.832 644.845 20.044C644.093 18.2561 642.996 16.6337 641.617 15.2696C640.238 13.9055 638.604 12.8264 636.808 12.0938C635.012 11.3613 633.09 10.9897 631.15 11.0002ZM631 45.8902H587.2V25.8002H631V45.8902Z"
|
||||
fill="white" id="path10" style="fill:#000000" />
|
||||
<path
|
||||
d="M544 70.2001H538L516.55 17.2001C515.815 15.3716 514.55 13.8045 512.918 12.6999C511.286 11.5952 509.361 11.0033 507.39 11.0001H494.08C492.786 10.9935 491.504 11.2418 490.307 11.7307C489.109 12.2197 488.02 12.9397 487.1 13.8497C486.181 14.7598 485.45 15.8419 484.949 17.0345C484.448 18.227 484.187 19.5066 484.18 20.8001V85.0001H499.18V25.8001H505.18L526.62 78.8001C527.367 80.6251 528.642 82.1858 530.281 83.283C531.919 84.3803 533.848 84.9641 535.82 84.9601H549.13C550.424 84.9667 551.706 84.7185 552.903 84.2295C554.101 83.7406 555.19 83.0205 556.11 82.1105C557.029 81.2005 557.76 80.1183 558.261 78.9258C558.762 77.7332 559.023 76.4537 559.03 75.1601V11.0001H544V70.2001Z"
|
||||
fill="white" id="path12" style="fill:#000000" />
|
||||
<path
|
||||
d="M341.1 11H326.1V70.31C326.11 72.2539 326.505 74.1766 327.26 75.9678C328.015 77.7591 329.116 79.3836 330.5 80.7484C331.884 82.1132 333.525 83.1912 335.326 83.9208C337.128 84.6504 339.056 85.0171 341 85H386V70.2H341.1V11Z"
|
||||
fill="white" id="path14" style="fill:#000000" />
|
||||
</g>
|
||||
<defs id="defs34">
|
||||
<linearGradient id="paint0_linear_1064_606" x1="10.81" y1="98.29" x2="98.89" y2="-1.01005"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#9945FF" id="stop18" />
|
||||
<stop offset="0.3" stop-color="#8752F3" id="stop20" />
|
||||
<stop offset="0.5" stop-color="#5497D5" id="stop22" />
|
||||
<stop offset="0.6" stop-color="#43B4CA" id="stop24" />
|
||||
<stop offset="0.72" stop-color="#28E0B9" id="stop26" />
|
||||
<stop offset="0.97" stop-color="#19FB9B" id="stop28" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1064_606">
|
||||
<rect width="646" height="96" fill="white" id="rect31" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.5 KiB |
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" fill="#5865F2" fill-rule="nonzero">
|
||||
␍</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
␍<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
␍<g fill-rule="evenodd" clip-rule="evenodd">
|
||||
␍<path fill="#E01E5A" d="M2.471 11.318a1.474 1.474 0 001.47-1.471v-1.47h-1.47A1.474 1.474 0 001 9.846c.001.811.659 1.469 1.47 1.47zm3.682-2.942a1.474 1.474 0 00-1.47 1.471v3.683c.002.811.66 1.468 1.47 1.47a1.474 1.474 0 001.47-1.47V9.846a1.474 1.474 0 00-1.47-1.47z"/>
|
||||
␍<path fill="#36C5F0" d="M4.683 2.471c.001.811.659 1.469 1.47 1.47h1.47v-1.47A1.474 1.474 0 006.154 1a1.474 1.474 0 00-1.47 1.47zm2.94 3.682a1.474 1.474 0 00-1.47-1.47H2.47A1.474 1.474 0 001 6.153c.002.812.66 1.469 1.47 1.47h3.684a1.474 1.474 0 001.47-1.47z"/>
|
||||
␍<path fill="#2EB67D" d="M9.847 7.624a1.474 1.474 0 001.47-1.47V2.47A1.474 1.474 0 009.848 1a1.474 1.474 0 00-1.47 1.47v3.684c.002.81.659 1.468 1.47 1.47zm3.682-2.941a1.474 1.474 0 00-1.47 1.47v1.47h1.47A1.474 1.474 0 0015 6.154a1.474 1.474 0 00-1.47-1.47z"/>
|
||||
␍<path fill="#ECB22E" d="M8.377 9.847c.002.811.659 1.469 1.47 1.47h3.683A1.474 1.474 0 0015 9.848a1.474 1.474 0 00-1.47-1.47H9.847a1.474 1.474 0 00-1.47 1.47zm2.94 3.682a1.474 1.474 0 00-1.47-1.47h-1.47v1.47c.002.812.659 1.469 1.47 1.47a1.474 1.474 0 001.47-1.47z"/>
|
||||
␍</g>
|
||||
␍</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 428 KiB |
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<path d="M128.080089,-0.000183105 C135.311053,0.0131003068 142.422517,0.624138494 149.335663,1.77979593 L149.335663,1.77979593 L149.335663,76.2997796 L202.166953,23.6044907 C208.002065,27.7488446 213.460883,32.3582023 218.507811,37.3926715 C223.557281,42.4271407 228.192318,47.8867213 232.346817,53.7047992 L232.346817,53.7047992 L179.512985,106.400063 L254.227854,106.400063 C255.387249,113.29414 256,120.36111 256,127.587243 L256,127.587243 L256,127.759881 C256,134.986013 255.387249,142.066204 254.227854,148.960282 L254.227854,148.960282 L179.500273,148.960282 L232.346817,201.642324 C228.192318,207.460402 223.557281,212.919983 218.523066,217.954452 L218.523066,217.954452 L218.507811,217.954452 C213.460883,222.988921 208.002065,227.6115 202.182208,231.742607 L202.182208,231.742607 L149.335663,179.04709 L149.335663,253.5672 C142.435229,254.723036 135.323765,255.333244 128.092802,255.348499 L128.092802,255.348499 L127.907197,255.348499 C120.673691,255.333244 113.590195,254.723036 106.677048,253.5672 L106.677048,253.5672 L106.677048,179.04709 L53.8457596,231.742607 C42.1780766,223.466917 31.977435,213.278734 23.6658953,201.642324 L23.6658953,201.642324 L76.4997269,148.960282 L1.78485803,148.960282 C0.612750404,142.052729 0,134.946095 0,127.719963 L0,127.719963 L0,127.349037 C0.0121454869,125.473817 0.134939797,123.182933 0.311311815,120.812834 L0.36577283,120.099764 C0.887996182,113.428547 1.78485803,106.400063 1.78485803,106.400063 L1.78485803,106.400063 L76.4997269,106.400063 L23.6658953,53.7047992 C27.8076812,47.8867213 32.4300059,42.4403618 37.4769335,37.4193681 L37.4769335,37.4193681 L37.5023588,37.3926715 C42.5391163,32.3582023 48.0106469,27.7488446 53.8457596,23.6044907 L53.8457596,23.6044907 L106.677048,76.2997796 L106.677048,1.77979593 C113.590195,0.624138494 120.688946,0.0131003068 127.932622,-0.000183105 L127.932622,-0.000183105 L128.080089,-0.000183105 Z M128.067377,95.7600714 L127.945335,95.7600714 C118.436262,95.7600714 109.32891,97.5001809 100.910584,100.661566 C97.7553011,109.043534 96.0085811,118.129275 95.9958684,127.613685 L95.9958684,127.733184 C96.0085811,137.217594 97.7553011,146.303589 100.923296,154.685303 C109.32891,157.846943 118.436262,159.587052 127.945335,159.587052 L128.067377,159.587052 C137.576449,159.587052 146.683802,157.846943 155.089415,154.685303 C158.257411,146.290368 160.004131,137.217594 160.004131,127.733184 L160.004131,127.613685 C160.004131,118.129275 158.257411,109.043534 155.089415,100.661566 C146.683802,97.5001809 137.576449,95.7600714 128.067377,95.7600714 Z" fill="#FF4A00" fill-rule="nonzero">
|
||||
␍</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function flattenKeys(obj, prefix = "", result = []) {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (
|
||||
typeof obj[key] === "object" &&
|
||||
obj[key] !== null &&
|
||||
!Array.isArray(obj[key])
|
||||
) {
|
||||
flattenKeys(obj[key], newKey, result);
|
||||
} else {
|
||||
result.push(newKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Paths
|
||||
const localesDir = path.join(__dirname, "../client/src/locales");
|
||||
const inputPath = path.join(localesDir, "en.json");
|
||||
const outputPath = path.join(localesDir, "keys.json");
|
||||
|
||||
try {
|
||||
const jsonContent = fs.readFileSync(inputPath, "utf8");
|
||||
const jsonData = JSON.parse(jsonContent);
|
||||
|
||||
const flattenedKeys = flattenKeys(jsonData);
|
||||
flattenedKeys.sort();
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(flattenedKeys, null, 2), "utf8");
|
||||
|
||||
console.log(`Keys written to: ${outputPath}`);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
"express": "^4.19.2",
|
||||
"express-rate-limit": "8.0.1",
|
||||
"gamedig": "^5.3.1",
|
||||
"globalping": "^0.2.0",
|
||||
"got": "14.4.7",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.0.0",
|
||||
@@ -874,7 +875,6 @@
|
||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/generator": "^7.28.6",
|
||||
@@ -1504,7 +1504,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -1549,7 +1548,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -2261,6 +2259,132 @@
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/client-fetch": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.10.2.tgz",
|
||||
"integrity": "sha512-AGiFYDx+y8VT1wlQ3EbzzZtfU8EfV+hLLRTtr8Y/tjYZaxIECwJagVZf24YzNbtEBXONFV50bwcU1wLVGXe1ow==",
|
||||
"deprecated": "Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts.",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/hey-api"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@hey-api/openapi-ts": "< 2"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/codegen-core": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.7.0.tgz",
|
||||
"integrity": "sha512-HglL4B4QwpzocE+c8qDU6XK8zMf8W8Pcv0RpFDYxHuYALWLTnpDUuEsglC7NQ4vC1maoXsBpMbmwpco0N4QviA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hey-api/types": "0.1.3",
|
||||
"ansi-colors": "4.1.3",
|
||||
"c12": "3.3.3",
|
||||
"color-support": "1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/hey-api"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/json-schema-ref-parser": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-3tQJ8N2egHXZjQWUeceoWrl88APWjo7gRrQ/L4HWJKnh6HowczCv7yNNFeSusPoWGV6HGdoFiCvq6UsLkrwKhg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jsdevtools/ono": "7.1.3",
|
||||
"@types/json-schema": "7.0.15",
|
||||
"js-yaml": "4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/hey-api"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/openapi-ts": {
|
||||
"version": "0.92.4",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.92.4.tgz",
|
||||
"integrity": "sha512-RA3wnL7Odr5xczuS3xpvnPClgJ/K8jivK3hvD8J0m5GBuvJFkZ1A1xp+6Ve1G0BV8p4LwxwgN1Qhb+4BFsLfMg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hey-api/codegen-core": "0.7.0",
|
||||
"@hey-api/json-schema-ref-parser": "1.3.0",
|
||||
"@hey-api/shared": "0.2.0",
|
||||
"@hey-api/types": "0.1.3",
|
||||
"ansi-colors": "4.1.3",
|
||||
"color-support": "1.1.3",
|
||||
"commander": "14.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"openapi-ts": "bin/run.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/hey-api"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/openapi-ts/node_modules/commander": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
||||
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/shared": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.2.0.tgz",
|
||||
"integrity": "sha512-t7C+65ES12OqAE5k6DB/y5nDuTjydtqdxf/Qe4zflVn2AzGs7hO/7KjXvGXZYnpNVF7QISAcj0LEObASU9I53Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hey-api/codegen-core": "0.7.0",
|
||||
"@hey-api/json-schema-ref-parser": "1.3.0",
|
||||
"@hey-api/types": "0.1.3",
|
||||
"ansi-colors": "4.1.3",
|
||||
"cross-spawn": "7.0.6",
|
||||
"open": "11.0.0",
|
||||
"semver": "7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/hey-api"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/types": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/types/-/types-0.1.3.tgz",
|
||||
"integrity": "sha512-mZaiPOWH761yD4GjDQvtjS2ZYLu5o5pI1TVSvV/u7cmbybv51/FVtinFBeaE1kFQCKZ8OQpn2ezjLBJrKsGATw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -3223,6 +3347,13 @@
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz",
|
||||
@@ -4229,6 +4360,7 @@
|
||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
@@ -4405,7 +4537,6 @@
|
||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
@@ -4506,7 +4637,6 @@
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsonwebtoken": {
|
||||
@@ -4566,7 +4696,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -5053,7 +5182,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5632,7 +5760,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -5739,6 +5866,22 @@
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"run-applescript": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -5759,6 +5902,78 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/c12": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/c12/-/c12-3.3.3.tgz",
|
||||
"integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^5.0.0",
|
||||
"confbox": "^0.2.2",
|
||||
"defu": "^6.1.4",
|
||||
"dotenv": "^17.2.3",
|
||||
"exsolve": "^1.0.8",
|
||||
"giget": "^2.0.0",
|
||||
"jiti": "^2.6.1",
|
||||
"ohash": "^2.0.11",
|
||||
"pathe": "^2.0.3",
|
||||
"perfect-debounce": "^2.0.0",
|
||||
"pkg-types": "^2.3.0",
|
||||
"rc9": "^2.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"magicast": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"magicast": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/c12/node_modules/chokidar": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/c12/node_modules/dotenv": {
|
||||
"version": "17.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/c12/node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/c8": {
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz",
|
||||
@@ -6023,6 +6238,16 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/citty": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"consola": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/cjs-module-lexer": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz",
|
||||
@@ -6220,6 +6445,16 @@
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/colord": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
|
||||
@@ -6357,6 +6592,23 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -6575,7 +6827,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
|
||||
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssnano-preset-default": "^7.0.9",
|
||||
"lilconfig": "^3.1.3"
|
||||
@@ -6821,6 +7072,36 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
|
||||
"integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bundle-name": "^4.1.0",
|
||||
"default-browser-id": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser-id": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
|
||||
"integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
@@ -6830,6 +7111,26 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -6857,6 +7158,13 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destr": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
@@ -7330,7 +7638,6 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -7692,7 +7999,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -7767,6 +8073,13 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@@ -8395,6 +8708,24 @@
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/giget": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
||||
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.4.0",
|
||||
"defu": "^6.1.4",
|
||||
"node-fetch-native": "^1.6.6",
|
||||
"nypm": "^0.6.0",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"giget": "dist/cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
@@ -8452,6 +8783,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/globalping": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/globalping/-/globalping-0.2.0.tgz",
|
||||
"integrity": "sha512-rxyvqXF/oz5yBGJHR58TEsCkcbc4WQVmdKC+uZ8LXfIt4KwTdTvbzkfwhhjuyUmuQr8U/b8m/ZicdrA8ZIroKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hey-api/client-fetch": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "15.15.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
|
||||
@@ -8947,6 +9287,22 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
||||
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -8987,6 +9343,38 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-in-ssh": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
|
||||
"integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-inside-container": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-docker": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"is-inside-container": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-json": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz",
|
||||
@@ -9020,6 +9408,22 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
|
||||
"integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-inside-container": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
@@ -9341,7 +9745,6 @@
|
||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.2.0",
|
||||
"@jest/types": "30.2.0",
|
||||
@@ -9929,6 +10332,16 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jmespath": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
|
||||
@@ -11611,6 +12024,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-native": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
@@ -11780,6 +12200,31 @@
|
||||
"integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
||||
"integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"citty": "^0.2.0",
|
||||
"pathe": "^2.0.3",
|
||||
"tinyexec": "^1.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"nypm": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm/node_modules/citty": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz",
|
||||
"integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -11801,6 +12246,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ohash": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||||
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -11856,6 +12308,27 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz",
|
||||
"integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"default-browser": "^5.4.0",
|
||||
"define-lazy-prop": "^3.0.0",
|
||||
"is-in-ssh": "^1.0.0",
|
||||
"is-inside-container": "^1.0.0",
|
||||
"powershell-utils": "^0.1.0",
|
||||
"wsl-utils": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -12080,6 +12553,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
|
||||
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -12199,6 +12686,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.2",
|
||||
"exsolve": "^1.0.7",
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/plimit-lit": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz",
|
||||
@@ -12231,7 +12730,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -12825,6 +13323,19 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/powershell-utils": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
|
||||
"integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -13088,6 +13599,17 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/rc9": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
@@ -13280,6 +13802,19 @@
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/run-applescript": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
|
||||
"integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -14179,6 +14714,7 @@
|
||||
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@trysound/sax": "0.2.0",
|
||||
"commander": "^7.2.0",
|
||||
@@ -14205,6 +14741,7 @@
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
@@ -14215,6 +14752,7 @@
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.30",
|
||||
"source-map-js": "^1.0.1"
|
||||
@@ -14228,7 +14766,8 @@
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||
"license": "CC0-1.0",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.30.0",
|
||||
@@ -14365,6 +14904,16 @@
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
@@ -14513,7 +15062,6 @@
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -14674,9 +15222,7 @@
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -15306,6 +15852,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wsl-utils": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz",
|
||||
"integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-wsl": "^3.1.0",
|
||||
"powershell-utils": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"express": "^4.19.2",
|
||||
"express-rate-limit": "8.0.1",
|
||||
"gamedig": "^5.3.1",
|
||||
"globalping": "^0.2.0",
|
||||
"got": "14.4.7",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.0.0",
|
||||
|
||||
@@ -29,8 +29,8 @@ export interface InitializedControllers {
|
||||
export const initializeControllers = (services: InitializedServices): InitializedControllers => {
|
||||
return {
|
||||
authController: new AuthController(services.userService),
|
||||
monitorController: new MonitorController(services.monitorService),
|
||||
settingsController: new SettingsController(services.settingsService, services.emailService, services.db),
|
||||
monitorController: new MonitorController(services.monitorService, services.settingsService),
|
||||
settingsController: new SettingsController(services.settingsService, services.emailService),
|
||||
checkController: new CheckController(services.checkService),
|
||||
inviteController: new InviteController(services.inviteService),
|
||||
maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService),
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
INotificationsService,
|
||||
} from "@/service/index.js";
|
||||
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import GlobalpingService from "../service/infrastructure/globalpingService.js";
|
||||
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||
import UserService from "../service/business/userService.js";
|
||||
import CheckService from "../service/business/checkService.js";
|
||||
@@ -23,6 +24,7 @@ import MaintenanceWindowService from "../service/business/maintenanceWindowServi
|
||||
import { MonitorService } from "@/service/index.js";
|
||||
import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js";
|
||||
import IncidentService from "../service/business/incidentService.js";
|
||||
import { NotificationMessageBuilder, INotificationMessageBuilder } from "../service/infrastructure/notificationMessageBuilder.js";
|
||||
import axios from "axios";
|
||||
import got from "got";
|
||||
import ping from "ping";
|
||||
@@ -41,18 +43,6 @@ import crypto from "crypto";
|
||||
import { games, GameDig } from "gamedig";
|
||||
import jmespath from "jmespath";
|
||||
|
||||
// DB Modules
|
||||
import { GenerateAvatarImage } from "../utils/imageProcessing.js";
|
||||
import { ParseBoolean } from "../utils/utils.js";
|
||||
|
||||
// Models
|
||||
import InviteToken from "../db/models/Invite.js";
|
||||
import Team from "../db/models/Team.js";
|
||||
import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
|
||||
import MonitorStats from "../db/models/MonitorStats.js";
|
||||
import NotificationModel from "../db/models/Notification.js";
|
||||
import RecoveryToken from "../db/models/RecoveryToken.js";
|
||||
|
||||
// repositories
|
||||
import {
|
||||
MongoMonitorsRepository,
|
||||
@@ -100,6 +90,7 @@ export type InitializedServices = {
|
||||
logger: any;
|
||||
notificationsService: INotificationsService;
|
||||
statusPageService: IStatusPageService;
|
||||
notificationMessageBuilder: INotificationMessageBuilder;
|
||||
|
||||
// Repositories
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
@@ -161,11 +152,14 @@ export const initializeServices = async ({
|
||||
});
|
||||
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
||||
|
||||
const notificationMessageBuilder = new NotificationMessageBuilder();
|
||||
|
||||
const incidentService = new IncidentService({
|
||||
logger,
|
||||
incidentsRepository,
|
||||
monitorsRepository,
|
||||
usersRepository,
|
||||
notificationMessageBuilder,
|
||||
});
|
||||
|
||||
const checkService = new CheckService({
|
||||
@@ -176,7 +170,7 @@ export const initializeServices = async ({
|
||||
|
||||
const bufferService = new BufferService({ logger, checkService, settingsService });
|
||||
|
||||
const statusService = new StatusService({ logger, buffer: bufferService, monitorsRepository });
|
||||
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
|
||||
|
||||
const webhookProvider = new WebhookProvider(logger);
|
||||
const slackProvider = new SlackProvider(logger);
|
||||
@@ -194,9 +188,13 @@ export const initializeServices = async ({
|
||||
discordProvider,
|
||||
pagerDutyProvider,
|
||||
matrixProvider,
|
||||
logger
|
||||
settingsService,
|
||||
logger,
|
||||
notificationMessageBuilder
|
||||
);
|
||||
|
||||
const globalpingService = new GlobalpingService({ settingsService, logger });
|
||||
|
||||
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
|
||||
logger,
|
||||
networkService,
|
||||
@@ -205,8 +203,13 @@ export const initializeServices = async ({
|
||||
checkService,
|
||||
buffer: bufferService,
|
||||
incidentService,
|
||||
globalpingService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
teamsRepository,
|
||||
monitorStatsRepository,
|
||||
checksRepository,
|
||||
incidentsRepository,
|
||||
});
|
||||
|
||||
const superSimpleQueue = await SuperSimpleQueue.create({
|
||||
@@ -250,6 +253,7 @@ export const initializeServices = async ({
|
||||
checksRepository,
|
||||
monitorStatsRepository,
|
||||
statusPagesRepository,
|
||||
incidentsRepository,
|
||||
});
|
||||
|
||||
const statusPageService = new StatusPageService(statusPagesRepository);
|
||||
@@ -273,6 +277,7 @@ export const initializeServices = async ({
|
||||
logger,
|
||||
notificationsService,
|
||||
statusPageService,
|
||||
notificationMessageBuilder,
|
||||
|
||||
// Repositories
|
||||
monitorsRepository,
|
||||
|
||||
@@ -26,15 +26,19 @@ import {
|
||||
} from "./controllerUtils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorService } from "@/service/index.js";
|
||||
import { getGlobalpingLocationsByTier, LOCATION_LABELS } from "@/service/infrastructure/globalpingService.js";
|
||||
import type { ISettingsService } from "@/service/system/settingsService.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
class MonitorController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private monitorService: IMonitorService;
|
||||
private settingsService: ISettingsService;
|
||||
|
||||
constructor(monitorService: IMonitorService) {
|
||||
constructor(monitorService: IMonitorService, settingsService: ISettingsService) {
|
||||
this.monitorService = monitorService;
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -387,6 +391,21 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
getGlobalpingLocations = async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const settings = await this.settingsService.getDBSettings();
|
||||
const tier = settings.globalpingLocationsTier ?? 6;
|
||||
const locations = getGlobalpingLocationsByTier(tier);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Globalping locations retrieved successfully",
|
||||
data: { locations, labels: LOCATION_LABELS },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = req?.user?.teamId;
|
||||
|
||||
@@ -8,11 +8,9 @@ class SettingsController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private settingsService: any;
|
||||
private emailService: any;
|
||||
private db: any;
|
||||
constructor(settingsService: any, emailService: any, db: any) {
|
||||
constructor(settingsService: any, emailService: any) {
|
||||
this.settingsService = settingsService;
|
||||
this.emailService = emailService;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -23,8 +21,9 @@ class SettingsController {
|
||||
const sanitizedSettings = { ...dbSettings };
|
||||
delete sanitizedSettings.version;
|
||||
delete sanitizedSettings.jwtSecret;
|
||||
const returnSettings = {
|
||||
const returnSettings: any = {
|
||||
pagespeedKeySet: false,
|
||||
globalpingKeySet: false,
|
||||
emailPasswordSet: false,
|
||||
settings: null,
|
||||
};
|
||||
@@ -33,6 +32,10 @@ class SettingsController {
|
||||
returnSettings.pagespeedKeySet = true;
|
||||
delete sanitizedSettings.pagespeedApiKey;
|
||||
}
|
||||
if (typeof sanitizedSettings.globalpingApiKey !== "undefined") {
|
||||
returnSettings.globalpingKeySet = true;
|
||||
delete sanitizedSettings.globalpingApiKey;
|
||||
}
|
||||
if (typeof sanitizedSettings.systemEmailPassword !== "undefined") {
|
||||
returnSettings.emailPasswordSet = true;
|
||||
delete sanitizedSettings.systemEmailPassword;
|
||||
@@ -57,15 +60,19 @@ class SettingsController {
|
||||
};
|
||||
|
||||
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
|
||||
await updateAppSettingsBodyValidation.validateAsync(req.body);
|
||||
try {
|
||||
await updateAppSettingsBodyValidation.validateAsync(req.body);
|
||||
|
||||
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
|
||||
const returnSettings = this.buildAppSettings(updatedSettings);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "App settings updated successfully",
|
||||
data: returnSettings,
|
||||
});
|
||||
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
|
||||
const returnSettings = this.buildAppSettings(updatedSettings);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "App settings updated successfully",
|
||||
data: returnSettings,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { MonitorStatsModel } from "../models/MonitorStats.js";
|
||||
import { logger } from "@/utils/logger.js";
|
||||
|
||||
/**
|
||||
* Cleanup duplicate MonitorStats documents
|
||||
* Keeps the most recent document (by updatedAt) for each monitorId
|
||||
* and deletes all older duplicates
|
||||
*/
|
||||
export async function cleanupDuplicateMonitorStats(): Promise<void> {
|
||||
const SERVICE_NAME = "Migration:CleanupDuplicateMonitorStats";
|
||||
|
||||
try {
|
||||
logger.info({ service: SERVICE_NAME, message: "Starting cleanup of duplicate MonitorStats" });
|
||||
|
||||
// Find all duplicate monitorIds
|
||||
const duplicates = await MonitorStatsModel.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: "$monitorId",
|
||||
ids: { $push: "$_id" },
|
||||
updatedAts: { $push: "$updatedAt" },
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $match: { count: { $gt: 1 } } },
|
||||
]);
|
||||
|
||||
if (duplicates.length === 0) {
|
||||
logger.info({ service: SERVICE_NAME, message: "No duplicate MonitorStats found" });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info({
|
||||
service: SERVICE_NAME,
|
||||
message: `Found ${duplicates.length} monitors with duplicate stats`,
|
||||
});
|
||||
|
||||
let totalDeleted = 0;
|
||||
|
||||
// For each set of duplicates, keep the newest and delete the rest
|
||||
for (const duplicate of duplicates) {
|
||||
const monitorId = duplicate._id;
|
||||
const { ids, updatedAts } = duplicate;
|
||||
|
||||
type DocPair = { id: any; updatedAt: Date };
|
||||
|
||||
// Create array of {id, updatedAt} pairs and sort by updatedAt descending
|
||||
const docs: DocPair[] = ids.map((id: any, index: number) => ({
|
||||
id,
|
||||
updatedAt: updatedAts[index],
|
||||
}));
|
||||
|
||||
docs.sort((a: DocPair, b: DocPair) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
||||
|
||||
// Keep the first (newest), delete the rest
|
||||
const toDelete = docs.slice(1).map((doc: DocPair) => doc.id);
|
||||
|
||||
if (toDelete.length > 0) {
|
||||
const result = await MonitorStatsModel.deleteMany({
|
||||
_id: { $in: toDelete },
|
||||
});
|
||||
|
||||
totalDeleted += result.deletedCount ?? 0;
|
||||
|
||||
logger.debug({
|
||||
service: SERVICE_NAME,
|
||||
message: `Deleted ${result.deletedCount} duplicate stats for monitor ${monitorId}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info({
|
||||
service: SERVICE_NAME,
|
||||
message: `Cleanup complete. Deleted ${totalDeleted} duplicate MonitorStats documents`,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error({ service: SERVICE_NAME, message: `Error during MonitorStats cleanup: ${errorMessage}` });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
|
||||
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
|
||||
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
|
||||
import MigrationModel from "../models/Migration.js";
|
||||
|
||||
type MigrationEntry = {
|
||||
@@ -10,6 +11,7 @@ type MigrationEntry = {
|
||||
const migrations: MigrationEntry[] = [
|
||||
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
|
||||
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
|
||||
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
||||
];
|
||||
|
||||
const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
||||
|
||||
@@ -23,6 +23,8 @@ const AppSettingsSchema = new Schema<AppSettingsDocument>(
|
||||
language: { type: String, default: "gb" },
|
||||
jwtSecret: { type: String },
|
||||
pagespeedApiKey: { type: String },
|
||||
globalpingApiKey: { type: String },
|
||||
globalpingLocationsTier: { type: Number, default: 6 },
|
||||
systemEmailHost: { type: String },
|
||||
systemEmailPort: { type: Number },
|
||||
systemEmailAddress: { type: String },
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
CheckNetworkInterfaceInfo,
|
||||
GotTimings,
|
||||
ILighthouseAudit,
|
||||
LocationResult,
|
||||
} from "@/types/check.js";
|
||||
|
||||
type CheckMetadataDocument = Omit<CheckMetadata, "monitorId" | "teamId"> & {
|
||||
@@ -175,6 +176,17 @@ const auditsSchema = new Schema<CheckAudits>(
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const locationResultSchema = new Schema<LocationResult>(
|
||||
{
|
||||
location: { type: String, required: true },
|
||||
status: { type: Boolean, required: true },
|
||||
responseTime: { type: Number, required: true },
|
||||
statusCode: { type: Number, required: true },
|
||||
message: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const metadataSchema = new Schema<CheckMetadataDocument>(
|
||||
{
|
||||
monitorId: {
|
||||
@@ -281,6 +293,10 @@ const CheckSchema = new Schema<CheckDocument>(
|
||||
type: auditsSchema,
|
||||
default: undefined,
|
||||
},
|
||||
locationResults: {
|
||||
type: [locationResultSchema],
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema, model, Types, type UpdateQuery } from "mongoose";
|
||||
import type { Monitor, MonitorMatchMethod, MonitorThresholds, CheckSnapshot } from "@/types/monitor.js";
|
||||
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
|
||||
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
|
||||
|
||||
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
|
||||
@@ -13,7 +13,6 @@ type MonitorDocumentBase = Omit<
|
||||
notifications: Types.ObjectId[];
|
||||
selectedDisks: string[];
|
||||
matchMethod?: MonitorMatchMethod;
|
||||
thresholds?: MonitorThresholds;
|
||||
};
|
||||
|
||||
interface MonitorDocument extends MonitorDocumentBase {
|
||||
@@ -24,16 +23,6 @@ interface MonitorDocument extends MonitorDocumentBase {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const thresholdsSchema = new Schema<MonitorThresholds>(
|
||||
{
|
||||
usage_cpu: { type: Number },
|
||||
usage_memory: { type: Number },
|
||||
usage_disk: { type: Number },
|
||||
usage_temperature: { type: Number },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const checkSnapshotSchema = new Schema<CheckSnapshotDocument>(
|
||||
{
|
||||
id: { type: String, required: true },
|
||||
@@ -132,36 +121,37 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
secret: {
|
||||
type: String,
|
||||
},
|
||||
thresholds: {
|
||||
type: thresholdsSchema,
|
||||
cpuAlertThreshold: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
alertThreshold: {
|
||||
cpuAlertCounter: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
cpuAlertThreshold: {
|
||||
type: Number,
|
||||
default: function () {
|
||||
return this.alertThreshold;
|
||||
},
|
||||
},
|
||||
memoryAlertThreshold: {
|
||||
type: Number,
|
||||
default: function () {
|
||||
return this.alertThreshold;
|
||||
},
|
||||
default: 100,
|
||||
},
|
||||
memoryAlertCounter: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
diskAlertThreshold: {
|
||||
type: Number,
|
||||
default: function () {
|
||||
return this.alertThreshold;
|
||||
},
|
||||
default: 100,
|
||||
},
|
||||
diskAlertCounter: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
tempAlertThreshold: {
|
||||
type: Number,
|
||||
default: function () {
|
||||
return this.alertThreshold;
|
||||
},
|
||||
default: 100,
|
||||
},
|
||||
tempAlertCounter: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
selectedDisks: {
|
||||
type: [String],
|
||||
@@ -170,6 +160,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
gameId: {
|
||||
type: String,
|
||||
},
|
||||
globalpingEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
trim: true,
|
||||
@@ -189,33 +183,6 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
}
|
||||
);
|
||||
|
||||
MonitorSchema.pre("save", function (next) {
|
||||
if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) {
|
||||
this.cpuAlertThreshold = this.alertThreshold;
|
||||
}
|
||||
if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) {
|
||||
this.memoryAlertThreshold = this.alertThreshold;
|
||||
}
|
||||
if (!this.diskAlertThreshold || this.isModified("alertThreshold")) {
|
||||
this.diskAlertThreshold = this.alertThreshold;
|
||||
}
|
||||
if (!this.tempAlertThreshold || this.isModified("alertThreshold")) {
|
||||
this.tempAlertThreshold = this.alertThreshold;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
MonitorSchema.pre("findOneAndUpdate", function (next) {
|
||||
const update = this.getUpdate() as UpdateQuery<MonitorDocument> | null;
|
||||
if (update && !Array.isArray(update) && update.alertThreshold !== undefined) {
|
||||
update.cpuAlertThreshold = update.alertThreshold;
|
||||
update.memoryAlertThreshold = update.alertThreshold;
|
||||
update.diskAlertThreshold = update.alertThreshold;
|
||||
update.tempAlertThreshold = update.alertThreshold;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
MonitorSchema.index({ teamId: 1, type: 1 });
|
||||
|
||||
const MonitorModel = model<MonitorDocument>("Monitor", MonitorSchema);
|
||||
|
||||
@@ -16,7 +16,6 @@ const MonitorStatsSchema = new Schema<MonitorStatsDocument>(
|
||||
monitorId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
@@ -24,6 +23,10 @@ const MonitorStatsSchema = new Schema<MonitorStatsDocument>(
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
maxResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
Check,
|
||||
ChecksQueryResult,
|
||||
ChecksSummary,
|
||||
LocationResult,
|
||||
MonitorType,
|
||||
PageSpeedChecksResult,
|
||||
HardwareChecksResult,
|
||||
@@ -11,6 +12,7 @@ import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory
|
||||
|
||||
export interface IChecksRepository {
|
||||
// create
|
||||
create(check: Check): Promise<Check>;
|
||||
createChecks(checks: Check[]): Promise<Check[]>;
|
||||
|
||||
// single fetch
|
||||
@@ -34,8 +36,11 @@ export interface IChecksRepository {
|
||||
options?: { type?: MonitorType }
|
||||
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>;
|
||||
findSummaryByTeamId(teamId: string, dateRange: string): Promise<ChecksSummary>;
|
||||
findLocationChecksByMonitorId(monitorId: string, startDate: Date, endDate: Date, dateString: string): Promise<Record<string, UptimeChecksResult>>;
|
||||
// update
|
||||
updateLocationResults(checkId: string, locationResults: LocationResult[]): Promise<void>;
|
||||
//delete
|
||||
deleteByMonitorId(monitorId: string): Promise<number>;
|
||||
deleteByTeamId(teamId: string): Promise<number>;
|
||||
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
|
||||
}
|
||||
@@ -11,9 +11,10 @@ import type {
|
||||
CheckMetadata,
|
||||
CheckNetworkInterfaceInfo,
|
||||
GotTimings,
|
||||
LocationResult,
|
||||
MonitorType,
|
||||
} from "@/types/index.js";
|
||||
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
|
||||
import { CheckModel, MonitorModel, type CheckDocument } from "@/db/models/index.js";
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const SERVICE_NAME = "StatusService";
|
||||
@@ -194,6 +195,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
seo: doc.seo,
|
||||
performance: doc.performance,
|
||||
audits: mapAudits(doc.audits),
|
||||
locationResults: (doc as any).locationResults,
|
||||
__v: doc.__v ?? 0,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
@@ -210,23 +212,25 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
private toDocument = (check: Partial<Check>): CheckDocument => {
|
||||
// Map id to _id for MongoDB storage
|
||||
const { id, metadata, ...rest } = check;
|
||||
if (!metadata || !metadata.monitorId || !metadata.teamId) {
|
||||
throw new Error(`Check must have valid metadata with monitorId and teamId. Got: ${JSON.stringify({ id, metadata })}`);
|
||||
}
|
||||
return {
|
||||
_id: id ? new mongoose.Types.ObjectId(id) : new mongoose.Types.ObjectId(),
|
||||
metadata: metadata
|
||||
? {
|
||||
monitorId: new mongoose.Types.ObjectId(metadata.monitorId),
|
||||
teamId: new mongoose.Types.ObjectId(metadata.teamId),
|
||||
type: metadata.type,
|
||||
}
|
||||
: {
|
||||
monitorId: new mongoose.Types.ObjectId(),
|
||||
teamId: new mongoose.Types.ObjectId(),
|
||||
type: "http",
|
||||
},
|
||||
metadata: {
|
||||
monitorId: new mongoose.Types.ObjectId(metadata.monitorId),
|
||||
teamId: new mongoose.Types.ObjectId(metadata.teamId),
|
||||
type: metadata.type,
|
||||
},
|
||||
...rest,
|
||||
} as unknown as CheckDocument;
|
||||
};
|
||||
|
||||
create = async (check: Check) => {
|
||||
const savedCheck = await CheckModel.create(check);
|
||||
return this.toEntity(savedCheck);
|
||||
};
|
||||
|
||||
createChecks = async (checks: Check[]) => {
|
||||
const docs = checks.map((check) => this.toDocument(check));
|
||||
const inserted = await CheckModel.insertMany(docs);
|
||||
@@ -402,6 +406,10 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
};
|
||||
};
|
||||
|
||||
updateLocationResults = async (checkId: string, locationResults: LocationResult[]): Promise<void> => {
|
||||
await CheckModel.updateOne({ _id: new mongoose.Types.ObjectId(checkId) }, { $set: { locationResults } });
|
||||
};
|
||||
|
||||
deleteByMonitorId = async (monitorId: string): Promise<number> => {
|
||||
const result = await CheckModel.deleteMany({ "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) });
|
||||
return result.deletedCount;
|
||||
@@ -412,6 +420,66 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
return deleteResult.deletedCount;
|
||||
};
|
||||
|
||||
deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise<number> => {
|
||||
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
const result = await CheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } });
|
||||
return result.deletedCount ?? 0;
|
||||
};
|
||||
|
||||
findLocationChecksByMonitorId = async (monitorId: string, startDate: Date, endDate: Date, dateString: string): Promise<Record<string, any>> => {
|
||||
const monitorObjectId = new mongoose.Types.ObjectId(monitorId);
|
||||
|
||||
const results = await CheckModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
createdAt: { $gte: startDate, $lte: endDate },
|
||||
locationResults: { $type: "array", $ne: [] },
|
||||
},
|
||||
},
|
||||
{ $unwind: "$locationResults" },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
location: "$locationResults.location",
|
||||
bucket: { $dateToString: { format: dateString, date: "$createdAt" } },
|
||||
},
|
||||
avgResponseTime: { $avg: "$locationResults.responseTime" },
|
||||
totalChecks: { $sum: 1 },
|
||||
upChecks: { $sum: { $cond: [{ $eq: ["$locationResults.status", true] }, 1, 0] } },
|
||||
},
|
||||
},
|
||||
{ $sort: { "_id.bucket": 1 } },
|
||||
{
|
||||
$group: {
|
||||
_id: "$_id.location",
|
||||
groupedChecks: {
|
||||
$push: {
|
||||
bucketDate: "$_id.bucket",
|
||||
avgResponseTime: "$avgResponseTime",
|
||||
totalChecks: "$totalChecks",
|
||||
},
|
||||
},
|
||||
totalUpChecks: { $sum: "$upChecks" },
|
||||
totalAllChecks: { $sum: "$totalChecks" },
|
||||
overallAvgResponseTime: { $avg: "$avgResponseTime" },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const locationChecks: Record<string, any> = {};
|
||||
for (const loc of results) {
|
||||
const uptimePercentage = loc.totalAllChecks > 0 ? loc.totalUpChecks / loc.totalAllChecks : 0;
|
||||
locationChecks[loc._id] = {
|
||||
groupedChecks: loc.groupedChecks,
|
||||
uptimePercentage,
|
||||
avgResponseTime: loc.overallAvgResponseTime ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
return locationChecks;
|
||||
};
|
||||
|
||||
private findUptimeDateRangeChecks = async (
|
||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
|
||||
monitorObjectId: mongoose.Types.ObjectId,
|
||||
@@ -419,7 +487,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
endDate: Date,
|
||||
dateString: string
|
||||
) => {
|
||||
const matchStage = {
|
||||
const matchStage: Record<string, any> = {
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
createdAt: { $gte: startDate, $lte: endDate },
|
||||
};
|
||||
|
||||
@@ -23,5 +23,7 @@ export interface IIncidentsRepository {
|
||||
// update
|
||||
updateById(incidentId: string, teamId: string, updateData: Partial<Incident>): Promise<Incident>;
|
||||
// delete
|
||||
deleteByMonitorId(monitorId: string, teamId: string): Promise<number>;
|
||||
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
|
||||
// other
|
||||
}
|
||||
@@ -273,5 +273,19 @@ class MongoIncidentRepository implements IIncidentsRepository {
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
deleteByMonitorId = async (monitorId: string, teamId: string) => {
|
||||
const result = await IncidentModel.deleteMany({
|
||||
monitorId: new mongoose.Types.ObjectId(monitorId),
|
||||
teamId: new mongoose.Types.ObjectId(teamId),
|
||||
});
|
||||
return result.deletedCount || 0;
|
||||
};
|
||||
|
||||
deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise<number> => {
|
||||
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
const result = await IncidentModel.deleteMany({ monitorId: { $nin: objectIds } });
|
||||
return result.deletedCount ?? 0;
|
||||
};
|
||||
}
|
||||
export default MongoIncidentRepository;
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { MonitorStats } from "@/types/index.js";
|
||||
export interface IMonitorStatsRepository {
|
||||
// create
|
||||
create(data: Omit<MonitorStats, "id" | "createdAt" | "updatedAt">): Promise<MonitorStats>;
|
||||
// single fetch
|
||||
findByMonitorId(monitorId: string): Promise<MonitorStats>;
|
||||
// update
|
||||
updateByMonitorId(monitorId: string, data: Omit<MonitorStats, "id" | "monitorId" | "createdAt" | "updatedAt">): Promise<MonitorStats>;
|
||||
// delete
|
||||
deleteByMonitorId(monitorId: string): Promise<MonitorStats>;
|
||||
deleteByMonitorIds(monitorIds: string[]): Promise<number>;
|
||||
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
|
||||
// other
|
||||
}
|
||||
@@ -20,6 +20,7 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository {
|
||||
id: toStringId(doc._id),
|
||||
monitorId: toStringId(doc.monitorId),
|
||||
avgResponseTime: doc.avgResponseTime,
|
||||
maxResponseTime: doc.maxResponseTime,
|
||||
totalChecks: doc.totalChecks,
|
||||
totalUpChecks: doc.totalUpChecks,
|
||||
totalDownChecks: doc.totalDownChecks,
|
||||
@@ -32,6 +33,11 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository {
|
||||
};
|
||||
};
|
||||
|
||||
create = async (data: Omit<MonitorStats, "id" | "createdAt" | "updatedAt">): Promise<MonitorStats> => {
|
||||
const created = await MonitorStatsModel.create(data);
|
||||
return this.toEntity(created);
|
||||
};
|
||||
|
||||
findByMonitorId = async (monitorId: string): Promise<MonitorStats> => {
|
||||
const monitorStats = await MonitorStatsModel.findOne({ monitorId: new mongoose.Types.ObjectId(monitorId) });
|
||||
if (!monitorStats) {
|
||||
@@ -40,6 +46,14 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository {
|
||||
return this.toEntity(monitorStats);
|
||||
};
|
||||
|
||||
updateByMonitorId = async (monitorId: string, data: Omit<MonitorStats, "id" | "monitorId" | "createdAt" | "updatedAt">): Promise<MonitorStats> => {
|
||||
const updated = await MonitorStatsModel.findOneAndUpdate({ monitorId: new mongoose.Types.ObjectId(monitorId) }, { $set: data }, { new: true });
|
||||
if (!updated) {
|
||||
throw new AppError({ message: "Monitor stats not found", status: 404 });
|
||||
}
|
||||
return this.toEntity(updated);
|
||||
};
|
||||
|
||||
deleteByMonitorId = async (monitorId: string) => {
|
||||
const deleted = await MonitorStatsModel.findOneAndDelete({ monitorId: new mongoose.Types.ObjectId(monitorId) });
|
||||
if (!deleted) {
|
||||
@@ -47,6 +61,18 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository {
|
||||
}
|
||||
return this.toEntity(deleted);
|
||||
};
|
||||
|
||||
deleteByMonitorIds = async (monitorIds: string[]): Promise<number> => {
|
||||
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
const result = await MonitorStatsModel.deleteMany({ monitorId: { $in: objectIds } });
|
||||
return result.deletedCount ?? 0;
|
||||
};
|
||||
|
||||
deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise<number> => {
|
||||
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
const result = await MonitorStatsModel.deleteMany({ monitorId: { $nin: objectIds } });
|
||||
return result.deletedCount ?? 0;
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoMonitorStatsRepository;
|
||||
@@ -41,4 +41,6 @@ export interface IMonitorsRepository {
|
||||
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>;
|
||||
findGroupsByTeamId(teamId: string): Promise<string[]>;
|
||||
removeNotificationFromMonitors(notificationId: string): Promise<void>;
|
||||
deleteByTeamIdsNotIn(teamIds: string[]): Promise<number>;
|
||||
findAllMonitorIds(): Promise<string[]>;
|
||||
}
|
||||
@@ -252,13 +252,28 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
$cond: [{ $eq: ["$status", "maintenance"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
breachedMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", "breached"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ $project: { _id: 0 } },
|
||||
];
|
||||
|
||||
const [summary] = await MonitorModel.aggregate(pipeline);
|
||||
return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0, initializingMonitors: 0, maintenanceMonitors: 0 };
|
||||
return (
|
||||
summary ?? {
|
||||
totalMonitors: 0,
|
||||
upMonitors: 0,
|
||||
downMonitors: 0,
|
||||
pausedMonitors: 0,
|
||||
initializingMonitors: 0,
|
||||
maintenanceMonitors: 0,
|
||||
breachedMonitors: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
findGroupsByTeamId = async (teamId: string): Promise<string[]> => {
|
||||
@@ -317,15 +332,18 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
uptimePercentage: doc.uptimePercentage ?? undefined,
|
||||
notifications: notificationIds,
|
||||
secret: doc.secret ?? undefined,
|
||||
thresholds: doc.thresholds ?? undefined,
|
||||
alertThreshold: doc.alertThreshold,
|
||||
cpuAlertThreshold: doc.cpuAlertThreshold,
|
||||
cpuAlertCounter: doc.cpuAlertCounter,
|
||||
memoryAlertThreshold: doc.memoryAlertThreshold,
|
||||
memoryAlertCounter: doc.memoryAlertCounter,
|
||||
diskAlertThreshold: doc.diskAlertThreshold,
|
||||
diskAlertCounter: doc.diskAlertCounter,
|
||||
tempAlertThreshold: doc.tempAlertThreshold,
|
||||
tempAlertCounter: doc.tempAlertCounter,
|
||||
selectedDisks: doc.selectedDisks ?? [],
|
||||
gameId: doc.gameId ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
globalpingEnabled: doc.globalpingEnabled ?? false,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
@@ -370,15 +388,18 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
uptimePercentage: doc.uptimePercentage ?? undefined,
|
||||
notifications: notificationIds,
|
||||
secret: doc.secret ?? undefined,
|
||||
thresholds: doc.thresholds ?? undefined,
|
||||
alertThreshold: doc.alertThreshold,
|
||||
cpuAlertThreshold: doc.cpuAlertThreshold,
|
||||
cpuAlertCounter: doc.cpuAlertCounter,
|
||||
memoryAlertThreshold: doc.memoryAlertThreshold,
|
||||
memoryAlertCounter: doc.memoryAlertCounter,
|
||||
diskAlertThreshold: doc.diskAlertThreshold,
|
||||
diskAlertCounter: doc.diskAlertCounter,
|
||||
tempAlertThreshold: doc.tempAlertThreshold,
|
||||
tempAlertCounter: doc.tempAlertCounter,
|
||||
selectedDisks: doc.selectedDisks ?? [],
|
||||
gameId: doc.gameId ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
globalpingEnabled: doc.globalpingEnabled ?? false,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
@@ -412,6 +433,17 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
};
|
||||
};
|
||||
|
||||
deleteByTeamIdsNotIn = async (teamIds: string[]): Promise<number> => {
|
||||
const objectIds = teamIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
const result = await MonitorModel.deleteMany({ teamId: { $nin: objectIds } });
|
||||
return result.deletedCount ?? 0;
|
||||
};
|
||||
|
||||
findAllMonitorIds = async (): Promise<string[]> => {
|
||||
const monitors = await MonitorModel.find({}, { _id: 1 }).lean();
|
||||
return monitors.map((doc) => doc._id.toString());
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoMonitorsRepository;
|
||||
@@ -2,7 +2,6 @@ import mongoose from "mongoose";
|
||||
import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js";
|
||||
import type { Settings } from "@/types/index.js";
|
||||
import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js";
|
||||
import { SERVFAIL } from "dns";
|
||||
|
||||
class MongoSettingsRepository implements ISettingsRepository {
|
||||
private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => {
|
||||
@@ -39,6 +38,8 @@ class MongoSettingsRepository implements ISettingsRepository {
|
||||
systemEmailRequireTLS: doc.systemEmailRequireTLS ?? false,
|
||||
systemEmailRejectUnauthorized: doc.systemEmailRejectUnauthorized ?? true,
|
||||
showURL: doc.showURL ?? false,
|
||||
globalpingApiKey: doc.globalpingApiKey ?? undefined,
|
||||
globalpingLocationsTier: doc.globalpingLocationsTier ?? 6,
|
||||
singleton: doc.singleton,
|
||||
version: doc.version ?? 1,
|
||||
globalThresholds: doc.globalThresholds ?? undefined,
|
||||
@@ -61,22 +62,27 @@ class MongoSettingsRepository implements ISettingsRepository {
|
||||
};
|
||||
|
||||
update = async (settings: Partial<Settings>) => {
|
||||
const update: Record<string, any> = { $set: { ...settings } };
|
||||
const update: Record<string, any> = { $set: {}, $unset: {} };
|
||||
|
||||
if (settings.pagespeedApiKey === "") {
|
||||
update.$unset = { pagespeedApiKey: "" };
|
||||
delete update.$set.pagespeedApiKey;
|
||||
}
|
||||
|
||||
if (settings.systemEmailPassword === "") {
|
||||
update.$unset = { systemEmailPassword: "" };
|
||||
delete update.$set.systemEmailPassword;
|
||||
}
|
||||
|
||||
await AppSettingsModel.findOneAndUpdate({}, update, {
|
||||
upsert: true,
|
||||
// Iterate through settings and separate into $set and $unset
|
||||
Object.entries(settings).forEach(([key, value]) => {
|
||||
if (value === undefined || value === "") {
|
||||
// Unset undefined or empty string values
|
||||
update.$unset[key] = "";
|
||||
} else {
|
||||
// Set defined values
|
||||
update.$set[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up empty operators
|
||||
if (Object.keys(update.$set).length === 0) {
|
||||
delete update.$set;
|
||||
}
|
||||
if (Object.keys(update.$unset).length === 0) {
|
||||
delete update.$unset;
|
||||
}
|
||||
|
||||
const updatedSettings = await AppSettingsModel.findOneAndUpdate({}, update, {
|
||||
upsert: true,
|
||||
new: true,
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface ITeamsRepository {
|
||||
// update
|
||||
// delete
|
||||
// other
|
||||
findAllTeamIds(): Promise<string[]>;
|
||||
}
|
||||
@@ -31,6 +31,11 @@ class MongoTeamsRepository implements ITeamsRepository {
|
||||
const team = await TeamModel.create({ email });
|
||||
return this.toEntity(team);
|
||||
};
|
||||
|
||||
findAllTeamIds = async (): Promise<string[]> => {
|
||||
const teams = await TeamModel.find({}, { _id: 1 }).lean();
|
||||
return teams.map((team) => this.toStringId(team._id));
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoTeamsRepository;
|
||||
@@ -21,6 +21,9 @@ class MonitorRoutes {
|
||||
this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId);
|
||||
this.router.get("/team/groups", this.monitorController.getGroupsByTeamId);
|
||||
|
||||
// Globalping locations
|
||||
this.router.get("/globalping/locations", this.monitorController.getGlobalpingLocations);
|
||||
|
||||
// Uptime routes
|
||||
this.router.get("/uptime/details/:monitorId", this.monitorController.getUptimeDetailsById);
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const SERVICE_NAME = "incidentService";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import type { MonitorStatusResponse } from "@/types/network.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
||||
import type { Incident } from "@/types/index.js";
|
||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||
|
||||
const dateRangeLookup: Record<string, Date | undefined> = {
|
||||
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
|
||||
@@ -21,56 +24,96 @@ class IncidentService {
|
||||
private incidentsRepository: IIncidentsRepository;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private usersRepository: IUsersRepository;
|
||||
private notificationMessageBuilder: INotificationMessageBuilder;
|
||||
|
||||
constructor({
|
||||
logger,
|
||||
incidentsRepository,
|
||||
monitorsRepository,
|
||||
usersRepository,
|
||||
notificationMessageBuilder,
|
||||
}: {
|
||||
logger: any;
|
||||
incidentsRepository: IIncidentsRepository;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
usersRepository: IUsersRepository;
|
||||
notificationMessageBuilder: INotificationMessageBuilder;
|
||||
}) {
|
||||
this.logger = logger;
|
||||
this.incidentsRepository = incidentsRepository;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.usersRepository = usersRepository;
|
||||
this.notificationMessageBuilder = notificationMessageBuilder;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return IncidentService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
handleIncident = async (monitor: Monitor, code: number): Promise<Incident | null> => {
|
||||
handleIncident = async (
|
||||
monitor: Monitor,
|
||||
code: number,
|
||||
decision: MonitorActionDecision,
|
||||
monitorStatusResponse?: MonitorStatusResponse
|
||||
): Promise<Incident | null> => {
|
||||
if (!decision.shouldCreateIncident && !decision.shouldResolveIncident) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeIncident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId);
|
||||
// Monitor is down, create an incident
|
||||
if (monitor.status === "down") {
|
||||
|
||||
if (decision.shouldCreateIncident) {
|
||||
if (activeIncident) {
|
||||
return activeIncident;
|
||||
} else {
|
||||
let statusCode = code;
|
||||
let message: string | undefined;
|
||||
|
||||
// For threshold breaches, use 9999 status code and build descriptive message
|
||||
if (decision.incidentReason === "threshold_breach") {
|
||||
statusCode = 9999;
|
||||
message = this.buildThresholdBreachMessage(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
const incident = {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
startTime: Date.now().toString(),
|
||||
status: true,
|
||||
statusCode: code,
|
||||
statusCode,
|
||||
message,
|
||||
};
|
||||
return await this.incidentsRepository.create(incident);
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor is up, resolve active incidents
|
||||
if (!activeIncident) {
|
||||
return null;
|
||||
if (decision.shouldResolveIncident) {
|
||||
if (!activeIncident) {
|
||||
return null;
|
||||
}
|
||||
activeIncident.status = false;
|
||||
activeIncident.endTime = Date.now().toString();
|
||||
activeIncident.resolutionType = "automatic";
|
||||
return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident);
|
||||
}
|
||||
activeIncident.status = false;
|
||||
activeIncident.endTime = Date.now().toString();
|
||||
activeIncident.resolutionType = "automatic";
|
||||
return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
private buildThresholdBreachMessage(monitor: Monitor, monitorStatusResponse?: MonitorStatusResponse): string {
|
||||
if (!monitorStatusResponse) {
|
||||
return "Threshold breach detected";
|
||||
}
|
||||
|
||||
const breaches = this.notificationMessageBuilder.extractThresholdBreaches(monitor, monitorStatusResponse);
|
||||
|
||||
if (breaches.length === 0) {
|
||||
return "Threshold breach detected";
|
||||
}
|
||||
|
||||
return breaches.map((b) => `${b.metric.toUpperCase()}: ${b.formattedValue} (threshold: ${b.threshold}${b.unit})`).join(", ");
|
||||
}
|
||||
|
||||
resolveIncident = async (incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string) => {
|
||||
try {
|
||||
if (!incidentId) {
|
||||
|
||||
@@ -6,9 +6,16 @@ import type {
|
||||
UptimeDetailsResult,
|
||||
HardwareDetailsResult,
|
||||
PageSpeedDetailsResult,
|
||||
LocationCheckData,
|
||||
GamesMap,
|
||||
} from "@/types/monitor.js";
|
||||
import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js";
|
||||
import type {
|
||||
IChecksRepository,
|
||||
IIncidentsRepository,
|
||||
IMonitorsRepository,
|
||||
IMonitorStatsRepository,
|
||||
IStatusPagesRepository,
|
||||
} from "@/repositories/index.js";
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
@@ -79,6 +86,7 @@ export class MonitorService implements IMonitorService {
|
||||
private checksRepository: IChecksRepository;
|
||||
private monitorStatsRepository: IMonitorStatsRepository;
|
||||
private statusPagesRepository: IStatusPagesRepository;
|
||||
private incidentsRepository: IIncidentsRepository;
|
||||
|
||||
constructor({
|
||||
jobQueue,
|
||||
@@ -89,6 +97,7 @@ export class MonitorService implements IMonitorService {
|
||||
checksRepository,
|
||||
monitorStatsRepository,
|
||||
statusPagesRepository,
|
||||
incidentsRepository,
|
||||
}: {
|
||||
jobQueue: ISuperSimpleQueue;
|
||||
emailService: any;
|
||||
@@ -98,6 +107,7 @@ export class MonitorService implements IMonitorService {
|
||||
checksRepository: IChecksRepository;
|
||||
monitorStatsRepository: IMonitorStatsRepository;
|
||||
statusPagesRepository: IStatusPagesRepository;
|
||||
incidentsRepository: IIncidentsRepository;
|
||||
}) {
|
||||
this.jobQueue = jobQueue;
|
||||
this.emailService = emailService;
|
||||
@@ -107,6 +117,7 @@ export class MonitorService implements IMonitorService {
|
||||
this.checksRepository = checksRepository;
|
||||
this.monitorStatsRepository = monitorStatsRepository;
|
||||
this.statusPagesRepository = statusPagesRepository;
|
||||
this.incidentsRepository = incidentsRepository;
|
||||
}
|
||||
|
||||
get serviceName(): string {
|
||||
@@ -212,6 +223,15 @@ export class MonitorService implements IMonitorService {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
||||
}
|
||||
|
||||
// Fetch location checks if Globalping is enabled for this monitor
|
||||
let locationChecks: Record<string, LocationCheckData> | undefined;
|
||||
if (monitor.globalpingEnabled && (monitor.type === "http" || monitor.type === "ping")) {
|
||||
const locData = await this.checksRepository.findLocationChecksByMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey));
|
||||
if (Object.keys(locData).length > 0) {
|
||||
locationChecks = locData;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
monitorData: {
|
||||
monitor,
|
||||
@@ -220,6 +240,7 @@ export class MonitorService implements IMonitorService {
|
||||
groupedDownChecks: NormalizeDataUptimeDetails(checksData.groupedDownChecks, 10, 100),
|
||||
groupedAvgResponseTime: checksData.avgResponseTime,
|
||||
groupedUptimePercentage: checksData.uptimePercentage,
|
||||
...(locationChecks ? { locationChecks } : {}),
|
||||
},
|
||||
monitorStats,
|
||||
};
|
||||
@@ -418,6 +439,15 @@ export class MonitorService implements IMonitorService {
|
||||
stack: err.stack,
|
||||
});
|
||||
});
|
||||
|
||||
await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId).catch((err: any) => {
|
||||
this.logger.warn({
|
||||
message: `Error deleting incidents for monitor ${monitor.id} with name ${monitor.name}`,
|
||||
service: SERVICE_NAME,
|
||||
stack: err.stack,
|
||||
});
|
||||
});
|
||||
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
@@ -97,6 +97,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
this.scheduler.start();
|
||||
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
||||
this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob());
|
||||
const monitors = await this.monitorsRepository.findAll();
|
||||
if (!monitors) {
|
||||
return true;
|
||||
@@ -108,6 +109,8 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
}, randomOffset);
|
||||
}
|
||||
|
||||
this.scheduler.addJob({ id: "cleanup-orphaned", template: "cleanup-orphaned", active: true });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
|
||||
@@ -2,8 +2,31 @@ const SERVICE_NAME = "JobQueueHelper";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
|
||||
import type { StatusChangeResult, MonitorStatusResponse, HardwareStatusPayload, MonitorStatus } from "@/types/index.js";
|
||||
import IncidentService from "@/service/business/incidentService.js";
|
||||
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||
import type GlobalpingService from "@/service/infrastructure/globalpingService.js";
|
||||
import {
|
||||
IMaintenanceWindowsRepository,
|
||||
IMonitorsRepository,
|
||||
ITeamsRepository,
|
||||
IMonitorStatsRepository,
|
||||
IChecksRepository,
|
||||
IIncidentsRepository,
|
||||
} from "@/repositories/index.js";
|
||||
|
||||
export interface MonitorActionDecision {
|
||||
shouldCreateIncident: boolean;
|
||||
shouldResolveIncident: boolean;
|
||||
shouldSendNotification: boolean;
|
||||
incidentReason: "status_down" | "threshold_breach" | null;
|
||||
notificationReason: "status_change" | "threshold_breach" | null;
|
||||
thresholdBreaches?: {
|
||||
cpu?: boolean;
|
||||
memory?: boolean;
|
||||
disk?: boolean;
|
||||
temp?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
class SuperSimpleQueueHelper {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
@@ -15,8 +38,13 @@ class SuperSimpleQueueHelper {
|
||||
private checkService: any;
|
||||
private buffer: any;
|
||||
private incidentService: IncidentService;
|
||||
private globalpingService: GlobalpingService;
|
||||
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private teamsRepository: ITeamsRepository;
|
||||
private monitorStatsRepository: IMonitorStatsRepository;
|
||||
private checksRepository: IChecksRepository;
|
||||
private incidentsRepository: IIncidentsRepository;
|
||||
|
||||
constructor({
|
||||
logger,
|
||||
@@ -26,8 +54,13 @@ class SuperSimpleQueueHelper {
|
||||
checkService,
|
||||
buffer,
|
||||
incidentService,
|
||||
globalpingService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
teamsRepository,
|
||||
monitorStatsRepository,
|
||||
checksRepository,
|
||||
incidentsRepository,
|
||||
}: {
|
||||
logger: any;
|
||||
networkService: INetworkService;
|
||||
@@ -36,8 +69,13 @@ class SuperSimpleQueueHelper {
|
||||
checkService: any;
|
||||
buffer: any;
|
||||
incidentService: IncidentService;
|
||||
globalpingService: GlobalpingService;
|
||||
maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
teamsRepository: ITeamsRepository;
|
||||
monitorStatsRepository: IMonitorStatsRepository;
|
||||
checksRepository: IChecksRepository;
|
||||
incidentsRepository: IIncidentsRepository;
|
||||
}) {
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
@@ -46,8 +84,13 @@ class SuperSimpleQueueHelper {
|
||||
this.buffer = buffer;
|
||||
this.notificationsService = notificationsService;
|
||||
this.incidentService = incidentService;
|
||||
this.globalpingService = globalpingService;
|
||||
this.maintenanceWindowsRepository = maintenanceWindowsRepository;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.teamsRepository = teamsRepository;
|
||||
this.monitorStatsRepository = monitorStatsRepository;
|
||||
this.checksRepository = checksRepository;
|
||||
this.incidentsRepository = incidentsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -84,18 +127,60 @@ class SuperSimpleQueueHelper {
|
||||
throw new Error("No network response");
|
||||
}
|
||||
|
||||
// Step 3. Build check
|
||||
// Step 3. Build check
|
||||
const check = await this.checkService.buildCheck(status);
|
||||
if (!check) {
|
||||
this.logger.warn({
|
||||
message: `No check could be built for monitor ${monitorId}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
details: { code: status.code, message: status.message },
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Step 4. Add check to buffer
|
||||
this.buffer.addToBuffer(check);
|
||||
|
||||
// Step 4 Add check to buffer
|
||||
this.buffer.addToBuffer({ check });
|
||||
// Step 4. Update monitor status
|
||||
// Step 4b. Run Globalping checks and backfill into the same check document.
|
||||
// Fire-and-forget: Globalping results are supplementary and must not block the
|
||||
// local check pipeline. The dual-write strategy below is safe against mid-flush
|
||||
// races because flushBuffer() replaces the buffer array reference atomically
|
||||
// (this.buffer = []) — if findInBuffer returns a hit, the check is still in the
|
||||
// current buffer and our mutation will be included in the upcoming flush; if it
|
||||
// returns undefined the check has already been flushed, so we fall back to a
|
||||
// direct DB update.
|
||||
if (monitor.globalpingEnabled && (monitor.type === "http" || monitor.type === "ping") && check.id) {
|
||||
const checkId = check.id;
|
||||
this.globalpingService
|
||||
.runChecks(monitor)
|
||||
.then(async (locationResults) => {
|
||||
if (locationResults.length > 0) {
|
||||
const buffered = this.buffer.findInBuffer(checkId);
|
||||
if (buffered) {
|
||||
buffered.locationResults = locationResults;
|
||||
} else {
|
||||
await this.checksRepository.updateLocationResults(checkId, locationResults);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.logger.warn({
|
||||
message: `Globalping checks failed for monitor ${monitorId}: ${err.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Step 5. Update monitor status
|
||||
const statusChangeResult = await this.statusService.updateMonitorStatus(status, check);
|
||||
|
||||
// Step 5 handle notifications (best effort, continue even in event of failure, don't wait)
|
||||
this.notificationsService
|
||||
.handleNotifications(statusChangeResult.monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged)
|
||||
.catch((error: any) => {
|
||||
// Step 6. Get decisions
|
||||
const decision = this.evaluateMonitorAction(statusChangeResult);
|
||||
|
||||
// Step 7. Handle notifications (best effort, continue even in event of failure, don't wait)
|
||||
if (decision.shouldSendNotification) {
|
||||
this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: any) => {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
@@ -104,19 +189,18 @@ class SuperSimpleQueueHelper {
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
|
||||
// Step 6. Handle incidents (best effort, don't wait)
|
||||
if (statusChangeResult.statusChanged) {
|
||||
this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code).catch((error: any) => {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
details: `Error handling incident for job ${monitor.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Step 8. Handle incidents (best effort, don't wait)
|
||||
this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: any) => {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
details: `Error handling incident for job ${monitor.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
} catch (error: any) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
@@ -129,6 +213,87 @@ class SuperSimpleQueueHelper {
|
||||
};
|
||||
};
|
||||
|
||||
getCleanupOrphanedJob = () => {
|
||||
return async () => {
|
||||
try {
|
||||
this.logger.info({
|
||||
message: "Starting cleanup of orphaned data",
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
});
|
||||
|
||||
// Get all valid team IDs
|
||||
const validTeamIds = await this.teamsRepository.findAllTeamIds();
|
||||
this.logger.debug({
|
||||
message: `Found ${validTeamIds.length} valid teams`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
});
|
||||
|
||||
// Remove orphaned monitors (monitors without a valid team)
|
||||
const deletedMonitorCount = await this.monitorsRepository.deleteByTeamIdsNotIn(validTeamIds);
|
||||
if (deletedMonitorCount > 0) {
|
||||
this.logger.info({
|
||||
message: `Deleted ${deletedMonitorCount} orphaned monitors`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
});
|
||||
}
|
||||
|
||||
// Remove orphaned monitorStats (stats without a valid monitor)
|
||||
const allMonitorIds = await this.monitorsRepository.findAllMonitorIds();
|
||||
this.logger.debug({
|
||||
message: `Found ${allMonitorIds.length} valid monitors`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
});
|
||||
|
||||
const deletedStatsCount = await this.monitorStatsRepository.deleteByMonitorIdsNotIn(allMonitorIds);
|
||||
if (deletedStatsCount > 0) {
|
||||
this.logger.info({
|
||||
message: `Deleted ${deletedStatsCount} orphaned monitor stats`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
});
|
||||
}
|
||||
|
||||
// Remove orphaned checks
|
||||
const deletedChecksCount = await this.checksRepository.deleteByMonitorIdsNotIn(allMonitorIds);
|
||||
if (deletedChecksCount > 0) {
|
||||
this.logger.info({
|
||||
message: `Deleted ${deletedChecksCount} orphaned checks`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
});
|
||||
}
|
||||
|
||||
// Remove orphaned incidents
|
||||
const deletedIncidentsCount = await this.incidentsRepository.deleteByMonitorIdsNotIn(allMonitorIds);
|
||||
if (deletedIncidentsCount > 0) {
|
||||
this.logger.info({
|
||||
message: `Deleted ${deletedIncidentsCount} orphaned incidents`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.info({
|
||||
message: "Cleanup of orphaned data completed",
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
});
|
||||
} catch (error: any) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
async isInMaintenanceWindow(monitorId: string, teamId: string) {
|
||||
const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
|
||||
// Check for active maintenance window:
|
||||
@@ -158,6 +323,44 @@ class SuperSimpleQueueHelper {
|
||||
}, false);
|
||||
return maintenanceWindowIsActive;
|
||||
}
|
||||
|
||||
private evaluateMonitorAction(statusChangeResult: StatusChangeResult): MonitorActionDecision {
|
||||
const { monitor, statusChanged, prevStatus } = statusChangeResult;
|
||||
|
||||
// Initialize result
|
||||
const decision: MonitorActionDecision = {
|
||||
shouldCreateIncident: false,
|
||||
shouldResolveIncident: false,
|
||||
shouldSendNotification: false,
|
||||
incidentReason: null,
|
||||
notificationReason: null,
|
||||
};
|
||||
|
||||
if (!statusChanged) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
if (monitor.status === "down") {
|
||||
// Monitor went down (unreachable)
|
||||
decision.shouldCreateIncident = true;
|
||||
decision.shouldSendNotification = true;
|
||||
decision.incidentReason = "status_down";
|
||||
decision.notificationReason = "status_change";
|
||||
} else if (monitor.status === "breached") {
|
||||
// Hardware monitor exceeded thresholds
|
||||
decision.shouldCreateIncident = true;
|
||||
decision.shouldSendNotification = true;
|
||||
decision.incidentReason = "threshold_breach";
|
||||
decision.notificationReason = "threshold_breach";
|
||||
} else if (monitor.status === "up" && (prevStatus === "down" || prevStatus === "breached")) {
|
||||
// Monitor recovered from down or breached state
|
||||
decision.shouldResolveIncident = true;
|
||||
decision.shouldSendNotification = true;
|
||||
decision.notificationReason = "status_change";
|
||||
}
|
||||
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
|
||||
export default SuperSimpleQueueHelper;
|
||||
@@ -2,7 +2,15 @@ import type { Check } from "@/types/index.js";
|
||||
|
||||
const SERVICE_NAME = "BufferService";
|
||||
|
||||
class BufferService {
|
||||
export interface IBufferService {
|
||||
addToBuffer(check: Check): void;
|
||||
removeCheckFromBuffer(check: Check): boolean;
|
||||
findInBuffer(checkId: string): Check | undefined;
|
||||
scheduleNextFlush(): void;
|
||||
flushBuffer(): Promise<void>;
|
||||
}
|
||||
|
||||
class BufferService implements IBufferService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private BUFFER_TIMEOUT: number;
|
||||
private logger: any;
|
||||
@@ -13,7 +21,6 @@ class BufferService {
|
||||
|
||||
constructor({ logger, checkService, settingsService }: { logger: any; checkService: any; settingsService: any }) {
|
||||
this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute
|
||||
console.log(this.BUFFER_TIMEOUT);
|
||||
this.logger = logger;
|
||||
this.checksService = checkService;
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
@@ -30,7 +37,7 @@ class BufferService {
|
||||
return BufferService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
addToBuffer({ check }: { check: Check }) {
|
||||
addToBuffer(check: Check) {
|
||||
try {
|
||||
this.buffer.push(check);
|
||||
} catch (error: any) {
|
||||
@@ -43,6 +50,10 @@ class BufferService {
|
||||
}
|
||||
}
|
||||
|
||||
findInBuffer(checkId: string): Check | undefined {
|
||||
return this.buffer.find((c: Check) => c.id?.toString() === checkId);
|
||||
}
|
||||
|
||||
removeCheckFromBuffer(checkToRemove: Check) {
|
||||
try {
|
||||
if (!checkToRemove) {
|
||||
@@ -54,9 +65,9 @@ class BufferService {
|
||||
return check.id.toString() === checkToRemove.id.toString();
|
||||
}
|
||||
return (
|
||||
check.monitorId?.toString() === checkToRemove.metadata.monitorId &&
|
||||
check.teamId?.toString() === checkToRemove.metadata.teamId &&
|
||||
check.type === checkToRemove.metadata.type &&
|
||||
check.metadata.monitorId?.toString() === checkToRemove.metadata.monitorId &&
|
||||
check.metadata.teamId?.toString() === checkToRemove.metadata.teamId &&
|
||||
check.metadata.type === checkToRemove.metadata.type &&
|
||||
check.status === checkToRemove.status &&
|
||||
check.statusCode === checkToRemove.statusCode &&
|
||||
check.responseTime === checkToRemove.responseTime &&
|
||||
|
||||
@@ -60,11 +60,9 @@ class EmailService {
|
||||
welcomeEmailTemplate: this.loadTemplate("welcomeEmail"),
|
||||
employeeActivationTemplate: this.loadTemplate("employeeActivation"),
|
||||
noIncidentsThisWeekTemplate: this.loadTemplate("noIncidentsThisWeek"),
|
||||
serverIsDownTemplate: this.loadTemplate("serverIsDown"),
|
||||
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
|
||||
passwordResetTemplate: this.loadTemplate("passwordReset"),
|
||||
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
|
||||
testEmailTemplate: this.loadTemplate("testEmailTemplate"),
|
||||
unifiedNotificationTemplate: this.loadTemplate("unifiedNotification"),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import { Globalping } from "globalping";
|
||||
import type { ISettingsService } from "@/service/system/settingsService.js";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import type { GlobalpingCheckResult } from "@/types/network.js";
|
||||
|
||||
const SERVICE_NAME = "GlobalpingService";
|
||||
|
||||
export interface GlobalpingLocationOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Maps location IDs to Globalping API location objects
|
||||
const LOCATION_MAP: Record<string, Record<string, string>> = {
|
||||
"north-america": { continent: "NA" },
|
||||
europe: { continent: "EU" },
|
||||
asia: { continent: "AS" },
|
||||
"south-america": { continent: "SA" },
|
||||
oceania: { continent: "OC" },
|
||||
africa: { continent: "AF" },
|
||||
"us-east": { country: "US", state: "VA" },
|
||||
"us-west": { country: "US", state: "CA" },
|
||||
canada: { country: "CA", city: "Toronto" },
|
||||
uk: { country: "GB", city: "London" },
|
||||
germany: { country: "DE", city: "Frankfurt" },
|
||||
netherlands: { country: "NL", city: "Amsterdam" },
|
||||
poland: { country: "PL", city: "Warsaw" },
|
||||
japan: { country: "JP", city: "Tokyo" },
|
||||
singapore: { country: "SG", city: "Singapore" },
|
||||
india: { country: "IN", city: "Mumbai" },
|
||||
australia: { country: "AU", city: "Sydney" },
|
||||
brazil: { country: "BR" },
|
||||
chile: { country: "CL", city: "Santiago" },
|
||||
"south-africa": { country: "ZA", city: "Johannesburg" },
|
||||
uae: { country: "AE", city: "Dubai" },
|
||||
};
|
||||
|
||||
const LOCATIONS_TIER_3: GlobalpingLocationOption[] = [
|
||||
{ id: "north-america", label: "North America" },
|
||||
{ id: "europe", label: "Europe" },
|
||||
{ id: "asia", label: "Asia" },
|
||||
];
|
||||
|
||||
const LOCATIONS_TIER_6: GlobalpingLocationOption[] = [
|
||||
...LOCATIONS_TIER_3,
|
||||
{ id: "south-america", label: "South America" },
|
||||
{ id: "oceania", label: "Oceania" },
|
||||
{ id: "africa", label: "Africa" },
|
||||
];
|
||||
|
||||
const LOCATIONS_TIER_15: GlobalpingLocationOption[] = [
|
||||
{ id: "us-east", label: "US East (Virginia)" },
|
||||
{ id: "us-west", label: "US West (California)" },
|
||||
{ id: "canada", label: "Canada (Toronto)" },
|
||||
{ id: "uk", label: "UK (London)" },
|
||||
{ id: "germany", label: "Germany (Frankfurt)" },
|
||||
{ id: "netherlands", label: "Netherlands (Amsterdam)" },
|
||||
{ id: "poland", label: "Poland (Warsaw)" },
|
||||
{ id: "japan", label: "Japan (Tokyo)" },
|
||||
{ id: "singapore", label: "Singapore" },
|
||||
{ id: "india", label: "India (Mumbai)" },
|
||||
{ id: "australia", label: "Australia (Sydney)" },
|
||||
{ id: "brazil", label: "Brazil" },
|
||||
{ id: "chile", label: "Chile (Santiago)" },
|
||||
{ id: "south-africa", label: "South Africa (Johannesburg)" },
|
||||
{ id: "uae", label: "UAE (Dubai)" },
|
||||
];
|
||||
|
||||
const LOCATIONS_BY_TIER: Record<number, GlobalpingLocationOption[]> = {
|
||||
3: LOCATIONS_TIER_3,
|
||||
6: LOCATIONS_TIER_6,
|
||||
15: LOCATIONS_TIER_15,
|
||||
};
|
||||
|
||||
// Build a lookup from location ID to human-readable label
|
||||
const LOCATION_LABELS: Record<string, string> = {};
|
||||
for (const tier of Object.values(LOCATIONS_BY_TIER)) {
|
||||
for (const loc of tier) {
|
||||
LOCATION_LABELS[loc.id] = loc.label;
|
||||
}
|
||||
}
|
||||
|
||||
const HOSTNAME_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
|
||||
|
||||
export const getGlobalpingLocationsByTier = (tier: number): GlobalpingLocationOption[] => {
|
||||
return LOCATIONS_BY_TIER[tier] ?? LOCATIONS_TIER_6;
|
||||
};
|
||||
|
||||
class GlobalpingService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: any;
|
||||
private settingsService: ISettingsService;
|
||||
private client: Globalping<false> | null = null;
|
||||
private lastApiKey: string | undefined;
|
||||
private inFlightChecks = new Set<string>();
|
||||
|
||||
constructor({ settingsService, logger }: { settingsService: ISettingsService; logger: any }) {
|
||||
this.settingsService = settingsService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return GlobalpingService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads settings once, returns both the SDK client and configured location IDs.
|
||||
* Returns null client if the API key is not configured.
|
||||
*/
|
||||
private async getClientAndLocations(): Promise<{
|
||||
client: Globalping<false> | null;
|
||||
locationIds: string[];
|
||||
}> {
|
||||
const dbSettings = await this.settingsService.getDBSettings();
|
||||
const apiKey = dbSettings.globalpingApiKey;
|
||||
|
||||
if (!apiKey) {
|
||||
return { client: null, locationIds: [] };
|
||||
}
|
||||
|
||||
if (!this.client || this.lastApiKey !== apiKey) {
|
||||
this.client = new Globalping({ auth: apiKey, throwApiErrors: false, timeout: 30000 });
|
||||
this.lastApiKey = apiKey;
|
||||
}
|
||||
|
||||
const tier = dbSettings.globalpingLocationsTier ?? 6;
|
||||
const locationIds = getGlobalpingLocationsByTier(tier).map((loc) => loc.id);
|
||||
|
||||
return { client: this.client, locationIds };
|
||||
}
|
||||
|
||||
async runChecks(monitor: Monitor): Promise<GlobalpingCheckResult[]> {
|
||||
if (monitor.type !== "http" && monitor.type !== "ping") {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Prevent overlapping checks for the same monitor
|
||||
if (this.inFlightChecks.has(monitor.id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.inFlightChecks.add(monitor.id);
|
||||
try {
|
||||
return await this.executeChecks(monitor);
|
||||
} finally {
|
||||
this.inFlightChecks.delete(monitor.id);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeChecks(monitor: Monitor): Promise<GlobalpingCheckResult[]> {
|
||||
const { client, locationIds } = await this.getClientAndLocations();
|
||||
if (!client || locationIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const measurementType = monitor.type === "ping" ? "ping" : "http";
|
||||
const monitorUrl = monitor.url ?? "";
|
||||
|
||||
let target: string;
|
||||
try {
|
||||
if (measurementType === "http") {
|
||||
const url = new URL(monitorUrl);
|
||||
target = url.hostname;
|
||||
} else {
|
||||
target = monitorUrl.replace(/^https?:\/\//, "").split("/")[0] ?? monitorUrl;
|
||||
}
|
||||
} catch {
|
||||
target = monitorUrl;
|
||||
}
|
||||
|
||||
if (!target || !HOSTNAME_REGEX.test(target)) {
|
||||
this.logger.warn({
|
||||
message: `Invalid target hostname for Globalping: "${target}"`,
|
||||
service: SERVICE_NAME,
|
||||
method: "executeChecks",
|
||||
monitorId: monitor.id,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
const locations = locationIds.map((loc) => LOCATION_MAP[loc]).filter(Boolean);
|
||||
|
||||
if (locations.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const createResult =
|
||||
measurementType === "http"
|
||||
? await client.createMeasurement({
|
||||
type: "http" as const,
|
||||
target,
|
||||
locations: locations as any,
|
||||
measurementOptions: { request: { method: "HEAD" }, protocol: "HTTPS" },
|
||||
})
|
||||
: await client.createMeasurement({
|
||||
type: "ping" as const,
|
||||
target,
|
||||
locations: locations as any,
|
||||
});
|
||||
|
||||
if (!createResult.ok) {
|
||||
this.logger.warn({
|
||||
message: "Failed to create Globalping measurement",
|
||||
service: SERVICE_NAME,
|
||||
method: "executeChecks",
|
||||
details: JSON.stringify(createResult.data),
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
const measurementId = (createResult.data as any).id;
|
||||
const awaitResult = await client.awaitMeasurement(measurementId);
|
||||
|
||||
if (!awaitResult.ok) {
|
||||
this.logger.warn({
|
||||
message: "Failed to await Globalping measurement",
|
||||
service: SERVICE_NAME,
|
||||
method: "executeChecks",
|
||||
details: JSON.stringify(awaitResult.data),
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
const measurement = awaitResult.data as any;
|
||||
const results: GlobalpingCheckResult[] = [];
|
||||
|
||||
for (const item of measurement.results || []) {
|
||||
const probe = item.probe;
|
||||
const result = item.result;
|
||||
const locationId = this.resolveLocationId(probe?.continent, probe?.country, locationIds);
|
||||
|
||||
if (measurementType === "http") {
|
||||
results.push({
|
||||
location: locationId,
|
||||
status: result.statusCode >= 200 && result.statusCode < 400,
|
||||
statusCode: result.statusCode || 0,
|
||||
message: result.statusCodeName || result.rawOutput || "",
|
||||
responseTime: result.timings?.total ?? 0,
|
||||
});
|
||||
} else {
|
||||
// Ping: loss < 100% is considered up (consistent with local ping checks)
|
||||
const stats = result.stats;
|
||||
results.push({
|
||||
location: locationId,
|
||||
status: stats?.loss !== undefined ? stats.loss < 100 : false,
|
||||
statusCode: stats?.loss === 0 ? 200 : 5000,
|
||||
message: stats?.loss === 0 ? "Ping successful" : `Packet loss: ${stats?.loss}%`,
|
||||
responseTime: stats?.avg ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error: any) {
|
||||
this.logger.warn({
|
||||
message: `Globalping check failed: ${error.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "executeChecks",
|
||||
stack: error.stack,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a probe's continent/country back to a configured location ID.
|
||||
* For city-level tiers, matches by country code.
|
||||
* For continent-level tiers, matches by continent code.
|
||||
*/
|
||||
private resolveLocationId(continent?: string, country?: string, configuredIds?: string[]): string {
|
||||
if (!configuredIds || configuredIds.length === 0) {
|
||||
return continent && country ? `${continent}-${country}` : continent || country || "unknown";
|
||||
}
|
||||
|
||||
// Try country-level match first (for tier 15)
|
||||
if (country) {
|
||||
for (const id of configuredIds) {
|
||||
const loc = LOCATION_MAP[id];
|
||||
if (loc?.country === country) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to continent-level match (for tier 3/6)
|
||||
if (continent) {
|
||||
for (const id of configuredIds) {
|
||||
const loc = LOCATION_MAP[id];
|
||||
if (loc?.continent === continent) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return continent && country ? `${continent}-${country}` : continent || country || "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export { LOCATION_LABELS };
|
||||
export default GlobalpingService;
|
||||
@@ -0,0 +1,285 @@
|
||||
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
|
||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import type {
|
||||
NotificationMessage,
|
||||
NotificationType,
|
||||
NotificationSeverity,
|
||||
ThresholdBreach,
|
||||
NotificationContent,
|
||||
} from "@/types/notificationMessage.js";
|
||||
|
||||
export interface INotificationMessageBuilder {
|
||||
buildMessage(
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
decision: MonitorActionDecision,
|
||||
clientHost: string
|
||||
): NotificationMessage;
|
||||
extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): ThresholdBreach[];
|
||||
}
|
||||
|
||||
const SERVICE_NAME = "NotificationMessageBuilder";
|
||||
|
||||
export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
buildMessage(
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
decision: MonitorActionDecision,
|
||||
clientHost: string
|
||||
): NotificationMessage {
|
||||
const type = this.determineNotificationType(decision, monitor);
|
||||
const severity = this.determineSeverity(type, monitor.status);
|
||||
const content = this.buildContent(type, monitor, monitorStatusResponse, decision, clientHost);
|
||||
|
||||
return {
|
||||
type,
|
||||
severity,
|
||||
monitor: {
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
type: monitor.type,
|
||||
status: monitor.status,
|
||||
},
|
||||
content,
|
||||
clientHost,
|
||||
metadata: {
|
||||
teamId: monitor.teamId,
|
||||
notificationReason: decision.notificationReason || "status_change",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private determineNotificationType(decision: MonitorActionDecision, monitor: Monitor): NotificationType {
|
||||
// Down status has highest priority (critical)
|
||||
if (monitor.status === "down") {
|
||||
return "monitor_down";
|
||||
}
|
||||
|
||||
// Threshold breach (only if not down)
|
||||
if (decision.notificationReason === "threshold_breach") {
|
||||
return "threshold_breach";
|
||||
}
|
||||
|
||||
// Recovery from threshold breach (only for hardware monitors)
|
||||
if (decision.notificationReason === "status_change" && monitor.status === "up" && monitor.type === "hardware") {
|
||||
return "threshold_resolved";
|
||||
}
|
||||
|
||||
// Standard recovery (up)
|
||||
if (monitor.status === "up") {
|
||||
return "monitor_up";
|
||||
}
|
||||
|
||||
// Default to monitor_up for any other case
|
||||
return "monitor_up";
|
||||
}
|
||||
|
||||
private determineSeverity(type: NotificationType, monitorStatus: string): NotificationSeverity {
|
||||
switch (type) {
|
||||
case "monitor_down":
|
||||
return "critical";
|
||||
case "threshold_breach":
|
||||
return "warning";
|
||||
case "monitor_up":
|
||||
case "threshold_resolved":
|
||||
return "success";
|
||||
case "test":
|
||||
return "info";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
}
|
||||
|
||||
private buildContent(
|
||||
type: NotificationType,
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
decision: MonitorActionDecision,
|
||||
clientHost: string
|
||||
): NotificationContent {
|
||||
switch (type) {
|
||||
case "monitor_down":
|
||||
return this.buildMonitorDownContent(monitor, monitorStatusResponse, clientHost);
|
||||
case "monitor_up":
|
||||
return this.buildMonitorUpContent(monitor, clientHost);
|
||||
case "threshold_breach":
|
||||
return this.buildThresholdBreachContent(monitor, monitorStatusResponse, decision, clientHost);
|
||||
case "threshold_resolved":
|
||||
return this.buildThresholdResolvedContent(monitor, clientHost);
|
||||
default:
|
||||
return this.buildDefaultContent(monitor, clientHost);
|
||||
}
|
||||
}
|
||||
|
||||
private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, clientHost: string): NotificationContent {
|
||||
const title = `Monitor Down: ${monitor.name}`;
|
||||
const summary = `Monitor "${monitor.name}" is currently down and unreachable.`;
|
||||
const details = [`URL: ${monitor.url}`, `Status: Down`, `Type: ${monitor.type}`];
|
||||
|
||||
// Add response code if available
|
||||
if (monitorStatusResponse.code) {
|
||||
details.push(`Response Code: ${monitorStatusResponse.code}`);
|
||||
}
|
||||
|
||||
// Add error message if available
|
||||
if (monitorStatusResponse.message) {
|
||||
details.push(`Error: ${monitorStatusResponse.message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
summary,
|
||||
details,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
private buildMonitorUpContent(monitor: Monitor, clientHost: string): NotificationContent {
|
||||
const title = `Monitor Recovered: ${monitor.name}`;
|
||||
const summary = `Monitor "${monitor.name}" is back up and operational.`;
|
||||
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
|
||||
|
||||
return {
|
||||
title,
|
||||
summary,
|
||||
details,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
private buildThresholdBreachContent(
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
decision: MonitorActionDecision,
|
||||
clientHost: string
|
||||
): NotificationContent {
|
||||
const title = `Threshold Breach: ${monitor.name}`;
|
||||
const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`;
|
||||
const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`];
|
||||
|
||||
const thresholds = this.extractThresholdBreaches(monitor, monitorStatusResponse);
|
||||
|
||||
return {
|
||||
title,
|
||||
summary,
|
||||
details,
|
||||
thresholds,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
private buildThresholdResolvedContent(monitor: Monitor, clientHost: string): NotificationContent {
|
||||
const title = `Thresholds Resolved: ${monitor.name}`;
|
||||
const summary = `Monitor "${monitor.name}" thresholds have returned to normal.`;
|
||||
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
|
||||
|
||||
return {
|
||||
title,
|
||||
summary,
|
||||
details,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
private buildDefaultContent(monitor: Monitor, clientHost: string): NotificationContent {
|
||||
return {
|
||||
title: `Monitor: ${monitor.name}`,
|
||||
summary: `Status update for monitor "${monitor.name}".`,
|
||||
details: [`URL: ${monitor.url}`, `Status: ${monitor.status}`, `Type: ${monitor.type}`],
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
public extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): ThresholdBreach[] {
|
||||
const breaches: ThresholdBreach[] = [];
|
||||
|
||||
// Check if this is a hardware monitor with threshold data
|
||||
if (monitor.type !== "hardware" || !monitorStatusResponse.payload) {
|
||||
return breaches;
|
||||
}
|
||||
|
||||
// Cast to HardwareStatusPayload type
|
||||
const payload = monitorStatusResponse.payload as { data?: any };
|
||||
const hardware = payload.data;
|
||||
|
||||
if (!hardware) {
|
||||
return breaches;
|
||||
}
|
||||
|
||||
// Note: usage_percent values in hardware payload are decimals (0-1)
|
||||
if (monitor.cpuAlertThreshold !== undefined && monitor.cpuAlertThreshold !== null && hardware.cpu?.usage_percent !== undefined) {
|
||||
const cpuUsageDecimal = hardware.cpu.usage_percent;
|
||||
const cpuPercent = cpuUsageDecimal * 100;
|
||||
const threshold = monitor.cpuAlertThreshold;
|
||||
if (cpuPercent > threshold) {
|
||||
breaches.push({
|
||||
metric: "cpu",
|
||||
currentValue: cpuPercent,
|
||||
threshold,
|
||||
unit: "%",
|
||||
formattedValue: `${cpuPercent.toFixed(1)}%`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Memory threshold breach
|
||||
if (monitor.memoryAlertThreshold !== undefined && monitor.memoryAlertThreshold !== null && hardware.memory?.usage_percent !== undefined) {
|
||||
const memoryUsageDecimal = hardware.memory.usage_percent;
|
||||
const memoryPercent = memoryUsageDecimal * 100;
|
||||
const threshold = monitor.memoryAlertThreshold;
|
||||
if (memoryPercent > threshold) {
|
||||
breaches.push({
|
||||
metric: "memory",
|
||||
currentValue: memoryPercent,
|
||||
threshold,
|
||||
unit: "%",
|
||||
formattedValue: `${memoryPercent.toFixed(1)}%`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Disk threshold breach
|
||||
if (monitor.diskAlertThreshold !== undefined && monitor.diskAlertThreshold !== null && Array.isArray(hardware.disk)) {
|
||||
// Find the highest disk usage
|
||||
let maxDiskUsageDecimal = 0;
|
||||
for (const disk of hardware.disk) {
|
||||
if (disk.usage_percent !== undefined && disk.usage_percent > maxDiskUsageDecimal) {
|
||||
maxDiskUsageDecimal = disk.usage_percent;
|
||||
}
|
||||
}
|
||||
const maxDiskPercent = maxDiskUsageDecimal * 100;
|
||||
const threshold = monitor.diskAlertThreshold;
|
||||
if (maxDiskPercent > threshold) {
|
||||
breaches.push({
|
||||
metric: "disk",
|
||||
currentValue: maxDiskPercent,
|
||||
threshold,
|
||||
unit: "%",
|
||||
formattedValue: `${maxDiskPercent.toFixed(1)}%`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature threshold breach
|
||||
if (monitor.tempAlertThreshold !== undefined && monitor.tempAlertThreshold !== null && hardware.cpu?.temperature) {
|
||||
// Temperature is an array in cpu.temperature
|
||||
const temps = Array.isArray(hardware.cpu.temperature) ? hardware.cpu.temperature : [hardware.cpu.temperature];
|
||||
const maxTemp = Math.max(...temps.filter((t: number) => !isNaN(t)));
|
||||
const threshold = monitor.tempAlertThreshold;
|
||||
if (maxTemp >= threshold) {
|
||||
breaches.push({
|
||||
metric: "temp",
|
||||
currentValue: maxTemp,
|
||||
threshold,
|
||||
unit: "°C",
|
||||
formattedValue: `${maxTemp.toFixed(1)}°C`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return breaches;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js";
|
||||
import type { Notification } from "@/types/index.js";
|
||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
|
||||
export interface INotificationProvider {
|
||||
sendAlert: (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise<boolean>;
|
||||
sendMessage: (notification: Notification, message: NotificationMessage) => Promise<boolean>;
|
||||
sendTestAlert(notification: Notification): Promise<boolean>;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
const SERVICE_NAME = "DiscordProvider";
|
||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
||||
import type { Notification } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
import got from "got";
|
||||
|
||||
export class DiscordProvider implements INotificationProvider {
|
||||
@@ -10,42 +11,7 @@ export class DiscordProvider implements INotificationProvider {
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
return discordPayload;
|
||||
};
|
||||
|
||||
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
let body;
|
||||
if (monitor.type === "hardware") {
|
||||
body = this.getHardwareContent(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
body = buildDiscordBody(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: body,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "Discord alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
sendTestAlert = async (notification: Notification) => {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
@@ -69,4 +35,110 @@ export class DiscordProvider implements INotificationProvider {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* New unified message format implementation
|
||||
*/
|
||||
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||
if (!notification.address) {
|
||||
this.logger.warn({
|
||||
message: "Discord notification missing webhook URL",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const embed = this.buildDiscordEmbed(message);
|
||||
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: { embeds: [embed] },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "Discord notification failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private buildDiscordEmbed(message: NotificationMessage): any {
|
||||
// Map severity to Discord embed colors
|
||||
const colorMap: Record<string, number> = {
|
||||
critical: 0xdc2626, // red-600
|
||||
warning: 0xf59e0b, // amber-500
|
||||
info: 0x3b82f6, // blue-500
|
||||
success: 0x10b981, // green-500
|
||||
};
|
||||
|
||||
const color = colorMap[message.severity] || colorMap.info;
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
// Add monitor details
|
||||
fields.push({
|
||||
name: "Monitor",
|
||||
value: message.monitor.name,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
fields.push({
|
||||
name: "Type",
|
||||
value: message.monitor.type.toUpperCase(),
|
||||
inline: true,
|
||||
});
|
||||
|
||||
fields.push({
|
||||
name: "Status",
|
||||
value: message.monitor.status.charAt(0).toUpperCase() + message.monitor.status.slice(1),
|
||||
inline: true,
|
||||
});
|
||||
|
||||
// Add monitor URL
|
||||
fields.push({
|
||||
name: "URL",
|
||||
value: message.monitor.url,
|
||||
inline: false,
|
||||
});
|
||||
|
||||
// Add threshold breaches if present
|
||||
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||
const thresholdLines = message.content.thresholds
|
||||
.map((t) => `• **${t.metric.toUpperCase()}**: ${t.formattedValue} (threshold: ${t.threshold}${t.unit})`)
|
||||
.join("\n");
|
||||
|
||||
fields.push({
|
||||
name: "Threshold Breaches",
|
||||
value: thresholdLines,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Add details if present
|
||||
if (message.content.details && message.content.details.length > 0) {
|
||||
const detailsText = message.content.details.join("\n");
|
||||
fields.push({
|
||||
name: "Details",
|
||||
value: detailsText,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
title: message.content.title,
|
||||
description: message.content.summary,
|
||||
color,
|
||||
fields,
|
||||
timestamp: message.content.timestamp.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
const SERVICE_NAME = "EmailProvider";
|
||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import { buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
|
||||
export class EmailProvider implements INotificationProvider {
|
||||
private emailService: any;
|
||||
@@ -12,41 +14,6 @@ export class EmailProvider implements INotificationProvider {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
const html = buildHardwareEmail(this.emailService, monitor, alertsToSend);
|
||||
return html;
|
||||
};
|
||||
|
||||
async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> {
|
||||
// For grouped notifications (identified by ":" in name), customize subject to indicate multiple services.
|
||||
// Example: "2 services: Service A, Service B" becomes "Alert: 2 services are down"
|
||||
const isGroupedNotification = monitor.name.includes(":");
|
||||
const subject = isGroupedNotification ? `Alert: ${monitor.name} are down` : `Monitor ${monitor.name} is down`;
|
||||
|
||||
let html;
|
||||
if (monitor.type === "hardware") {
|
||||
html = this.buildHardwareEmail(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
html = await buildEmail(this.emailService, monitor);
|
||||
}
|
||||
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
|
||||
if (!messageId) {
|
||||
this.logger.warn({
|
||||
message: "Email alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendAlert",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
||||
const subject = "Test notification";
|
||||
const html = await buildTestEmail(this.emailService);
|
||||
@@ -62,4 +29,85 @@ export class EmailProvider implements INotificationProvider {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const subject = this.buildSubject(message);
|
||||
const html = await this.buildEmailFromMessage(message);
|
||||
|
||||
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
|
||||
if (!messageId) {
|
||||
this.logger.warn({
|
||||
message: "Email notification failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private buildSubject(message: NotificationMessage): string {
|
||||
switch (message.type) {
|
||||
case "monitor_down":
|
||||
return `Monitor ${message.monitor.name} is down`;
|
||||
case "monitor_up":
|
||||
return `Monitor ${message.monitor.name} is back up`;
|
||||
case "threshold_breach":
|
||||
return `Monitor ${message.monitor.name} threshold breached`;
|
||||
case "threshold_resolved":
|
||||
return `Monitor ${message.monitor.name} thresholds resolved`;
|
||||
default:
|
||||
return `Alert: ${message.monitor.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async buildEmailFromMessage(message: NotificationMessage): Promise<string> {
|
||||
const context = {
|
||||
title: message.content.title,
|
||||
summary: message.content.summary,
|
||||
monitorName: message.monitor.name,
|
||||
monitorUrl: message.monitor.url,
|
||||
monitorType: message.monitor.type,
|
||||
monitorStatus: message.monitor.status,
|
||||
headerColor: this.getColorForSeverity(message.severity),
|
||||
thresholds: message.content.thresholds,
|
||||
details: message.content.details,
|
||||
incidentUrl: message.content.incident?.url,
|
||||
};
|
||||
|
||||
this.logger.info({
|
||||
message: "[DEBUG] Building email from message",
|
||||
service: SERVICE_NAME,
|
||||
method: "buildEmailFromMessage",
|
||||
details: { context },
|
||||
});
|
||||
|
||||
const html = await this.emailService.buildEmail("unifiedNotificationTemplate", context);
|
||||
|
||||
this.logger.info({
|
||||
message: "[DEBUG] Email HTML generated",
|
||||
service: SERVICE_NAME,
|
||||
method: "buildEmailFromMessage",
|
||||
details: {
|
||||
htmlLength: html?.length || 0,
|
||||
htmlPreview: html?.substring(0, 200),
|
||||
},
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
private getColorForSeverity(severity: string): string {
|
||||
const colorMap: Record<string, string> = {
|
||||
critical: "red",
|
||||
warning: "#f59e0b",
|
||||
info: "#3b82f6",
|
||||
success: "green",
|
||||
};
|
||||
return colorMap[severity] ?? "#3b82f6";
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
const SERVICE_NAME = "MatrixProvider";
|
||||
import got from "got";
|
||||
import type { INotificationProvider } from "@/service/index.js";
|
||||
import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js";
|
||||
import {
|
||||
buildHardwareAlerts,
|
||||
buildHardwareWebhookBody,
|
||||
buildWebhookBody,
|
||||
getTestMessage,
|
||||
} from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import type { Notification } from "@/types/index.js";
|
||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
|
||||
export class MatrixProvider implements INotificationProvider {
|
||||
private logger: any;
|
||||
@@ -15,57 +11,6 @@ export class MatrixProvider implements INotificationProvider {
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
const body = buildHardwareWebhookBody(alertsToSend, monitor);
|
||||
return body;
|
||||
};
|
||||
|
||||
private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const body = buildWebhookBody(monitor, monitorStatusResponse);
|
||||
return body;
|
||||
};
|
||||
|
||||
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { homeserverUrl, accessToken, roomId } = notification;
|
||||
|
||||
let content;
|
||||
if (monitor.type === "hardware") {
|
||||
content = this.getHardwareContent(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
content = this.getContent(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
const title = `Checkmate status for ${monitor.name}`;
|
||||
|
||||
const formattedMessage = `## ${title}\n${content}`;
|
||||
|
||||
const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`;
|
||||
const body = {
|
||||
msgtype: "m.text",
|
||||
body: formattedMessage,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: formattedMessage,
|
||||
};
|
||||
try {
|
||||
await got.post(url, {
|
||||
json: body,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "Matrix alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
sendTestAlert = async (notification: Notification) => {
|
||||
const { homeserverUrl, accessToken, roomId } = notification;
|
||||
@@ -96,4 +41,146 @@ export class MatrixProvider implements INotificationProvider {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* New unified message format - builds Matrix HTML message from NotificationMessage
|
||||
*/
|
||||
sendMessage = async (notification: Notification, message: NotificationMessage): Promise<boolean> => {
|
||||
const { homeserverUrl, accessToken, roomId } = notification;
|
||||
|
||||
if (!homeserverUrl || !accessToken || !roomId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { plainText, htmlText } = this.buildMatrixMessage(message);
|
||||
|
||||
const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`;
|
||||
const body = {
|
||||
msgtype: "m.text",
|
||||
body: plainText,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: htmlText,
|
||||
};
|
||||
|
||||
try {
|
||||
await got.post(url, {
|
||||
json: body,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "Matrix notification failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build Matrix message from NotificationMessage
|
||||
* Returns both plain text and HTML formatted versions
|
||||
* Matrix supports HTML subset for rich formatting
|
||||
*/
|
||||
private buildMatrixMessage(message: NotificationMessage): { plainText: string; htmlText: string } {
|
||||
const plainLines: string[] = [];
|
||||
const htmlLines: string[] = [];
|
||||
|
||||
// Determine color based on severity
|
||||
const colorMap = {
|
||||
critical: "#FF0000", // Red
|
||||
warning: "#FFA500", // Orange
|
||||
success: "#00FF00", // Green
|
||||
info: "#0000FF", // Blue
|
||||
};
|
||||
const color = colorMap[message.severity] || "#808080";
|
||||
|
||||
// Title
|
||||
plainLines.push(`# ${message.content.title}`);
|
||||
htmlLines.push(`<h2 style="color: ${color};">${this.escapeHtml(message.content.title)}</h2>`);
|
||||
|
||||
// Summary
|
||||
plainLines.push("");
|
||||
plainLines.push(message.content.summary);
|
||||
htmlLines.push(`<p>${this.escapeHtml(message.content.summary)}</p>`);
|
||||
|
||||
// Monitor details
|
||||
plainLines.push("");
|
||||
plainLines.push("## Monitor Details");
|
||||
plainLines.push(`- Name: ${message.monitor.name}`);
|
||||
plainLines.push(`- URL: ${message.monitor.url}`);
|
||||
plainLines.push(`- Type: ${message.monitor.type}`);
|
||||
plainLines.push(`- Status: ${message.monitor.status}`);
|
||||
|
||||
htmlLines.push(`<h3>Monitor Details</h3>`);
|
||||
htmlLines.push(`<ul>`);
|
||||
htmlLines.push(`<li><strong>Name:</strong> ${this.escapeHtml(message.monitor.name)}</li>`);
|
||||
htmlLines.push(`<li><strong>URL:</strong> <a href="${this.escapeHtml(message.monitor.url)}">${this.escapeHtml(message.monitor.url)}</a></li>`);
|
||||
htmlLines.push(`<li><strong>Type:</strong> ${this.escapeHtml(message.monitor.type)}</li>`);
|
||||
htmlLines.push(`<li><strong>Status:</strong> ${this.escapeHtml(message.monitor.status)}</li>`);
|
||||
htmlLines.push(`</ul>`);
|
||||
|
||||
// Threshold breaches (if any)
|
||||
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||
plainLines.push("");
|
||||
plainLines.push("## Threshold Breaches");
|
||||
htmlLines.push(`<h3>Threshold Breaches</h3>`);
|
||||
htmlLines.push(`<ul>`);
|
||||
|
||||
message.content.thresholds.forEach((breach) => {
|
||||
plainLines.push(`- ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`);
|
||||
htmlLines.push(
|
||||
`<li><strong>${this.escapeHtml(breach.metric.toUpperCase())}:</strong> ${this.escapeHtml(breach.formattedValue)} (threshold: ${breach.threshold}${this.escapeHtml(breach.unit)})</li>`
|
||||
);
|
||||
});
|
||||
|
||||
htmlLines.push(`</ul>`);
|
||||
}
|
||||
|
||||
// Additional details (if any)
|
||||
if (message.content.details && message.content.details.length > 0) {
|
||||
plainLines.push("");
|
||||
plainLines.push("## Additional Information");
|
||||
htmlLines.push(`<h3>Additional Information</h3>`);
|
||||
htmlLines.push(`<ul>`);
|
||||
|
||||
message.content.details.forEach((detail) => {
|
||||
plainLines.push(`- ${detail}`);
|
||||
htmlLines.push(`<li>${this.escapeHtml(detail)}</li>`);
|
||||
});
|
||||
|
||||
htmlLines.push(`</ul>`);
|
||||
}
|
||||
|
||||
// Incident link (if incident exists)
|
||||
if (message.content.incident) {
|
||||
const incidentUrl = `${message.clientHost}/infrastructure/${message.monitor.id}`;
|
||||
plainLines.push("");
|
||||
plainLines.push(`View Incident: ${incidentUrl}`);
|
||||
htmlLines.push(`<p><a href="${this.escapeHtml(incidentUrl)}">View Incident</a></p>`);
|
||||
}
|
||||
|
||||
// Footer with timestamp
|
||||
plainLines.push("");
|
||||
plainLines.push(`Checkmate | ${new Date(message.content.timestamp).toUTCString()}`);
|
||||
htmlLines.push(`<hr>`);
|
||||
htmlLines.push(`<p><small>Checkmate | ${new Date(message.content.timestamp).toUTCString()}</small></p>`);
|
||||
|
||||
return {
|
||||
plainText: plainLines.join("\n"),
|
||||
htmlText: htmlLines.join(""),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML special characters for safe rendering
|
||||
*/
|
||||
private escapeHtml(text: string): string {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
const SERVICE_NAME = "PagerDutyProvider";
|
||||
import got from "got";
|
||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import {
|
||||
buildHardwareAlerts,
|
||||
buildHardwareWebhookBody,
|
||||
buildWebhookBody,
|
||||
getTestMessage,
|
||||
} from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
|
||||
export class PagerDutyProvider implements INotificationProvider {
|
||||
private logger: any;
|
||||
@@ -15,51 +12,6 @@ export class PagerDutyProvider implements INotificationProvider {
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
const body = buildHardwareWebhookBody(alertsToSend, monitor);
|
||||
return body;
|
||||
};
|
||||
|
||||
private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const body = buildWebhookBody(monitor, monitorStatusResponse);
|
||||
return body;
|
||||
};
|
||||
|
||||
async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> {
|
||||
let body;
|
||||
if (monitor.type === "hardware") {
|
||||
body = this.getHardwareContent(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
body = this.getContent(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
try {
|
||||
await got.post("https://events.pagerduty.com/v2/enqueue", {
|
||||
json: {
|
||||
routing_key: notification.address,
|
||||
event_action: "trigger",
|
||||
payload: {
|
||||
summary: body,
|
||||
severity: "critical",
|
||||
source: monitor.url,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "PagerDuty alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
||||
try {
|
||||
@@ -88,4 +40,103 @@ export class PagerDutyProvider implements INotificationProvider {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New unified message format implementation
|
||||
*/
|
||||
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||
if (!notification.address) {
|
||||
this.logger.warn({
|
||||
message: "PagerDuty notification missing routing key",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.buildPagerDutyPayload(notification, message);
|
||||
await got.post("https://events.pagerduty.com/v2/enqueue", {
|
||||
json: payload,
|
||||
});
|
||||
this.logger.info({
|
||||
message: "[NEW] PagerDuty notification sent via sendMessage",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "PagerDuty notification failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private buildPagerDutyPayload(notification: Notification, message: NotificationMessage): any {
|
||||
// Map our notification type to PagerDuty event_action
|
||||
const eventAction = message.type === "monitor_up" || message.type === "threshold_resolved" ? "resolve" : "trigger";
|
||||
|
||||
// Map severity to PagerDuty severity levels
|
||||
const severityMap: Record<string, string> = {
|
||||
critical: "critical",
|
||||
warning: "warning",
|
||||
info: "info",
|
||||
success: "info",
|
||||
};
|
||||
|
||||
const severity = severityMap[message.severity] || "error";
|
||||
|
||||
// Build deduplication key based on monitor ID for event grouping
|
||||
const dedupKey = `checkmate-${message.monitor.id}`;
|
||||
|
||||
// Build summary
|
||||
let summary = `${message.content.title} - ${message.content.summary}`;
|
||||
|
||||
// Add threshold details to summary if present
|
||||
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||
const thresholdInfo = message.content.thresholds
|
||||
.map((t) => `${t.metric.toUpperCase()}: ${t.formattedValue}/${t.threshold}${t.unit}`)
|
||||
.join(", ");
|
||||
summary += ` [${thresholdInfo}]`;
|
||||
}
|
||||
|
||||
// Build custom details
|
||||
const customDetails: any = {
|
||||
monitor_name: message.monitor.name,
|
||||
monitor_url: message.monitor.url,
|
||||
monitor_type: message.monitor.type,
|
||||
monitor_status: message.monitor.status,
|
||||
notification_type: message.type,
|
||||
};
|
||||
|
||||
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||
customDetails.threshold_breaches = message.content.thresholds.map((t) => ({
|
||||
metric: t.metric,
|
||||
current: t.formattedValue,
|
||||
threshold: `${t.threshold}${t.unit}`,
|
||||
}));
|
||||
}
|
||||
|
||||
if (message.content.details && message.content.details.length > 0) {
|
||||
customDetails.details = message.content.details;
|
||||
}
|
||||
|
||||
return {
|
||||
routing_key: notification.address,
|
||||
dedup_key: dedupKey,
|
||||
event_action: eventAction,
|
||||
payload: {
|
||||
summary,
|
||||
severity,
|
||||
source: message.monitor.url,
|
||||
timestamp: message.content.timestamp.toISOString(),
|
||||
custom_details: customDetails,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
const SERVICE_NAME = "SlackProvider";
|
||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import {
|
||||
buildHardwareAlerts,
|
||||
buildHardwareWebhookBody,
|
||||
buildWebhookBody,
|
||||
getTestMessage,
|
||||
} from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import got, { HTTPError } from "got";
|
||||
|
||||
export class SlackProvider implements INotificationProvider {
|
||||
@@ -15,48 +12,6 @@ export class SlackProvider implements INotificationProvider {
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
const body = buildHardwareWebhookBody(alertsToSend, monitor);
|
||||
return body;
|
||||
};
|
||||
|
||||
private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const body = buildWebhookBody(monitor, monitorStatusResponse);
|
||||
return body;
|
||||
};
|
||||
|
||||
async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> {
|
||||
let body;
|
||||
if (monitor.type === "hardware") {
|
||||
body = this.getHardwareContent(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
body = this.getContent(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: { text: body },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "Slack alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async sendTestAlert(notification: Notification): Promise<boolean> {
|
||||
if (!notification.address) {
|
||||
@@ -82,4 +37,162 @@ export class SlackProvider implements INotificationProvider {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New unified message format - builds Slack Block Kit payload from NotificationMessage
|
||||
*/
|
||||
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const payload = this.buildSlackPayload(message);
|
||||
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: payload,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
this.logger.info({
|
||||
message: "[NEW] Slack notification sent via sendMessage",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "[NEW] Slack alert failed via sendMessage",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Slack Block Kit payload from NotificationMessage
|
||||
* Uses Slack's rich formatting with sections, fields, and context blocks
|
||||
*/
|
||||
private buildSlackPayload(message: NotificationMessage): object {
|
||||
const blocks: any[] = [];
|
||||
|
||||
// Determine color based on severity
|
||||
const colorMap = {
|
||||
critical: "#FF0000", // Red
|
||||
warning: "#FFA500", // Orange
|
||||
success: "#00FF00", // Green
|
||||
info: "#0000FF", // Blue
|
||||
};
|
||||
const color = colorMap[message.severity] || "#808080";
|
||||
|
||||
// Header block with title
|
||||
blocks.push({
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: message.content.title,
|
||||
emoji: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Summary section
|
||||
blocks.push({
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: message.content.summary,
|
||||
},
|
||||
});
|
||||
|
||||
// Monitor details as fields
|
||||
const monitorFields = [
|
||||
{ type: "mrkdwn", text: `*Name:*\n${message.monitor.name}` },
|
||||
{ type: "mrkdwn", text: `*Type:*\n${message.monitor.type}` },
|
||||
{ type: "mrkdwn", text: `*Status:*\n${message.monitor.status}` },
|
||||
{ type: "mrkdwn", text: `*URL:*\n${message.monitor.url}` },
|
||||
];
|
||||
|
||||
blocks.push({
|
||||
type: "section",
|
||||
fields: monitorFields,
|
||||
});
|
||||
|
||||
// Divider
|
||||
blocks.push({ type: "divider" });
|
||||
|
||||
// Threshold breaches (if any)
|
||||
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||
const thresholdText = message.content.thresholds
|
||||
.map((breach) => `• *${breach.metric.toUpperCase()}:* ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`)
|
||||
.join("\n");
|
||||
|
||||
blocks.push({
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Threshold Breaches:*\n${thresholdText}`,
|
||||
},
|
||||
});
|
||||
|
||||
blocks.push({ type: "divider" });
|
||||
}
|
||||
|
||||
// Additional details (if any)
|
||||
if (message.content.details && message.content.details.length > 0) {
|
||||
const detailsText = message.content.details.map((detail) => `• ${detail}`).join("\n");
|
||||
|
||||
blocks.push({
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Additional Information:*\n${detailsText}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Incident link button (if incident exists)
|
||||
if (message.content.incident) {
|
||||
blocks.push({
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "View Incident",
|
||||
emoji: true,
|
||||
},
|
||||
url: `${message.clientHost}/infrastructure/${message.monitor.id}`,
|
||||
style: "primary",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Context footer with timestamp
|
||||
blocks.push({
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `Checkmate | ${new Date(message.content.timestamp).toUTCString()}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Return Slack payload with blocks and attachment color
|
||||
return {
|
||||
blocks,
|
||||
attachments: [
|
||||
{
|
||||
color,
|
||||
blocks: [], // Empty blocks in attachment for color bar
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,196 +1,5 @@
|
||||
import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js";
|
||||
|
||||
const getTime = (): { localTimeZone: string; localTime: string; utcTime: string } => {
|
||||
const now = new Date();
|
||||
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const localTime = new Intl.DateTimeFormat(undefined, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
timeZone: localTimeZone,
|
||||
timeZoneName: "short",
|
||||
}).format(now);
|
||||
const utcTime = now.toUTCString();
|
||||
return { localTimeZone, localTime, utcTime };
|
||||
};
|
||||
|
||||
export const buildHardwareAlerts = (
|
||||
clientHost: string,
|
||||
monitor: Monitor,
|
||||
networkResponse: MonitorStatusResponse
|
||||
): { alertsToSend: string[]; discordPayload: any } => {
|
||||
const thresholds = monitor.thresholds || {};
|
||||
const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds;
|
||||
|
||||
const payload = networkResponse?.payload as HardwareStatusPayload;
|
||||
const metrics = payload.data || {};
|
||||
const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics;
|
||||
|
||||
const alerts: Record<string, boolean> = {
|
||||
cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false,
|
||||
memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false,
|
||||
disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false,
|
||||
};
|
||||
|
||||
const alertsToSend = [];
|
||||
const discordEmbeds = [];
|
||||
const monitorInfoFields = [
|
||||
{ name: "Monitor", value: monitor.name, inline: true },
|
||||
{ name: "URL", value: monitor.url, inline: false },
|
||||
];
|
||||
const goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor.id}` };
|
||||
const formatDiscordAlert = {
|
||||
cpu: () => ({
|
||||
title: "CPU alert",
|
||||
description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
|
||||
color: 15548997,
|
||||
fields: [...monitorInfoFields, goToIncidentField],
|
||||
footer: { text: "Checkmate" },
|
||||
}),
|
||||
|
||||
memory: () => ({
|
||||
title: "Memory alert",
|
||||
description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
|
||||
color: 15548997,
|
||||
fields: [...monitorInfoFields, goToIncidentField],
|
||||
footer: { text: "Checkmate" },
|
||||
}),
|
||||
|
||||
disk: () => ({
|
||||
title: "Disk alert",
|
||||
description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
|
||||
color: 15548997,
|
||||
footer: { text: "Checkmate" },
|
||||
fields: [
|
||||
...monitorInfoFields,
|
||||
...(Array.isArray(disk) ? disk : []).map((d: any, idx: number) => ({
|
||||
name: `Disk ${idx}`,
|
||||
value: `${(d?.usage_percent * 100).toFixed(0)}%`,
|
||||
inline: true,
|
||||
})),
|
||||
goToIncidentField,
|
||||
],
|
||||
}),
|
||||
};
|
||||
const alertTypes = ["cpu", "memory", "disk"] as const;
|
||||
const alertThresholdKeyMap: Record<(typeof alertTypes)[number], "cpuAlertThreshold" | "memoryAlertThreshold" | "diskAlertThreshold"> = {
|
||||
cpu: "cpuAlertThreshold",
|
||||
memory: "memoryAlertThreshold",
|
||||
disk: "diskAlertThreshold",
|
||||
};
|
||||
for (const type of alertTypes) {
|
||||
const thresholdKey = alertThresholdKeyMap[type];
|
||||
// Iterate over each alert type to see if any need to be decremented
|
||||
if (alerts[type] === true) {
|
||||
const nextValue = ((monitor[thresholdKey] ?? monitor.alertThreshold) as number) - 1;
|
||||
monitor[thresholdKey] = nextValue; // Decrement threshold if an alert is triggered
|
||||
|
||||
if (monitor[thresholdKey] <= 0) {
|
||||
// If threshold drops below 0, reset and send notification
|
||||
monitor[thresholdKey] = monitor.alertThreshold;
|
||||
|
||||
const formatAlert = {
|
||||
cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
|
||||
memory: () =>
|
||||
`Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
|
||||
disk: () =>
|
||||
`Your current disk usage: ${disk
|
||||
.map((d, idx) => `(Disk${idx}: ${(d?.usage_percent ?? 0 * 100).toFixed(0)}%)`)
|
||||
.join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
|
||||
};
|
||||
alertsToSend.push(formatAlert[type]());
|
||||
discordEmbeds.push(formatDiscordAlert[type]());
|
||||
}
|
||||
}
|
||||
}
|
||||
const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null;
|
||||
return { alertsToSend, discordPayload };
|
||||
};
|
||||
|
||||
export const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => {
|
||||
const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`];
|
||||
const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor.id}`];
|
||||
const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : [];
|
||||
return alertText.map((alert) => alert).join("\n");
|
||||
};
|
||||
|
||||
export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): string => {
|
||||
const content = alerts.map((alert) => alert).join("\n");
|
||||
return content;
|
||||
};
|
||||
|
||||
export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => {
|
||||
const thresholds = monitor.thresholds || {};
|
||||
const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds;
|
||||
|
||||
const payload = networkResponse?.payload as HardwareStatusPayload;
|
||||
const metrics = payload.data || {};
|
||||
const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics;
|
||||
|
||||
const cpuBreach = cpuThreshold !== -1 && cpuUsage > cpuThreshold;
|
||||
if (cpuBreach && ((monitor.cpuAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const memoryBreach = memoryThreshold !== -1 && memoryUsage > memoryThreshold;
|
||||
if (memoryBreach && ((monitor.memoryAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const diskBreach = disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold);
|
||||
if (diskBreach && ((monitor.diskAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { status, code } = monitorStatusResponse;
|
||||
const { localTimeZone, localTime, utcTime } = getTime();
|
||||
return `Monitor: ${monitor.name}\nLocal Time (${localTimeZone}): ${localTime}\nUTC Time: ${utcTime}\nStatus: ${status ? "UP" : "DOWN"}\nStatus Code: ${code}\n\u200B\n`;
|
||||
};
|
||||
|
||||
export const buildHardwareEmail = async (emailService: any, monitor: Monitor, alerts: string[]) => {
|
||||
const template = "hardwareIncidentTemplate";
|
||||
const context = { monitor: monitor.name, url: monitor.url, alerts };
|
||||
const html = await emailService.buildEmail(template, context);
|
||||
return html;
|
||||
};
|
||||
|
||||
export const buildEmail = async (emailService: any, monitor: Monitor): Promise<string> => {
|
||||
const template = monitor.status === "up" ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
const context = { monitor: monitor.name, url: monitor.url };
|
||||
const html = await emailService.buildEmail(template, context);
|
||||
return html;
|
||||
};
|
||||
|
||||
export const buildDiscordBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { localTime, utcTime } = getTime();
|
||||
let body = {
|
||||
embeds: [
|
||||
{
|
||||
title: `Monitor name: ${monitor.name}`,
|
||||
color: monitorStatusResponse.status ? 5763719 : 15548997,
|
||||
|
||||
fields: [
|
||||
{ name: "Monitor", value: monitor.name, inline: false },
|
||||
{ name: "Status", value: monitorStatusResponse.status ? "Up" : "Down", inline: false },
|
||||
{ name: "Status Code", value: String(monitorStatusResponse.code), inline: false },
|
||||
{ name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: false },
|
||||
{ name: "URL", value: monitor.url, inline: false },
|
||||
],
|
||||
footer: { text: "Checkmate" },
|
||||
},
|
||||
],
|
||||
};
|
||||
return body;
|
||||
};
|
||||
|
||||
// Test notification helpers - used by all providers for test alerts
|
||||
export const buildTestEmail = async (emailService: any) => {
|
||||
const context = { testName: "Monitoring System" };
|
||||
const html = await emailService.buildEmail("testEmailTemplate", context);
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
const SERVICE_NAME = "WebhookProvider";
|
||||
import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import {
|
||||
buildHardwareAlerts,
|
||||
buildHardwareWebhookBody,
|
||||
buildWebhookBody,
|
||||
getTestMessage,
|
||||
} from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import got from "got";
|
||||
|
||||
export class WebhookProvider implements INotificationProvider {
|
||||
@@ -15,49 +12,96 @@ export class WebhookProvider implements INotificationProvider {
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
const body = buildHardwareWebhookBody(alertsToSend, monitor);
|
||||
return body;
|
||||
};
|
||||
|
||||
private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): string => {
|
||||
const body = buildWebhookBody(monitor, monitorStatusResponse);
|
||||
return body;
|
||||
};
|
||||
|
||||
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
let body;
|
||||
if (monitor.type === "hardware") {
|
||||
body = this.getHardwareContent(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
body = this.getContent(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
sendMessage = async (notification: Notification, message: NotificationMessage): Promise<boolean> => {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build webhook payload from unified message
|
||||
const payload = this.buildWebhookPayload(message);
|
||||
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: { text: body },
|
||||
json: payload,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
this.logger.info({
|
||||
message: "Webhook notification sent",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "Webhook alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendAlert",
|
||||
method: "sendMessage",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build webhook payload from NotificationMessage
|
||||
* Format: { text: string, severity: string, monitor: object, details: object }
|
||||
*/
|
||||
private buildWebhookPayload(message: NotificationMessage): object {
|
||||
const lines: string[] = [];
|
||||
|
||||
// Title and summary
|
||||
lines.push(`**${message.content.title}**`);
|
||||
lines.push(message.content.summary);
|
||||
lines.push("");
|
||||
|
||||
// Monitor information
|
||||
lines.push("**Monitor Details:**");
|
||||
lines.push(`- Name: ${message.monitor.name}`);
|
||||
lines.push(`- URL: ${message.monitor.url}`);
|
||||
lines.push(`- Type: ${message.monitor.type}`);
|
||||
lines.push(`- Status: ${message.monitor.status}`);
|
||||
lines.push("");
|
||||
|
||||
// Additional details
|
||||
if (message.content.details && message.content.details.length > 0) {
|
||||
lines.push("**Additional Information:**");
|
||||
message.content.details.forEach((detail) => lines.push(`- ${detail}`));
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
// Threshold breaches (for hardware monitors)
|
||||
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||
lines.push("**Threshold Breaches:**");
|
||||
message.content.thresholds.forEach((breach) => {
|
||||
lines.push(`- ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`);
|
||||
});
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
// Incident link
|
||||
if (message.content.incident) {
|
||||
lines.push(`[View Incident](${message.clientHost}/infrastructure/${message.monitor.id})`);
|
||||
}
|
||||
|
||||
// Return webhook payload with both text and structured data
|
||||
return {
|
||||
text: lines.join("\n"),
|
||||
severity: message.severity,
|
||||
type: message.type,
|
||||
monitor: {
|
||||
id: message.monitor.id,
|
||||
name: message.monitor.name,
|
||||
url: message.monitor.url,
|
||||
status: message.monitor.status,
|
||||
},
|
||||
timestamp: message.content.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
sendTestAlert = async (notification: Notification) => {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification, MonitorStatus } from "@/types/index.js";
|
||||
import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||
import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js";
|
||||
import { INotificationProvider } from "./notificationProviders/INotificationProvider.js";
|
||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import type { ISettingsService } from "@/service/system/settingsService.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||
|
||||
export interface INotificationsService {
|
||||
createNotification: (notificationData: Partial<Notification>) => Promise<Notification>;
|
||||
@@ -9,12 +13,7 @@ export interface INotificationsService {
|
||||
findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>;
|
||||
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
|
||||
deleteById: (id: string, teamId: string) => Promise<Notification>;
|
||||
handleNotifications: (
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
prevStatus: MonitorStatus,
|
||||
statusChanged: boolean
|
||||
) => Promise<boolean>;
|
||||
handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise<boolean>;
|
||||
|
||||
sendTestNotification: (notification: Notification) => Promise<boolean>;
|
||||
testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
|
||||
@@ -33,19 +32,9 @@ export class NotificationsService implements INotificationsService {
|
||||
private discordProvider: INotificationProvider;
|
||||
private pagerDutyProvider: INotificationProvider;
|
||||
private matrixProvider: INotificationProvider;
|
||||
private logger: any;
|
||||
|
||||
// Email grouping (batching) configuration
|
||||
private emailGroupingWindowMs: number;
|
||||
private pendingEmailGroups: Map<
|
||||
string,
|
||||
{
|
||||
monitors: Monitor[];
|
||||
statusResponses: MonitorStatusResponse[];
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
createdAt: number;
|
||||
}
|
||||
>;
|
||||
private logger: ILogger;
|
||||
private settingsService: ISettingsService;
|
||||
private notificationMessageBuilder: INotificationMessageBuilder;
|
||||
|
||||
constructor(
|
||||
notificationsRepository: INotificationsRepository,
|
||||
@@ -56,7 +45,9 @@ export class NotificationsService implements INotificationsService {
|
||||
discordProvider: INotificationProvider,
|
||||
pagerDutyProvider: INotificationProvider,
|
||||
matrixProvider: INotificationProvider,
|
||||
logger: any
|
||||
settingsService: ISettingsService,
|
||||
logger: ILogger,
|
||||
notificationMessageBuilder: INotificationMessageBuilder
|
||||
) {
|
||||
this.notificationsRepository = notificationsRepository;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
@@ -66,57 +57,61 @@ export class NotificationsService implements INotificationsService {
|
||||
this.discordProvider = discordProvider;
|
||||
this.pagerDutyProvider = pagerDutyProvider;
|
||||
this.matrixProvider = matrixProvider;
|
||||
this.settingsService = settingsService;
|
||||
this.logger = logger;
|
||||
|
||||
// Configure email grouping window (in milliseconds).
|
||||
// When > 0, multiple DOWN events for monitors that share the same
|
||||
// email notification within this window will be batched into a single email.
|
||||
const rawGroupingWindow = process.env.NOTIFICATION_GROUP_WINDOW_MS ?? process.env.NOTIFICATION_GROUP_WINDOW_SECONDS;
|
||||
let groupingWindowMs = 0;
|
||||
if (rawGroupingWindow) {
|
||||
const parsed = Number(rawGroupingWindow);
|
||||
if (!Number.isNaN(parsed) && parsed > 0) {
|
||||
// If value looks like seconds (small number), convert to ms.
|
||||
// This allows either milliseconds (e.g. 60000) or seconds (e.g. 60).
|
||||
groupingWindowMs = parsed <= 300 ? parsed * 1000 : parsed;
|
||||
}
|
||||
}
|
||||
this.emailGroupingWindowMs = groupingWindowMs;
|
||||
this.pendingEmailGroups = new Map();
|
||||
this.notificationMessageBuilder = notificationMessageBuilder;
|
||||
}
|
||||
|
||||
private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> => {
|
||||
private send = async (
|
||||
notification: Notification,
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
decision: MonitorActionDecision,
|
||||
notificationMessage: NotificationMessage | undefined
|
||||
): Promise<boolean> => {
|
||||
if (!notificationMessage) {
|
||||
this.logger.warn({
|
||||
message: "Notification message not provided",
|
||||
service: SERVICE_NAME,
|
||||
method: "send",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Route to provider based on notification type
|
||||
switch (notification.type) {
|
||||
case "email":
|
||||
return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
case "slack":
|
||||
return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
case "discord":
|
||||
return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
case "pager_duty":
|
||||
return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
case "matrix":
|
||||
return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
case "webhook":
|
||||
return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
return await this.webhookProvider.sendMessage!(notification, notificationMessage);
|
||||
case "slack":
|
||||
return await this.slackProvider.sendMessage!(notification, notificationMessage);
|
||||
case "matrix":
|
||||
return await this.matrixProvider.sendMessage!(notification, notificationMessage);
|
||||
case "pager_duty":
|
||||
return await this.pagerDutyProvider.sendMessage!(notification, notificationMessage);
|
||||
case "discord":
|
||||
return await this.discordProvider.sendMessage!(notification, notificationMessage);
|
||||
case "email":
|
||||
return await this.emailProvider.sendMessage!(notification, notificationMessage);
|
||||
default:
|
||||
this.logger.warn({
|
||||
message: `Unknown notification type: ${notification.type}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "send",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
|
||||
const notificationIds = monitor.notifications ?? [];
|
||||
const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds);
|
||||
|
||||
const tasks = notifications.map((notification) => {
|
||||
// Only group emails, only for DOWN transitions, and only if a window is configured.
|
||||
if (notification.type === "email" && this.emailGroupingWindowMs > 0 && monitorStatusResponse.status === false) {
|
||||
return this.queueGroupedEmailNotification(notification, monitor, monitorStatusResponse);
|
||||
}
|
||||
// Build notification message once for all notifications
|
||||
const settings = this.settingsService.getSettings();
|
||||
const clientHost = settings.clientHost || "Host not defined";
|
||||
const notificationMessage = this.notificationMessageBuilder.buildMessage(monitor, monitorStatusResponse, decision, clientHost);
|
||||
|
||||
// For all other cases (UP notifications or non-email channels), send immediately.
|
||||
return this.send(notification, monitor, monitorStatusResponse);
|
||||
});
|
||||
const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse, decision, notificationMessage));
|
||||
|
||||
const outcomes = await Promise.all(tasks);
|
||||
const succeeded = outcomes.filter(Boolean).length;
|
||||
@@ -125,136 +120,20 @@ export class NotificationsService implements INotificationsService {
|
||||
this.logger.warn({
|
||||
message: `Notification send completed with ${succeeded} success, ${failed} failure(s)`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
method: "sendNotifications",
|
||||
});
|
||||
}
|
||||
// Return true if all notificaitons succeeded
|
||||
// Return true if all notifications succeeded
|
||||
return succeeded === notifications.length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Queue a DOWN email notification to be potentially grouped with other
|
||||
* DOWN events for the same email notification within the configured window.
|
||||
*
|
||||
* This method returns immediately; the actual email is sent asynchronously
|
||||
* when the grouping window expires.
|
||||
*/
|
||||
private queueGroupedEmailNotification = async (
|
||||
notification: Notification,
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse
|
||||
): Promise<boolean> => {
|
||||
// If grouping is disabled, fallback to immediate send.
|
||||
if (this.emailGroupingWindowMs <= 0) {
|
||||
return await this.send(notification, monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
const key = notification.id;
|
||||
const now = Date.now();
|
||||
const existingGroup = this.pendingEmailGroups.get(key);
|
||||
|
||||
if (!existingGroup) {
|
||||
// Create a new group and schedule a flush after the window expires.
|
||||
const timer = setTimeout(async () => {
|
||||
const group = this.pendingEmailGroups.get(key);
|
||||
if (!group) return;
|
||||
|
||||
this.pendingEmailGroups.delete(key);
|
||||
|
||||
try {
|
||||
await this.flushEmailGroup(notification, group.monitors, group.statusResponses);
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: error?.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "flushEmailGroup",
|
||||
stack: error?.stack,
|
||||
});
|
||||
}
|
||||
}, this.emailGroupingWindowMs);
|
||||
|
||||
this.pendingEmailGroups.set(key, {
|
||||
monitors: [monitor],
|
||||
statusResponses: [monitorStatusResponse],
|
||||
timer,
|
||||
createdAt: now,
|
||||
});
|
||||
} else {
|
||||
// Append to existing group.
|
||||
existingGroup.monitors.push(monitor);
|
||||
existingGroup.statusResponses.push(monitorStatusResponse);
|
||||
}
|
||||
|
||||
// Consider queueing as "succeeded" from the caller's perspective.
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Flush a grouped set of DOWN events into a single email.
|
||||
*
|
||||
* To avoid changing email templates, we construct a synthetic Monitor
|
||||
* whose name concisely lists all affected services. The existing
|
||||
* `serverIsDownTemplate` is then reused.
|
||||
*
|
||||
* @param notification The email notification to send to
|
||||
* @param monitors Array of monitors that went down
|
||||
* @param statusResponses Array of status responses (parallel to monitors)
|
||||
* @returns true if email was sent successfully, false otherwise
|
||||
*/
|
||||
private flushEmailGroup = async (notification: Notification, monitors: Monitor[], statusResponses: MonitorStatusResponse[]): Promise<boolean> => {
|
||||
if (!monitors.length || !statusResponses.length) {
|
||||
handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
|
||||
if (!decision.shouldSendNotification) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build a combined monitor name listing all affected services.
|
||||
// Example: "Service A, Service B" (2 services) or "Service A" (1 service)
|
||||
const uniqueNames = Array.from(new Set(monitors.map((m) => m.name)));
|
||||
const servicesCount = uniqueNames.length;
|
||||
const servicesList = uniqueNames.join(", ");
|
||||
|
||||
const combinedName = servicesCount === 1 ? servicesList : `${servicesCount} services: ${servicesList}`;
|
||||
|
||||
// Use the first monitor as a base for URL and other fields.
|
||||
const baseMonitor = monitors[0]!;
|
||||
const baseStatus = statusResponses[0]!;
|
||||
|
||||
// Create a shallow clone so we don't mutate the original entity.
|
||||
// This preserves monitor properties while overriding the name for grouped display.
|
||||
const syntheticMonitor: Monitor = {
|
||||
...baseMonitor,
|
||||
name: combinedName,
|
||||
};
|
||||
|
||||
// Reuse existing email provider to send grouped notification.
|
||||
return await this.emailProvider.sendAlert(notification, syntheticMonitor, baseStatus);
|
||||
};
|
||||
|
||||
handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, prevStatus: MonitorStatus, statusChanged: boolean) => {
|
||||
const { type } = monitor;
|
||||
const payload = monitorStatusResponse.payload as HardwareStatusPayload;
|
||||
// If this is a non-hardeware type monitor and status did not change, we're done
|
||||
if (type !== "hardware" && statusChanged === false) return false;
|
||||
// if prevStatus is undefined, monitor is resuming, we're done
|
||||
if (type !== "hardware" && prevStatus === undefined) return false;
|
||||
|
||||
// Deal with hardware thresholds
|
||||
if (type === "hardware") {
|
||||
const thresholds = monitor.thresholds;
|
||||
|
||||
if (thresholds === undefined) return false; // No thresholds set, we're done
|
||||
const metrics = payload?.data ?? null;
|
||||
if (metrics === null) return false; // No metrics, we're done
|
||||
|
||||
// We should send a notificaiton
|
||||
|
||||
const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse);
|
||||
if (shouldSend === false) return false;
|
||||
|
||||
return await this.sendNotifications(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
// We should send a notification for non-hardware monitor status change
|
||||
return await this.sendNotifications(monitor, monitorStatusResponse);
|
||||
// Send notifications based on decision
|
||||
return await this.sendNotifications(monitor, monitorStatusResponse, decision);
|
||||
};
|
||||
|
||||
sendTestNotification = async (notification: Notification) => {
|
||||
|
||||