Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d25ce9e498 | ||
|
|
b7c9e2710a | ||
|
|
bfdf28a2cc | ||
|
|
4ca24c794e | ||
|
|
7f9d2f275e | ||
|
|
1966134647 | ||
|
|
7646d11ea0 | ||
|
|
c10d9be0f0 | ||
|
|
27c3a9e12b | ||
|
|
134c2ebbf4 | ||
|
|
e39b44db6c |
No files matched your search
@@ -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":
|
||||
@@ -100,6 +102,7 @@ export const useMonitorForm = ({
|
||||
matchMethod: "",
|
||||
expectedValue: "",
|
||||
jsonPath: "",
|
||||
globalpingEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
|
||||
},
|
||||
checkTTL: data?.checkTTL ?? 30,
|
||||
pagespeedApiKey: "",
|
||||
globalpingApiKey: "",
|
||||
globalpingLocationsTier: data?.globalpingLocationsTier ?? 6,
|
||||
systemEmailPassword: "",
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
@@ -489,6 +488,27 @@ const CreateMonitorPage = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 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
|
||||
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
@@ -34,6 +35,10 @@ const Settings = () => {
|
||||
fetchedSettings?.pagespeedKeySet ?? false
|
||||
);
|
||||
const [apiKeyHasBeenReset, setApiKeyHasBeenReset] = useState(false);
|
||||
const [isGlobalpingKeySet, setIsGlobalpingKeySet] = useState(
|
||||
fetchedSettings?.globalpingKeySet ?? false
|
||||
);
|
||||
const [globalpingKeyHasBeenReset, setGlobalpingKeyHasBeenReset] = useState(false);
|
||||
const [isEmailPasswordSet, setIsEmailPasswordSet] = useState(
|
||||
fetchedSettings?.emailPasswordSet ?? false
|
||||
);
|
||||
@@ -63,6 +68,7 @@ const Settings = () => {
|
||||
useEffect(() => {
|
||||
if (fetchedSettings) {
|
||||
setIsApiKeySet(fetchedSettings?.pagespeedKeySet);
|
||||
setIsGlobalpingKeySet(fetchedSettings?.globalpingKeySet);
|
||||
setIsEmailPasswordSet(fetchedSettings?.emailPasswordSet);
|
||||
}
|
||||
}, [fetchedSettings]);
|
||||
@@ -74,10 +80,14 @@ 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);
|
||||
}
|
||||
@@ -102,6 +112,22 @@ const Settings = () => {
|
||||
})
|
||||
);
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
@@ -134,6 +160,16 @@ const Settings = () => {
|
||||
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}
|
||||
control={control}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface Monitor {
|
||||
selectedDisks: string[];
|
||||
gameId?: string;
|
||||
group: string | null;
|
||||
globalpingEnabled: boolean;
|
||||
recentChecks: CheckSnapshot[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -97,6 +98,12 @@ export interface MonitorStats {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface LocationCheckData {
|
||||
groupedChecks: GroupedCheck[];
|
||||
uptimePercentage: number;
|
||||
avgResponseTime: number;
|
||||
}
|
||||
|
||||
export interface MonitorData {
|
||||
monitor: Monitor;
|
||||
groupedChecks: GroupedCheck[];
|
||||
@@ -104,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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -34,6 +34,8 @@ export const settingsSchema = z.object({
|
||||
.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(),
|
||||
|
||||
@@ -445,6 +445,15 @@
|
||||
},
|
||||
"title": "Check frequency"
|
||||
},
|
||||
"globalping": {
|
||||
"title": "Global monitoring",
|
||||
"description": "Run checks from multiple locations around the world using the Globalping network. Configure the API key and location count in settings.",
|
||||
"option": {
|
||||
"enabled": {
|
||||
"label": "Enable Globalping"
|
||||
}
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"option": {
|
||||
"container": {
|
||||
@@ -950,6 +959,27 @@
|
||||
"labelApiKey": "PageSpeed API key",
|
||||
"title": "Google PageSpeed API key"
|
||||
},
|
||||
"globalpingSettings": {
|
||||
"description": "Configure Globalping to run uptime checks from multiple geographic regions worldwide.",
|
||||
"labelApiKeySet": "API key is set. Click Reset to change it.",
|
||||
"labelApiKey": "Globalping API key",
|
||||
"optionalLabel": "(Optional)",
|
||||
"title": "Globalping",
|
||||
"locationsTierTitle": "Global locations",
|
||||
"locationsTierDescription": "Choose how many probe locations are available when creating monitors.",
|
||||
"tier3Label": "3 locations — core regions",
|
||||
"tier3Description": "North America, Europe, Asia",
|
||||
"tier6Label": "6 locations — all continents",
|
||||
"tier6Description": "North America, Europe, Asia, South America, Oceania, Africa",
|
||||
"tier15Label": "15 locations — city level",
|
||||
"tier15Description": "US East, US West, Canada, UK, Germany, Netherlands, Poland, Japan, Singapore, India, Australia, Brazil, Chile, South Africa, UAE",
|
||||
"rateLimitsTitle": "Rate limits",
|
||||
"rateLimitsDescription": "Globalping limits how many tests you can run per hour. Each location check on each monitor uses 1 credit per interval.",
|
||||
"rateLimitNoAuth": "Without a token: 250 tests per hour (no authentication needed)",
|
||||
"rateLimitAuth": "With a free token: 500 tests per hour (sign in at dash.globalping.io)",
|
||||
"rateLimitSponsor": "GitHub sponsors: higher limits based on sponsorship tier + hosting probes",
|
||||
"rateLimitsNote": "For example, 3 monitors with 5 locations each, checked every minute, uses 15 credits per minute (900/hour). Choose your tier accordingly."
|
||||
},
|
||||
"saveButtonLabel": "Save",
|
||||
"statsSettings": {
|
||||
"clearAllStatsButton": "Clear all stats",
|
||||
@@ -1106,6 +1136,14 @@
|
||||
}
|
||||
},
|
||||
"uptime": {
|
||||
"details": {
|
||||
"locations": {
|
||||
"title": "Location breakdown",
|
||||
"label": "Location",
|
||||
"avgResponseTime": "Avg response time",
|
||||
"uptime": "Uptime"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"search": {
|
||||
"placeholder": "Search monitors..."
|
||||
@@ -1113,7 +1151,8 @@
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"responseTime": "Response time"
|
||||
"responseTime": "Response time",
|
||||
"globalpingEnabled": "Global monitoring enabled"
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
|
||||
Generated
+580
-17
@@ -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,7 +29,7 @@ export interface InitializedControllers {
|
||||
export const initializeControllers = (services: InitializedServices): InitializedControllers => {
|
||||
return {
|
||||
authController: new AuthController(services.userService),
|
||||
monitorController: new MonitorController(services.monitorService),
|
||||
monitorController: new MonitorController(services.monitorService, services.settingsService),
|
||||
settingsController: new SettingsController(services.settingsService, services.emailService),
|
||||
checkController: new CheckController(services.checkService),
|
||||
inviteController: new InviteController(services.inviteService),
|
||||
|
||||
@@ -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";
|
||||
@@ -192,6 +193,8 @@ export const initializeServices = async ({
|
||||
notificationMessageBuilder
|
||||
);
|
||||
|
||||
const globalpingService = new GlobalpingService({ settingsService, logger });
|
||||
|
||||
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
|
||||
logger,
|
||||
networkService,
|
||||
@@ -200,6 +203,7 @@ export const initializeServices = async ({
|
||||
checkService,
|
||||
buffer: bufferService,
|
||||
incidentService,
|
||||
globalpingService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
teamsRepository,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,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,
|
||||
};
|
||||
@@ -31,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;
|
||||
@@ -55,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) => {
|
||||
|
||||
@@ -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,12 +13,14 @@ import type {
|
||||
CheckNetworkInterfaceInfo,
|
||||
GotTimings,
|
||||
ILighthouseAudit,
|
||||
LocationResult,
|
||||
} from "@/types/check.js";
|
||||
|
||||
type CheckMetadataDocument = Omit<CheckMetadata, "monitorId" | "teamId"> & {
|
||||
monitorId: Types.ObjectId;
|
||||
teamId: Types.ObjectId;
|
||||
type: MonitorType;
|
||||
location?: string;
|
||||
};
|
||||
|
||||
type CheckDocumentBase = Omit<Check, "id" | "metadata" | "expiry" | "ackAt" | "createdAt" | "updatedAt"> & {
|
||||
@@ -175,6 +177,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: {
|
||||
@@ -197,6 +210,9 @@ const metadataSchema = new Schema<CheckMetadataDocument>(
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
@@ -281,6 +297,10 @@ const CheckSchema = new Schema<CheckDocument>(
|
||||
type: auditsSchema,
|
||||
default: undefined,
|
||||
},
|
||||
locationResults: {
|
||||
type: [locationResultSchema],
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
@@ -299,6 +319,7 @@ CheckSchema.index({ createdAt: 1 });
|
||||
CheckSchema.index({ "metadata.teamId": 1, createdAt: -1 });
|
||||
CheckSchema.index({ "metadata.monitorId": 1, "metadata.type": 1, createdAt: -1 });
|
||||
CheckSchema.index({ "metadata.teamId": 1, status: 1, createdAt: -1 });
|
||||
CheckSchema.index({ "metadata.monitorId": 1, "metadata.location": 1, createdAt: -1 });
|
||||
|
||||
const CheckModel = model<CheckDocument>("Check", CheckSchema);
|
||||
|
||||
|
||||
@@ -160,6 +160,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
gameId: {
|
||||
type: String,
|
||||
},
|
||||
globalpingEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
trim: true,
|
||||
|
||||
@@ -35,6 +35,7 @@ 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
|
||||
//delete
|
||||
deleteByMonitorId(monitorId: string): Promise<number>;
|
||||
|
||||
@@ -169,6 +169,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
monitorId: toStringId(metadata.monitorId),
|
||||
teamId: toStringId(metadata.teamId),
|
||||
type: metadata.type,
|
||||
...(metadata.location ? { location: metadata.location } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -219,6 +220,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
monitorId: new mongoose.Types.ObjectId(metadata.monitorId),
|
||||
teamId: new mongoose.Types.ObjectId(metadata.teamId),
|
||||
type: metadata.type,
|
||||
...(metadata.location ? { location: metadata.location } : {}),
|
||||
},
|
||||
...rest,
|
||||
} as unknown as CheckDocument;
|
||||
@@ -420,6 +422,59 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
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 matchStage = {
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
"metadata.location": { $exists: true, $ne: null },
|
||||
createdAt: { $gte: startDate, $lte: endDate },
|
||||
};
|
||||
|
||||
const results = await CheckModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { createdAt: 1 } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
location: "$metadata.location",
|
||||
bucket: { $dateToString: { format: dateString, date: "$createdAt" } },
|
||||
},
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
totalChecks: { $sum: 1 },
|
||||
upChecks: { $sum: { $cond: [{ $eq: ["$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,
|
||||
@@ -427,9 +482,10 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
endDate: Date,
|
||||
dateString: string
|
||||
) => {
|
||||
const matchStage = {
|
||||
const matchStage: Record<string, any> = {
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
createdAt: { $gte: startDate, $lte: endDate },
|
||||
"metadata.location": { $exists: false },
|
||||
};
|
||||
const [result] = await CheckModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
|
||||
@@ -343,6 +343,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
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),
|
||||
@@ -398,6 +399,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
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),
|
||||
|
||||
@@ -38,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,
|
||||
@@ -81,10 +83,6 @@ class MongoSettingsRepository implements ISettingsRepository {
|
||||
delete update.$unset;
|
||||
}
|
||||
|
||||
await AppSettingsModel.findOneAndUpdate({}, update, {
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
const updatedSettings = await AppSettingsModel.findOneAndUpdate({}, update, {
|
||||
upsert: true,
|
||||
new: true,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class CheckService {
|
||||
};
|
||||
|
||||
buildCheck = (statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>) => {
|
||||
const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse;
|
||||
const { monitorId, teamId, type, status, responseTime, code, message, payload, timings, location } = statusResponse;
|
||||
|
||||
const check: Partial<Check> = {
|
||||
id: new Types.ObjectId().toString(),
|
||||
@@ -46,6 +46,7 @@ class CheckService {
|
||||
monitorId,
|
||||
teamId,
|
||||
type,
|
||||
...(location ? { location } : {}),
|
||||
},
|
||||
status,
|
||||
statusCode: code,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
UptimeDetailsResult,
|
||||
HardwareDetailsResult,
|
||||
PageSpeedDetailsResult,
|
||||
LocationCheckData,
|
||||
GamesMap,
|
||||
} from "@/types/monitor.js";
|
||||
import type {
|
||||
@@ -222,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,
|
||||
@@ -230,6 +240,7 @@ export class MonitorService implements IMonitorService {
|
||||
groupedDownChecks: NormalizeDataUptimeDetails(checksData.groupedDownChecks, 10, 100),
|
||||
groupedAvgResponseTime: checksData.avgResponseTime,
|
||||
groupedUptimePercentage: checksData.uptimePercentage,
|
||||
...(locationChecks ? { locationChecks } : {}),
|
||||
},
|
||||
monitorStats,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 type GlobalpingService from "@/service/infrastructure/globalpingService.js";
|
||||
import {
|
||||
IMaintenanceWindowsRepository,
|
||||
IMonitorsRepository,
|
||||
@@ -37,6 +38,7 @@ class SuperSimpleQueueHelper {
|
||||
private checkService: any;
|
||||
private buffer: any;
|
||||
private incidentService: IncidentService;
|
||||
private globalpingService: GlobalpingService;
|
||||
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private teamsRepository: ITeamsRepository;
|
||||
@@ -52,6 +54,7 @@ class SuperSimpleQueueHelper {
|
||||
checkService,
|
||||
buffer,
|
||||
incidentService,
|
||||
globalpingService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
teamsRepository,
|
||||
@@ -66,6 +69,7 @@ class SuperSimpleQueueHelper {
|
||||
checkService: any;
|
||||
buffer: any;
|
||||
incidentService: IncidentService;
|
||||
globalpingService: GlobalpingService;
|
||||
maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
teamsRepository: ITeamsRepository;
|
||||
@@ -80,6 +84,7 @@ class SuperSimpleQueueHelper {
|
||||
this.buffer = buffer;
|
||||
this.notificationsService = notificationsService;
|
||||
this.incidentService = incidentService;
|
||||
this.globalpingService = globalpingService;
|
||||
this.maintenanceWindowsRepository = maintenanceWindowsRepository;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.teamsRepository = teamsRepository;
|
||||
@@ -122,7 +127,34 @@ class SuperSimpleQueueHelper {
|
||||
throw new Error("No network response");
|
||||
}
|
||||
|
||||
// Step 3. Build check
|
||||
// Step 2b. Run Globalping checks (fire-and-forget, doesn't affect local check pipeline)
|
||||
if (monitor.globalpingEnabled && (monitor.type === "http" || monitor.type === "ping")) {
|
||||
this.globalpingService
|
||||
.runChecks(monitor)
|
||||
.then((locationStatuses) => {
|
||||
for (const locStatus of locationStatuses) {
|
||||
const locCheck = this.checkService.buildCheck(locStatus);
|
||||
if (locCheck) {
|
||||
this.buffer.addToBuffer(locCheck);
|
||||
} else {
|
||||
this.logger.warn({
|
||||
message: `Failed to build Globalping check for monitor ${monitorId}, location: ${locStatus.location}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.logger.warn({
|
||||
message: `Globalping checks failed for monitor ${monitorId}: ${err.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Step 3. Build check
|
||||
const check = await this.checkService.buildCheck(status);
|
||||
if (!check) {
|
||||
this.logger.warn({
|
||||
@@ -133,15 +165,15 @@ class SuperSimpleQueueHelper {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Step 4 Add check to buffer
|
||||
// Step 4. Add check to buffer
|
||||
this.buffer.addToBuffer(check);
|
||||
// Step 4. Update monitor status
|
||||
// Step 5. Update monitor status
|
||||
const statusChangeResult = await this.statusService.updateMonitorStatus(status, check);
|
||||
|
||||
// Step 5. Get decisions
|
||||
// Step 6. Get decisions
|
||||
const decision = this.evaluateMonitorAction(statusChangeResult);
|
||||
|
||||
// Step 6. Handle notifications (best effort, continue even in event of failure, don't wait)
|
||||
// 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({
|
||||
@@ -154,7 +186,7 @@ class SuperSimpleQueueHelper {
|
||||
});
|
||||
}
|
||||
|
||||
// Step 7. Handle incidents (best effort, don't wait)
|
||||
// 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,
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import { Globalping } from "globalping";
|
||||
import type { ISettingsService } from "@/service/system/settingsService.js";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import type { MonitorStatusResponse } 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<MonitorStatusResponse[]> {
|
||||
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<MonitorStatusResponse[]> {
|
||||
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: MonitorStatusResponse[] = [];
|
||||
|
||||
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({
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: result.statusCode >= 200 && result.statusCode < 400,
|
||||
code: result.statusCode || 0,
|
||||
message: result.statusCodeName || result.rawOutput || "",
|
||||
responseTime: result.timings?.total ?? 0,
|
||||
location: locationId,
|
||||
});
|
||||
} else {
|
||||
// Ping: loss < 100% is considered up (consistent with local ping checks)
|
||||
const stats = result.stats;
|
||||
results.push({
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: stats?.loss !== undefined ? stats.loss < 100 : false,
|
||||
code: stats?.loss === 0 ? 200 : 5000,
|
||||
message: stats?.loss === 0 ? "Ping successful" : `Packet loss: ${stats?.loss}%`,
|
||||
responseTime: stats?.avg ?? 0,
|
||||
location: locationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -6,6 +6,15 @@ export interface CheckMetadata {
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
type: MonitorType;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export interface LocationResult {
|
||||
location: string;
|
||||
status: boolean;
|
||||
responseTime: number;
|
||||
statusCode: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface CheckCpuInfo {
|
||||
@@ -113,6 +122,7 @@ export interface Check {
|
||||
seo?: number;
|
||||
performance?: number;
|
||||
audits?: CheckAudits;
|
||||
locationResults?: LocationResult[];
|
||||
__v: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface Monitor {
|
||||
selectedDisks: string[];
|
||||
gameId?: string;
|
||||
group: string | null;
|
||||
globalpingEnabled: boolean;
|
||||
recentChecks: CheckSnapshot[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -64,6 +65,12 @@ export interface MonitorsWithChecksByTeamIdResult {
|
||||
monitors: Monitor[];
|
||||
}
|
||||
|
||||
export interface LocationCheckData {
|
||||
groupedChecks: import("./check.js").GroupedCheck[];
|
||||
uptimePercentage: number;
|
||||
avgResponseTime: number;
|
||||
}
|
||||
|
||||
export interface UptimeDetailsResult {
|
||||
monitorData: {
|
||||
monitor: Monitor;
|
||||
@@ -72,6 +79,7 @@ export interface UptimeDetailsResult {
|
||||
groupedDownChecks: import("./check.js").GroupedCheck[];
|
||||
groupedAvgResponseTime: number;
|
||||
groupedUptimePercentage: number;
|
||||
locationChecks?: Record<string, LocationCheckData>;
|
||||
};
|
||||
monitorStats: import("./monitorStats.js").MonitorStats | null;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,15 @@ export interface MonitorStatusResponse<T = any> {
|
||||
matchMethod?: MonitorMatchMethod;
|
||||
expectedValue?: string;
|
||||
extracted?: unknown;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export interface GlobalpingCheckResult {
|
||||
location: string;
|
||||
status: boolean;
|
||||
responseTime: number;
|
||||
statusCode: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface PingStatusPayload {
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface Settings {
|
||||
systemEmailRequireTLS: boolean;
|
||||
systemEmailRejectUnauthorized: boolean;
|
||||
showURL: boolean;
|
||||
globalpingApiKey?: string;
|
||||
globalpingLocationsTier?: number;
|
||||
singleton: boolean;
|
||||
version: number;
|
||||
globalThresholds?: SettingsThresholds;
|
||||
|
||||
@@ -172,6 +172,7 @@ const createMonitorBodyValidation = joi.object({
|
||||
gameId: joi.string().allow(""),
|
||||
selectedDisks: joi.array().items(joi.string()).optional(),
|
||||
group: joi.string().max(50).trim().allow(null, "").optional(),
|
||||
globalpingEnabled: joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const createMonitorsBodyValidation = joi.array().items(
|
||||
@@ -203,6 +204,7 @@ const editMonitorBodyValidation = joi
|
||||
gameId: joi.string().allow(""),
|
||||
selectedDisks: joi.array().items(joi.string()).optional(),
|
||||
group: joi.string().max(50).trim().allow(null, "").optional(),
|
||||
globalpingEnabled: joi.boolean().optional(),
|
||||
})
|
||||
.options({ stripUnknown: true });
|
||||
|
||||
@@ -428,6 +430,8 @@ const updateAppSettingsBodyValidation = joi.object({
|
||||
systemEmailRequireTLS: joi.boolean(),
|
||||
systemEmailRejectUnauthorized: joi.boolean(),
|
||||
|
||||
globalpingApiKey: joi.string().allow(""),
|
||||
globalpingLocationsTier: joi.number().valid(3, 6, 15).optional(),
|
||||
globalThresholds: joi
|
||||
.object({
|
||||
cpu: joi.number().min(1).max(100).allow(""),
|
||||
|
||||
Reference in new issue
Block a user