## Changes ### Server - Expand LOCATION_MAP to support city/country-level locations for the 15-location tier (US East/West, Toronto, London, Frankfurt, Amsterdam, Warsaw, Tokyo, Singapore, Mumbai, Sydney, Brazil, Santiago, Johannesburg, Dubai) - Add globalpingLocationsTier field to Settings type, Mongoose model, repository mapper, and Joi validation (valid: 3, 6, 15, default: 6) - Update getGlobalpingLocations endpoint to return locations based on the configured tier from settings - Pass settingsService to MonitorController for tier lookup ### Client - Add tier selector (radio buttons) to SettingsGlobalping component with descriptions for each option: - 3 locations: North America, Europe, Asia - 6 locations: all continents - 15 locations: city-level global coverage - Add rate limits info box explaining credit usage per tier: - Without token: 250 tests/hour - With free token: 500 tests/hour - GitHub sponsors: higher limits - Add all translation keys for tier labels, descriptions, and rate info ## Benefits - Admins can choose the right balance of coverage vs API usage - Rate limit info helps users decide whether they need authentication
74 lines
2.2 KiB
TypeScript
74 lines
2.2 KiB
TypeScript
import { z } from "zod";
|
|
|
|
// Helper to normalize empty strings to undefined
|
|
const optionalString = () =>
|
|
z.preprocess(
|
|
(val) =>
|
|
val === "" || val === null || val === undefined ? undefined : String(val).trim(),
|
|
z.string().optional()
|
|
);
|
|
|
|
// Helper for threshold fields
|
|
const thresholdField = (min: number, max: number, unit: string) =>
|
|
z.preprocess(
|
|
(val) => (val === "" || val === null || val === undefined ? undefined : val),
|
|
z.coerce
|
|
.number()
|
|
.int()
|
|
.min(min, `Min ${min}${unit}`)
|
|
.max(max, `Max ${max}${unit}`)
|
|
.optional()
|
|
);
|
|
|
|
export const settingsSchema = z.object({
|
|
systemEmailIgnoreTLS: z.boolean(),
|
|
systemEmailRequireTLS: z.boolean(),
|
|
systemEmailRejectUnauthorized: z.boolean(),
|
|
systemEmailConnectionHost: optionalString(),
|
|
systemEmailSecure: z.boolean().optional(),
|
|
systemEmailPool: z.boolean().optional(),
|
|
showURL: z.boolean().optional(),
|
|
checkTTL: z.coerce
|
|
.number()
|
|
.int()
|
|
.min(1, "Please enter a value")
|
|
.max(365, "Maximum 365 days"),
|
|
pagespeedApiKey: optionalString(),
|
|
globalpingApiKey: optionalString(),
|
|
globalpingLocationsTier: z.coerce.number().optional(),
|
|
systemEmailHost: z.preprocess(
|
|
(val) =>
|
|
val === "" || val === null || val === undefined ? undefined : String(val).trim(),
|
|
z
|
|
.string()
|
|
.regex(/^[a-zA-Z0-9.-]+$/, "Invalid hostname or IP address")
|
|
.optional()
|
|
),
|
|
systemEmailPort: z.preprocess(
|
|
(val) => (val === "" || val === null || val === undefined ? undefined : val),
|
|
z.coerce
|
|
.number()
|
|
.int()
|
|
.min(1, "Port must be at least 1")
|
|
.max(65535, "Port must be at most 65535")
|
|
.optional()
|
|
),
|
|
systemEmailAddress: z.preprocess((val) => {
|
|
if (val === "" || val === null || val === undefined) return undefined;
|
|
return String(val).toLowerCase().trim();
|
|
}, z.string().email("Please enter a valid email address").optional()),
|
|
systemEmailUser: optionalString(),
|
|
systemEmailPassword: optionalString(),
|
|
systemEmailTLSServername: optionalString(),
|
|
globalThresholds: z
|
|
.object({
|
|
cpu: thresholdField(0, 100, "%"),
|
|
memory: thresholdField(0, 100, "%"),
|
|
disk: thresholdField(0, 100, "%"),
|
|
temperature: thresholdField(0, 150, "°C"),
|
|
})
|
|
.optional(),
|
|
});
|
|
|
|
export type SettingsFormData = z.infer<typeof settingsSchema>;
|