diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index e74c83242..633fcb5c9 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -37,7 +37,6 @@ export const useMonitorForm = ({ matchMethod: data?.matchMethod || "", expectedValue: data?.expectedValue || "", jsonPath: data?.jsonPath || "", - locations: data?.locations || [], }; break; case "ping": @@ -45,7 +44,6 @@ export const useMonitorForm = ({ ...base, type: "ping", url: data?.url || "", - locations: data?.locations || [], }; break; case "port": diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 97bd36802..aa7de0ffd 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -27,18 +27,12 @@ import { SliderWithLabel, Dialog, } from "@/Components/v2/inputs"; -import Chip from "@mui/material/Chip"; import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor"; import type { Notification } from "@/Types/Notification"; import type { MonitorFormData } from "@/Validation/monitor"; -interface GlobalpingLocation { - id: string; - label: string; -} - interface GeneralSettingsConfig { urlLabel: string; urlPlaceholder: string; @@ -152,10 +146,6 @@ const CreateMonitorPage = () => { const { data: notifications } = useGet("/notifications/team"); const { data: games } = useGet("/monitors/games"); - const { data: globalpingLocations } = useGet( - "/monitors/globalping/locations" - ); - const { schema, defaults } = useMonitorForm({ data: existingMonitor ?? null, defaultType, @@ -498,56 +488,6 @@ const CreateMonitorPage = () => { } /> - {/* Global locations - only for http and ping types */} - {(watchedType === "http" || watchedType === "ping") && globalpingLocations && globalpingLocations.length > 0 && ( - { - const locationOptions = (globalpingLocations ?? []).map((loc) => ({ - ...loc, - name: loc.label, - })); - const selectedLocations = locationOptions.filter((loc) => - (field.value ?? []).includes(loc.id) - ); - return ( - - option.name} - onChange={(_: unknown, newValue: typeof locationOptions) => { - const ids = newValue.map((loc) => loc.id); - if (ids.length <= 5) { - field.onChange(ids); - } - }} - isOptionEqualToValue={(option, value) => option.id === value.id} - renderTags={(value, getTagProps) => - value.map((option, index) => ( - - )) - } - /> - - ); - }} - /> - } - /> - )} - {/* Alert Thresholds - only for hardware type */} {generalSettingsConfig.showSecret && ( { const checks = checksData?.checks ?? []; const checksCount = checksData?.checksCount ?? 0; + const { data: globalpingData } = useGet<{ + locations: { id: string; label: string }[]; + labels: Record; + }>("/monitors/globalping/locations"); + const locationLabels = globalpingData?.labels ?? {}; + const locationChecks = monitorData?.locationChecks; const locationKeys = useMemo( () => (locationChecks ? Object.keys(locationChecks).sort() : []), @@ -182,7 +188,7 @@ const UptimeDetailsPage = () => { {locationKeys.map((loc) => ( ))} diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index b841dabac..68a15fbec 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -31,14 +31,12 @@ const httpSchema = baseSchema.extend({ matchMethod: z.enum(["equal", "include", "regex", ""]).optional(), expectedValue: z.string().optional(), jsonPath: z.string().optional(), - locations: z.array(z.string()).max(5).optional(), }); // Ping monitor schema const pingSchema = baseSchema.extend({ type: z.literal("ping"), url: z.string().min(1, "Host is required"), - locations: z.array(z.string()).max(5).optional(), }); // Port monitor schema diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 1b38bf4d0..5bbe56567 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -26,7 +26,7 @@ import { } from "./controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorService } from "@/service/index.js"; -import { getGlobalpingLocationsByTier } from "@/service/infrastructure/globalpingService.js"; +import { getGlobalpingLocationsByTier, LOCATION_LABELS } from "@/service/infrastructure/globalpingService.js"; import type { ISettingsService } from "@/service/system/settingsService.js"; const SERVICE_NAME = "monitorController"; @@ -399,7 +399,7 @@ class MonitorController { return res.status(200).json({ success: true, msg: "Globalping locations retrieved successfully", - data: locations, + data: { locations, labels: LOCATION_LABELS }, }); } catch (error) { next(error); diff --git a/server/src/controllers/settingsController.ts b/server/src/controllers/settingsController.ts index e5357e2ce..7672ec726 100644 --- a/server/src/controllers/settingsController.ts +++ b/server/src/controllers/settingsController.ts @@ -60,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) => { diff --git a/server/src/repositories/settings/MongoSettingsRepository.ts b/server/src/repositories/settings/MongoSettingsRepository.ts index 82ca17933..02d240db2 100644 --- a/server/src/repositories/settings/MongoSettingsRepository.ts +++ b/server/src/repositories/settings/MongoSettingsRepository.ts @@ -83,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, diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 5e7ff317b..e1b9bc9cd 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -6,6 +6,7 @@ import type { UptimeDetailsResult, HardwareDetailsResult, PageSpeedDetailsResult, + LocationCheckData, GamesMap, } from "@/types/monitor.js"; import type { @@ -222,9 +223,9 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 }); } - // Fetch location checks if monitor has locations configured - let locationChecks: Record | undefined; - if (monitor.locations && monitor.locations.length > 0) { + // Fetch location checks if this is an HTTP or ping monitor + let locationChecks: Record | undefined; + if (monitor.type === "http" || monitor.type === "ping") { const locData = await this.checksRepository.findLocationChecksByMonitorId( monitor.id, start, diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 7435a2b06..f9bf9ad64 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -128,7 +128,7 @@ class SuperSimpleQueueHelper { } // Step 2b. Run Globalping checks (fire-and-forget, doesn't affect local check pipeline) - if (monitor.locations && monitor.locations.length > 0 && (monitor.type === "http" || monitor.type === "ping")) { + if (monitor.type === "http" || monitor.type === "ping") { this.globalpingService .runChecks(monitor) .then((locationStatuses) => { diff --git a/server/src/service/infrastructure/globalpingService.ts b/server/src/service/infrastructure/globalpingService.ts index be25a966c..0f6094a24 100644 --- a/server/src/service/infrastructure/globalpingService.ts +++ b/server/src/service/infrastructure/globalpingService.ts @@ -5,20 +5,23 @@ import type { MonitorStatusResponse } from "@/types/network.js"; const SERVICE_NAME = "GlobalpingService"; +interface GlobalpingLocationOption { + id: string; + label: string; +} + // Maps location IDs to Globalping API location objects const LOCATION_MAP: Record> = { - // Continent-level (used for 3 and 6 tier) "north-america": { continent: "NA" }, europe: { continent: "EU" }, asia: { continent: "AS" }, "south-america": { continent: "SA" }, oceania: { continent: "OC" }, africa: { continent: "AF" }, - // City-level (used for 15 tier) "us-east": { country: "US", state: "VA" }, "us-west": { country: "US", state: "CA" }, canada: { country: "CA", city: "Toronto" }, - "uk": { country: "GB", city: "London" }, + uk: { country: "GB", city: "London" }, germany: { country: "DE", city: "Frankfurt" }, netherlands: { country: "NL", city: "Amsterdam" }, poland: { country: "PL", city: "Warsaw" }, @@ -32,29 +35,19 @@ const LOCATION_MAP: Record> = { uae: { country: "AE", city: "Dubai" }, }; -interface GlobalpingLocationOption { - id: string; - label: string; -} - -// 3 locations: core regions const LOCATIONS_TIER_3: GlobalpingLocationOption[] = [ { id: "north-america", label: "North America" }, { id: "europe", label: "Europe" }, { id: "asia", label: "Asia" }, ]; -// 6 locations: all continents const LOCATIONS_TIER_6: GlobalpingLocationOption[] = [ - { id: "north-america", label: "North America" }, - { id: "europe", label: "Europe" }, - { id: "asia", label: "Asia" }, + ...LOCATIONS_TIER_3, { id: "south-america", label: "South America" }, { id: "oceania", label: "Oceania" }, { id: "africa", label: "Africa" }, ]; -// 15 locations: city/country level const LOCATIONS_TIER_15: GlobalpingLocationOption[] = [ { id: "us-east", label: "US East (Virginia)" }, { id: "us-west", label: "US West (California)" }, @@ -79,6 +72,14 @@ const LOCATIONS_BY_TIER: Record = { 15: LOCATIONS_TIER_15, }; +// Build a lookup from location ID to human-readable label +const LOCATION_LABELS: Record = {}; +for (const tier of Object.values(LOCATIONS_BY_TIER)) { + for (const loc of tier) { + LOCATION_LABELS[loc.id] = loc.label; + } +} + export const getGlobalpingLocationsByTier = (tier: number): GlobalpingLocationOption[] => { return LOCATIONS_BY_TIER[tier] ?? LOCATIONS_TIER_6; }; @@ -117,25 +118,34 @@ class GlobalpingService { return this.client; } + /** + * Gets the current tier's location IDs from settings. + * This is the source of truth for which locations to check. + */ + private async getTierLocationIds(): Promise { + const dbSettings = await this.settingsService.getDBSettings(); + const tier = dbSettings.globalpingLocationsTier ?? 6; + return getGlobalpingLocationsByTier(tier).map((loc) => loc.id); + } + async runChecks(monitor: Monitor): Promise { - if (!monitor.locations || monitor.locations.length === 0) { + if (monitor.type !== "http" && monitor.type !== "ping") { return []; } const client = await this.getClient(); if (!client) { - this.logger.warn({ - message: "Globalping API key not configured, skipping location checks", - service: SERVICE_NAME, - method: "runChecks", - }); + return []; + } + + const locationIds = await this.getTierLocationIds(); + if (locationIds.length === 0) { return []; } const measurementType = monitor.type === "ping" ? "ping" : "http"; const monitorUrl = monitor.url ?? ""; - // Parse target from monitor URL let target: string; try { if (measurementType === "http") { @@ -148,7 +158,7 @@ class GlobalpingService { target = monitorUrl; } - const locations = monitor.locations + const locations = locationIds .map((loc) => LOCATION_MAP[loc]) .filter(Boolean); @@ -199,7 +209,7 @@ class GlobalpingService { for (const item of measurement.results || []) { const probe = item.probe; const result = item.result; - const locationLabel = this.getLocationLabel(probe?.continent, probe?.country); + const locationId = this.resolveLocationId(probe?.continent, probe?.country, locationIds); if (measurementType === "http") { results.push({ @@ -210,10 +220,9 @@ class GlobalpingService { code: result.statusCode || 0, message: result.statusCodeName || result.rawOutput || "", responseTime: result.timings?.total ?? 0, - location: locationLabel, + location: locationId, }); } else { - // Ping const stats = result.stats; results.push({ monitorId: monitor.id, @@ -223,7 +232,7 @@ class GlobalpingService { code: stats?.loss === 0 ? 200 : 5000, message: stats?.loss === 0 ? "Ping successful" : `Packet loss: ${stats?.loss}%`, responseTime: stats?.avg ?? 0, - location: locationLabel, + location: locationId, }); } } @@ -240,11 +249,37 @@ class GlobalpingService { } } - private getLocationLabel(continent?: string, country?: string): string { - if (continent && country) { - return `${continent}-${country}`; + /** + * 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"; } - return 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"; } async getLimits(): Promise { @@ -265,4 +300,5 @@ class GlobalpingService { } } +export { LOCATION_LABELS }; export default GlobalpingService;