Compare commits

...
Author SHA1 Message Date
Alex Holliday 35c022df0f geo job 2026-02-23 00:00:45 +00:00
Alex Holliday 19fc85ae2d front end 2026-02-22 23:25:04 +00:00
Alex Holliday b4f8fa7d72 aggregation 2026-02-22 21:15:02 +00:00
Alex Holliday ef5629d8e4 remove unused 2026-02-22 20:35:13 +00:00
Alex Holliday 4018125224 initial commit 2026-02-22 20:28:42 +00:00
15 changed files with 511 additions and 67 deletions

No files matched your search

+3
View File
@@ -14,6 +14,9 @@ const getBaseDefaults = (data?: Monitor | null) => ({
notifications: data?.notifications || [],
statusWindowSize: data?.statusWindowSize || 5,
statusWindowThreshold: data?.statusWindowThreshold || 60,
geoCheckEnabled: data?.geoCheckEnabled || false,
geoCheckLocations: data?.geoCheckLocations || [],
geoCheckInterval: data?.geoCheckInterval || 300000,
});
export const useMonitorForm = ({
+85
View File
@@ -30,6 +30,7 @@ import {
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
import { useMonitorForm } from "@/Hooks/useMonitorForm";
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
import { GeoCheckLocations } from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification";
import type { MonitorFormData } from "@/Validation/monitor";
@@ -165,6 +166,7 @@ const CreateMonitorPage = () => {
const watchedType = watch("type") as MonitorType;
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
const watchGeoCheckEnabled = watch("geoCheckEnabled") as boolean;
useEffect(() => {
clearErrors();
@@ -823,6 +825,89 @@ const CreateMonitorPage = () => {
/>
)}
{/* Geo-distributed Checks Configuration */}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.geoChecks.title")}
subtitle={t("pages.createMonitor.form.geoChecks.description")}
rightContent={
<Stack spacing={theme.spacing(8)}>
<Controller
name="geoCheckEnabled"
control={control}
render={({ field }) => (
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(2)}
>
<Switch
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
<Typography>
{t("pages.createMonitor.form.geoChecks.option.enabled.label")}
</Typography>
</Stack>
)}
/>
{watchGeoCheckEnabled && (
<Stack spacing={theme.spacing(8)}>
<Controller
name="geoCheckLocations"
control={control}
render={({ field }) => {
const locationOptions = GeoCheckLocations.map((loc) => ({
id: loc,
name: t(
`pages.createMonitor.form.geoChecks.option.locations.options.${loc}`
),
}));
const selectedLocations = locationOptions.filter((loc) =>
(field.value ?? []).includes(loc.id)
);
return (
<Autocomplete
multiple={true}
options={locationOptions}
value={selectedLocations}
getOptionLabel={(option) => option.name}
onChange={(_, newValue: typeof locationOptions) => {
field.onChange(newValue.map((loc) => loc.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
fieldLabel={t(
"pages.createMonitor.form.geoChecks.option.locations.label"
)}
/>
);
}}
/>
<Controller
name="geoCheckInterval"
control={control}
render={({ field }) => (
<Select
value={field.value ?? 300000}
onChange={(e) => field.onChange(Number(e.target.value))}
fieldLabel={t(
"pages.createMonitor.form.geoChecks.option.interval.label"
)}
>
<MenuItem value={300000}>5 min</MenuItem>
<MenuItem value={600000}>10 min</MenuItem>
<MenuItem value={900000}>15 min</MenuItem>
<MenuItem value={1800000}>30 min</MenuItem>
</Select>
)}
/>
</Stack>
)}
</Stack>
}
/>
)}
<Stack
direction="row"
justifyContent="flex-end"
+9
View File
@@ -160,6 +160,14 @@ export interface PageSpeedGroupedCheck {
totalChecks: number;
}
export interface GroupedGeoCheck {
bucketDate: string;
continent: string;
avgResponseTime: number;
totalChecks: number;
uptimePercentage: number;
}
export interface LatestCheck {
status: boolean;
responseTime: number;
@@ -236,6 +244,7 @@ export interface UptimeChecksResult {
groupedDownChecks: GroupedCheck[];
uptimePercentage: number;
avgResponseTime: number;
groupedGeoChecks?: GroupedGeoCheck[];
}
export interface ChecksSummary {
+6
View File
@@ -25,6 +25,9 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
export const GeoCheckLocations = ["EU", "NA", "AS", "SA", "AF", "OC"] as const;
export type GeoCheckLocation = (typeof GeoCheckLocations)[number];
export interface Monitor {
id: string;
userId: string;
@@ -59,6 +62,9 @@ export interface Monitor {
selectedDisks: string[];
gameId?: string;
group: string | null;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoCheckLocation[];
geoCheckInterval?: number;
recentChecks: CheckSnapshot[];
createdAt: string;
updatedAt: string;
+3
View File
@@ -20,6 +20,9 @@ const baseSchema = z.object({
.number({ message: "Threshold percentage is required" })
.min(1, "Incident percentage must be at least 1")
.max(100, "Incident percentage must be at most 100"),
geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(["EU", "NA", "AS", "SA", "AF", "OC"])).optional(),
geoCheckInterval: z.number().min(5, "Interval must be at least 5 minutes").optional(),
});
// HTTP monitor schema
+25
View File
@@ -431,6 +431,31 @@
},
"title": "Advanced settings"
},
"geoChecks": {
"title": "Geo-distributed checks",
"description": "Run checks from multiple geographic locations to monitor global availability and performance.",
"option": {
"enabled": {
"label": "Enable geo-distributed checks"
},
"locations": {
"label": "Locations",
"placeholder": "Select locations",
"options": {
"EU": "Europe",
"NA": "North America",
"AS": "Asia",
"SA": "South America",
"AF": "Africa",
"OC": "Oceania"
}
},
"interval": {
"label": "Check interval (minutes)",
"helper": "Minimum 5 minutes between geo checks"
}
}
},
"frequency": {
"description": "How often do you want to check the status of this monitor?",
"option": {
+34
View File
@@ -11,6 +11,8 @@ import type {
CheckMemoryInfo,
CheckMetadata,
CheckNetworkInterfaceInfo,
GeoCheckResult,
GeoCheckTimings,
GotTimings,
ILighthouseAudit,
} from "@/types/check.js";
@@ -175,6 +177,34 @@ const auditsSchema = new Schema<CheckAudits>(
{ _id: false }
);
const geoCheckTimingsSchema = new Schema<GeoCheckTimings>(
{
total: { type: Number, required: true },
dns: { type: Number },
tcp: { type: Number },
tls: { type: Number },
firstByte: { type: Number },
download: { type: Number },
},
{ _id: false }
);
const geoCheckResultSchema = new Schema<GeoCheckResult>(
{
continent: { type: String, required: true },
region: { type: String },
country: { type: String, required: true },
state: { type: String },
city: { type: String, required: true },
longitude: { type: Number, required: true },
latitude: { type: Number, required: true },
status: { type: Boolean, required: true },
statusCode: { type: Number, required: true },
timings: { type: geoCheckTimingsSchema, required: true },
},
{ _id: false }
);
const metadataSchema = new Schema<CheckMetadataDocument>(
{
monitorId: {
@@ -281,6 +311,10 @@ const CheckSchema = new Schema<CheckDocument>(
type: auditsSchema,
default: undefined,
},
geoResults: {
type: [geoCheckResultSchema],
default: undefined,
},
},
{
timestamps: true,
+15 -2
View File
@@ -1,6 +1,6 @@
import { Schema, model, Types, type UpdateQuery } from "mongoose";
import { Schema, model, Types } from "mongoose";
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
import { MonitorTypes, MonitorStatuses, GeoCheckLocations } from "@/types/monitor.js";
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
@@ -169,6 +169,19 @@ const MonitorSchema = new Schema<MonitorDocument>(
return value && value.trim() ? value.trim() : null;
},
},
geoCheckEnabled: {
type: Boolean,
default: false,
},
geoCheckLocations: {
type: [String],
enum: GeoCheckLocations,
default: [],
},
geoCheckInterval: {
type: Number,
default: 300000,
},
recentChecks: {
type: [checkSnapshotSchema],
default: [],
@@ -10,6 +10,8 @@ import type {
CheckMemoryInfo,
CheckMetadata,
CheckNetworkInterfaceInfo,
GeoCheckResult,
GeoCheckTimings,
GotTimings,
MonitorType,
} from "@/types/index.js";
@@ -165,6 +167,33 @@ class MongoChecksRepository implements IChecksRepository {
};
};
const mapGeoTimings = (timings?: GeoCheckTimings): GeoCheckTimings => ({
total: timings?.total ?? 0,
dns: timings?.dns,
tcp: timings?.tcp,
tls: timings?.tls,
firstByte: timings?.firstByte,
download: timings?.download,
});
const mapGeoResults = (geoResults?: GeoCheckResult[]): GeoCheckResult[] | undefined => {
if (!geoResults?.length) {
return undefined;
}
return geoResults.map((result) => ({
continent: result.continent ?? "",
region: result.region,
country: result.country ?? "",
state: result.state,
city: result.city ?? "",
longitude: result.longitude ?? 0,
latitude: result.latitude ?? 0,
status: result.status ?? false,
statusCode: result.statusCode ?? 0,
timings: mapGeoTimings(result.timings),
}));
};
const mapMetadata = (metadata: CheckDocument["metadata"]): CheckMetadata => ({
monitorId: toStringId(metadata.monitorId),
teamId: toStringId(metadata.teamId),
@@ -194,6 +223,7 @@ class MongoChecksRepository implements IChecksRepository {
seo: doc.seo,
performance: doc.performance,
audits: mapAudits(doc.audits),
geoResults: mapGeoResults(doc.geoResults),
__v: doc.__v ?? 0,
createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt),
@@ -427,85 +457,131 @@ class MongoChecksRepository implements IChecksRepository {
endDate: Date,
dateString: string
) => {
const matchStage = {
// Match stage for primary checks (exclude geo checks)
const primaryCheckMatchStage = {
"metadata.monitorId": monitorObjectId,
createdAt: { $gte: startDate, $lte: endDate },
geoResults: { $exists: false },
};
const [result] = await CheckModel.aggregate([
{ $match: matchStage },
{ $sort: { createdAt: 1 } },
{
$facet: {
uptimePercentage: [
{
$group: {
_id: null,
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
totalChecks: { $sum: 1 },
},
},
{
$project: {
_id: 0,
percentage: {
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
// Match stage for geo checks only
const geoCheckMatchStage = {
"metadata.monitorId": monitorObjectId,
createdAt: { $gte: startDate, $lte: endDate },
geoResults: { $exists: true, $ne: null },
};
// Run two separate aggregations - one for primary checks, one for geo checks
// TODO, only do this if the monitor has geochecks enabled, otherwise we only need one aggregation
const [primaryResult, geoResults] = await Promise.all([
CheckModel.aggregate([
{ $match: primaryCheckMatchStage },
{ $sort: { createdAt: 1 } },
{
$facet: {
uptimePercentage: [
{
$group: {
_id: null,
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
totalChecks: { $sum: 1 },
},
},
},
],
groupedAvgResponseTime: [
{
$group: {
_id: null,
avgResponseTime: { $avg: "$responseTime" },
},
},
],
groupedChecks: [
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
{
$project: {
_id: 0,
percentage: {
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
},
},
avgResponseTime: { $avg: "$responseTime" },
totalChecks: { $sum: 1 },
},
},
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
groupedUpChecks: [
{ $match: { status: true } },
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
],
groupedAvgResponseTime: [
{
$group: {
_id: null,
avgResponseTime: { $avg: "$responseTime" },
},
totalChecks: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" },
},
},
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
groupedDownChecks: [
{ $match: { status: false } },
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
],
groupedChecks: [
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
},
avgResponseTime: { $avg: "$responseTime" },
totalChecks: { $sum: 1 },
},
totalChecks: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" },
},
},
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
groupedUpChecks: [
{ $match: { status: true } },
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
},
totalChecks: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" },
},
},
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
groupedDownChecks: [
{ $match: { status: false } },
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
},
totalChecks: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" },
},
},
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
},
},
},
]),
CheckModel.aggregate([
{ $match: geoCheckMatchStage },
{ $sort: { createdAt: 1 } },
{ $unwind: "$geoResults" },
{
$group: {
_id: {
date: { $dateToString: { format: dateString, date: "$createdAt" } },
continent: "$geoResults.continent",
},
avgResponseTime: { $avg: "$geoResults.timings.total" },
totalChecks: { $sum: 1 },
upChecks: { $sum: { $cond: [{ $eq: ["$geoResults.status", true] }, 1, 0] } },
},
},
{
$project: {
_id: 0,
bucketDate: "$_id.date",
continent: "$_id.continent",
avgResponseTime: 1,
totalChecks: 1,
uptimePercentage: {
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
},
},
},
{ $sort: { bucketDate: 1, continent: 1 } },
]),
]);
const [result] = primaryResult;
const groupedGeoChecks = geoResults ?? [];
const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0;
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
@@ -516,6 +592,7 @@ class MongoChecksRepository implements IChecksRepository {
groupedDownChecks: result?.groupedDownChecks ?? [],
uptimePercentage,
avgResponseTime,
groupedGeoChecks,
};
};
@@ -353,6 +353,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined,
group: doc.group ?? null,
geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 5,
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt),
@@ -408,6 +411,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined,
group: doc.group ?? null,
geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 5,
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt),
@@ -97,6 +97,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
this.scheduler.start();
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
this.scheduler.addTemplate("geo-check-job", this.helper.getGeoCheckJob());
this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob());
const monitors = await this.monitorsRepository.findAll();
if (!monitors) {
@@ -131,14 +132,28 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
active: monitor.isActive,
data: monitor,
});
// Add geo check job if enabled
if (monitor.geoCheckEnabled && monitor.type === "http") {
this.scheduler.addJob({
id: `${monitorId}-geo`,
template: "geo-check-job",
repeat: monitor.geoCheckInterval,
active: monitor.isActive,
data: monitor,
});
}
};
deleteJob = async (monitor: any) => {
this.scheduler.removeJob(monitor.id);
this.scheduler.removeJob(`${monitor.id}-geo`);
};
pauseJob = async (monitor: any) => {
const result = await this.scheduler.pauseJob(monitor.id);
// Also pause geo check job if it exists
await this.scheduler.pauseJob(`${monitor.id}-geo`);
if (result === false) {
throw new Error("Failed to resume monitor");
}
@@ -151,6 +166,8 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
resumeJob = async (monitor: any) => {
const result = await this.scheduler.resumeJob(monitor.id);
// Also resume geo check job if it exists
await this.scheduler.resumeJob(`${monitor.id}-geo`);
if (result === false) {
throw new Error("Failed to resume monitor");
}
@@ -163,6 +180,29 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
updateJob = async (monitor: any) => {
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
// Handle geo check job
const geoJobId = `${monitor.id}-geo`;
const geoJobExists = await this.scheduler.getJob(geoJobId);
if (monitor.geoCheckEnabled && monitor.type === "http") {
if (geoJobExists) {
// Update existing geo job
this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, data: monitor, active: monitor.isActive });
} else {
// Add new geo job
this.scheduler.addJob({
id: geoJobId,
template: "geo-check-job",
repeat: monitor.geoCheckInterval,
active: monitor.isActive,
data: monitor,
});
}
} else if (geoJobExists) {
// Remove geo job if it exists but is no longer enabled
this.scheduler.removeJob(geoJobId);
}
};
shutdown = async () => {
@@ -176,6 +176,98 @@ class SuperSimpleQueueHelper {
};
};
getGeoCheckJob = () => {
return async (monitor: Monitor) => {
try {
const monitorId = monitor.id;
const teamId = monitor.teamId;
if (!monitorId) {
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
}
// Safety checks
if (!monitor.geoCheckEnabled || monitor.type !== "http") {
this.logger.debug({
message: `Geo check skipped for monitor ${monitorId} - not enabled or not HTTP type`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
this.logger.warn({
message: `Geo check skipped for monitor ${monitorId} - no locations configured`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
// Check for maintenance window
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
if (maintenanceWindowActive) {
this.logger.debug({
message: `Geo check skipped for monitor ${monitorId} - in maintenance window`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
this.logger.debug({
message: `Starting geo check for monitor ${monitorId}`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
// TODO: Implement GlobalPing API integration
// Step 1: Create measurement via GlobalPing API
// const measurementId = await this.globalPingService.createMeasurement(
// monitor.url,
// monitor.geoCheckLocations
// );
// Step 2: Poll for results (blocks 2-30s, but doesn't affect primary monitor checks)
// const geoResults = await this.globalPingService.pollForResults(measurementId);
// Step 3: Create check document with only geo results
// const check = {
// metadata: {
// monitorId: monitor.id,
// teamId: monitor.teamId,
// type: monitor.type,
// },
// status: true,
// responseTime: 0,
// statusCode: 200,
// message: "Geo check",
// geoResults,
// ack: false,
// };
// Step 4: Save check
// await this.checksRepository.create([check]);
this.logger.info({
message: `Geo check completed for monitor ${monitorId}`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
} catch (error: any) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "getGeoCheckJob",
details: error,
stack: error.stack,
});
throw error;
}
};
};
getCleanupOrphanedJob = () => {
return async () => {
try {
+32
View File
@@ -90,6 +90,28 @@ export interface ILighthouseAudit {
numericUnit?: string;
}
export interface GeoCheckTimings {
total: number;
dns?: number;
tcp?: number;
tls?: number;
firstByte?: number;
download?: number;
}
export interface GeoCheckResult {
continent: string;
region?: string;
country: string;
state?: string;
city: string;
longitude: number;
latitude: number;
status: boolean;
statusCode: number;
timings: GeoCheckTimings;
}
export interface Check {
id: string;
metadata: CheckMetadata;
@@ -113,6 +135,7 @@ export interface Check {
seo?: number;
performance?: number;
audits?: CheckAudits;
geoResults?: GeoCheckResult[];
__v: number;
createdAt: string;
updatedAt: string;
@@ -179,6 +202,14 @@ export interface PageSpeedGroupedCheck {
totalChecks: number;
}
export interface GroupedGeoCheck {
bucketDate: string;
continent: string;
avgResponseTime: number;
totalChecks: number;
uptimePercentage: number;
}
export interface UptimeChecksResult {
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
groupedChecks: GroupedCheck[];
@@ -186,6 +217,7 @@ export interface UptimeChecksResult {
groupedDownChecks: GroupedCheck[];
uptimePercentage: number;
avgResponseTime: number;
groupedGeoChecks?: GroupedGeoCheck[];
}
export interface ChecksSummary {
+6
View File
@@ -7,6 +7,9 @@ export type MonitorType = (typeof MonitorTypes)[number];
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
export type MonitorStatus = (typeof MonitorStatuses)[number];
export const GeoCheckLocations = ["EU", "NA", "AS", "SA", "AF", "OC"] as const;
export type GeoCheckLocation = (typeof GeoCheckLocations)[number];
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
export interface Monitor {
@@ -43,6 +46,9 @@ export interface Monitor {
selectedDisks: string[];
gameId?: string;
group: string | null;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoCheckLocation[];
geoCheckInterval?: number;
recentChecks: CheckSnapshot[];
createdAt: string;
updatedAt: string;
+13
View File
@@ -1,5 +1,6 @@
import joi, { type CustomHelpers } from "joi";
import { type UserRole, UserRoles } from "@/types/user.js";
import { GeoCheckLocations } from "@/types/monitor.js";
//****************************************
// Custom Validators
@@ -172,6 +173,12 @@ const createMonitorBodyValidation = joi.object({
gameId: joi.string().allow(""),
selectedDisks: joi.array().items(joi.string()).optional(),
group: joi.string().max(50).trim().allow(null, "").optional(),
geoCheckEnabled: joi.boolean().optional(),
geoCheckLocations: joi
.array()
.items(joi.string().valid(...GeoCheckLocations))
.optional(),
geoCheckInterval: joi.number().min(300000).optional(),
});
const createMonitorsBodyValidation = joi.array().items(
@@ -203,6 +210,12 @@ const editMonitorBodyValidation = joi
gameId: joi.string().allow(""),
selectedDisks: joi.array().items(joi.string()).optional(),
group: joi.string().max(50).trim().allow(null, "").optional(),
geoCheckEnabled: joi.boolean().optional(),
geoCheckLocations: joi
.array()
.items(joi.string().valid(...GeoCheckLocations))
.optional(),
geoCheckInterval: joi.number().min(5).optional(),
})
.options({ stripUnknown: true });