From fb3795bd7058eb4b1e1ab1ad7fb95c979ac0dd84 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 12:15:29 +0000 Subject: [PATCH 001/103] 3120 remove URL's from data when they are supposed to be disabled --- server/src/config/services.js | 2 +- server/src/db/v1/modules/statusPageModule.js | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/server/src/config/services.js b/server/src/config/services.js index 9744c647d..6d6a613d3 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -83,7 +83,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService // Create DB const checkModule = new CheckModule({ logger, Check, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); - const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); + const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const monitorModule = new MonitorModule({ diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index aadde0c81..c9027125a 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -1,4 +1,3 @@ -// import StatusPage from "../../models/StatusPage.js"; // import { NormalizeData } from "../../../utils/dataUtils.js"; // import ServiceRegistry from "../../../service/system/serviceRegistry.js"; // import StringService from "../../../service/system/stringService.js"; @@ -6,10 +5,11 @@ const SERVICE_NAME = "statusPageModule"; class StatusPageModule { - constructor({ StatusPage, NormalizeData, stringService }) { + constructor({ StatusPage, NormalizeData, stringService, AppSettings }) { this.StatusPage = StatusPage; this.NormalizeData = NormalizeData; this.stringService = stringService; + this.AppSettings = AppSettings; } createStatusPage = async ({ statusPageData, image, userId, teamId }) => { @@ -217,7 +217,7 @@ class StatusPageModule { showUptimePercentage: 1, timezone: 1, showAdminLoginLink: 1, - url: 1, + url: { $cond: [{ $eq: ["$statusPage.showMonitorUrl", true] }, "$monitors.url", "$$REMOVE"] }, }, monitors: { _id: 1, @@ -260,11 +260,16 @@ class StatusPageModule { const { statusPage, monitors } = statusPageQuery[0]; + const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); + const showURL = appSettings?.showURL === true; + const normalizedMonitors = monitors.map((monitor) => { - return { - ...monitor, - checks: this.NormalizeData(monitor.checks, 10, 100), - }; + const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); + if (showURL !== true) { + const { url, ...rest } = monitor; + return { ...rest, checks: normalizedChecks }; + } + return { ...monitor, checks: normalizedChecks }; }); return { statusPage, monitors: normalizedMonitors }; From ab4cc80628528cae3b65c69a15b22e972c5e6095 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 21:29:15 +0000 Subject: [PATCH 002/103] 3120 persist admin value --- client/src/Pages/v1/Settings/index.jsx | 10 +++++----- server/src/controllers/v1/settingsController.ts | 4 ++++ server/src/db/v1/modules/statusPageModule.js | 12 ++++++------ server/src/service/v1/system/settingsService.js | 1 + server/src/validation/joi.js | 2 +- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index b6137121b..2cb794b77 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -80,11 +80,6 @@ const Settings = () => { const handleChange = async (e) => { const { name, value, checked } = e.target; - // Special case for showURL until handled properly in the backend - if (name === "showURL") { - dispatch(setShowURL(value)); - return; - } let newValue; if ( name === "systemEmailIgnoreTLS" || @@ -167,6 +162,11 @@ const Settings = () => { } setSettingsData(newSettingsData); + + // Update Redux immediately for UI feedback + if (name === "showURL") { + dispatch(setShowURL(newValue ?? value)); + } }; const handleSave = () => { diff --git a/server/src/controllers/v1/settingsController.ts b/server/src/controllers/v1/settingsController.ts index 5d3f6ed0e..30b4cf8ea 100644 --- a/server/src/controllers/v1/settingsController.ts +++ b/server/src/controllers/v1/settingsController.ts @@ -57,6 +57,10 @@ class SettingsController { }; updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { + // Diagnostics: capture inbound payload + // eslint-disable-next-line no-console + console.log("=== SETTINGS CONTROLLER REQUEST BODY ===", req.body); + await updateAppSettingsBodyValidation.validateAsync(req.body); const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body); diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index c9027125a..48c46ad4c 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -217,7 +217,7 @@ class StatusPageModule { showUptimePercentage: 1, timezone: 1, showAdminLoginLink: 1, - url: { $cond: [{ $eq: ["$statusPage.showMonitorUrl", true] }, "$monitors.url", "$$REMOVE"] }, + url: 1, }, monitors: { _id: 1, @@ -261,12 +261,12 @@ class StatusPageModule { const { statusPage, monitors } = statusPageQuery[0]; const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); - const showURL = appSettings?.showURL === true; + const showURL = appSettings?.showURL === true || appSettings?.showURL === "true"; - const normalizedMonitors = monitors.map((monitor) => { - const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); - if (showURL !== true) { - const { url, ...rest } = monitor; + const normalizedMonitors = monitors.map((monitor) => { + const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); + if (showURL !== true) { + const { url, port, secret, notifications, ...rest } = monitor; return { ...rest, checks: normalizedChecks }; } return { ...monitor, checks: normalizedChecks }; diff --git a/server/src/service/v1/system/settingsService.js b/server/src/service/v1/system/settingsService.js index 70d8d192c..cea46f7ed 100755 --- a/server/src/service/v1/system/settingsService.js +++ b/server/src/service/v1/system/settingsService.js @@ -60,6 +60,7 @@ class SettingsService { await this.AppSettings.create({}); settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); } + return settings; } } diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index b3342de83..d0e1aeeb3 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -413,7 +413,7 @@ const updateAppSettingsBodyValidation = joi.object({ pagespeedApiKey: joi.string().allow(""), language: joi.string().allow(""), timezone: joi.string().allow(""), - // showURL: joi.bool().required(), + showURL: joi.bool().optional(), systemEmailHost: joi.string().allow(""), systemEmailPort: joi.number().allow(""), systemEmailAddress: joi.string().allow(""), From 643a743d707d04f1eab2ec3915dad9ed9b15b109 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 21:49:10 +0000 Subject: [PATCH 003/103] more logging --- client/src/Pages/v1/Settings/index.jsx | 5 ++++- server/src/db/v1/modules/statusPageModule.js | 21 +++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index 2cb794b77..69369daed 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -80,7 +80,8 @@ const Settings = () => { const handleChange = async (e) => { const { name, value, checked } = e.target; - let newValue; + // eslint-disable-next-line no-console + console.log("=== SETTINGS HANDLER CHANGE ===", { name, value, checked, type: typeof value }); if ( name === "systemEmailIgnoreTLS" || name === "systemEmailRequireTLS" || @@ -170,6 +171,8 @@ const Settings = () => { }; const handleSave = () => { + // eslint-disable-next-line no-console + console.log("=== HANDLE SAVE ===", { settingsDataSettings: settingsData?.settings }); const { error } = settingsValidation.validate(settingsData.settings, { abortEarly: false, }); diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index 48c46ad4c..48622c5d6 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -263,16 +263,27 @@ class StatusPageModule { const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); const showURL = appSettings?.showURL === true || appSettings?.showURL === "true"; + // eslint-disable-next-line no-console + console.log("=== STATUS PAGE FETCH ==="); + // eslint-disable-next-line no-console + console.log("appSettings object:", JSON.stringify(appSettings, null, 2)); + // eslint-disable-next-line no-console + console.log("appSettings.showURL value:", appSettings?.showURL); + // eslint-disable-next-line no-console + console.log("appSettings.showURL type:", typeof appSettings?.showURL); + // eslint-disable-next-line no-console + console.log("showURL after check:", showURL); + const normalizedMonitors = monitors.map((monitor) => { const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); if (showURL !== true) { const { url, port, secret, notifications, ...rest } = monitor; - return { ...rest, checks: normalizedChecks }; - } - return { ...monitor, checks: normalizedChecks }; - }); + return { ...rest, checks: normalizedChecks }; + } + return { ...monitor, checks: normalizedChecks }; + }); - return { statusPage, monitors: normalizedMonitors }; + return { statusPage, monitors: normalizedMonitors }; } catch (error) { error.service = SERVICE_NAME; error.method = "getStatusPageByUrl"; From f659876845a5ae98ad39b5710901a2e6394bc1ee Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 21:58:13 +0000 Subject: [PATCH 004/103] select bug --- client/src/Pages/v1/Settings/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index 69369daed..b50c1887a 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -166,7 +166,7 @@ const Settings = () => { // Update Redux immediately for UI feedback if (name === "showURL") { - dispatch(setShowURL(newValue ?? value)); + dispatch(setShowURL(value)); } }; From a13f6a34c46e3b5ebb81d7021a43e34949fa6424 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 22:02:36 +0000 Subject: [PATCH 005/103] missing value --- client/src/Pages/v1/Settings/index.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index b50c1887a..a1335777c 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -82,6 +82,8 @@ const Settings = () => { // eslint-disable-next-line no-console console.log("=== SETTINGS HANDLER CHANGE ===", { name, value, checked, type: typeof value }); + + let newValue; if ( name === "systemEmailIgnoreTLS" || name === "systemEmailRequireTLS" || From 526fa24eab7b00d9d0af030d04afe8abf24b36b3 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 22:23:31 +0000 Subject: [PATCH 006/103] validation bug --- client/src/Validation/validation.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 112bbb349..4663afd37 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -328,6 +328,7 @@ const settingsValidation = joi.object({ systemEmailIgnoreTLS: joi.boolean(), systemEmailRequireTLS: joi.boolean(), systemEmailRejectUnauthorized: joi.boolean(), + showURL: joi.boolean().optional(), globalThresholds: joi .object({ cpu: joi.number().min(1).max(100).allow("").optional(), From 12b28ffb3f9858ac24eea269e6b9622e88d1b819 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 22:30:03 +0000 Subject: [PATCH 007/103] type issue --- client/src/Pages/v1/Settings/index.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index a1335777c..a0a99d943 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -93,6 +93,12 @@ const Settings = () => { ) { newValue = checked; } + + // Ensure showURL is a proper boolean + if (name === "showURL") { + newValue = value === true || value === "true"; + } + // Build next state early const newSettingsData = { ...settingsData, From 418068e70298b88edba1ae0db9df27eecf864fbc Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 18:18:38 +0000 Subject: [PATCH 008/103] loggin settings --- server/src/db/v1/modules/settingsModule.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/db/v1/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js index 37b851bf2..35622c4c2 100755 --- a/server/src/db/v1/modules/settingsModule.js +++ b/server/src/db/v1/modules/settingsModule.js @@ -8,6 +8,11 @@ class SettingsModule { updateAppSettings = async (newSettings) => { try { + // eslint-disable-next-line no-console + console.log("=== SETTINGS MODULE UPDATE ==="); + // eslint-disable-next-line no-console + console.log("Incoming showURL:", newSettings.showURL, "type:", typeof newSettings.showURL); + const update = { $set: { ...newSettings } }; if (newSettings.pagespeedApiKey === "") { @@ -24,6 +29,8 @@ class SettingsModule { upsert: true, }); const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); + // eslint-disable-next-line no-console + console.log("Saved showURL:", settings.showURL, "type:", typeof settings.showURL); return settings; } catch (error) { error.service = SERVICE_NAME; From 76ab2695db585572e23ef95236bf07abc1862f1e Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 18:23:58 +0000 Subject: [PATCH 009/103] missing setting --- server/src/db/v1/models/AppSettings.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/db/v1/models/AppSettings.js b/server/src/db/v1/models/AppSettings.js index 2313d638c..a1a9964f2 100755 --- a/server/src/db/v1/models/AppSettings.js +++ b/server/src/db/v1/models/AppSettings.js @@ -55,6 +55,10 @@ const AppSettingsSchema = mongoose.Schema( type: Boolean, default: true, }, + showURL: { + type: Boolean, + default: false, + }, singleton: { type: Boolean, required: true, From 7b619e6b313343a7f50219ccffd835f4ea1c0e14 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 18:56:16 +0000 Subject: [PATCH 010/103] Remove debug logging and add feature documentation - Remove all console.log statements added during development - Clean up code for production readiness --- client/src/Pages/v1/Settings/index.jsx | 5 ----- server/src/controllers/v1/settingsController.ts | 4 ---- server/src/db/v1/modules/settingsModule.js | 7 ------- server/src/db/v1/modules/statusPageModule.js | 15 ++------------- 4 files changed, 2 insertions(+), 29 deletions(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index a0a99d943..907479b6a 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -80,9 +80,6 @@ const Settings = () => { const handleChange = async (e) => { const { name, value, checked } = e.target; - // eslint-disable-next-line no-console - console.log("=== SETTINGS HANDLER CHANGE ===", { name, value, checked, type: typeof value }); - let newValue; if ( name === "systemEmailIgnoreTLS" || @@ -179,8 +176,6 @@ const Settings = () => { }; const handleSave = () => { - // eslint-disable-next-line no-console - console.log("=== HANDLE SAVE ===", { settingsDataSettings: settingsData?.settings }); const { error } = settingsValidation.validate(settingsData.settings, { abortEarly: false, }); diff --git a/server/src/controllers/v1/settingsController.ts b/server/src/controllers/v1/settingsController.ts index 30b4cf8ea..5d3f6ed0e 100644 --- a/server/src/controllers/v1/settingsController.ts +++ b/server/src/controllers/v1/settingsController.ts @@ -57,10 +57,6 @@ class SettingsController { }; updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { - // Diagnostics: capture inbound payload - // eslint-disable-next-line no-console - console.log("=== SETTINGS CONTROLLER REQUEST BODY ===", req.body); - await updateAppSettingsBodyValidation.validateAsync(req.body); const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body); diff --git a/server/src/db/v1/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js index 35622c4c2..37b851bf2 100755 --- a/server/src/db/v1/modules/settingsModule.js +++ b/server/src/db/v1/modules/settingsModule.js @@ -8,11 +8,6 @@ class SettingsModule { updateAppSettings = async (newSettings) => { try { - // eslint-disable-next-line no-console - console.log("=== SETTINGS MODULE UPDATE ==="); - // eslint-disable-next-line no-console - console.log("Incoming showURL:", newSettings.showURL, "type:", typeof newSettings.showURL); - const update = { $set: { ...newSettings } }; if (newSettings.pagespeedApiKey === "") { @@ -29,8 +24,6 @@ class SettingsModule { upsert: true, }); const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); - // eslint-disable-next-line no-console - console.log("Saved showURL:", settings.showURL, "type:", typeof settings.showURL); return settings; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index 48622c5d6..870a1ee55 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -261,20 +261,9 @@ class StatusPageModule { const { statusPage, monitors } = statusPageQuery[0]; const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); - const showURL = appSettings?.showURL === true || appSettings?.showURL === "true"; + const showURL = appSettings?.showURL === true; - // eslint-disable-next-line no-console - console.log("=== STATUS PAGE FETCH ==="); - // eslint-disable-next-line no-console - console.log("appSettings object:", JSON.stringify(appSettings, null, 2)); - // eslint-disable-next-line no-console - console.log("appSettings.showURL value:", appSettings?.showURL); - // eslint-disable-next-line no-console - console.log("appSettings.showURL type:", typeof appSettings?.showURL); - // eslint-disable-next-line no-console - console.log("showURL after check:", showURL); - - const normalizedMonitors = monitors.map((monitor) => { + const normalizedMonitors = monitors.map((monitor) => { const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); if (showURL !== true) { const { url, port, secret, notifications, ...rest } = monitor; From 7556086f713a86c2358207fd8f7927333feb9a7c Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 20:51:25 +0000 Subject: [PATCH 011/103] code review updates Summary of what we fixed based on AI review feedback: Formatting - Ran Prettier to fix code formatting Redux state rollback - Moved Redux dispatch to after successful save in settingsHooks.js Performance - Added caching layer in settingsModule (1-minute TTL) to prevent DB query on every status page request Architecture - Inject settingsModule instead of raw AppSettings model for better encapsulation --- client/src/Hooks/v1/settingsHooks.js | 8 ++++++ client/src/Pages/v1/Settings/index.jsx | 5 ---- server/src/config/services.js | 4 +-- server/src/db/v1/modules/settingsModule.js | 26 ++++++++++++++++++++ server/src/db/v1/modules/statusPageModule.js | 22 ++++++++--------- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/client/src/Hooks/v1/settingsHooks.js b/client/src/Hooks/v1/settingsHooks.js index 787e46ff9..ac754ca69 100644 --- a/client/src/Hooks/v1/settingsHooks.js +++ b/client/src/Hooks/v1/settingsHooks.js @@ -56,6 +56,14 @@ const useSaveSettings = ({ setEmailPasswordHasBeenReset(false); } setSettingsData(settingsResponse.data.data); + + // Update Redux state after successful save + if (typeof settings.showURL !== "undefined") { + const { setShowURL } = await import("../../Features/UI/uiSlice.js"); + const { default: store } = await import("../../store.js"); + store.dispatch(setShowURL(settings.showURL)); + } + createToast({ body: t("settingsSuccessSaved") }); } catch (error) { createToast({ body: t("settingsFailedToSave") }); diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index 907479b6a..926f2ce65 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -168,11 +168,6 @@ const Settings = () => { } setSettingsData(newSettingsData); - - // Update Redux immediately for UI feedback - if (name === "showURL") { - dispatch(setShowURL(value)); - } }; const handleSave = () => { diff --git a/server/src/config/services.js b/server/src/config/services.js index 6d6a613d3..f3768be25 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -83,7 +83,8 @@ export const initializeServices = async ({ logger, envSettings, settingsService // Create DB const checkModule = new CheckModule({ logger, Check, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); - const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); + const settingsModule = new SettingsModule({ AppSettings }); + const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, settingsModule }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const monitorModule = new MonitorModule({ @@ -100,7 +101,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService }); const notificationModule = new NotificationModule({ Notification, Monitor }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); - const settingsModule = new SettingsModule({ AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ diff --git a/server/src/db/v1/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js index 37b851bf2..cb689f467 100755 --- a/server/src/db/v1/modules/settingsModule.js +++ b/server/src/db/v1/modules/settingsModule.js @@ -4,8 +4,29 @@ const SERVICE_NAME = "SettingsModule"; class SettingsModule { constructor({ AppSettings }) { this.AppSettings = AppSettings; + this.settingsCache = null; + this.cacheTimestamp = null; + this.CACHE_TTL = 60000; // 1 minute cache } + getAppSettings = async () => { + try { + const now = Date.now(); + if (this.settingsCache && this.cacheTimestamp && (now - this.cacheTimestamp) < this.CACHE_TTL) { + return this.settingsCache; + } + + const settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); + this.settingsCache = settings; + this.cacheTimestamp = now; + return settings; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getAppSettings"; + throw error; + } + }; + updateAppSettings = async (newSettings) => { try { const update = { $set: { ...newSettings } }; @@ -24,6 +45,11 @@ class SettingsModule { upsert: true, }); const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); + + // Invalidate cache after update + this.settingsCache = settings; + this.cacheTimestamp = Date.now(); + return settings; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index 870a1ee55..8c48d93a0 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -5,11 +5,11 @@ const SERVICE_NAME = "statusPageModule"; class StatusPageModule { - constructor({ StatusPage, NormalizeData, stringService, AppSettings }) { + constructor({ StatusPage, NormalizeData, stringService, settingsModule }) { this.StatusPage = StatusPage; this.NormalizeData = NormalizeData; this.stringService = stringService; - this.AppSettings = AppSettings; + this.settingsModule = settingsModule; } createStatusPage = async ({ statusPageData, image, userId, teamId }) => { @@ -260,19 +260,19 @@ class StatusPageModule { const { statusPage, monitors } = statusPageQuery[0]; - const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); + const appSettings = await this.settingsModule.getAppSettings(); const showURL = appSettings?.showURL === true; const normalizedMonitors = monitors.map((monitor) => { - const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); - if (showURL !== true) { - const { url, port, secret, notifications, ...rest } = monitor; - return { ...rest, checks: normalizedChecks }; - } - return { ...monitor, checks: normalizedChecks }; - }); + const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); + if (showURL !== true) { + const { url, port, secret, notifications, ...rest } = monitor; + return { ...rest, checks: normalizedChecks }; + } + return { ...monitor, checks: normalizedChecks }; + }); - return { statusPage, monitors: normalizedMonitors }; + return { statusPage, monitors: normalizedMonitors }; } catch (error) { error.service = SERVICE_NAME; error.method = "getStatusPageByUrl"; From e4d101237e0385a18e2800ce8cf75110c66680f5 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 21:51:59 +0000 Subject: [PATCH 012/103] bug fix - settings handlechange --- client/src/Hooks/v1/settingsHooks.js | 7 ------- client/src/Pages/v1/Settings/index.jsx | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/client/src/Hooks/v1/settingsHooks.js b/client/src/Hooks/v1/settingsHooks.js index ac754ca69..f4a929e7f 100644 --- a/client/src/Hooks/v1/settingsHooks.js +++ b/client/src/Hooks/v1/settingsHooks.js @@ -57,13 +57,6 @@ const useSaveSettings = ({ } setSettingsData(settingsResponse.data.data); - // Update Redux state after successful save - if (typeof settings.showURL !== "undefined") { - const { setShowURL } = await import("../../Features/UI/uiSlice.js"); - const { default: store } = await import("../../store.js"); - store.dispatch(setShowURL(settings.showURL)); - } - createToast({ body: t("settingsSuccessSaved") }); } catch (error) { createToast({ body: t("settingsFailedToSave") }); diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index 926f2ce65..ea714c34a 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -168,6 +168,11 @@ const Settings = () => { } setSettingsData(newSettingsData); + + // Update Redux immediately for UI feedback + if (name === "showURL") { + dispatch(setShowURL(newValue ?? value)); + } }; const handleSave = () => { From b6f48a910fed7f3909b1999825f74d61db448d6c Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 22:08:15 +0000 Subject: [PATCH 013/103] Revert caching changes - restore working implementation - Remove settingsModule caching that caused inconsistent state - Restore direct AppSettings query in statusPageModule - Keep immediate Redux dispatch for UI feedback - All functionality verified working" --- server/src/config/services.js | 4 +-- server/src/db/v1/modules/settingsModule.js | 26 -------------------- server/src/db/v1/modules/statusPageModule.js | 6 ++--- 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/server/src/config/services.js b/server/src/config/services.js index f3768be25..6d6a613d3 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -83,8 +83,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService // Create DB const checkModule = new CheckModule({ logger, Check, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); - const settingsModule = new SettingsModule({ AppSettings }); - const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, settingsModule }); + const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const monitorModule = new MonitorModule({ @@ -101,6 +100,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService }); const notificationModule = new NotificationModule({ Notification, Monitor }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); + const settingsModule = new SettingsModule({ AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ diff --git a/server/src/db/v1/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js index cb689f467..37b851bf2 100755 --- a/server/src/db/v1/modules/settingsModule.js +++ b/server/src/db/v1/modules/settingsModule.js @@ -4,29 +4,8 @@ const SERVICE_NAME = "SettingsModule"; class SettingsModule { constructor({ AppSettings }) { this.AppSettings = AppSettings; - this.settingsCache = null; - this.cacheTimestamp = null; - this.CACHE_TTL = 60000; // 1 minute cache } - getAppSettings = async () => { - try { - const now = Date.now(); - if (this.settingsCache && this.cacheTimestamp && (now - this.cacheTimestamp) < this.CACHE_TTL) { - return this.settingsCache; - } - - const settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); - this.settingsCache = settings; - this.cacheTimestamp = now; - return settings; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAppSettings"; - throw error; - } - }; - updateAppSettings = async (newSettings) => { try { const update = { $set: { ...newSettings } }; @@ -45,11 +24,6 @@ class SettingsModule { upsert: true, }); const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); - - // Invalidate cache after update - this.settingsCache = settings; - this.cacheTimestamp = Date.now(); - return settings; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index 8c48d93a0..f276814f9 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -5,11 +5,11 @@ const SERVICE_NAME = "statusPageModule"; class StatusPageModule { - constructor({ StatusPage, NormalizeData, stringService, settingsModule }) { + constructor({ StatusPage, NormalizeData, stringService, AppSettings }) { this.StatusPage = StatusPage; this.NormalizeData = NormalizeData; this.stringService = stringService; - this.settingsModule = settingsModule; + this.AppSettings = AppSettings; } createStatusPage = async ({ statusPageData, image, userId, teamId }) => { @@ -260,7 +260,7 @@ class StatusPageModule { const { statusPage, monitors } = statusPageQuery[0]; - const appSettings = await this.settingsModule.getAppSettings(); + const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); const showURL = appSettings?.showURL === true; const normalizedMonitors = monitors.map((monitor) => { From a70959e4a66769604c9298a125d44a455b75aed3 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 22:36:05 +0000 Subject: [PATCH 014/103] Nitpick fix - ref - client/src/Pages/v1/Settings/index.jsx (1) 171-175: LGTM! The Redux dispatch provides immediate UI feedback while the actual persistence happens on Save. This is the correct pattern for a server-persisted setting. Minor note: The ?? value fallback is unreachable for showURL since newValue is always set to a boolean at line 96. Consider simplifying to dispatch(setShowURL(newValue)) for clarity, though the current code is functionally correct. --- client/src/Pages/v1/Settings/index.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index ea714c34a..23478e2e8 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -171,11 +171,7 @@ const Settings = () => { // Update Redux immediately for UI feedback if (name === "showURL") { - dispatch(setShowURL(newValue ?? value)); - } - }; - - const handleSave = () => { + dispatch(setShowURL(newValue)); const { error } = settingsValidation.validate(settingsData.settings, { abortEarly: false, }); From 1494df9eb39b18044978045f339d3cd7e27a1832 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 22:39:49 +0000 Subject: [PATCH 015/103] missing brace --- client/src/Pages/v1/Settings/index.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index 23478e2e8..b988b9e2c 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -172,10 +172,10 @@ const Settings = () => { // Update Redux immediately for UI feedback if (name === "showURL") { dispatch(setShowURL(newValue)); - const { error } = settingsValidation.validate(settingsData.settings, { - abortEarly: false, - }); - if (!error || error.details.length === 0) { + } +}; + +const handleSave = () => { setErrors({}); } else { const newErrors = {}; From 3ce40b1ab8839ead5789f98d39feb9619ab151b4 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 22:48:17 +0000 Subject: [PATCH 016/103] Fix handleSave function syntax errors --- client/package-lock.json | 1678 +++++++++++++++++------- client/src/Pages/v1/Settings/index.jsx | 27 +- 2 files changed, 1212 insertions(+), 493 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 75dbe43da..7ca70d20f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -56,19 +56,6 @@ "vite": "6.3.6" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -84,31 +71,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", - "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -130,15 +116,15 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -161,6 +147,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -175,14 +170,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -211,9 +206,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -229,25 +224,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -289,9 +284,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -312,40 +307,40 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@ctrl/tinycolor": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz", - "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", "license": "MIT", "engines": { "node": ">=14" @@ -390,9 +385,9 @@ "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", - "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0" @@ -409,7 +404,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -449,11 +443,10 @@ "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", - "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -499,10 +492,266 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -515,10 +764,154 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -535,9 +928,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -568,22 +961,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", @@ -595,31 +972,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", - "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", - "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.0", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -627,9 +1004,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, "node_modules/@hapi/hoek": { @@ -715,17 +1092,23 @@ "license": "BSD-3-Clause" }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -737,25 +1120,16 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -766,7 +1140,7 @@ "version": "5.0.0-beta.42", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.42.tgz", "integrity": "sha512-fWRiUJVCHCPF+mxd5drn08bY2qRw3jj5f1SSQdUXmaJ/yKpk23ys8MgLO2KGVTRtbks/+ctRfgffGPbXifj0Ug==", - "deprecated": "This package has been replaced by @base-ui-components/react", + "deprecated": "This package has been replaced by @base-ui/react", "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.4", @@ -796,9 +1170,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.11.tgz", - "integrity": "sha512-CzAQs9CTzlwbsF9ZYB4o4lLwBv1/qNE264NjuYao+ctAXsmlPtYa8RtER4UsUXSMxNN9Qi+aQdYcKl2sUpnmAw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz", + "integrity": "sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q==", "license": "MIT", "funding": { "type": "opencollective", @@ -900,9 +1274,9 @@ } }, "node_modules/@mui/lab/node_modules/@mui/styled-engine": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.11.tgz", - "integrity": "sha512-74AUmlHXaGNbyUqdK/+NwDJOZqgRQw6BcNvhoWYLq3LGbLTkE+khaJ7soz6cIabE4CPYqO2/QAIU1Z/HEjjpcw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.5.0.tgz", + "integrity": "sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -934,14 +1308,14 @@ } }, "node_modules/@mui/lab/node_modules/@mui/system": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.11.tgz", - "integrity": "sha512-gibtsrZEwnDaT5+I/KloOj/yHluX5G8heknuxBpQOdEQ3Gc0avjSImn5hSeKp8D4thiwZiApuggIjZw1dQguUA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.5.0.tgz", + "integrity": "sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", "@mui/private-theming": "^6.4.9", - "@mui/styled-engine": "^6.4.11", + "@mui/styled-engine": "^6.5.0", "@mui/types": "~7.2.24", "@mui/utils": "^6.4.9", "clsx": "^2.1.1", @@ -992,7 +1366,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz", "integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.26.0", "@mui/core-downloads-tracker": "^6.4.11", @@ -1065,9 +1438,9 @@ } }, "node_modules/@mui/material/node_modules/@mui/styled-engine": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.11.tgz", - "integrity": "sha512-74AUmlHXaGNbyUqdK/+NwDJOZqgRQw6BcNvhoWYLq3LGbLTkE+khaJ7soz6cIabE4CPYqO2/QAIU1Z/HEjjpcw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.5.0.tgz", + "integrity": "sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1099,14 +1472,14 @@ } }, "node_modules/@mui/material/node_modules/@mui/system": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.11.tgz", - "integrity": "sha512-gibtsrZEwnDaT5+I/KloOj/yHluX5G8heknuxBpQOdEQ3Gc0avjSImn5hSeKp8D4thiwZiApuggIjZw1dQguUA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.5.0.tgz", + "integrity": "sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", "@mui/private-theming": "^6.4.9", - "@mui/styled-engine": "^6.4.11", + "@mui/styled-engine": "^6.5.0", "@mui/types": "~7.2.24", "@mui/utils": "^6.4.9", "clsx": "^2.1.1", @@ -1153,13 +1526,14 @@ } }, "node_modules/@mui/private-theming": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz", - "integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.7.tgz", + "integrity": "sha512-w7r1+CYhG0syCAQUWAuV5zSaU2/67WA9JXUderdb7DzCIJdp/5RmJv6L85wRjgKCMsxFF0Kfn0kPgPbPgw/jdw==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/utils": "^7.1.0", + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.7", "prop-types": "^15.8.1" }, "engines": { @@ -1180,17 +1554,18 @@ } }, "node_modules/@mui/private-theming/node_modules/@mui/utils": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", - "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.7.tgz", + "integrity": "sha512-+YjnjMRnyeTkWnspzoxRdiSOgkrcpTikhNPoxOZW0APXx+urHtUoXJ9lbtCZRCA5a4dg5gSbd19alL1DvRs5fg==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/types": "^7.4.2", - "@types/prop-types": "^15.7.14", + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.10", + "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.1.0" + "react-is": "^19.2.3" }, "engines": { "node": ">=14.0.0" @@ -1210,16 +1585,17 @@ } }, "node_modules/@mui/styled-engine": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz", - "integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.7.tgz", + "integrity": "sha512-y/QkNXv6cF6dZ5APztd/dFWfQ6LHKPx3skyYO38YhQD4+Cxd6sFAL3Z38WMSSC8LQz145Mpp3CcLrSCLKPwYAg==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@emotion/cache": "^11.13.5", + "@babel/runtime": "^7.28.4", + "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", + "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "engines": { @@ -1244,19 +1620,19 @@ } }, "node_modules/@mui/system": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz", - "integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.7.tgz", + "integrity": "sha512-DovL3k+FBRKnhmatzUMyO5bKkhMLlQ9L7Qw5qHrre3m8zCZmE+31NDVBFfqrbrA7sq681qaEIHdkWD5nmiAjyQ==", "license": "MIT", "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/private-theming": "^7.1.0", - "@mui/styled-engine": "^7.1.0", - "@mui/types": "^7.4.2", - "@mui/utils": "^7.1.0", + "@babel/runtime": "^7.28.4", + "@mui/private-theming": "^7.3.7", + "@mui/styled-engine": "^7.3.7", + "@mui/types": "^7.4.10", + "@mui/utils": "^7.3.7", "clsx": "^2.1.1", - "csstype": "^3.1.3", + "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "engines": { @@ -1285,17 +1661,18 @@ } }, "node_modules/@mui/system/node_modules/@mui/utils": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", - "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.7.tgz", + "integrity": "sha512-+YjnjMRnyeTkWnspzoxRdiSOgkrcpTikhNPoxOZW0APXx+urHtUoXJ9lbtCZRCA5a4dg5gSbd19alL1DvRs5fg==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/types": "^7.4.2", - "@types/prop-types": "^15.7.14", + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.10", + "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.1.0" + "react-is": "^19.2.3" }, "engines": { "node": ">=14.0.0" @@ -1315,12 +1692,12 @@ } }, "node_modules/@mui/types": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz", - "integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==", + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.10.tgz", + "integrity": "sha512-0+4mSjknSu218GW3isRqoxKRTOrTLd/vHi/7UC4+wZcUrOAqD9kRk7UQRL1mcrzqRoe7s3UT6rsRpbLkW5mHpQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.1" + "@babel/runtime": "^7.28.4" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1666,18 +2043,25 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -1696,10 +2080,114 @@ } } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", - "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", "cpu": [ "arm64" ], @@ -1709,10 +2197,114 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", - "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", "cpu": [ "x64" ], @@ -1723,9 +2315,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", - "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", "cpu": [ "x64" ], @@ -1735,6 +2327,84 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -1757,9 +2427,9 @@ "license": "BSD-3-Clause" }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, "node_modules/@standard-schema/utils": { @@ -2043,19 +2713,19 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, "node_modules/@types/d3-color": { @@ -2122,9 +2792,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/node": { @@ -2133,7 +2803,6 @@ "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2145,20 +2814,19 @@ "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.22", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", - "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { @@ -2194,15 +2862,16 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.10", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -2210,16 +2879,15 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2304,18 +2972,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2455,9 +3125,9 @@ } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2496,6 +3166,15 @@ "node": ">= 0.6.0" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2508,9 +3187,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -2526,12 +3205,12 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -2611,9 +3290,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", "funding": [ { "type": "opencollective", @@ -2630,6 +3309,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2743,9 +3439,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/d3-array": { @@ -2939,13 +3635,12 @@ "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3083,9 +3778,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "license": "ISC" }, "node_modules/entities": { @@ -3101,18 +3796,18 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/es-abstract": { - "version": "1.23.10", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", - "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -3143,7 +3838,9 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", @@ -3158,6 +3855,7 @@ "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -3194,27 +3892,27 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.1", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", + "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" }, "engines": { @@ -3280,9 +3978,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3292,31 +3990,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -3347,7 +4046,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3445,9 +4143,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3515,39 +4213,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -3567,9 +4232,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3632,9 +4297,9 @@ "license": "MIT" }, "node_modules/fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3655,9 +4320,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -3665,10 +4330,13 @@ } }, "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -3743,9 +4411,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -3779,9 +4447,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -3855,6 +4523,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3955,12 +4633,19 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -4155,7 +4840,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -4179,9 +4863,9 @@ } }, "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", "funding": { "type": "opencollective", @@ -4424,14 +5108,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -4468,6 +5153,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -4692,9 +5390,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4966,9 +5664,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/numbered": { @@ -5256,9 +5954,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -5278,9 +5976,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -5297,7 +5995,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5316,9 +6014,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", "bin": { @@ -5423,7 +6121,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5437,7 +6134,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5450,16 +6146,16 @@ } }, "node_modules/react-i18next": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", - "integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz", + "integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.0", + "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { - "i18next": ">= 23.2.3", + "i18next": ">= 23.4.0", "react": ">= 16.8.0", "typescript": "^5" }, @@ -5485,9 +6181,9 @@ } }, "node_modules/react-is": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", - "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", + "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", "license": "MIT" }, "node_modules/react-redux": { @@ -5524,12 +6220,12 @@ } }, "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -5539,13 +6235,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -5641,8 +6337,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-persist": { "version": "6.0.0", @@ -5713,12 +6408,12 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -5776,13 +6471,12 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", - "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "license": "MIT", - "peer": true, "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -5792,26 +6486,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.0", - "@rollup/rollup-android-arm64": "4.41.0", - "@rollup/rollup-darwin-arm64": "4.41.0", - "@rollup/rollup-darwin-x64": "4.41.0", - "@rollup/rollup-freebsd-arm64": "4.41.0", - "@rollup/rollup-freebsd-x64": "4.41.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", - "@rollup/rollup-linux-arm-musleabihf": "4.41.0", - "@rollup/rollup-linux-arm64-gnu": "4.41.0", - "@rollup/rollup-linux-arm64-musl": "4.41.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-musl": "4.41.0", - "@rollup/rollup-linux-s390x-gnu": "4.41.0", - "@rollup/rollup-linux-x64-gnu": "4.41.0", - "@rollup/rollup-linux-x64-musl": "4.41.0", - "@rollup/rollup-win32-arm64-msvc": "4.41.0", - "@rollup/rollup-win32-ia32-msvc": "4.41.0", - "@rollup/rollup-win32-x64-msvc": "4.41.0", + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" } }, @@ -6088,6 +6787,20 @@ "node": ">=0.10.0" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -6285,13 +6998,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -6416,7 +7129,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6452,9 +7164,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -6492,9 +7204,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -6536,7 +7248,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6607,12 +7318,12 @@ } }, "node_modules/vite-plugin-svgr": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", - "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.5.0.tgz", + "integrity": "sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==", "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.3", + "@rollup/pluginutils": "^5.2.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, @@ -6757,6 +7468,23 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index b988b9e2c..e950dfd41 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -153,29 +153,20 @@ const Settings = () => { return; } - // Validate - const { error } = settingsValidation.validate(newSettingsData.settings, { - abortEarly: false, - }); - if (!error || error.details.length === 0) { - setErrors({}); - } else { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - } - setSettingsData(newSettingsData); // Update Redux immediately for UI feedback if (name === "showURL") { - dispatch(setShowURL(newValue)); - } -}; + dispatch(setShowURL(newValue)); + } + }; -const handleSave = () => { + const handleSave = () => { + // Validate + const { error } = settingsValidation.validate(settingsData.settings, { + abortEarly: false, + }); + if (!error || error.details.length === 0) { setErrors({}); } else { const newErrors = {}; From 1c1ac25857eb0a2db2fb8e98b0bc7b19939ea2a9 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Mon, 12 Jan 2026 08:56:24 +0000 Subject: [PATCH 017/103] fix saveSettings is currently being called even when validation fails. --- client/src/Pages/v1/Settings/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index e950dfd41..c37a8b1bc 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -168,6 +168,7 @@ const Settings = () => { }); if (!error || error.details.length === 0) { setErrors({}); + saveSettings(settingsData?.settings); } else { const newErrors = {}; error.details.forEach((err) => { @@ -176,7 +177,6 @@ const Settings = () => { setErrors(newErrors); } - saveSettings(settingsData?.settings); }; return ( From cdbaad5bd90616a40cb167d16fe2a99c56f396a2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 23:34:05 +0000 Subject: [PATCH 018/103] initial --- client/src/Hooks/monitorHooks.js | 6 ++- client/src/Pages/Uptime/Monitors/index.jsx | 28 +++++------- server/src/db/modules/monitorModule.js | 13 +++--- .../monitors/IMonitorsRepository.ts | 7 +++ .../monitors/MongoMonitorsRepository.ts | 44 +++++++++++++++++-- server/src/service/business/monitorService.ts | 7 +-- .../service/infrastructure/statusService.js | 9 ---- 7 files changed, 72 insertions(+), 42 deletions(-) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index bc391ebf6..92844ad16 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -50,6 +50,7 @@ export const useFetchMonitorsWithChecks = ({ const [isLoading, setIsLoading] = useState(false); const [count, setCount] = useState(undefined); const [monitors, setMonitors] = useState(undefined); + const [summary, setSummary] = useState(undefined); const [networkError, setNetworkError] = useState(false); const theme = useTheme(); @@ -68,10 +69,11 @@ export const useFetchMonitorsWithChecks = ({ order, }); - const { count, monitors } = res?.data?.data ?? {}; + const { count, monitors, summary } = res?.data?.data ?? {}; const mappedMonitors = monitors.map((monitor) => getMonitorWithPercentage(monitor, theme) ); + setSummary(summary); setMonitors(mappedMonitors); setCount(count || 0); } catch (error) { @@ -97,7 +99,7 @@ export const useFetchMonitorsWithChecks = ({ types, monitorUpdateTrigger, ]); - return [monitors, count, isLoading, networkError]; + return [summary, monitors, count, isLoading, networkError]; }; export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => { diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index fb8d6246b..e5bcf45fb 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -26,10 +26,7 @@ import { useNavigate } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; import PropTypes from "prop-types"; -import { - useFetchMonitorsWithSummary, - useFetchMonitorsWithChecks, -} from "@/Hooks/monitorHooks.js"; +import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js"; import { useTranslation } from "react-i18next"; const TYPES = ["http", "ping", "docker", "port", "game"]; @@ -103,12 +100,6 @@ const UptimeMonitors = () => { setMonitorUpdateTrigger((prev) => !prev); }, []); - const [monitors, monitorsSummary, monitorsWithSummaryIsLoading, networkError] = - useFetchMonitorsWithSummary({ - types: TYPES, - monitorUpdateTrigger, - }); - const handleReset = () => { setSelectedState(undefined); setSelectedTypes(undefined); @@ -129,10 +120,11 @@ const UptimeMonitors = () => { const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES; const [ + summary, monitorsWithChecks, monitorsWithChecksCount, monitorsWithChecksIsLoading, - monitorsWithChecksNetworkError, + networkError, ] = useFetchMonitorsWithChecks({ types: effectiveTypes, limit: 25, @@ -150,14 +142,14 @@ const UptimeMonitors = () => { } }, [isSearching]); - const isLoading = monitorsWithSummaryIsLoading || monitorsWithChecksIsLoading; + const isLoading = monitorsWithChecksIsLoading; return ( <> @@ -174,14 +166,14 @@ const UptimeMonitors = () => { /> { handleReset={handleReset} /> diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index 84ff86996..e13da4b59 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -318,23 +318,22 @@ class MonitorModule { } }; - getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { + findMonitorsSummaryByTeamId = async ({ type, teamId, explain }) => { try { const matchStage = { teamId: new this.ObjectId(teamId) }; if (type !== undefined) { matchStage.type = Array.isArray(type) ? { $in: type } : type; } + const pipeline = buildMonitorsSummaryByTeamIdPipeline({ matchStage }); if (explain === true) { - return this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats"); + return this.Monitor.aggregate(pipeline).explain("executionStats"); } - - const queryResult = await this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })); - const { monitors, summary } = queryResult?.[0] ?? {}; - return { monitors, summary }; + const [summary] = await this.Monitor.aggregate(pipeline); + return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; } catch (error) { error.service = SERVICE_NAME; - error.method = "getMonitorsAndSummaryByTeamId"; + error.method = "findMonitorsSummaryByTeamId"; throw error; } }; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 0da9c7279..50fc79b2b 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -10,6 +10,10 @@ export interface TeamQueryConfig { order?: "asc" | "desc"; } +export interface SummaryConfig { + type?: MonitorType | MonitorType[]; +} + export interface IMonitorsRepository { // create create(monitor: Monitor, teamId: string, userId: string): Promise; @@ -27,4 +31,7 @@ export interface IMonitorsRepository { // counts findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise; + + // other + findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index c9af6668d..d839b1a78 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,8 +1,8 @@ import { MonitorModel } from "@/db/models/index.js"; -import type { MonitorDocument } from "@/db/models/Monitor.js"; -import type { Monitor, MonitorType } from "@/types/monitor.js"; +import type { MonitorDocument } from "@/db/models/index.js"; +import type { Monitor, MonitorType } from "@/types/index.js"; import mongoose, { type FilterQuery } from "mongoose"; -import type { IMonitorsRepository, TeamQueryConfig } from "./IMonitorsRepository.js"; +import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; class MongoMonitorsRepository implements IMonitorsRepository { @@ -120,6 +120,44 @@ class MongoMonitorsRepository implements IMonitorsRepository { return { monitors: this.mapDocuments(monitors), deletedCount }; }; + findMonitorsSummaryByTeamId = async ( + teamId: string, + config?: SummaryConfig + ): Promise<{ totalMonitors: number; upMonitors: number; downMonitors: number; pausedMonitors: number }> => { + const match: FilterQuery = { teamId: new mongoose.Types.ObjectId(teamId) }; + if (config?.type !== undefined) { + match.type = Array.isArray(config.type) ? { $in: config.type } : config.type; + } + const pipeline = [ + { $match: match }, + { + $group: { + _id: null, + totalMonitors: { $sum: 1 }, + upMonitors: { + $sum: { + $cond: [{ $eq: ["$status", true] }, 1, 0], + }, + }, + downMonitors: { + $sum: { + $cond: [{ $eq: ["$status", false] }, 1, 0], + }, + }, + pausedMonitors: { + $sum: { + $cond: [{ $eq: ["$isActive", false] }, 1, 0], + }, + }, + }, + }, + { $project: { _id: 0 } }, + ]; + + const [summary] = await MonitorModel.aggregate(pipeline); + return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 3793ac35e..e88fdbea3 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -47,7 +47,7 @@ export interface IMonitorService { field?: string; order?: "asc" | "desc"; explain?: boolean; - }): Promise<{ count: number; monitors: any[] }>; + }): Promise<{ summary: any; count: number; monitors: any[] }>; getAllGames(): any; getGroupsByTeamId(args: { teamId: string }): Promise; @@ -399,7 +399,8 @@ export class MonitorService implements IMonitorService { field?: string; order?: "asc" | "desc"; explain?: boolean; - }): Promise<{ count: number; monitors: any[] }> => { + }): Promise<{ summary: any; count: number; monitors: any[] }> => { + const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId); const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter }); const monitors = await this.monitorsRepository.findByTeamId(teamId, { limit, @@ -433,7 +434,7 @@ export class MonitorService implements IMonitorService { }; }); - return { count, monitors: monitorsWithChecks }; + return { summary: summary ?? null, count, monitors: monitorsWithChecks }; }; getAllGames = (): any => { diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index e849bbb83..105f89c44 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -5,15 +5,6 @@ const SERVICE_NAME = "StatusService"; class StatusService { static SERVICE_NAME = SERVICE_NAME; - /** - * @param {{ - * db: any - * logger: any - * buffer: import("./bufferService.js").BufferService - * incidentService: import("../business/incidentService.js").IncidentService - * monitorsRepository: any - * }} - */ constructor({ db, logger, buffer, incidentService, monitorsRepository }) { this.db = db; this.logger = logger; From 93c24d2f5ce232bdf85435ea8dce724b4f069d63 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 15:44:52 -0800 Subject: [PATCH 019/103] pagination and filter fix --- client/src/Pages/Uptime/Monitors/index.jsx | 2 +- server/src/controllers/monitorController.ts | 20 ------------------- .../checks/MongoChecksRepistory.ts | 4 ++-- .../monitors/MongoMonitorsRepository.ts | 5 ++--- server/src/routes/v1/monitorRoute.ts | 1 - server/src/service/business/monitorService.ts | 18 ----------------- 6 files changed, 5 insertions(+), 45 deletions(-) diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index e5bcf45fb..4328715f2 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -114,7 +114,7 @@ const UptimeMonitors = () => { ]); const activeFilter = [...filterLookup].find(([key]) => key !== undefined); - const field = activeFilter?.[1] || sort?.field; + const field = activeFilter?.[1] || (search ? "name" : sort?.field); const filter = activeFilter?.[0] || search; const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES; diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 90ab5e7a5..859a8fa4b 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -339,26 +339,6 @@ class MonitorController { } }; - getMonitorsAndSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - await getMonitorsByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - - const explain = optionalBoolean(req?.query?.explain, "explain"); - const type = parseMonitorTypeFilter(req?.query?.type); - const teamId = requireTeamId(req?.user?.teamId); - - const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain }); - - return res.status(200).json({ - msg: "Monitors and summary retrieved successfully", - data: result, - }); - } catch (error) { - next(error); - } - }; - getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 7ede5587e..a6a049840 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -23,7 +23,7 @@ import { export type LatestChecksMap = Record; -class MongoChecksRepistory implements IChecksRepository { +class MongoChecksRepository implements IChecksRepository { private toEntity = (doc: CheckDocument): Check => { const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => { if (!value) { @@ -392,4 +392,4 @@ class MongoChecksRepistory implements IChecksRepository { }; } -export default MongoChecksRepistory; +export default MongoChecksRepository; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index d839b1a78..970371b17 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -44,7 +44,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { - const { page = 0, rowsPerPage = 25, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; + const { page = 0, rowsPerPage = 5, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; const query: Record = { teamId: new mongoose.Types.ObjectId(teamId), @@ -75,9 +75,8 @@ class MongoMonitorsRepository implements IMonitorsRepository { const sort = { [field]: order === "asc" ? 1 : -1 } as const; const skip = Math.max(page, 0) * rowsPerPage; - const limitValue = limit ?? rowsPerPage; - const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limitValue).exec(); + const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(rowsPerPage); return this.mapDocuments(documents); }; diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts index 24bc64ec8..ab6b0c98e 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/v1/monitorRoute.ts @@ -19,7 +19,6 @@ class MonitorRoutes { // Team routes this.router.get("/team", this.monitorController.getMonitorsByTeamId); this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId); - this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId); this.router.get("/team/groups", this.monitorController.getGroupsByTeamId); // Uptime routes diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index e88fdbea3..0a767779e 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -36,7 +36,6 @@ export interface IMonitorService { field?: string; order?: "asc" | "desc"; }): Promise; - getMonitorsAndSummaryByTeamId(args: { teamId: string; type?: string | string[]; explain?: boolean }): Promise; getMonitorsWithChecksByTeamId(args: { teamId: string; limit?: number; @@ -362,23 +361,6 @@ export class MonitorService implements IMonitorService { return monitors; }; - getMonitorsAndSummaryByTeamId = async ({ - teamId, - type, - explain, - }: { - teamId: string; - type?: string | string[]; - explain?: boolean; - }): Promise => { - const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({ - type, - explain, - teamId, - }); - return result; - }; - getMonitorsWithChecksByTeamId = async ({ teamId, limit, From e7e4ce7f57112389c08a834c42ae060be8f8b6ea Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 15:57:17 -0800 Subject: [PATCH 020/103] pagination fix --- client/src/Pages/Infrastructure/Monitors/index.jsx | 2 +- .../Monitors/Components/MonitorGrid/index.jsx | 1 + client/src/Pages/PageSpeed/Monitors/index.jsx | 1 + client/src/Pages/Uptime/Monitors/index.jsx | 14 +++++++++++--- .../monitors/MongoMonitorsRepository.ts | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx index b25636fa9..e4add415c 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/index.jsx @@ -71,7 +71,7 @@ const InfrastructureMonitors = () => { const field = toFilterStatus !== undefined ? "status" : undefined; - const [monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({ + const [summary, monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({ types: TYPES, limit: 1, page: page, diff --git a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx index 6b0a800fb..8708502cd 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx @@ -2,6 +2,7 @@ import { Grid, Grid2 } from "@mui/material"; import Card from "../Card/index.jsx"; const MonitorGrid = ({ shouldRender, monitors }) => { + console.log(monitors); return ( { const isAdmin = useIsAdmin(); const [ + summary, monitorsWithChecks, monitorsWithChecksCount, monitorsWithChecksIsLoading, diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index 4328715f2..101020eea 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -26,7 +26,10 @@ import { useNavigate } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; import PropTypes from "prop-types"; -import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js"; +import { + useFetchMonitorsWithChecks, + useFetchMonitorsByTeamId, +} from "@/Hooks/monitorHooks.js"; import { useTranslation } from "react-i18next"; const TYPES = ["http", "ping", "docker", "port", "game"]; @@ -136,6 +139,11 @@ const UptimeMonitors = () => { monitorUpdateTrigger, }); + const [monitors, listIsLoading, listNetworkError] = useFetchMonitorsByTeamId({ + type: ["http", "ping", "docker", "port", "game"], + }); + + console.log(monitors); useEffect(() => { if (isSearching) { setPage(undefined); @@ -149,7 +157,7 @@ const UptimeMonitors = () => { @@ -187,7 +195,7 @@ const UptimeMonitors = () => { handleReset={handleReset} /> diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 970371b17..a7b933851 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -44,7 +44,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { - const { page = 0, rowsPerPage = 5, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; + const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; const query: Record = { teamId: new mongoose.Types.ObjectId(teamId), From 1472a0971837d1ab425d495a63db3f96e08853dd Mon Sep 17 00:00:00 2001 From: karenvicent Date: Wed, 14 Jan 2026 22:43:25 -0500 Subject: [PATCH 021/103] add new uptime intervals --- client/src/Pages/v1/Uptime/Create/index.jsx | 5 +++++ client/src/locales/en.json | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/v1/Uptime/Create/index.jsx b/client/src/Pages/v1/Uptime/Create/index.jsx index 202f64d85..003869b10 100644 --- a/client/src/Pages/v1/Uptime/Create/index.jsx +++ b/client/src/Pages/v1/Uptime/Create/index.jsx @@ -107,11 +107,16 @@ const UptimeCreate = ({ isClone = false }) => { // Constants const MS_PER_MINUTE = 60000; const FREQUENCIES = [ + { _id: 0.25, name: t("time.fifteenSeconds") }, + { _id: 0.5, name: t("time.thirtySeconds") }, { _id: 1, name: t("time.oneMinute") }, { _id: 2, name: t("time.twoMinutes") }, { _id: 3, name: t("time.threeMinutes") }, { _id: 4, name: t("time.fourMinutes") }, { _id: 5, name: t("time.fiveMinutes") }, + { _id: 10, name: t("time.tenMinutes") }, + { _id: 15, name: t("time.fifteenMinutes") }, + { _id: 30, name: t("time.thirtyMinutes") }, ]; const GAMELIST = Object.entries(games).map(([key, value]) => ({ diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ed07fd8d2..a658d6d66 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -985,6 +985,10 @@ }, "pageSpeedLighthouseAPI": "Use the Lighthouse PageSpeed API to monitor your website", "time": { + "fifteenSeconds": "15 seconds", + "thirtySeconds": "30 seconds", + "fifteenMinutes": "15 minutes", + "thirtyMinutes": "30 minutes", "threeMinutes": "3 minutes", "fiveMinutes": "5 minutes", "tenMinutes": "10 minutes", @@ -994,9 +998,7 @@ "oneWeek": "1 week", "fourMinutes": "4 minutes", "oneMinute": "1 minute", - "twoMinutes": "2 minutes", - "fifteenSeconds": "15 seconds", - "thirtySeconds": "30 seconds" + "twoMinutes": "2 minutes" }, "general": { "noOptionsFound": "No {{unit}} found" From 420164d616afe7d4e9c247fcbfe70196a2ad0e4b Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Thu, 15 Jan 2026 12:52:11 +0000 Subject: [PATCH 022/103] conditional simplification as reqeusted --- server/src/db/modules/statusPageModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/db/modules/statusPageModule.js b/server/src/db/modules/statusPageModule.js index 60fd698de..e1b73a691 100755 --- a/server/src/db/modules/statusPageModule.js +++ b/server/src/db/modules/statusPageModule.js @@ -265,7 +265,7 @@ class StatusPageModule { const normalizedMonitors = monitors.map((monitor) => { const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); - if (showURL !== true) { + if (!showURL) { const { url, port, secret, notifications, ...rest } = monitor; return { ...rest, checks: normalizedChecks }; } From 6b5fd8d17ffb928cdc4c32f3b0c5151aa1863dd0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 18:21:36 +0000 Subject: [PATCH 023/103] update, pause --- .../v1/MonitorDetailsControlHeader/index.jsx | 10 +++---- client/src/Hooks/monitorHooks.js | 2 +- client/src/Pages/Uptime/Create/index.jsx | 4 +-- client/src/Validation/validation.js | 2 +- .../checks/MongoChecksRepistory.ts | 7 +---- .../monitors/IMonitorsRepository.ts | 5 +++- .../monitors/MongoMonitorsRepository.ts | 29 ++++++++++++++++++- server/src/service/business/monitorService.ts | 17 ++++++----- .../service/infrastructure/statusService.js | 4 +-- 9 files changed, 53 insertions(+), 27 deletions(-) diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx index bb4399b85..a86565f49 100644 --- a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx @@ -66,7 +66,7 @@ const MonitorDetailsControlHeader = ({ gap={theme.spacing(2)} > @@ -78,7 +78,7 @@ const MonitorDetailsControlHeader = ({ startIcon={} disabled={isTestNotificationsDisabled} onClick={() => { - testAllNotifications({ monitorId: monitor?._id }); + testAllNotifications({ monitorId: monitor?.id }); }} sx={{ whiteSpace: "nowrap", @@ -92,7 +92,7 @@ const MonitorDetailsControlHeader = ({ variant="contained" color="secondary" onClick={(e) => { - navigate(`/incidents/${monitor?._id}`); + navigate(`/incidents/${monitor?.id}`); }} > {t("menu.incidents")} @@ -107,7 +107,7 @@ const MonitorDetailsControlHeader = ({ } onClick={() => { pauseMonitor({ - monitorId: monitor?._id, + monitorId: monitor?.id, triggerUpdate, }); }} @@ -120,7 +120,7 @@ const MonitorDetailsControlHeader = ({ variant="contained" color="secondary" startIcon={} - onClick={() => navigate(`/${path}/configure/${monitor._id}`)} + onClick={() => navigate(`/${path}/configure/${monitor.id}`)} > Configure diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 92844ad16..d865ffab8 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -387,7 +387,7 @@ export const useUpdateMonitor = () => { }), }; await networkService.updateMonitor({ - monitorId: monitor._id, + monitorId: monitor.id, updatedFields, }); diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index c3421e875..c54af3d07 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -194,7 +194,7 @@ const UptimeCreate = ({ isClone = false }) => { }; } else { form = { - _id: monitor._id, + id: monitor.id, url: monitor.url, name: monitor.name || monitor.url.substring(0, 50), statusWindowSize: monitor.statusWindowSize, @@ -287,7 +287,7 @@ const UptimeCreate = ({ isClone = false }) => { const handleRemove = async (event) => { event.preventDefault(); - const TEMP_MONITOR = { id: monitor._id }; + const TEMP_MONITOR = { id: monitor.id }; await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/uptime" }); }; diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 112bbb349..9a84e2e39 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -112,7 +112,7 @@ const loginCredentials = joi.object({ }); const monitorValidation = joi.object({ - _id: joi.string(), + id: joi.string(), userId: joi.string(), teamId: joi.string(), statusWindowSize: joi.number().min(1).max(20).default(5).messages({ diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index a6a049840..b5489309d 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -184,18 +184,13 @@ class MongoChecksRepository implements IChecksRepository { } const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); const limitPerMonitor = options?.limitPerMonitor ?? 25; - const maxIntervalMs = Number(10 * 60 * 1000); - const bufferMs = Number(maxIntervalMs); - const lookbackMs = limitPerMonitor * maxIntervalMs + bufferMs; - - const cutoffDate = new Date(Date.now() - lookbackMs); const checkGroups = await CheckModel.aggregate([ { $match: { "metadata.monitorId": { $in: mongoIds }, - createdAt: { $gte: cutoffDate }, }, }, + { $sort: { "metadata.monitorId": 1, createdAt: -1 } }, { $group: { _id: "$metadata.monitorId", diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 50fc79b2b..1007842e7 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -24,8 +24,10 @@ export interface IMonitorsRepository { // collection fetch findAll(): Promise; findByTeamId(teamId: string, config: TeamQueryConfig): Promise; + // update - update(monitorId: string, updates: Partial): Promise; + updateById(monitorId: string, updates: Partial): Promise; + togglePauseById(monitorId: string, teamId: string): Promise; // delete deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }>; @@ -34,4 +36,5 @@ export interface IMonitorsRepository { // other findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; + findGroupsByTeamId(teamId: string): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index a7b933851..5e579c703 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -96,7 +96,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { return count; }; - update = async (monitorId: string, patch: Partial) => { + updateById = async (monitorId: string, patch: Partial) => { const updatedMonitor = await MonitorModel.findOneAndUpdate( { _id: monitorId }, { @@ -112,6 +112,25 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.toEntity(updatedMonitor); }; + togglePauseById = async (monitorId: string, teamId: string) => { + const monitor = await MonitorModel.findOneAndUpdate( + { _id: monitorId, teamId }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + status: "$$REMOVE", + }, + }, + ], + { new: true } + ); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 }); + } + return this.toEntity(monitor); + }; + deleteByTeamId = async (teamId: string) => { const monitors = await MonitorModel.find({ teamId }); const { deletedCount } = await MonitorModel.deleteMany({ teamId }); @@ -157,6 +176,14 @@ class MongoMonitorsRepository implements IMonitorsRepository { return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; }; + findGroupsByTeamId = async (teamId: string): Promise => { + const groups = await MonitorModel.distinct("group", { + teamId: new mongoose.Types.ObjectId(teamId), + group: { $nin: [null, ""] }, + }); + return groups.sort(); + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 0a767779e..cb439f1cc 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -48,10 +48,10 @@ export interface IMonitorService { explain?: boolean; }): Promise<{ summary: any; count: number; monitors: any[] }>; getAllGames(): any; - getGroupsByTeamId(args: { teamId: string }): Promise; + getGroupsByTeamId(args: { teamId: string }): Promise; // update - editMonitor(args: { teamId: string; monitorId: string; body: any }): Promise; + editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise; pauseMonitor(args: { teamId: string; monitorId: string }): Promise; // delete @@ -423,21 +423,22 @@ export class MonitorService implements IMonitorService { return this.games; }; - getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise => { - const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId }); + getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise => { + const groups = await this.monitorsRepository.findGroupsByTeamId(teamId); return groups; }; - editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: any }): Promise => { + editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => { await this.verifyTeamAccess({ teamId, monitorId }); - const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body }); + const editedMonitor = await this.monitorsRepository.updateById(monitorId, body); await this.jobQueue.updateJob(editedMonitor); + return editedMonitor; }; pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.pauseMonitor({ monitorId }); - monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor); + const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId); + monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor); return monitor; }; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 105f89c44..cfbf1b339 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -195,7 +195,7 @@ class StatusService { // Return early if not enough data points if (monitor.statusWindow.length < monitor.statusWindowSize) { - const updated = await this.monitorsRepository.update(monitor.id, monitor); + const updated = await this.monitorsRepository.updateById(monitor.id, monitor); return { monitor: updated, statusChanged: false, @@ -285,7 +285,7 @@ class StatusService { } monitor.status = newStatus; - const updated = await this.monitorsRepository.update(monitor.id, monitor); + const updated = await this.monitorsRepository.updateById(monitor.id, monitor); return { monitor: updated, From 75bcf9df58642168926172a2dc55aaa34a99fb8e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 18:24:58 +0000 Subject: [PATCH 024/103] deleteById --- .../src/repositories/monitors/IMonitorsRepository.ts | 1 + .../repositories/monitors/MongoMonitorsRepository.ts | 10 ++++++++++ server/src/service/business/monitorService.ts | 12 ++++++------ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 1007842e7..ba28d58e5 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -29,6 +29,7 @@ export interface IMonitorsRepository { updateById(monitorId: string, updates: Partial): Promise; togglePauseById(monitorId: string, teamId: string): Promise; // delete + deleteById(monitorId: string, teamId: string): Promise; deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }>; // counts diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 5e579c703..a2aa9de66 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -131,6 +131,16 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.toEntity(monitor); }; + deleteById = async (monitorId: string, teamId: string) => { + const deletedMonitor = await MonitorModel.findOneAndDelete({ _id: monitorId, teamId }); + + if (!deletedMonitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 }); + } + + return this.toEntity(deletedMonitor); + }; + deleteByTeamId = async (teamId: string) => { const monitors = await MonitorModel.find({ teamId }); const { deletedCount } = await MonitorModel.deleteMany({ teamId }); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index cb439f1cc..bce6a1283 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -52,10 +52,10 @@ export interface IMonitorService { // update editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise; - pauseMonitor(args: { teamId: string; monitorId: string }): Promise; + pauseMonitor(args: { teamId: string; monitorId: string }): Promise; // delete - deleteMonitor(args: { teamId: string; monitorId: string }): Promise; + deleteMonitor(args: { teamId: string; monitorId: string }): Promise; deleteAllMonitors(args: { teamId: string }): Promise; // other @@ -435,18 +435,18 @@ export class MonitorService implements IMonitorService { return editedMonitor; }; - pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId); monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor); return monitor; }; - deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId }); + const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); await this.jobQueue.deleteJob(monitor); - await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id); + await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor.id); return monitor; }; From 74a7f0a701c8519b963c4bc868955ed62d42778c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 18:33:03 +0000 Subject: [PATCH 025/103] remove verify team access --- .../monitors/IMonitorsRepository.ts | 2 +- .../monitors/MongoMonitorsRepository.ts | 4 +- server/src/service/business/monitorService.ts | 75 +------------------ .../service/infrastructure/statusService.js | 4 +- 4 files changed, 8 insertions(+), 77 deletions(-) diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index ba28d58e5..06c57ddb9 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -26,7 +26,7 @@ export interface IMonitorsRepository { findByTeamId(teamId: string, config: TeamQueryConfig): Promise; // update - updateById(monitorId: string, updates: Partial): Promise; + updateById(monitorId: string, teamId: string, updates: Partial): Promise; togglePauseById(monitorId: string, teamId: string): Promise; // delete deleteById(monitorId: string, teamId: string): Promise; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index a2aa9de66..fe5958c2a 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -96,9 +96,9 @@ class MongoMonitorsRepository implements IMonitorsRepository { return count; }; - updateById = async (monitorId: string, patch: Partial) => { + updateById = async (monitorId: string, teamId: string, patch: Partial) => { const updatedMonitor = await MonitorModel.findOneAndUpdate( - { _id: monitorId }, + { _id: monitorId, teamId }, { $set: { ...patch, diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index bce6a1283..ff79259eb 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -14,7 +14,6 @@ type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; export interface IMonitorService { readonly serviceName: string; - verifyTeamAccess(args: { teamId: string; monitorId: string }): Promise; // create createMonitor(teamId: string, userId: string, body: Monitor): Promise; @@ -60,7 +59,6 @@ export interface IMonitorService { // other sendTestEmail(args: { to: string }): Promise; - exportMonitorsToCSV(args: { teamId: string }): Promise; exportMonitorsToJSON(args: { teamId: string }): Promise; } @@ -146,13 +144,6 @@ export class MonitorService implements IMonitorService { return formatLookup[dateRange]; }; - verifyTeamAccess = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - if (!monitor?.teamId?.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - }; - createMonitor = async (teamId: string, userId: string, body: Monitor): Promise => { const monitor = await this.monitorsRepository.create(body, teamId, userId); if (!monitor) { @@ -255,8 +246,6 @@ export class MonitorService implements IMonitorService { dateRange: string; normalize?: boolean; }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -292,7 +281,6 @@ export class MonitorService implements IMonitorService { }; getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -324,7 +312,6 @@ export class MonitorService implements IMonitorService { }; getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -348,7 +335,6 @@ export class MonitorService implements IMonitorService { }; }; getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.findById(monitorId, teamId); return monitor; }; @@ -429,21 +415,18 @@ export class MonitorService implements IMonitorService { }; editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const editedMonitor = await this.monitorsRepository.updateById(monitorId, body); + const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body); await this.jobQueue.updateJob(editedMonitor); return editedMonitor; }; pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId); monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor); return monitor; }; deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); await this.jobQueue.deleteJob(monitor); await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor.id); @@ -486,65 +469,13 @@ export class MonitorService implements IMonitorService { return messageId; }; - exportMonitorsToCSV = async ({ teamId }: { teamId: string }): Promise => { - const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId }); - - if (!monitors || monitors.length === 0) { - throw this.errorService.createNotFoundError("No monitors to export"); - } - - const csvData = monitors?.filteredMonitors?.map((monitor: any) => ({ - name: monitor.name, - description: monitor.description, - type: monitor.type, - url: monitor.url, - interval: monitor.interval, - port: monitor.port, - ignoreTlsErrors: monitor.ignoreTlsErrors, - isActive: monitor.isActive, - })); - - const csv = this.papaparse.unparse(csvData); - return csv; - }; exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise => { - const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId }); + const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); if (!monitors || monitors.length === 0) { throw this.errorService.createNotFoundError("No monitors to export"); } - const json = monitors?.filteredMonitors - ?.map((monitor: any) => { - const initialType = monitor.type; - let parsedType; - - if (initialType === "hardware") { - parsedType = "infrastructure"; - } else if (initialType === "http") { - if (monitor.url.startsWith("https://")) { - parsedType = "https"; - } else { - parsedType = "http"; - } - } else if (initialType === "pagespeed") { - parsedType = initialType; - } else { - // Skip unsupported types - return; - } - - return { - name: monitor.name, - url: monitor.url, - type: parsedType, - interval: monitor.interval, - n: monitor.statusWindowSize, - secret: monitor.secret, - }; - }) - .filter(Boolean); - - return json; + return monitors; }; } diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index cfbf1b339..0b2e9f44c 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -195,7 +195,7 @@ class StatusService { // Return early if not enough data points if (monitor.statusWindow.length < monitor.statusWindowSize) { - const updated = await this.monitorsRepository.updateById(monitor.id, monitor); + const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); return { monitor: updated, statusChanged: false, @@ -285,7 +285,7 @@ class StatusService { } monitor.status = newStatus; - const updated = await this.monitorsRepository.updateById(monitor.id, monitor); + const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); return { monitor: updated, From 68559741ba6c4933b7c8edc9368c3f45c01721df Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 19:36:41 +0000 Subject: [PATCH 026/103] remove mongo middleware --- server/src/config/services.ts | 5 +- server/src/controllers/monitorController.ts | 15 --- server/src/db/models/Monitor.ts | 29 ----- server/src/db/models/StatusPage.js | 85 ------------- server/src/db/models/StatusPage.ts | 112 ++++++++++++++++++ server/src/db/models/index.ts | 3 + server/src/db/modules/statusPageModule.js | 1 + .../repositories/checks/IChecksRepository.ts | 6 + .../checks/MongoChecksRepistory.ts | 5 + server/src/repositories/index.ts | 3 + .../monitor-stats/IMonitorStatsRepository.ts | 1 + .../MongoMonitorStatsRepository.ts | 8 ++ .../monitors/MongoMonitorsRepository.ts | 2 +- .../status-pages/IStatusPagesRepository.ts | 11 ++ .../MongoStatusPagesRepository.ts | 52 ++++++++ server/src/routes/v1/monitorRoute.ts | 1 - server/src/service/business/monitorService.ts | 19 +-- server/src/types/index.ts | 1 + server/src/types/statusPage.ts | 29 +++++ 19 files changed, 247 insertions(+), 141 deletions(-) delete mode 100755 server/src/db/models/StatusPage.js create mode 100644 server/src/db/models/StatusPage.ts create mode 100644 server/src/repositories/status-pages/IStatusPagesRepository.ts create mode 100644 server/src/repositories/status-pages/MongoStatusPagesRepository.ts create mode 100644 server/src/types/statusPage.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 74c68c86e..990dc63b3 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -70,7 +70,7 @@ import SettingsModule from "../db/modules/settingsModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; // repositories -import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository } from "@/repositories/index.js"; +import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository, MongoStatusPagesRepository } from "@/repositories/index.js"; export const initializeServices = async ({ logger, envSettings, settingsService }: { logger: any; envSettings: any; settingsService: any }) => { const serviceRegistry = new ServiceRegistry({ logger }); @@ -124,6 +124,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService const monitorsRepository = new MongoMonitorsRepository(); const checksRepository = new MongoChecksRepository(); const monitorStatsRepository = new MongoMonitorStatsRepository(); + const statusPagesRepository = new MongoStatusPagesRepository(); const networkService = new NetworkService({ axios, @@ -217,7 +218,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService errorService, }); const monitorService = new MonitorService({ - db, jobQueue: superSimpleQueue, stringService, emailService, @@ -228,6 +228,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService monitorsRepository, checksRepository, monitorStatsRepository, + statusPagesRepository, }); const services = { diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 859a8fa4b..d46845026 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -374,21 +374,6 @@ class MonitorController { next(error); } }; - exportMonitorsToCSV = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } - - const csv = await this.monitorService.exportMonitorsToCSV({ teamId }); - res.setHeader("Content-Type", "text/csv"); - res.setHeader("Content-Disposition", "attachment; filename=monitors.csv"); - return res.send(csv); - } catch (error) { - next(error); - } - }; exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { try { diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index f6f9d4371..17565a2e4 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -171,35 +171,6 @@ const MonitorSchema = new Schema( } ); -MonitorSchema.pre("findOneAndDelete", async function (next) { - try { - const doc = await this.model.findOne(this.getFilter()); - - if (!doc) { - throw new Error("Monitor not found"); - } - - await Check.deleteMany({ monitorId: doc._id }); - await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } }); - await MonitorStats.deleteMany({ monitorId: doc?._id.toString() }); - next(); - } catch (error) { - next(error as Error); - } -}); - -MonitorSchema.pre("deleteMany", async function (next) { - const filter = this.getFilter(); - const monitors = (await this.model.find(filter).select(["_id", "type"]).lean()) as { _id: Types.ObjectId }[]; - - for (const monitor of monitors) { - await Check.deleteMany({ monitorId: monitor._id }); - await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } }); - await MonitorStats.deleteMany({ monitorId: monitor._id.toString() }); - } - next(); -}); - MonitorSchema.pre("save", function (next) { if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { this.cpuAlertThreshold = this.alertThreshold; diff --git a/server/src/db/models/StatusPage.js b/server/src/db/models/StatusPage.js deleted file mode 100755 index 621749fb8..000000000 --- a/server/src/db/models/StatusPage.js +++ /dev/null @@ -1,85 +0,0 @@ -import mongoose from "mongoose"; - -const StatusPageSchema = mongoose.Schema( - { - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - immutable: true, - required: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - type: { - type: String, - required: true, - default: "uptime", - enum: ["uptime"], - }, - companyName: { - type: String, - required: true, - default: "", - }, - url: { - type: String, - unique: true, - required: true, - default: "", - }, - timezone: { - type: String, - required: false, - }, - color: { - type: String, - required: false, - default: "#4169E1", - }, - monitors: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - required: true, - }, - ], - subMonitors: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - required: true, - }, - ], - logo: { - data: Buffer, - contentType: String, - }, - isPublished: { - type: Boolean, - default: false, - }, - showCharts: { - type: Boolean, - default: true, - }, - showUptimePercentage: { - type: Boolean, - default: true, - }, - showAdminLoginLink: { - type: Boolean, - default: false, - }, - customCSS: { - type: String, - default: "", - }, - }, - { timestamps: true } -); - -export default mongoose.model("StatusPage", StatusPageSchema); diff --git a/server/src/db/models/StatusPage.ts b/server/src/db/models/StatusPage.ts new file mode 100644 index 000000000..6b6e7721c --- /dev/null +++ b/server/src/db/models/StatusPage.ts @@ -0,0 +1,112 @@ +import { Schema, model, type Types } from "mongoose"; +import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js"; +import { StatusPageTypes } from "@/types/statusPage.js"; + +type StatusPageDocumentBase = Omit & { + monitors: Types.ObjectId[]; + subMonitors: Types.ObjectId[]; + originalMonitors?: Types.ObjectId[]; + logo?: StatusPageLogo | null; +}; + +interface StatusPageDocument extends StatusPageDocumentBase { + _id: Types.ObjectId; + userId: Types.ObjectId; + teamId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const logoSchema = new Schema( + { + data: { type: Buffer }, + contentType: { type: String }, + }, + { _id: false } +); + +const StatusPageSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + immutable: true, + required: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, + type: { + type: String, + required: true, + default: "uptime", + enum: StatusPageTypes, + }, + companyName: { + type: String, + required: true, + default: "", + }, + url: { + type: String, + unique: true, + required: true, + default: "", + }, + timezone: { + type: String, + }, + color: { + type: String, + default: "#4169E1", + }, + monitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + }, + ], + subMonitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + }, + ], + logo: { + type: logoSchema, + default: null, + }, + isPublished: { + type: Boolean, + default: false, + }, + showCharts: { + type: Boolean, + default: true, + }, + showUptimePercentage: { + type: Boolean, + default: true, + }, + showAdminLoginLink: { + type: Boolean, + default: false, + }, + customCSS: { + type: String, + default: "", + }, + }, + { timestamps: true } +); + +const StatusPageModel = model("StatusPage", StatusPageSchema); + +export type { StatusPageDocument }; +export { StatusPageModel }; +export default StatusPageModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 3ac99a390..b92ae18b7 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -6,3 +6,6 @@ export { default as CheckModel } from "@/db/models/Check.js"; export * from "@/db/models/MonitorStats.js"; export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js"; + +export * from "@/db/models/StatusPage.js"; +export { default as StatusPageModel } from "@/db/models/StatusPage.js"; diff --git a/server/src/db/modules/statusPageModule.js b/server/src/db/modules/statusPageModule.js index 999e0bdc9..081d3bea5 100755 --- a/server/src/db/modules/statusPageModule.js +++ b/server/src/db/modules/statusPageModule.js @@ -287,6 +287,7 @@ class StatusPageModule { deleteStatusPagesByMonitorId = async (monitorId) => { try { await this.StatusPage.deleteMany({ monitors: { $in: [monitorId] } }); + await this.StatusPage.deleteMany({ subMonitors: { $in: [monitorId] } }); } catch (error) { error.service = SERVICE_NAME; error.method = "deleteStatusPageByMonitorId"; diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index cedea8d86..19faa0159 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -54,6 +54,9 @@ export interface UptimeChecksResult { } export interface IChecksRepository { + // create + // single fetch + // collection fetch findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findDateRangeChecksByMonitor( monitorId: string, @@ -62,4 +65,7 @@ export interface IChecksRepository { dateString: string, options?: { type?: MonitorType } ): Promise; + // update + //delete + deleteByMonitorId(monitorId: string): Promise; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index b5489309d..615754933 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -385,6 +385,11 @@ class MongoChecksRepository implements IChecksRepository { checks: checks.map((doc) => this.toEntity(doc)), }; }; + + deleteByMonitorId = async (monitorId: string): Promise => { + const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId }); + return result.deletedCount; + }; } export default MongoChecksRepository; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 44d64f5bb..2c27591f1 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -6,3 +6,6 @@ export { default as MongoChecksRepository } from "@/repositories/checks/MongoChe export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js"; export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js"; + +export * from "@/repositories/status-pages/IStatusPagesRepository.js"; +export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js"; diff --git a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts index a0720619f..08309b4ee 100644 --- a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts @@ -5,5 +5,6 @@ export interface IMonitorStatsRepository { findByMonitorId(monitorId: string): Promise; // update // delete + deleteByMonitorId(monitorId: string): Promise; // other } diff --git a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts index d1d6a4825..52dbe8aa5 100644 --- a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts @@ -39,6 +39,14 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository { } return this.toEntity(monitorStats); }; + + deleteByMonitorId = async (monitorId: string) => { + const deleted = await MonitorStatsModel.findOneAndDelete({ monitorId: new mongoose.Types.ObjectId(monitorId) }); + if (!deleted) { + throw new AppError({ message: "Monitor stats not found", status: 404 }); + } + return this.toEntity(deleted); + }; } export default MongoMonitorStatsRepository; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index fe5958c2a..52f423154 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,6 +1,6 @@ import { MonitorModel } from "@/db/models/index.js"; import type { MonitorDocument } from "@/db/models/index.js"; -import type { Monitor, MonitorType } from "@/types/index.js"; +import type { Monitor } from "@/types/index.js"; import mongoose, { type FilterQuery } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; diff --git a/server/src/repositories/status-pages/IStatusPagesRepository.ts b/server/src/repositories/status-pages/IStatusPagesRepository.ts new file mode 100644 index 000000000..1b9f9303b --- /dev/null +++ b/server/src/repositories/status-pages/IStatusPagesRepository.ts @@ -0,0 +1,11 @@ +import type { StatusPage } from "@/types/statusPage.js"; + +export interface IStatusPagesRepository { + // create + // single fetch + // collection fetch + // update + // delete + // other + removeMonitorFromStatusPages(monitorId: string): Promise; +} diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts new file mode 100644 index 000000000..6beb23f0e --- /dev/null +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -0,0 +1,52 @@ +import { IStatusPagesRepository } from "@/repositories/index.js"; +import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js"; +import type { StatusPage } from "@/types/statusPage.js"; +import mongoose from "mongoose"; + +class MongoStatusPagesRepository implements IStatusPagesRepository { + private toEntity(doc: StatusPageDocument): StatusPage { + const toStringId = (value: unknown): string => { + if (value instanceof mongoose.Types.ObjectId) { + return value.toString(); + } + return value?.toString() ?? ""; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + const mapIdArray = (values?: mongoose.Types.ObjectId[]): string[] => { + return values?.map((value) => toStringId(value)) ?? []; + }; + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + type: doc.type, + companyName: doc.companyName, + url: doc.url, + timezone: doc.timezone ?? undefined, + color: doc.color, + monitors: mapIdArray(doc.monitors), + subMonitors: mapIdArray(doc.subMonitors), + originalMonitors: doc.originalMonitors?.map((value) => toStringId(value)), + logo: doc.logo ?? undefined, + isPublished: doc.isPublished, + showCharts: doc.showCharts, + showUptimePercentage: doc.showUptimePercentage, + showAdminLoginLink: doc.showAdminLoginLink, + customCSS: doc.customCSS, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + } + + removeMonitorFromStatusPages = async (monitorId: string): Promise => { + const res = await StatusPageModel.updateMany({ monitors: monitorId }, { $pull: { monitors: monitorId } }); + return res.modifiedCount; + }; +} + +export default MongoStatusPagesRepository; diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts index ab6b0c98e..2e6db630d 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/v1/monitorRoute.ts @@ -43,7 +43,6 @@ class MonitorRoutes { // Other static routes this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors); - this.router.get("/export", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToCSV); this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON); this.router.post("/bulk", isAllowed(["admin", "superadmin"]), upload.single("csvFile"), this.monitorController.createBulkMonitors); this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index ff79259eb..ecdf8ca02 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -2,7 +2,7 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; import type { MonitorType } from "@/types/monitor.js"; -import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository } from "@/repositories/index.js"; +import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js"; import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; @@ -76,9 +76,9 @@ export class MonitorService implements IMonitorService { private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; private monitorStatsRepository: IMonitorStatsRepository; + private statusPagesRepository: IStatusPagesRepository; constructor({ - db, jobQueue, stringService, emailService, @@ -89,8 +89,8 @@ export class MonitorService implements IMonitorService { monitorsRepository, checksRepository, monitorStatsRepository, + statusPagesRepository, }: { - db: any; jobQueue: any; stringService: any; emailService: any; @@ -101,8 +101,8 @@ export class MonitorService implements IMonitorService { monitorsRepository: IMonitorsRepository; checksRepository: IChecksRepository; monitorStatsRepository: IMonitorStatsRepository; + statusPagesRepository: IStatusPagesRepository; }) { - this.db = db; this.jobQueue = jobQueue; this.stringService = stringService; this.emailService = emailService; @@ -113,6 +113,7 @@ export class MonitorService implements IMonitorService { this.monitorsRepository = monitorsRepository; this.checksRepository = checksRepository; this.monitorStatsRepository = monitorStatsRepository; + this.statusPagesRepository = statusPagesRepository; } get serviceName(): string { @@ -428,8 +429,10 @@ export class MonitorService implements IMonitorService { deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id); + await this.checksRepository.deleteByMonitorId(monitor.id); + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); await this.jobQueue.deleteJob(monitor); - await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor.id); return monitor; }; @@ -439,9 +442,9 @@ export class MonitorService implements IMonitorService { monitors.map(async (monitor) => { try { await this.jobQueue.deleteJob(monitor); - await this.db.checkModule.deleteChecks(monitor.id); - await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor.id); - await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor.id); + await this.checksRepository.deleteByMonitorId(monitor.id); + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id); } catch (error: any) { this.logger.warn({ message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`, diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 2367f1c1c..81620da03 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -1,3 +1,4 @@ export * from "@/types/check.js"; export * from "@/types/monitor.js"; export * from "@/types/monitorStats.js"; +export * from "@/types/statusPage.js"; diff --git a/server/src/types/statusPage.ts b/server/src/types/statusPage.ts new file mode 100644 index 000000000..64a5ac50b --- /dev/null +++ b/server/src/types/statusPage.ts @@ -0,0 +1,29 @@ +export const StatusPageTypes = ["uptime"] as const; +export type StatusPageType = (typeof StatusPageTypes)[number]; + +export interface StatusPageLogo { + data: Buffer; + contentType: string; +} + +export interface StatusPage { + id: string; + userId: string; + teamId: string; + type: StatusPageType; + companyName: string; + url: string; + timezone?: string; + color: string; + monitors: string[]; + subMonitors: string[]; + originalMonitors?: string[]; + logo?: StatusPageLogo | null; + isPublished: boolean; + showCharts: boolean; + showUptimePercentage: boolean; + showAdminLoginLink: boolean; + customCSS: string; + createdAt: string; + updatedAt: string; +} From ec953ff26246ea1cab438d7ec48982740740afe3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 19:40:22 +0000 Subject: [PATCH 027/103] format --- server/src/db/models/StatusPage.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/db/models/StatusPage.ts b/server/src/db/models/StatusPage.ts index 6b6e7721c..42d11bc46 100644 --- a/server/src/db/models/StatusPage.ts +++ b/server/src/db/models/StatusPage.ts @@ -2,7 +2,10 @@ import { Schema, model, type Types } from "mongoose"; import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js"; import { StatusPageTypes } from "@/types/statusPage.js"; -type StatusPageDocumentBase = Omit & { +type StatusPageDocumentBase = Omit< + StatusPage, + "id" | "userId" | "teamId" | "monitors" | "subMonitors" | "originalMonitors" | "createdAt" | "updatedAt" +> & { monitors: Types.ObjectId[]; subMonitors: Types.ObjectId[]; originalMonitors?: Types.ObjectId[]; From 9255500bfcdb824e36ad5f5a866b13e6e2aa43e3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 20:55:01 +0000 Subject: [PATCH 028/103] remove db module --- server/src/config/controllers.ts | 6 +- server/src/config/services.ts | 71 +- .../src/controllers/notificationController.ts | 11 +- server/src/db/MongoDB.js | 2 - server/src/db/modules/monitorModule.js | 539 ------------- server/src/db/modules/monitorModuleQueries.js | 740 ------------------ .../checks/MongoChecksRepistory.ts | 190 ++++- .../monitors/IMonitorsRepository.ts | 3 +- .../monitors/MongoMonitorsRepository.ts | 20 +- .../MongoStatusPagesRepository.ts | 61 +- .../{checkService.js => checkService.ts} | 71 +- ...Service.js => maintenanceWindowService.ts} | 40 +- .../{userService.js => userService.ts} | 73 +- .../service/infrastructure/statusService.js | 5 +- 14 files changed, 415 insertions(+), 1417 deletions(-) delete mode 100755 server/src/db/modules/monitorModule.js delete mode 100755 server/src/db/modules/monitorModuleQueries.js rename server/src/service/business/{checkService.js => checkService.ts} (61%) rename server/src/service/business/{maintenanceWindowService.js => maintenanceWindowService.ts} (55%) rename server/src/service/business/{userService.js => userService.ts} (79%) diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 9fe834348..44b051580 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -10,8 +10,8 @@ import StatusPageController from "../controllers/statusPageController.js"; import NotificationController from "../controllers/notificationController.js"; import DiagnosticController from "../controllers/diagnosticController.js"; import IncidentController from "../controllers/incidentController.js"; - -export const initializeControllers = (services: any) => { +import type { InitializedSerivces } from "@/config/services.js"; +export const initializeControllers = (services: InitializedSerivces) => { const controllers: Record = {}; controllers.authController = new AuthController(services.userService); @@ -26,7 +26,7 @@ export const initializeControllers = (services: any) => { controllers.queueController = new QueueController(services.jobQueue); controllers.logController = new LogController(services.logger); controllers.statusPageController = new StatusPageController(services.db); - controllers.notificationController = new NotificationController(services.notificationService, services.db); + controllers.notificationController = new NotificationController(services.notificationService, services.db, services.monitorsRepository); controllers.diagnosticController = new DiagnosticController(services.diagnosticService); controllers.incidentController = new IncidentController(services.incidentService); diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 990dc63b3..8ebda0e16 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -63,16 +63,61 @@ import CheckModule from "../db/modules/checkModule.js"; import StatusPageModule from "../db/modules/statusPageModule.js"; import UserModule from "../db/modules/userModule.js"; import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js"; -import MonitorModule from "../db/modules/monitorModule.js"; import NotificationModule from "../db/modules/notificationModule.js"; import RecoveryModule from "../db/modules/recoveryModule.js"; import SettingsModule from "../db/modules/settingsModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; // repositories -import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository, MongoStatusPagesRepository } from "@/repositories/index.js"; +import { + MongoMonitorsRepository, + MongoChecksRepository, + MongoMonitorStatsRepository, + MongoStatusPagesRepository, + IMonitorsRepository, + IChecksRepository, + IMonitorStatsRepository, + IStatusPagesRepository, +} from "@/repositories/index.js"; -export const initializeServices = async ({ logger, envSettings, settingsService }: { logger: any; envSettings: any; settingsService: any }) => { +export type InitializedSerivces = { + //v1 + settingsService: any; + translationService: any; + stringService: any; + db: any; + networkService: any; + emailService: any; + bufferService: any; + statusService: any; + notificationService: any; + jobQueue: any; + userService: any; + checkService: any; + diagnosticService: any; + inviteService: any; + maintenanceWindowService: any; + monitorService: any; + incidentService: any; + errorService: any; + logger: any; + + // Repositories + monitorsRepository: IMonitorsRepository; + checksRepository: IChecksRepository; + monitorStatsRepository: IMonitorStatsRepository; + statusPagesRepository: IStatusPagesRepository; +}; + +export const initializeServices = async ({ + logger, + envSettings, + settingsService, +}: { + logger: any; + envSettings: any; + settingsService: any; +}): Promise => { const serviceRegistry = new ServiceRegistry({ logger }); (ServiceRegistry as any).instance = serviceRegistry; @@ -87,17 +132,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); - const monitorModule = new MonitorModule({ - Monitor, - MonitorStats, - stringService, - fs, - path, - fileURLToPath, - ObjectId, - NormalizeData, - NormalizeDataUptimeDetails, - }); const notificationModule = new NotificationModule({ Notification, Monitor }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); const settingsModule = new SettingsModule({ AppSettings }); @@ -111,7 +145,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService statusPageModule, userModule, maintenanceWindowModule, - monitorModule, notificationModule, recoveryModule, settingsModule, @@ -202,6 +235,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService settingsService, stringService, errorService, + monitorsRepository, }); const diagnosticService = new DiagnosticService(); const inviteService = new InviteService({ @@ -216,6 +250,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService settingsService, stringService, errorService, + monitorsRepository, }); const monitorService = new MonitorService({ jobQueue: superSimpleQueue, @@ -252,6 +287,12 @@ export const initializeServices = async ({ logger, envSettings, settingsService incidentService, errorService, logger, + + // Repositories + monitorsRepository, + checksRepository, + monitorStatsRepository, + statusPagesRepository, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 74849c6aa..defae2fd3 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -2,6 +2,7 @@ import { Request, Response, NextFunction } from "express"; import { createNotificationBodyValidation } from "@/validation/joi.js"; import { AppError } from "@/utils/AppError.js"; +import { IMonitorsRepository } from "@/repositories/index.js"; const SERVICE_NAME = "NotificationController"; @@ -9,9 +10,11 @@ class NotificationController { static SERVICE_NAME = SERVICE_NAME; private db: any; private notificationService: any; - constructor(notificationService: any, db: any) { + private monitorsRepository: IMonitorsRepository; + constructor(notificationService: any, db: any, monitorsRepository: IMonitorsRepository) { this.notificationService = notificationService; this.db = db; + this.monitorsRepository = monitorsRepository; } get serviceName() { @@ -167,11 +170,7 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - if (!monitor.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); - } + const monitor = await this.monitorsRepository.findById(monitorId, teamId); const notifications = monitor.notifications; if (notifications.length === 0) { diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index 3db17c78f..f7e58f34e 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -12,7 +12,6 @@ class MongoDB { statusPageModule, userModule, maintenanceWindowModule, - monitorModule, notificationModule, recoveryModule, settingsModule, @@ -25,7 +24,6 @@ class MongoDB { this.recoveryModule = recoveryModule; this.checkModule = checkModule; this.maintenanceWindowModule = maintenanceWindowModule; - this.monitorModule = monitorModule; this.notificationModule = notificationModule; this.settingsModule = settingsModule; this.statusPageModule = statusPageModule; diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js deleted file mode 100755 index e13da4b59..000000000 --- a/server/src/db/modules/monitorModule.js +++ /dev/null @@ -1,539 +0,0 @@ -import { - buildUptimeDetailsPipeline, - buildMonitorSummaryByTeamIdPipeline, - buildMonitorsByTeamIdPipeline, - buildMonitorsAndSummaryByTeamIdPipeline, - buildMonitorsWithChecksByTeamIdPipeline, - buildFilteredMonitorsByTeamIdPipeline, - getHardwareStats, - getUpChecks, - getAggregateData, -} from "./monitorModuleQueries.js"; - -import { CheckModel } from "@/db/models/index.js"; - -const SERVICE_NAME = "monitorModule"; - -class MonitorModule { - constructor({ Monitor, MonitorStats, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) { - this.Monitor = Monitor; - this.MonitorStats = MonitorStats; - this.stringService = stringService; - this.fs = fs; - this.path = path; - this.fileURLToPath = fileURLToPath; - this.ObjectId = ObjectId; - this.NormalizeData = NormalizeData; - this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails; - - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - - this.demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json"); - } - - // Helper - calculateUptimeDuration = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - const latestCheck = new Date(checks[0].createdAt); - let latestDownCheck = 0; - - for (let i = checks.length - 1; i >= 0; i--) { - if (checks[i].status === false) { - latestDownCheck = new Date(checks[i].createdAt); - break; - } - } - - // If no down check is found, uptime is from the last check to now - if (latestDownCheck === 0) { - return Date.now() - new Date(checks[checks.length - 1].createdAt); - } - - // Otherwise the uptime is from the last check to the last down check - return latestCheck - latestDownCheck; - }; - - // Helper - getLastChecked = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - // Data is sorted newest->oldest, so last check is the most recent - return new Date() - new Date(checks[0].createdAt); - }; - getLatestResponseTime = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - return checks[0]?.responseTime ?? 0; - }; - - // Helper - getAverageResponseTime = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - const validChecks = checks.filter((check) => typeof check.responseTime === "number"); - if (validChecks.length === 0) { - return 0; - } - const aggResponseTime = validChecks.reduce((sum, check) => { - return sum + check.responseTime; - }, 0); - return aggResponseTime / validChecks.length; - }; - - // Helper - getUptimePercentage = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - const upCount = checks.reduce((count, check) => { - return check.status === true ? count + 1 : count; - }, 0); - return (upCount / checks.length) * 100; - }; - - // Helper - getIncidents = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - return checks.reduce((acc, check) => { - return check.status === false ? (acc += 1) : acc; - }, 0); - }; - - // Helper - getDateRange = (dateRange) => { - const startDates = { - recent: new Date(new Date().setHours(new Date().getHours() - 2)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: new Date(0), - }; - return { - start: startDates[dateRange], - end: new Date(), - }; - }; - - //Helper - getMonitorChecks = async (monitorId, dateRange, sortOrder) => { - const objectId = new this.ObjectId(monitorId); - const indexSpec = { - "metadata.monitorId": 1, - updatedAt: sortOrder, - }; - - const matchBase = { "metadata.monitorId": objectId }; - - const [checksAll, checksForDateRange] = await Promise.all([ - CheckModel.find(matchBase).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), - CheckModel.find({ - ...matchBase, - createdAt: { $gte: dateRange.start, $lte: dateRange.end }, - }) - .hint(indexSpec) - .lean(), - ]); - - return { checksAll, checksForDateRange }; - }; - - // Helper - processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => { - let processedChecks = checks; - if (numToDisplay && checks.length > numToDisplay) { - const n = Math.ceil(checks.length / numToDisplay); - processedChecks = checks.filter((_, index) => index % n === 0); - } - return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks; - }; - - // Helper - groupChecksByTime = (checks, dateRange) => { - return checks.reduce((acc, check) => { - // Validate the date - const checkDate = new Date(check.createdAt); - if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { - return acc; - } - - const time = dateRange === "day" ? checkDate.setMinutes(0, 0, 0) : checkDate.toISOString().split("T")[0]; - - if (!acc[time]) { - acc[time] = { time, checks: [] }; - } - acc[time].checks.push(check); - return acc; - }, {}); - }; - - // Helper - calculateGroupStats = (group) => { - const totalChecks = group.checks.length; - - const checksWithResponseTime = group.checks.filter((check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime)); - - return { - time: group.time, - uptimePercentage: this.getUptimePercentage(group.checks), - totalChecks, - totalIncidents: group.checks.filter((check) => !check.status).length, - avgResponseTime: - checksWithResponseTime.length > 0 - ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / checksWithResponseTime.length - : 0, - }; - }; - - getMonitorById = async (monitorId) => { - try { - const monitor = await this.Monitor.findById(monitorId); - if (monitor === null || monitor === undefined) { - const error = new Error(this.stringService.getDbFindMonitorById(monitorId)); - error.status = 404; - throw error; - } - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorById"; - throw error; - } - }; - - getMonitorsByIds = async (monitorIds) => { - try { - const objectIds = monitorIds.map((id) => new this.ObjectId(id)); - return await this.Monitor.find({ _id: { $in: objectIds } }, { _id: 1, teamId: 1 }).lean(); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsByIds"; - throw error; - } - }; - getUptimeDetailsById = async ({ monitorId, dateRange }) => { - try { - const dates = this.getDateRange(dateRange); - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT00:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - }; - - const dateString = formatLookup[dateRange]; - - const results = await CheckModel.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString)); - - const monitorData = results[0]; - - monitorData.groupedUpChecks = this.NormalizeDataUptimeDetails(monitorData.groupedUpChecks, 10, 100); - - monitorData.groupedDownChecks = this.NormalizeDataUptimeDetails(monitorData.groupedDownChecks, 10, 100); - - const normalizedGroupChecks = this.NormalizeDataUptimeDetails(monitorData.groupedChecks, 10, 100); - - monitorData.groupedChecks = normalizedGroupChecks; - const monitorStats = await this.MonitorStats.findOne({ monitorId }); - return { monitorData, monitorStats }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUptimeDetailsById"; - throw error; - } - }; - - getHardwareDetailsById = async ({ monitorId, dateRange }) => { - try { - const monitor = await this.Monitor.findById(monitorId); - const dates = this.getDateRange(dateRange); - - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT00:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - }; - const dateString = formatLookup[dateRange]; - - const [aggregateData, upChecksCount, metrics] = await Promise.all([ - getAggregateData(monitorId, dates), - getUpChecks(monitorId, dates), - getHardwareStats(monitorId, dates, dateString), - ]); - - const stats = { - aggregateData: aggregateData, - upChecks: upChecksCount, - checks: metrics, - }; - - return { - ...monitor.toObject(), - stats, - }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getHardwareDetailsById"; - throw error; - } - }; - - getMonitorsByTeamId = async ({ teamId, type, filter }) => { - try { - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - if (filter !== undefined && filter !== null && filter !== "") { - matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; - } - const monitors = await this.Monitor.find(matchStage) - .sort({ name: 1 }) - .select({ - _id: 1, - name: 1, - type: 1, - url: 1, - status: 1, - isActive: 1, - teamId: 1, - }) - .lean(); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsByTeamId"; - throw error; - } - }; - - findMonitorsSummaryByTeamId = async ({ type, teamId, explain }) => { - try { - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - const pipeline = buildMonitorsSummaryByTeamIdPipeline({ matchStage }); - if (explain === true) { - return this.Monitor.aggregate(pipeline).explain("executionStats"); - } - const [summary] = await this.Monitor.aggregate(pipeline); - return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "findMonitorsSummaryByTeamId"; - throw error; - } - }; - - getMonitorsWithChecksByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId, explain }) => { - try { - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build match stage - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - if (explain === true) { - return this.Monitor.aggregate( - buildMonitorsWithChecksByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ).explain("executionStats"); - } - - const queryResult = await this.Monitor.aggregate( - buildMonitorsWithChecksByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ); - const monitors = queryResult[0]?.monitors; - const count = queryResult[0]?.count; - const normalizedFilteredMonitors = monitors.map((monitor) => { - if (!monitor.checks) { - return monitor; - } - monitor.checks = this.NormalizeData(monitor.checks, 10, 100); - return monitor; - }); - return { count, monitors: normalizedFilteredMonitors }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsWithChecksByTeamId"; - throw error; - } - }; - createMonitor = async ({ body, teamId, userId }) => { - try { - const monitor = new this.Monitor({ ...body, teamId, userId }); - const saved = await monitor.save(); - return saved; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createMonitor"; - throw error; - } - }; - - createBulkMonitors = async (req) => { - try { - const monitors = req.map((item) => new this.Monitor({ ...item, notifications: undefined })); - await this.Monitor.bulkSave(monitors); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createBulkMonitors"; - throw error; - } - }; - - deleteMonitor = async ({ teamId, monitorId }) => { - try { - const deletedMonitor = await this.Monitor.findOneAndDelete({ _id: monitorId, teamId }); - - if (!deletedMonitor) { - throw new Error(this.stringService.getDbFindMonitorById(monitorId)); - } - - return deletedMonitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMonitor"; - throw error; - } - }; - - deleteAllMonitors = async (teamId) => { - try { - const monitors = await this.Monitor.find({ teamId }); - const { deletedCount } = await this.Monitor.deleteMany({ teamId }); - - return { monitors, deletedCount }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteAllMonitors"; - throw error; - } - }; - - deleteMonitorsByUserId = async (userId) => { - try { - const result = await this.Monitor.deleteMany({ userId: userId }); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMonitorsByUserId"; - throw error; - } - }; - - editMonitor = async ({ monitorId, body }) => { - try { - const editedMonitor = await this.Monitor.findByIdAndUpdate(monitorId, body, { - new: true, - }); - return editedMonitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editMonitor"; - throw error; - } - }; - - addDemoMonitors = async (userId, teamId) => { - try { - const demoMonitors = JSON.parse(this.fs.readFileSync(this.demoMonitorsPath, "utf8")); - - const demoMonitorsToInsert = demoMonitors.map((monitor) => { - return { - userId, - teamId, - name: monitor.name, - description: monitor.name, - type: "http", - url: monitor.url, - interval: 60000, - }; - }); - const insertedMonitors = await this.Monitor.insertMany(demoMonitorsToInsert); - return insertedMonitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "addDemoMonitors"; - throw error; - } - }; - - pauseMonitor = async ({ monitorId }) => { - try { - const monitor = await this.Monitor.findOneAndUpdate( - { _id: monitorId }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - status: "$$REMOVE", - }, - }, - ], - { new: true } - ); - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "pauseMonitor"; - throw error; - } - }; - - getGroupsByTeamId = async ({ teamId }) => { - try { - const groups = await this.Monitor.distinct("group", { - teamId: new this.ObjectId(teamId), - group: { $ne: null, $ne: "" }, - }); - - return groups.filter(Boolean).sort(); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getGroupsByTeamId"; - throw error; - } - }; -} - -export default MonitorModule; diff --git a/server/src/db/modules/monitorModuleQueries.js b/server/src/db/modules/monitorModuleQueries.js deleted file mode 100755 index 596f11791..000000000 --- a/server/src/db/modules/monitorModuleQueries.js +++ /dev/null @@ -1,740 +0,0 @@ -import { ObjectId } from "mongodb"; -import { CheckModel } from "@/db/models/index.js"; - -const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { - return [ - { - $match: { - "metadata.monitorId": new ObjectId(monitorId), - updatedAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { - $sort: { - updatedAt: 1, - }, - }, - { - $facet: { - // For the response time chart, should return checks for date window - // Grouped by: {day: hour}, {week: day}, {month: day} - 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"] }], - }, - }, - }, - ], - groupedAvgResponseTime: [ - { - $group: { - _id: null, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - ], - groupedChecks: [ - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - totalChecks: { - $sum: 1, - }, - }, - }, - { - $sort: { - _id: 1, - }, - }, - ], - // Up checks grouped by: {day: hour}, {week: day}, {month: day} - groupedUpChecks: [ - { - $match: { - status: true, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - totalChecks: { - $sum: 1, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - { - $sort: { _id: 1 }, - }, - ], - // Down checks grouped by: {day: hour}, {week: day}, {month: day} for the date window - groupedDownChecks: [ - { - $match: { - status: false, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - totalChecks: { - $sum: 1, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - { - $sort: { _id: 1 }, - }, - ], - }, - }, - { - $lookup: { - from: "monitors", - let: { monitor_id: { $toObjectId: monitorId } }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$_id", "$$monitor_id"] }, - }, - }, - { - $project: { - _id: 1, - teamId: 1, - name: 1, - status: 1, - interval: 1, - type: 1, - url: 1, - isActive: 1, - notifications: 1, - }, - }, - ], - as: "monitor", - }, - }, - { - $project: { - groupedAvgResponseTime: { - $arrayElemAt: ["$groupedAvgResponseTime.avgResponseTime", 0], - }, - - groupedChecks: "$groupedChecks", - groupedUpChecks: "$groupedUpChecks", - groupedDownChecks: "$groupedDownChecks", - groupedUptimePercentage: { $arrayElemAt: ["$uptimePercentage.percentage", 0] }, - monitor: { $arrayElemAt: ["$monitor", 0] }, - }, - }, - ]; -}; - -const buildMonitorStatsPipeline = (monitor) => { - return [ - { - $match: { - monitorId: monitor._id, - }, - }, - { - $project: { - avgResponseTime: 1, - uptimePercentage: 1, - totalChecks: 1, - timeSinceLastCheck: { - $subtract: [Date.now(), "$lastCheckTimestamp"], - }, - lastCheckTimestamp: 1, - uptBurnt: { $toString: "$uptBurnt" }, - }, - }, - ]; -}; - -const buildMonitorSummaryByTeamIdPipeline = ({ matchStage }) => { - return [ - { $match: matchStage }, - { - $group: { - _id: null, - totalMonitors: { $sum: 1 }, - upMonitors: { - $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], - }, - }, - downMonitors: { - $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], - }, - }, - pausedMonitors: { - $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ]; -}; - -const buildMonitorsByTeamIdPipeline = ({ matchStage, field, order }) => { - const sort = { [field]: order === "asc" ? 1 : -1 }; - - return [ - { $match: matchStage }, - { $sort: sort }, - { - $project: { - _id: 1, - name: 1, - type: 1, - port: 1, - }, - }, - ]; -}; - -const buildMonitorsAndSummaryByTeamIdPipeline = ({ matchStage }) => { - return [ - { $match: matchStage }, - { - $facet: { - summary: [ - { - $group: { - _id: null, - totalMonitors: { $sum: 1 }, - upMonitors: { - $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], - }, - }, - downMonitors: { - $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], - }, - }, - pausedMonitors: { - $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ], - monitors: [ - { $sort: { name: 1 } }, - { - $project: { - _id: 1, - name: 1, - type: 1, - }, - }, - ], - }, - }, - { - $project: { - summary: { $arrayElemAt: ["$summary", 0] }, - monitors: 1, - }, - }, - ]; -}; - -const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => { - const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; - const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : []; - - // Match name - if (typeof filter !== "undefined" && field === "name") { - matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; - } - - // Match isActive - if (typeof filter !== "undefined" && field === "isActive") { - matchStage.isActive = filter === "true" ? true : false; - } - - if (typeof filter !== "undefined" && field === "status") { - matchStage.status = filter === "true" ? true : false; - } - - // Match type - if (typeof filter !== "undefined" && field === "type") { - matchStage.type = filter; - } - - const monitorsPipeline = [ - { $sort: sort }, - { $skip: skip }, - ...limitStage, - { - $project: { - _id: 1, - name: 1, - description: 1, - type: 1, - url: 1, - isActive: 1, - createdAt: 1, - updatedAt: 1, - uptimePercentage: 1, - status: 1, - }, - }, - ]; - - // Add checks - if (limit) { - const checksCollection = "checks"; - monitorsPipeline.push({ - $lookup: { - from: checksCollection, - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { updatedAt: -1 } }, - { $limit: limit }, - { - $project: { - _id: 1, - status: 1, - responseTime: 1, - statusCode: 1, - createdAt: 1, - updatedAt: 1, - originalResponseTime: 1, - }, - }, - ], - as: "checks", - }, - }); - } - - const pipeline = [ - { $match: matchStage }, - { - $facet: { - count: [{ $count: "monitorsCount" }], - monitors: monitorsPipeline, - }, - }, - { - $project: { - count: { $arrayElemAt: ["$count", 0] }, - monitors: 1, - }, - }, - ]; - return pipeline; -}; - -const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => { - const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; - const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : []; - - if (typeof filter !== "undefined" && field === "name") { - matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; - } - - if (typeof filter !== "undefined" && field === "status") { - matchStage.status = filter === "true"; - } - - const pipeline = [{ $match: matchStage }, { $sort: sort }, { $skip: skip }, ...limitStage]; - - // Add checks - if (limit) { - const checksCollection = "checks"; - - pipeline.push({ - $lookup: { - from: checksCollection, - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - { $limit: limit }, - ], - as: "checks", - }, - }); - } - - return pipeline; -}; - -const buildGetMonitorsByTeamIdPipeline = (req) => { - let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build the match stage - const matchStage = { teamId: new ObjectId(req.params.teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; - return [ - { $match: matchStage }, - { - $facet: { - summary: [ - { - $group: { - _id: null, - totalMonitors: { $sum: 1 }, - upMonitors: { - $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], - }, - }, - downMonitors: { - $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], - }, - }, - pausedMonitors: { - $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ], - monitors: [ - { $sort: sort }, - { - $project: { - _id: 1, - name: 1, - }, - }, - ], - filteredMonitors: [ - ...(filter !== undefined - ? [ - { - $match: { - $or: [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }], - }, - }, - ] - : []), - { $sort: sort }, - { $skip: skip }, - ...(rowsPerPage ? [{ $limit: rowsPerPage }] : []), - ...(limit - ? [ - { - $lookup: { - from: "checks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "standardchecks", - }, - }, - ] - : []), - - { - $addFields: { - checks: { - $switch: { - branches: [ - { - case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] }, - then: "$standardchecks", - }, - ], - default: [], - }, - }, - }, - }, - { - $project: { - standardchecks: 0, - }, - }, - ], - }, - }, - { - $project: { - summary: { $arrayElemAt: ["$summary", 0] }, - filteredMonitors: 1, - monitors: 1, - }, - }, - ]; -}; - -export { - buildUptimeDetailsPipeline, - buildMonitorStatsPipeline, - buildGetMonitorsByTeamIdPipeline, - buildMonitorSummaryByTeamIdPipeline, - buildMonitorsByTeamIdPipeline, - buildMonitorsAndSummaryByTeamIdPipeline, - buildMonitorsWithChecksByTeamIdPipeline, - buildFilteredMonitorsByTeamIdPipeline, -}; - -export const getAggregateData = async (monitorId, dates) => { - const result = await CheckModel.aggregate([ - { - $match: { - "metadata.monitorId": new ObjectId(monitorId), - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { $sort: { createdAt: -1 } }, - { - $group: { - _id: null, - latestCheck: { $first: "$$ROOT" }, - totalChecks: { $sum: 1 }, - }, - }, - ]); - return result[0] || { totalChecks: 0, latestCheck: null }; -}; - -export const getUpChecks = async (monitorId, dates) => { - const count = await CheckModel.countDocuments({ - "metadata.monitorId": new ObjectId(monitorId), - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - status: true, - }); - return { totalChecks: count }; -}; - -export const getHardwareStats = async (monitorId, dates, dateString) => { - return await CheckModel.aggregate([ - { - $match: { - "metadata.monitorId": new ObjectId(monitorId), - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { $sort: { createdAt: 1 } }, - { - $group: { - _id: { $dateToString: { format: dateString, date: "$createdAt" } }, - avgCpuUsage: { $avg: "$cpu.usage_percent" }, - avgMemoryUsage: { $avg: "$memory.usage_percent" }, - avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } }, - disks: { $push: "$disk" }, - net: { $push: "$net" }, - updatedAts: { $push: "$updatedAt" }, - sampleDoc: { $first: "$$ROOT" }, - }, - }, - { - $project: { - _id: 1, - avgCpuUsage: 1, - avgMemoryUsage: 1, - avgTemperature: { - $map: { - input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] }, - as: "idx", - in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } }, - }, - }, - disks: { - $map: { - input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] }, - as: "dIdx", - in: { - name: { $concat: ["disk", { $toString: "$$dIdx" }] }, - readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } }, - writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } }, - totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } }, - freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } }, - usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } }, - }, - }, - }, - net: { - $map: { - input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] }, - as: "nIdx", - in: { - name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] }, - bytesSentPerSecond: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaBytesRecv: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaPacketsSent: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaPacketsRecv: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaErrIn: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaErrOut: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaDropIn: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaDropOut: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - }, - }, - }, - }, - }, - { $sort: { _id: 1 } }, - ]); -}; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 615754933..3816d8cc5 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -15,13 +15,11 @@ import type { } from "@/types/index.js"; import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; -import { - getAggregateData as getHardwareAggregateData, - getHardwareStats, - getUpChecks as getHardwareUpChecks, -} from "@/db/modules/monitorModuleQueries.js"; export type LatestChecksMap = Record; +type DateRange = { start: Date; end: Date }; +type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number }; +type HardwareUpChecks = { totalChecks: number }; class MongoChecksRepository implements IChecksRepository { private toEntity = (doc: CheckDocument): Check => { @@ -323,9 +321,9 @@ class MongoChecksRepository implements IChecksRepository { const monitorId = monitorObjectId.toHexString(); const dates = { start: startDate, end: endDate }; const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([ - getHardwareAggregateData(monitorId, dates), - getHardwareUpChecks(monitorId, dates), - getHardwareStats(monitorId, dates, dateString), + this.getHardwareAggregateData(monitorId, dates), + this.getHardwareUpChecks(monitorId, dates), + this.getHardwareStats(monitorId, dates, dateString), ]); const aggregateData = { @@ -387,9 +385,183 @@ class MongoChecksRepository implements IChecksRepository { }; deleteByMonitorId = async (monitorId: string): Promise => { - const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId }); + const result = await CheckModel.deleteMany({ "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) }); return result.deletedCount; }; + + private getHardwareAggregateData = async (monitorId: string, dates: DateRange): Promise => { + const result = await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }, + }, + { $sort: { createdAt: -1 } }, + { + $group: { + _id: null, + latestCheck: { $first: "$$ROOT" }, + totalChecks: { $sum: 1 }, + }, + }, + ]); + return result[0] || { totalChecks: 0, latestCheck: null }; + }; + + private getHardwareUpChecks = async (monitorId: string, dates: DateRange): Promise => { + const count = await CheckModel.countDocuments({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + status: true, + }); + return { totalChecks: count }; + }; + + private getHardwareStats = async (monitorId: string, dates: DateRange, dateString: string) => { + return await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }, + }, + { $sort: { createdAt: 1 } }, + { + $group: { + _id: { $dateToString: { format: dateString, date: "$createdAt" } }, + avgCpuUsage: { $avg: "$cpu.usage_percent" }, + avgMemoryUsage: { $avg: "$memory.usage_percent" }, + avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } }, + disks: { $push: "$disk" }, + net: { $push: "$net" }, + updatedAts: { $push: "$updatedAt" }, + sampleDoc: { $first: "$$ROOT" }, + }, + }, + { + $project: { + _id: 1, + avgCpuUsage: 1, + avgMemoryUsage: 1, + avgTemperature: { + $map: { + input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] }, + as: "idx", + in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } }, + }, + }, + disks: { + $map: { + input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] }, + as: "dIdx", + in: { + name: { $concat: ["disk", { $toString: "$$dIdx" }] }, + readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } }, + writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } }, + totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } }, + freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } }, + usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } }, + }, + }, + }, + net: { + $map: { + input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] }, + as: "nIdx", + in: { + name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] }, + bytesSentPerSecond: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaBytesRecv: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaPacketsSent: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaPacketsRecv: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaErrIn: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaErrOut: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaDropIn: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaDropOut: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + }, + }, + }, + }, + }, + { $sort: { _id: 1 } }, + ]); + }; } export default MongoChecksRepository; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 06c57ddb9..29ebe37be 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -19,11 +19,12 @@ export interface IMonitorsRepository { create(monitor: Monitor, teamId: string, userId: string): Promise; createBulkMonitors(monitors: Monitor[]): Promise; // single fetch - findById(monitorId: string, teamId?: string): Promise; + findById(monitorId: string, teamId: string): Promise; // collection fetch findAll(): Promise; findByTeamId(teamId: string, config: TeamQueryConfig): Promise; + findByIds(monitorIds: string[]): Promise; // update updateById(monitorId: string, teamId: string, updates: Partial): Promise; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 52f423154..878d6f7cf 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -21,19 +21,11 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.mapDocuments(inserted); }; - findById = async (monitorId: string, teamId?: string): Promise => { - const match: { _id: string; teamId?: string } = { _id: monitorId }; - if (teamId) { - match.teamId = teamId; - } + findById = async (monitorId: string, teamId: string): Promise => { + const match: { _id: string; teamId: string } = { _id: monitorId, teamId }; const monitor = await MonitorModel.findOne(match); if (!monitor) { - if (monitor === null || monitor === undefined) { - throw new AppError({ - message: `Monitor with ID ${monitorId} not found.`, - status: 404, - }); - } + throw new AppError({ message: `Monitor with ID ${monitorId} not found`, status: 404 }); } return this.toEntity(monitor); }; @@ -81,6 +73,12 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.mapDocuments(documents); }; + findByIds = async (monitorIds: string[]): Promise => { + const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + const monitors = await MonitorModel.find({ _id: { $in: objectIds } }); + return this.mapDocuments(monitors); + }; + findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise => { const { type } = config ?? {}; diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts index 6beb23f0e..e1eb4715b 100644 --- a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -1,47 +1,60 @@ import { IStatusPagesRepository } from "@/repositories/index.js"; import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js"; -import type { StatusPage } from "@/types/statusPage.js"; +import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js"; import mongoose from "mongoose"; class MongoStatusPagesRepository implements IStatusPagesRepository { - private toEntity(doc: StatusPageDocument): StatusPage { - const toStringId = (value: unknown): string => { - if (value instanceof mongoose.Types.ObjectId) { - return value.toString(); - } - return value?.toString() ?? ""; - }; + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; - const toDateString = (value: Date | string): string => { - return value instanceof Date ? value.toISOString() : value; - }; + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; - const mapIdArray = (values?: mongoose.Types.ObjectId[]): string[] => { - return values?.map((value) => toStringId(value)) ?? []; - }; + private mapIdArray = (values?: Array): string[] => { + return values?.map((value) => this.toStringId(value)) ?? []; + }; + private mapLogo = (logo?: StatusPageLogo | null): StatusPageLogo | undefined => { + if (!logo) { + return undefined; + } return { - id: toStringId(doc._id), - userId: toStringId(doc.userId), - teamId: toStringId(doc.teamId), + data: logo.data, + contentType: logo.contentType, + }; + }; + + private toEntity = (doc: StatusPageDocument): StatusPage => { + return { + id: this.toStringId(doc._id), + userId: this.toStringId(doc.userId), + teamId: this.toStringId(doc.teamId), type: doc.type, companyName: doc.companyName, url: doc.url, timezone: doc.timezone ?? undefined, color: doc.color, - monitors: mapIdArray(doc.monitors), - subMonitors: mapIdArray(doc.subMonitors), - originalMonitors: doc.originalMonitors?.map((value) => toStringId(value)), - logo: doc.logo ?? undefined, + monitors: this.mapIdArray(doc.monitors), + subMonitors: this.mapIdArray(doc.subMonitors), + originalMonitors: this.mapIdArray(doc.originalMonitors), + logo: this.mapLogo(doc.logo), isPublished: doc.isPublished, showCharts: doc.showCharts, showUptimePercentage: doc.showUptimePercentage, showAdminLoginLink: doc.showAdminLoginLink, customCSS: doc.customCSS, - createdAt: toDateString(doc.createdAt), - updatedAt: toDateString(doc.updatedAt), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), }; - } + }; removeMonitorFromStatusPages = async (monitorId: string): Promise => { const res = await StatusPageModel.updateMany({ monitors: monitorId }, { $pull: { monitors: monitorId } }); diff --git a/server/src/service/business/checkService.js b/server/src/service/business/checkService.ts similarity index 61% rename from server/src/service/business/checkService.js rename to server/src/service/business/checkService.ts index 9beb43326..61e2fb6ad 100644 --- a/server/src/service/business/checkService.js +++ b/server/src/service/business/checkService.ts @@ -1,20 +1,41 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; + const SERVICE_NAME = "checkService"; class CheckService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, settingsService, stringService, errorService }) { + private db: any; + private settingsService: any; + private stringService: any; + private errorService: any; + private monitorsRepository: IMonitorsRepository; + + constructor({ + db, + settingsService, + stringService, + errorService, + monitorsRepository, + }: { + db: any; + settingsService: any; + stringService: any; + errorService: any; + monitorsRepository: IMonitorsRepository; + }) { this.db = db; this.settingsService = settingsService; this.stringService = stringService; this.errorService = errorService; + this.monitorsRepository = monitorsRepository; } get serviceName() { return CheckService.SERVICE_NAME; } - getChecksByMonitor = async ({ monitorId, query, teamId }) => { + getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); } @@ -23,15 +44,8 @@ class CheckService { throw this.errorService.createBadRequestError("No team ID in request"); } - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - if (!monitor) { - throw this.errorService.createNotFoundError("Monitor not found"); - } - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } + // For verificaiton, throws an error if monitor doesn't belong to team + await this.monitorsRepository.findById(monitorId, teamId); let { sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; const result = await this.db.checkModule.getChecksByMonitor({ @@ -47,7 +61,7 @@ class CheckService { return result; }; - getChecksByTeam = async ({ teamId, query }) => { + getChecksByTeam = async ({ teamId, query }: { teamId: string; query: any }) => { let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query; if (!teamId) { @@ -66,7 +80,7 @@ class CheckService { return checkData; }; - getChecksSummaryByTeamId = async ({ teamId }) => { + getChecksSummaryByTeamId = async ({ teamId }: { teamId: string }) => { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); } @@ -75,7 +89,7 @@ class CheckService { return summary; }; - ackCheck = async ({ checkId, teamId, ack }) => { + ackCheck = async ({ checkId, teamId, ack }: { checkId: string; teamId: string; ack: any }) => { if (!checkId) { throw this.errorService.createBadRequestError("No check ID in request"); } @@ -88,27 +102,21 @@ class CheckService { return updatedCheck; }; - ackAllChecks = async ({ monitorId, path, teamId, ack }) => { + ackAllChecks = async ({ monitorId, path, teamId, ack }: { monitorId: string; path: string; teamId: string; ack: any }) => { if (path === "monitor") { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); } - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - if (!monitor) { - throw this.errorService.createNotFoundError("Monitor not found"); - } - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } + // For verificaiton, throws an error if monitor doesn't belong to team + await this.monitorsRepository.findById(monitorId, teamId); } const updatedChecks = await this.db.checkModule.ackAllChecks(monitorId, teamId, ack, path); return updatedChecks; }; - deleteChecks = async ({ monitorId, teamId }) => { + deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); } @@ -117,20 +125,13 @@ class CheckService { throw this.errorService.createBadRequestError("No team ID in request"); } - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - if (!monitor) { - throw this.errorService.createNotFoundError("Monitor not found"); - } - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } + // For verificaiton, throws an error if monitor doesn't belong to team + await this.monitorsRepository.findById(monitorId, teamId); const deletedCount = await this.db.checkModule.deleteChecks(monitorId); return deletedCount; }; - deleteChecksByTeamId = async ({ teamId }) => { + deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); } @@ -139,7 +140,7 @@ class CheckService { return deletedCount; }; - updateChecksTTL = async ({ teamId, ttl }) => { + updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => { const SECONDS_PER_DAY = 86400; const newTTL = parseInt(ttl, 10) * SECONDS_PER_DAY; await this.db.checkModule.updateChecksTTL(teamId, newTTL); diff --git a/server/src/service/business/maintenanceWindowService.js b/server/src/service/business/maintenanceWindowService.ts similarity index 55% rename from server/src/service/business/maintenanceWindowService.js rename to server/src/service/business/maintenanceWindowService.ts index 9d52702e9..bb82821cf 100644 --- a/server/src/service/business/maintenanceWindowService.js +++ b/server/src/service/business/maintenanceWindowService.ts @@ -1,30 +1,50 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; + const SERVICE_NAME = "maintenanceWindowService"; class MaintenanceWindowService { static SERVICE_NAME = SERVICE_NAME; + private db: any; + private settingsService: any; + private stringService: any; + private errorService: any; + private monitorsRepository: IMonitorsRepository; - constructor({ db, settingsService, stringService, errorService }) { + constructor({ + db, + settingsService, + stringService, + errorService, + monitorsRepository, + }: { + db: any; + settingsService: any; + stringService: any; + errorService: any; + monitorsRepository: IMonitorsRepository; + }) { this.db = db; this.settingsService = settingsService; this.stringService = stringService; this.errorService = errorService; + this.monitorsRepository = monitorsRepository; } get serviceName() { return MaintenanceWindowService.SERVICE_NAME; } - createMaintenanceWindow = async ({ teamId, body }) => { + createMaintenanceWindow = async ({ teamId, body }: { teamId: string; body: any }) => { const monitorIds = body.monitors; - const monitors = await this.db.monitorModule.getMonitorsByIds(monitorIds); + const monitors = await this.monitorsRepository.findByIds(monitorIds); - const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId)); + const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId); if (unauthorizedMonitors.length > 0) { throw this.errorService.createAuthorizationError(); } - const dbTransactions = monitorIds.map((monitorId) => { + const dbTransactions = monitorIds.map((monitorId: string) => { return this.db.maintenanceWindowModule.createMaintenanceWindow({ teamId, monitorId, @@ -38,26 +58,26 @@ class MaintenanceWindowService { await Promise.all(dbTransactions); }; - getMaintenanceWindowById = async ({ id, teamId }) => { + getMaintenanceWindowById = async ({ id, teamId }: { id: string; teamId: string }) => { const maintenanceWindow = await this.db.maintenanceWindowModule.getMaintenanceWindowById({ id, teamId }); return maintenanceWindow; }; - getMaintenanceWindowsByTeamId = async ({ teamId, query }) => { + getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => { const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId(teamId, query); return maintenanceWindows; }; - getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => { + getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ monitorId, teamId }); return maintenanceWindows; }; - deleteMaintenanceWindow = async ({ id, teamId }) => { + deleteMaintenanceWindow = async ({ id, teamId }: { id: string; teamId: string }) => { await this.db.maintenanceWindowModule.deleteMaintenanceWindowById({ id, teamId }); }; - editMaintenanceWindow = async ({ id, teamId, body }) => { + editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: any }) => { const editedMaintenanceWindow = await this.db.maintenanceWindowModule.editMaintenanceWindowById({ id, body, teamId }); return editedMaintenanceWindow; }; diff --git a/server/src/service/business/userService.js b/server/src/service/business/userService.ts similarity index 79% rename from server/src/service/business/userService.js rename to server/src/service/business/userService.ts index 7d7f7b014..129ed1e59 100644 --- a/server/src/service/business/userService.js +++ b/server/src/service/business/userService.ts @@ -1,9 +1,44 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; + const SERVICE_NAME = "userService"; class UserService { static SERVICE_NAME = SERVICE_NAME; - constructor({ crypto, db, emailService, settingsService, logger, stringService, jwt, errorService, jobQueue }) { + private db: any; + private emailService: any; + private settingsService: any; + private logger: any; + private stringService: any; + private jwt: any; + private errorService: any; + private jobQueue: any; + private crypto: any; + private monitorsRepository: IMonitorsRepository; + + constructor({ + crypto, + db, + emailService, + settingsService, + logger, + stringService, + jwt, + errorService, + jobQueue, + monitorsRepository, + }: { + crypto: any; + db: any; + emailService: any; + settingsService: any; + logger: any; + stringService: any; + jwt: any; + errorService: any; + jobQueue: any; + monitorsRepository: IMonitorsRepository; + }) { this.db = db; this.emailService = emailService; this.settingsService = settingsService; @@ -13,20 +48,21 @@ class UserService { this.errorService = errorService; this.jobQueue = jobQueue; this.crypto = crypto; + this.monitorsRepository = monitorsRepository; } get serviceName() { return UserService.SERVICE_NAME; } - issueToken = (payload, appSettings) => { + issueToken = (payload: any, appSettings: any) => { const tokenTTL = appSettings?.jwtTTL ?? "2h"; const tokenSecret = appSettings?.jwtSecret; const payloadData = payload; return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); }; - registerUser = async (user, file) => { + registerUser = async (user: any, file: any) => { // Create a new user // If superAdmin exists, a token should be attached to all further register requests const superAdminExists = await this.db.userModule.checkSuperadmin(); @@ -61,7 +97,7 @@ class UserService { const html = await this.emailService.buildEmail("welcomeEmailTemplate", { name: newUser.firstName, }); - this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => { + this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: any) => { this.logger.warn({ message: error.message, service: SERVICE_NAME, @@ -69,7 +105,7 @@ class UserService { stack: error.stack, }); }); - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: SERVICE_NAME, @@ -81,7 +117,7 @@ class UserService { return { user: newUser, token }; }; - loginUser = async (email, password) => { + loginUser = async (email: string, password: string) => { // Check if user exists const user = await this.db.userModule.getUserByEmail(email); // Compare password @@ -103,7 +139,7 @@ class UserService { return { user: userWithoutPassword, token }; }; - editUser = async (updates, file, currentUser) => { + editUser = async (updates: any, file: any, currentUser: any) => { // Change Password check if (updates?.password && updates?.newPassword) { // Get user's email @@ -131,7 +167,7 @@ class UserService { return superAdminExists; }; - requestRecovery = async (email) => { + requestRecovery = async (email: string) => { const user = await this.db.userModule.getUserByEmail(email); const recoveryToken = await this.db.recoveryModule.requestRecoveryToken(email); const name = user.firstName; @@ -147,18 +183,18 @@ class UserService { return msgId; }; - validateRecovery = async (recoveryToken) => { + validateRecovery = async (recoveryToken: string) => { await this.db.recoveryModule.validateRecoveryToken(recoveryToken); }; - resetPassword = async (password, recoveryToken) => { + resetPassword = async (password: string, recoveryToken: string) => { const user = await this.db.recoveryModule.resetPassword(password, recoveryToken); const appSettings = await this.settingsService.getSettings(); const token = this.issueToken(user._doc, appSettings); return { user, token }; }; - deleteUser = async (user) => { + deleteUser = async (user: any) => { const email = user?.email; if (!email) { throw this.errorService.createBadRequestError("No email in request"); @@ -181,15 +217,14 @@ class UserService { } // 1. Find all the monitors associated with the team ID if superadmin - const result = await this.db.monitorModule.getMonitorsByTeamId({ - teamId: teamId, - }); + const res = await this.monitorsRepository.findByTeamId(teamId, {}); if (roles.includes("superadmin")) { // 2. Remove all jobs, delete checks and alerts - result?.monitors.length > 0 && + res && + res?.length > 0 && (await Promise.all( - result.monitors.map(async (monitor) => { + res.map(async (monitor) => { await this.jobQueue.deleteJob(monitor); }) )); @@ -203,15 +238,15 @@ class UserService { return users; }; - getUserById = async (roles, userId) => { + getUserById = async (roles: any, userId: any) => { const user = await this.db.userModule.getUserById(roles, userId); return user; }; - editUserById = async (userId, user) => { + editUserById = async (userId: any, user: any) => { await this.db.userModule.editUserById(userId, user); }; - setPasswordByUserId = async (userId, password) => { + setPasswordByUserId = async (userId: any, password: string) => { const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: { password: password }, file: null }); return updatedUser; }; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 0b2e9f44c..763b6d1fa 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -167,9 +167,8 @@ class StatusService { const check = this.buildCheck(networkResponse); await this.insertCheck(check); try { - const { monitorId, status, code } = networkResponse; - - const monitor = await this.monitorsRepository.findById(monitorId); + const { monitorId, teamId, status, code } = networkResponse; + const monitor = await this.monitorsRepository.findById(monitorId, teamId); // Update running stats this.updateRunningStats({ monitor, networkResponse }); From 8aac3086b8f8e1821867ef1e81135a42d5ca7d49 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 21:03:35 +0000 Subject: [PATCH 029/103] build error --- server/src/config/services.ts | 1 + server/test/monitorService.test.ts | 132 ++++++++++++++++------------- 2 files changed, 76 insertions(+), 57 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 8ebda0e16..daa576bf8 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -229,6 +229,7 @@ export const initializeServices = async ({ jwt, errorService, jobQueue: superSimpleQueue, + monitorsRepository, }); const checkService = new CheckService({ db, diff --git a/server/test/monitorService.test.ts b/server/test/monitorService.test.ts index 7a3bbe12f..02f2c456c 100644 --- a/server/test/monitorService.test.ts +++ b/server/test/monitorService.test.ts @@ -1,12 +1,19 @@ import { jest } from "@jest/globals"; import { MonitorService } from "../src/service/business/monitorService.ts"; -import type { IMonitorsRepository, IChecksRepository } from "../src/repositories/index.ts"; +import type { + IChecksRepository, + IMonitorStatsRepository, + IMonitorsRepository, + IStatusPagesRepository, +} from "../src/repositories/index.ts"; const createMonitorsRepositoryMock = () => ({ findMonitorCountByTeamIdAndType: jest.fn(), findByTeamId: jest.fn(), findById: jest.fn(), + findMonitorsSummaryByTeamId: jest.fn(), + findGroupsByTeamId: jest.fn(), create: jest.fn(), createBulkMonitors: jest.fn(), deleteByTeamId: jest.fn(), @@ -18,32 +25,34 @@ const createChecksRepositoryMock = () => findDateRangeChecksByMonitor: jest.fn(), }) as unknown as IChecksRepository; +const createMonitorStatsRepositoryMock = () => + ({ + findByMonitorId: jest.fn(), + deleteByMonitorId: jest.fn(), + }) as unknown as IMonitorStatsRepository; + +const createStatusPagesRepositoryMock = () => + ({ + removeMonitorFromStatusPages: jest.fn(), + }) as unknown as IStatusPagesRepository; + const createService = ({ monitorsRepository = createMonitorsRepositoryMock(), checksRepository = createChecksRepositoryMock(), - monitorStatsRepository = { findByMonitorId: jest.fn() }, - monitorModuleOverrides = {}, + monitorStatsRepository = createMonitorStatsRepositoryMock(), + statusPagesRepository = createStatusPagesRepositoryMock(), }: { monitorsRepository?: IMonitorsRepository; checksRepository?: IChecksRepository; - monitorStatsRepository?: { findByMonitorId: jest.Mock }; - monitorModuleOverrides?: Record; + monitorStatsRepository?: IMonitorStatsRepository; + statusPagesRepository?: IStatusPagesRepository; } = {}) => { - const monitorModule = { - getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: () => true } }), - getMonitorStatsById: jest.fn().mockResolvedValue({ latest: {} }), - getMonitorsByTeamId: jest.fn().mockResolvedValue([]), - getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [], summary: {} }), - ...monitorModuleOverrides, - }; - return new MonitorService({ db: { - monitorModule, - statusPageModule: { deleteStatusPagesByMonitorId: jest.fn() }, checkModule: { deleteChecks: jest.fn() }, + statusPageModule: { deleteStatusPagesByMonitorId: jest.fn() }, pageSpeedCheckModule: { deletePageSpeedChecksByMonitorId: jest.fn() }, - notificationsModule: { deleteNotificationsByMonitorId: jest.fn() }, + notificationModule: { deleteNotificationsByMonitorId: jest.fn() }, }, jobQueue: { addJob: jest.fn(), @@ -66,6 +75,7 @@ const createService = ({ monitorsRepository, checksRepository, monitorStatsRepository, + statusPagesRepository, }); }; @@ -79,17 +89,17 @@ describe("MonitorService", () => { { id: "m2", name: "Monitor 2", interval: 60000 }, ]); - const checksRepository = createChecksRepositoryMock(); - (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ - m1: [ + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ + m1: [ { responseTime: 10, status: true, message: "OK" }, { responseTime: 20, status: true, message: "OK" }, ], m2: [{ responseTime: 50, status: true, message: "OK" }], }); - const service = createService({ monitorsRepository, checksRepository }); - const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); + const service = createService({ monitorsRepository, checksRepository }); + const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); expect(result).toMatchObject({ count: 2 }); expect(result.monitors).toHaveLength(2); @@ -106,29 +116,41 @@ describe("MonitorService", () => { }); describe("getMonitorsByTeamId", () => { - it("returns monitors array from db module", async () => { - const monitorsPayload = [ + it("returns monitors array from repository", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([ { id: "m1", name: "Monitor 1" }, { id: "m2", name: "Monitor 2" }, - ]; - const monitorModuleOverrides = { - getMonitorsByTeamId: jest.fn().mockResolvedValue(monitorsPayload), - }; - const service = createService({ monitorModuleOverrides }); + ]); + const service = createService({ monitorsRepository }); const result = await service.getMonitorsByTeamId({ teamId: "team" } as any); expect(result).toHaveLength(2); expect(result[0]).toHaveProperty("id", "m1"); }); }); - describe("getMonitorsAndSummaryByTeamId", () => { - it("returns monitors with summary block", async () => { - const monitorModuleOverrides = { - getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } }), - }; - const service = createService({ monitorModuleOverrides }); - const result = await service.getMonitorsAndSummaryByTeamId({ teamId: "team" }); - expect(result).toEqual({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } }); + describe("getMonitorsWithChecksByTeamId summary", () => { + it("includes summary and monitors with checks", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1); + (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([{ id: "m1", type: "http" }]); + (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ + totalMonitors: 1, + upMonitors: 1, + downMonitors: 0, + pausedMonitors: 0, + }); + + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ m1: [] }); + + const service = createService({ monitorsRepository, checksRepository }); + const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); + expect(result).toEqual({ + summary: { totalMonitors: 1, upMonitors: 1, downMonitors: 0, pausedMonitors: 0 }, + count: 1, + monitors: [{ id: "m1", type: "http", checks: [] }], + }); }); }); @@ -163,7 +185,7 @@ describe("MonitorService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); const checksRepository = createChecksRepositoryMock(); (checksRepository.findDateRangeChecksByMonitor as jest.Mock).mockResolvedValue({ - monitorType: "uptime", + monitorType: "http", groupedChecks: [{ _id: "2024-01-01", avgResponseTime: 100, totalChecks: 2 }], groupedUpChecks: [{ _id: "2024-01-01", totalChecks: 2, avgResponseTime: 90 }], groupedDownChecks: [{ _id: "2024-01-01", totalChecks: 0, avgResponseTime: 0 }], @@ -171,27 +193,23 @@ describe("MonitorService", () => { avgResponseTime: 95, }); - const monitorStatsRepository = { - findByMonitorId: jest.fn().mockResolvedValue({ - id: "stats-1", - monitorId: monitor.id, - avgResponseTime: 90, - totalChecks: 10, - totalUpChecks: 9, - totalDownChecks: 1, - uptimePercentage: 0.9, - lastCheckTimestamp: 123456789, - lastResponseTime: 80, - timeOfLastFailure: 123456700, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }), - } as any; - const monitorModuleOverrides = { - getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: (value: string) => value === TEAM_ID } }), - }; + const monitorStatsRepository = createMonitorStatsRepositoryMock(); + (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue({ + id: "stats-1", + monitorId: monitor.id, + avgResponseTime: 90, + totalChecks: 10, + totalUpChecks: 9, + totalDownChecks: 1, + uptimePercentage: 0.9, + lastCheckTimestamp: 123456789, + lastResponseTime: 80, + timeOfLastFailure: 123456700, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); - const service = createService({ monitorsRepository, checksRepository, monitorModuleOverrides, monitorStatsRepository }); + const service = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); const result = await service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: "monitor-1", dateRange: "recent" }); expect(result).toHaveProperty("monitorData"); From 56bfbe2c7708620d1144b21666569aae43ab1611 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 21:05:00 +0000 Subject: [PATCH 030/103] format --- server/test/monitorService.test.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/server/test/monitorService.test.ts b/server/test/monitorService.test.ts index 02f2c456c..32708a42e 100644 --- a/server/test/monitorService.test.ts +++ b/server/test/monitorService.test.ts @@ -1,11 +1,6 @@ import { jest } from "@jest/globals"; import { MonitorService } from "../src/service/business/monitorService.ts"; -import type { - IChecksRepository, - IMonitorStatsRepository, - IMonitorsRepository, - IStatusPagesRepository, -} from "../src/repositories/index.ts"; +import type { IChecksRepository, IMonitorStatsRepository, IMonitorsRepository, IStatusPagesRepository } from "../src/repositories/index.ts"; const createMonitorsRepositoryMock = () => ({ @@ -89,17 +84,17 @@ describe("MonitorService", () => { { id: "m2", name: "Monitor 2", interval: 60000 }, ]); - const checksRepository = createChecksRepositoryMock(); - (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ - m1: [ + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ + m1: [ { responseTime: 10, status: true, message: "OK" }, { responseTime: 20, status: true, message: "OK" }, ], m2: [{ responseTime: 50, status: true, message: "OK" }], }); - const service = createService({ monitorsRepository, checksRepository }); - const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); + const service = createService({ monitorsRepository, checksRepository }); + const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); expect(result).toMatchObject({ count: 2 }); expect(result.monitors).toHaveLength(2); From 5a7b51bfec063f7c0ed94bcf71cffde4b53d5192 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 21:49:48 +0000 Subject: [PATCH 031/103] convert super simple queue to ts --- client/src/Pages/Incidents2/index.jsx | 4 +- server/src/config/services.ts | 2 - .../SuperSimpleQueue/SuperSimpleQueue.js | 177 ------------ .../SuperSimpleQueue/SuperSimpleQueue.ts | 254 ++++++++++++++++++ 4 files changed, 256 insertions(+), 181 deletions(-) delete mode 100644 server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js create mode 100644 server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts diff --git a/client/src/Pages/Incidents2/index.jsx b/client/src/Pages/Incidents2/index.jsx index 966388b51..2f31864bf 100644 --- a/client/src/Pages/Incidents2/index.jsx +++ b/client/src/Pages/Incidents2/index.jsx @@ -87,8 +87,8 @@ const Incidents2 = () => { useEffect(() => { const lookup = monitors?.reduce((acc, monitor) => { - acc[monitor._id] = { - _id: monitor._id, + acc[monitor.id] = { + _id: monitor.id, name: monitor.name, type: monitor.type, }; diff --git a/server/src/config/services.ts b/server/src/config/services.ts index daa576bf8..90d6bb518 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -211,8 +211,6 @@ export const initializeServices = async ({ }); const superSimpleQueue = await SuperSimpleQueue.create({ - envSettings, - db, logger, helper: superSimpleQueueHelper, monitorsRepository, diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js deleted file mode 100644 index a6637d4cc..000000000 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js +++ /dev/null @@ -1,177 +0,0 @@ -import Scheduler from "super-simple-scheduler"; -const SERVICE_NAME = "JobQueue"; - -class SuperSimpleQueue { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ envSettings, db, logger, helper, monitorsRepository }) { - this.envSettings = envSettings; - this.db = db; - this.logger = logger; - this.helper = helper; - this.monitorsRepository = monitorsRepository; - } - - get serviceName() { - return SuperSimpleQueue.SERVICE_NAME; - } - - static async create({ envSettings, db, logger, helper, monitorsRepository }) { - const instance = new SuperSimpleQueue({ envSettings, db, logger, helper, monitorsRepository }); - await instance.init(); - return instance; - } - - init = async () => { - try { - this.scheduler = new Scheduler({ - // storeType: "mongo", - // storeType: "redis", - logLevel: "debug", - debug: true, - // dbUri: this.envSettings.dbConnectionString, - }); - this.scheduler.start(); - - this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); - const monitors = await this.monitorsRepository.findAll(); - for (const monitor of monitors) { - const randomOffset = Math.floor(Math.random() * 100); - setTimeout(() => { - this.addJob(monitor.id, monitor); - }, randomOffset); - } - - return true; - } catch (error) { - this.logger.error({ - message: "Failed to initialize SuperSimpleQueue", - service: SERVICE_NAME, - method: "init", - details: error, - }); - return false; - } - }; - - addJob = async (monitorId, monitor) => { - this.scheduler.addJob({ - id: monitorId, - template: "monitor-job", - repeat: monitor.interval, - active: monitor.isActive, - data: monitor, - }); - }; - - deleteJob = async (monitor) => { - this.scheduler.removeJob(monitor.id); - }; - - pauseJob = async (monitor) => { - const result = this.scheduler.pauseJob(monitor.id); - if (result === false) { - throw new Error("Failed to resume monitor"); - } - this.logger.debug({ - message: `Paused monitor ${monitor.id}`, - service: SERVICE_NAME, - method: "pauseJob", - }); - }; - - resumeJob = async (monitor) => { - const result = this.scheduler.resumeJob(monitor.id); - if (result === false) { - throw new Error("Failed to resume monitor"); - } - this.logger.debug({ - message: `Resumed monitor ${monitor.id}`, - service: SERVICE_NAME, - method: "resumeJob", - }); - }; - - updateJob = async (monitor) => { - this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor }); - }; - - shutdown = async () => { - this.scheduler.stop(); - }; - - getMetrics = async () => { - const jobs = await this.scheduler.getJobs(); - const metrics = jobs.reduce( - (acc, job) => { - acc.totalRuns += job.runCount || 0; - acc.totalFailures += job.failCount || 0; - acc.jobs++; - if (job.failCount > 0 && job.lastFailedAt >= job.lsatRunAt) { - acc.failingJobs++; - } - - if (job.lockedAt) { - acc.activeJobs++; - } - - if (job.failCount > 0) { - acc.jobsWithFailures.push({ - monitorId: job.id, - monitorUrl: job?.data?.url || null, - monitorType: job?.data?.type || null, - failedAt: job.lastFailedAt, - failCount: job.failCount, - failReason: job.lastFailReason, - }); - } - return acc; - }, - { - jobs: 0, - activeJobs: 0, - failingJobs: 0, - jobsWithFailures: [], - totalRuns: 0, - totalFailures: 0, - } - ); - return metrics; - }; - - getJobs = async () => { - const jobs = await this.scheduler.getJobs(); - return jobs.map((job) => { - return { - monitorId: job.id, - monitorUrl: job?.data?.url || null, - monitorType: job?.data?.type || null, - monitorInterval: job?.data?.interval || null, - active: job.active, - lockedAt: job.lockedAt, - runCount: job.runCount || 0, - failCount: job.failCount || 0, - failReason: job.lastFailReason, - lastRunAt: job.lastRunAt, - lastFinishedAt: job.lastFinishedAt, - lastRunTook: job.lockedAt ? null : job.lastFinishedAt - job.lastRunAt, - lastFailedAt: job.lastFailedAt, - }; - }); - }; - - flushQueues = async () => { - const stopRes = this.scheduler.stop(); - const flushRes = this.scheduler.flushJobs(); - const initRes = await this.init(); - return { - success: stopRes && flushRes && initRes, - }; - }; - - obliterate = async () => { - console.log("obliterate not implemented"); - }; -} - -export default SuperSimpleQueue; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts new file mode 100644 index 000000000..daf7e130f --- /dev/null +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -0,0 +1,254 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; +import Scheduler from "super-simple-scheduler"; +const SERVICE_NAME = "JobQueue"; + +type QueueJobFailure = { + monitorId: string | number; + monitorUrl: string | null; + monitorType: string | null; + failedAt: number | null; + failCount: number; + failReason: string | null; +}; + +type QueueMetrics = { + jobs: number; + activeJobs: number; + failingJobs: number; + jobsWithFailures: QueueJobFailure[]; + totalRuns: number; + totalFailures: number; +}; + +type QueueJobSummary = { + monitorId: string | number; + monitorUrl: string | null; + monitorType: string | null; + monitorInterval: number | null; + active: boolean; + lockedAt: number | null; + runCount: number; + failCount: number; + failReason: string | null; + lastRunAt: number | null; + lastFinishedAt: number | null; + lastRunTook: number | null; + lastFailedAt: number | null; +}; + +export interface ISuperSimpleQueue { + readonly serviceName: string; + init(): Promise; + addJob(monitorId: string, monitor: any): Promise; + deleteJob(monitor: any): Promise; + pauseJob(monitor: any): Promise; + resumeJob(monitor: any): Promise; + updateJob(monitor: any): Promise; + shutdown(): Promise; + getMetrics(): Promise; + getJobs(): Promise; + flushQueues(): Promise<{ success: boolean }>; + obliterate(): Promise; +} + +class SuperSimpleQueue implements ISuperSimpleQueue { + static SERVICE_NAME = SERVICE_NAME; + + private logger: any; + private helper: any; + private monitorsRepository: IMonitorsRepository; + private readonly scheduler: Scheduler; + + constructor({ + logger, + helper, + monitorsRepository, + scheduler, + }: { + logger: any; + helper: any; + monitorsRepository: IMonitorsRepository; + scheduler: Scheduler; + }) { + this.logger = logger; + this.helper = helper; + this.monitorsRepository = monitorsRepository; + this.scheduler = scheduler; + } + + get serviceName() { + return SuperSimpleQueue.SERVICE_NAME; + } + + static async create({ logger, helper, monitorsRepository }: { logger: any; helper: any; monitorsRepository: IMonitorsRepository }) { + const scheduler = new Scheduler({ + // storeType: "mongo", + // storeType: "redis", + logLevel: "debug", + // dbUri: envSettings.dbConnectionString, + }); + const instance = new SuperSimpleQueue({ logger, helper, monitorsRepository, scheduler }); + await instance.init(); + return instance; + } + + init = async () => { + try { + this.scheduler.start(); + + this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); + const monitors = await this.monitorsRepository.findAll(); + if (!monitors) { + return true; + } + for (const monitor of monitors) { + const randomOffset = Math.floor(Math.random() * 100); + setTimeout(() => { + this.addJob(monitor.id, monitor); + }, randomOffset); + } + + return true; + } catch (error) { + this.logger.error({ + message: "Failed to initialize SuperSimpleQueue", + service: SERVICE_NAME, + method: "init", + details: error, + }); + return false; + } + }; + + addJob = async (monitorId: string, monitor: any) => { + this.scheduler.addJob({ + id: monitorId, + template: "monitor-job", + repeat: monitor.interval, + active: monitor.isActive, + data: monitor, + }); + }; + + deleteJob = async (monitor: any) => { + this.scheduler.removeJob(monitor.id); + }; + + pauseJob = async (monitor: any) => { + const result = await this.scheduler.pauseJob(monitor.id); + if (result === false) { + throw new Error("Failed to resume monitor"); + } + this.logger.debug({ + message: `Paused monitor ${monitor.id}`, + service: SERVICE_NAME, + method: "pauseJob", + }); + }; + + resumeJob = async (monitor: any) => { + const result = await this.scheduler.resumeJob(monitor.id); + if (result === false) { + throw new Error("Failed to resume monitor"); + } + this.logger.debug({ + message: `Resumed monitor ${monitor.id}`, + service: SERVICE_NAME, + method: "resumeJob", + }); + }; + + updateJob = async (monitor: any) => { + this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor }); + }; + + shutdown = async () => { + this.scheduler.stop(); + }; + + getMetrics = async () => { + const jobs = await this.scheduler.getJobs(); + const metrics = jobs.reduce( + (acc, job) => { + const runCount = job.runCount ?? 0; + const failCount = job.failCount ?? 0; + const lastFailedAt = job.lastFailedAt ?? 0; + const lastRunAt = job.lastRunAt ?? 0; + acc.totalRuns += runCount; + acc.totalFailures += failCount; + acc.jobs++; + if (failCount > 0 && lastFailedAt >= lastRunAt) { + acc.failingJobs++; + } + + if (job.lockedAt) { + acc.activeJobs++; + } + + if (failCount > 0) { + acc.jobsWithFailures.push({ + monitorId: job.id, + monitorUrl: job?.data?.url || null, + monitorType: job?.data?.type || null, + failedAt: job.lastFailedAt ?? null, + failCount, + failReason: job.lastFailReason ?? null, + }); + } + return acc; + }, + { + jobs: 0, + activeJobs: 0, + failingJobs: 0, + jobsWithFailures: [] as Array<{ + monitorId: string | number; + monitorUrl: any; + monitorType: any; + failedAt: number | null; + failCount: number; + failReason: string | null; + }>, + totalRuns: 0, + totalFailures: 0, + } + ); + return metrics; + }; + + getJobs = async () => { + const jobs = await this.scheduler.getJobs(); + return jobs.map((job) => { + return { + monitorId: job.id, + monitorUrl: job?.data?.url || null, + monitorType: job?.data?.type || null, + monitorInterval: job?.data?.interval || null, + active: job.active, + lockedAt: job.lockedAt ?? null, + runCount: job.runCount ?? 0, + failCount: job.failCount ?? 0, + failReason: job.lastFailReason ?? null, + lastRunAt: job.lastRunAt ?? null, + lastFinishedAt: job.lastFinishedAt ?? null, + lastRunTook: job.lockedAt ? null : (job.lastFinishedAt ?? 0) - (job.lastRunAt ?? 0), + lastFailedAt: job.lastFailedAt ?? null, + }; + }); + }; + + flushQueues = async () => { + const stopRes = await this.scheduler.stop(); + const flushRes = await this.scheduler.flushJobs(); + const initRes = await this.init(); + return { + success: Boolean(stopRes && flushRes && initRes), + }; + }; + + obliterate = async () => { + console.log("obliterate not implemented"); + }; +} + +export default SuperSimpleQueue; From cf67c82f5ecf1714a33eb1390c19dbdc452dba7b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 22:06:46 +0000 Subject: [PATCH 032/103] convert job queue and job queue helper --- server/jest.config.ts | 11 ++- ...eueHelper.js => SuperSimpleQueueHelper.ts} | 60 ++++++----- server/test/superSimpleQueueHelper.test.ts | 99 +++++++++++++++++++ server/tsconfig.jest.json | 3 +- server/tsconfig.json | 2 +- 5 files changed, 149 insertions(+), 26 deletions(-) rename server/src/service/infrastructure/SuperSimpleQueue/{SuperSimpleQueueHelper.js => SuperSimpleQueueHelper.ts} (69%) create mode 100644 server/test/superSimpleQueueHelper.test.ts diff --git a/server/jest.config.ts b/server/jest.config.ts index 2bcfcbf21..4fb2c9831 100644 --- a/server/jest.config.ts +++ b/server/jest.config.ts @@ -1,5 +1,14 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { pathsToModuleNameMapper } from "ts-jest"; import type { Config } from "jest"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const tsconfigPath = path.resolve(__dirname, "./tsconfig.json"); +const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")); + const config: Config = { rootDir: ".", testEnvironment: "node", @@ -8,7 +17,7 @@ const config: Config = { "^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }], }, moduleNameMapper: { - "^@/(.*)$": "/src/$1", + ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, { prefix: "/" }), }, testMatch: ["/test/**/*.test.ts"], setupFilesAfterEnv: [], diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts similarity index 69% rename from server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js rename to server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 3cd6b37a8..678986c62 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,18 +1,30 @@ const SERVICE_NAME = "JobQueueHelper"; +import type { Monitor } from "@/types/monitor.js"; +import { AppError } from "@/utils/AppError.js"; +import { error } from "winston"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; - /** - * @param {{ - * db: import("../database.js").Database, - * logger: import("../logger.js").Logger, - * networkService: import("../networkService.js").NetworkService, - * statusService: import("../statusService.js").StatusService, - * notificationService: import("../notificationService.js").NotificationService - * }} - */ - constructor({ db, logger, networkService, statusService, notificationService }) { + private db: any; + private logger: any; + private networkService: any; + private statusService: any; + private notificationService: any; + + constructor({ + db, + logger, + networkService, + statusService, + notificationService, + }: { + db: any; + logger: any; + networkService: any; + statusService: any; + notificationService: any; + }) { this.db = db; this.logger = logger; this.networkService = networkService; @@ -25,39 +37,41 @@ class SuperSimpleQueueHelper { } getMonitorJob = () => { - return async (monitor) => { + return async (monitor: Monitor) => { try { const monitorId = monitor.id; const teamId = monitor.teamId; if (!monitorId) { - throw new Error("No monitor id"); + throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" }); } + // Step 1. Check for maintenacne window, if found, skip the check const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId); if (maintenanceWindowActive) { - this.logger.info({ + this.logger.debug({ message: `Monitor ${monitorId} is in maintenance window`, service: SERVICE_NAME, method: "getMonitorJob", }); return; } - const networkResponse = await this.networkService.requestStatus(monitor); - if (!networkResponse) { + // Step 2. Request monitor status + const status = await this.networkService.requestStatus(monitor); + if (!status) { throw new Error("No network response"); } - const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse); + const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(status); this.notificationService .handleNotifications({ - ...networkResponse, + ...status, monitor: updatedMonitor, prevStatus, statusChanged, }) - .catch((error) => { + .catch((error: any) => { this.logger.error({ message: error.message, service: SERVICE_NAME, @@ -66,7 +80,7 @@ class SuperSimpleQueueHelper { stack: error.stack, }); }); - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: error.service || SERVICE_NAME, @@ -78,13 +92,13 @@ class SuperSimpleQueueHelper { }; }; - async isInMaintenanceWindow(monitorId, teamId) { + async isInMaintenanceWindow(monitorId: string, teamId: string) { const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ - monitorId: monitorId.toString(), - teamId: teamId.toString(), + monitorId: monitorId, + teamId: teamId, }); // Check for active maintenance window: - const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => { + const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => { if (window.active) { const start = new Date(window.start); const end = new Date(window.end); diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts new file mode 100644 index 000000000..056173c82 --- /dev/null +++ b/server/test/superSimpleQueueHelper.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import SuperSimpleQueueHelper from "../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts"; +import type { Monitor } from "../src/types/monitor.ts"; + +const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn() }); + +const createHelper = (overrides?: Partial[0]>) => { + const maintenanceWindowModule = { + getMaintenanceWindowsByMonitorId: jest.fn().mockResolvedValue([]), + }; + const helper = new SuperSimpleQueueHelper({ + db: { maintenanceWindowModule }, + logger: createLogger(), + networkService: { requestStatus: jest.fn() }, + statusService: { updateStatus: jest.fn() }, + notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + ...overrides, + }); + return { helper, maintenanceWindowModule }; +}; + +describe("SuperSimpleQueueHelper", () => { + describe("getMonitorJob", () => { + it("skips execution when monitor is in maintenance window", async () => { + const { helper } = createHelper(); + const spy = jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(true); + const job = helper.getMonitorJob(); + await job({ id: "m1", teamId: "team", interval: 60000 } as Monitor); + expect(helper["networkService"].requestStatus).not.toHaveBeenCalled(); + expect(helper["logger"].info).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining("Monitor m1 is in maintenance window") }) + ); + spy.mockRestore(); + }); + + it("processes monitor status and notifications when active", async () => { + const networkResponse = { monitor: { id: "m1" }, status: true }; + const updatedMonitor = { id: "m1", status: true }; + const { helper } = createHelper({ + networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) }, + statusService: { + updateStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), + }, + notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + }); + jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(false); + const job = helper.getMonitorJob(); + const monitor = { id: "m1", teamId: "team" } as Monitor; + await job(monitor); + expect(helper["networkService"].requestStatus).toHaveBeenCalledWith(monitor); + expect(helper["statusService"].updateStatus).toHaveBeenCalledWith(networkResponse); + expect(helper["notificationService"].handleNotifications).toHaveBeenCalledWith( + expect.objectContaining({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }) + ); + }); + + it("throws when monitor id is missing", async () => { + const { helper } = createHelper(); + const job = helper.getMonitorJob(); + await expect(job({} as Monitor)).rejects.toThrow("No monitor id"); + expect(helper["logger"].warn).toHaveBeenCalled(); + }); + }); + + describe("isInMaintenanceWindow", () => { + it("returns true when an active window spans now", async () => { + const now = new Date(); + const { helper, maintenanceWindowModule } = createHelper(); + maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([ + { + active: true, + start: new Date(now.getTime() - 1000).toISOString(), + end: new Date(now.getTime() + 1000).toISOString(), + repeat: 0, + }, + ]); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true); + }); + + it("returns true when repeat interval advances window into current time", async () => { + const now = Date.now(); + const { helper, maintenanceWindowModule } = createHelper(); + maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([ + { + active: true, + start: new Date(now - 7200000).toISOString(), + end: new Date(now - 6600000).toISOString(), + repeat: 3600000, + }, + ]); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true); + }); + + it("returns false when no active windows exist", async () => { + const { helper } = createHelper(); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(false); + }); + }); +}); diff --git a/server/tsconfig.jest.json b/server/tsconfig.jest.json index 928b0977c..80c321cf6 100644 --- a/server/tsconfig.jest.json +++ b/server/tsconfig.jest.json @@ -3,7 +3,8 @@ "compilerOptions": { "rootDir": ".", "types": ["jest"], - "noEmit": true + "noEmit": true, + "resolveJsonModule": true }, "include": ["src", "test"] } diff --git a/server/tsconfig.json b/server/tsconfig.json index cbb874656..bdb3a7b0e 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -4,7 +4,7 @@ "allowJs": true, "checkJs": false, "paths": { - "@/*": ["./src/*"] // allows "@/db" -> "./src/db" + "@/*": ["./src/*"] }, "outDir": "./dist", "strict": true, From ed8083781061773239d894686c2cb193b01d2efb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 22:33:25 +0000 Subject: [PATCH 033/103] related serverices to TS --- server/jest.config.ts | 16 +- server/src/service/business/checkService.ts | 3 + .../SuperSimpleQueueHelper.ts | 1 - .../{networkService.js => networkService.ts} | 141 +++++++++++++----- .../{statusService.js => statusService.ts} | 111 ++++++-------- server/test/superSimpleQueueHelper.test.ts | 4 +- 6 files changed, 162 insertions(+), 114 deletions(-) rename server/src/service/infrastructure/{networkService.js => networkService.ts} (84%) rename server/src/service/infrastructure/{statusService.js => statusService.ts} (78%) diff --git a/server/jest.config.ts b/server/jest.config.ts index 4fb2c9831..fd0c87ad7 100644 --- a/server/jest.config.ts +++ b/server/jest.config.ts @@ -1,14 +1,5 @@ -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import { pathsToModuleNameMapper } from "ts-jest"; import type { Config } from "jest"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const tsconfigPath = path.resolve(__dirname, "./tsconfig.json"); -const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")); - const config: Config = { rootDir: ".", testEnvironment: "node", @@ -17,7 +8,12 @@ const config: Config = { "^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }], }, moduleNameMapper: { - ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, { prefix: "/" }), + "^@/validation/(.*)\\.js$": "/src/validation/$1.js", + "^@/utils/(AppError)\\.js$": "/src/utils/$1.ts", + "^@/utils/(.*)\\.js$": "/src/utils/$1.js", + "^@/(.*)\\.ts$": "/src/$1.ts", + "^@/(.*)\\.js$": "/src/$1.ts", + "^@/(.*)$": "/src/$1", }, testMatch: ["/test/**/*.test.ts"], setupFilesAfterEnv: [], diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 61e2fb6ad..8aea221f6 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,4 +1,5 @@ import { IMonitorsRepository } from "@/repositories/index.js"; +import type { MonitorType } from "@/types/index.js"; const SERVICE_NAME = "checkService"; @@ -35,6 +36,8 @@ class CheckService { return CheckService.SERVICE_NAME; } + buildCheck = async (statusResponse: any, type: MonitorType) => {}; + getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 678986c62..b025a65c6 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,7 +1,6 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { error } from "winston"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.ts similarity index 84% rename from server/src/service/infrastructure/networkService.js rename to server/src/service/infrastructure/networkService.ts index 2375e7233..b9f02e7c9 100644 --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.ts @@ -1,10 +1,63 @@ +import { Got } from "got"; +import type { Monitor } from "@/types/index.js"; + import CacheableLookup from "cacheable-lookup"; const SERVICE_NAME = "NetworkService"; class NetworkService { static SERVICE_NAME = SERVICE_NAME; - constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) { + private TYPE_PING: string; + private TYPE_HTTP: string; + private TYPE_PAGESPEED: string; + private TYPE_HARDWARE: string; + private TYPE_DOCKER: string; + private TYPE_PORT: string; + private TYPE_GAME: string; + private SERVICE_NAME: string; + private NETWORK_ERROR: number; + private PING_ERROR: number; + + private axios: any; + private got: Got; + private https: any; + private jmespath: any; + private GameDig: any; + private ping: any; + private logger: any; + private http: any; + private Docker: any; + private net: any; + private stringService: any; + private settingsService: any; + + constructor({ + axios, + got, + https, + jmespath, + GameDig, + ping, + logger, + http, + Docker, + net, + stringService, + settingsService, + }: { + axios: any; + got: Got; + https: any; + jmespath: any; + GameDig: any; + ping: any; + logger: any; + http: any; + Docker: any; + net: any; + stringService: any; + settingsService: any; + }) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; @@ -17,11 +70,11 @@ class NetworkService { this.PING_ERROR = 5001; this.axios = axios; this.https = https; + this.http = http; this.jmespath = jmespath; this.GameDig = GameDig; this.ping = ping; this.logger = logger; - this.http = http; this.Docker = Docker; this.net = net; this.stringService = stringService; @@ -39,7 +92,7 @@ class NetworkService { } // Helper functions - async timeRequest(operation) { + async timeRequest(operation: any) { const start = process.hrtime.bigint(); try { const response = await operation(); @@ -52,7 +105,7 @@ class NetworkService { } // Main entry point - async requestStatus(monitor) { + async requestStatus(monitor: Monitor) { const type = monitor?.type || "unknown"; switch (type) { case this.TYPE_PING: @@ -74,7 +127,7 @@ class NetworkService { } } - async requestPing(monitor) { + async requestPing(monitor: Monitor) { try { if (!monitor?.url) { throw new Error("Monitor URL is required"); @@ -112,16 +165,16 @@ class NetworkService { } return pingResponse; - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestPing"; throw err; } } - async requestHttp(monitor) { + async requestHttp(monitor: Monitor) { const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; - const httpResponse = { + const httpResponse: Record = { monitorId: id, teamId: teamId, type, @@ -131,7 +184,7 @@ class NetworkService { if (!url) { throw new Error("Monitor URL is required"); } - const config = { + const config: Record = { headers: secret ? { Authorization: `Bearer ${secret}` } : undefined, }; @@ -145,7 +198,7 @@ class NetworkService { const response = await this.got(url, config); - let payload; + let payload: any; const contentType = response.headers["content-type"]; if (contentType && contentType.includes("application/json")) { @@ -231,7 +284,7 @@ class NetworkService { } } return httpResponse; - } catch (err) { + } catch (err: any) { if (err.name === "HTTPError" || err.name === "RequestError") { httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR; httpResponse.status = false; @@ -247,7 +300,7 @@ class NetworkService { } } - async requestPageSpeed(monitor) { + async requestPageSpeed(monitor: Monitor) { try { const url = monitor.url; if (!url) { @@ -269,24 +322,24 @@ class NetworkService { ...monitor, url: pageSpeedUrl, }); - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestPageSpeed"; throw err; } } - async requestHardware(monitor) { + async requestHardware(monitor: Monitor) { try { return await this.requestHttp(monitor); - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestHardware"; throw err; } } - async requestDocker(monitor) { + async requestDocker(monitor: Monitor) { try { if (!monitor.url) { throw new Error("Monitor URL is required"); @@ -297,7 +350,7 @@ class NetworkService { handleError: true, // Enable error handling }); - const dockerResponse = { + const dockerResponse: Record = { monitorId: monitor.id, type: monitor.type, }; @@ -309,18 +362,18 @@ class NetworkService { // Priority-based matching to avoid ambiguity: // 1. Exact full ID match (64-char) - let exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput); + let exactIdMatch = containers.find((c: any) => c.Id.toLowerCase() === normalizedInput); // 2. Exact container name match (case-insensitive) - let exactNameMatch = containers.find((c) => - c.Names.some((name) => { + let exactNameMatch = containers.find((c: any) => + c.Names.some((name: string) => { const cleanName = name.replace(/^\/+/, "").toLowerCase(); return cleanName === normalizedInput; }) ); // 3. Partial ID match (fallback for backwards compatibility) - let partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput)); + let partialIdMatch = containers.find((c: any) => c.Id.toLowerCase().startsWith(normalizedInput)); // Select container based on priority let targetContainer = exactIdMatch || exactNameMatch || partialIdMatch; @@ -360,7 +413,9 @@ class NetworkService { } const container = docker.getContainer(targetContainer.Id); - const { response, responseTime, error } = await this.timeRequest(() => container.inspect()); + const { response, responseTime, error }: { response?: any; responseTime?: number; error?: any } = await this.timeRequest(() => + container.inspect() + ); dockerResponse.responseTime = responseTime; dockerResponse.status = response?.State?.Status === "running" ? true : false; @@ -375,14 +430,14 @@ class NetworkService { } return dockerResponse; - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestDocker"; throw err; } } - async requestPort(monitor) { + async requestPort(monitor: Monitor) { try { const { url, port } = monitor; const { response, responseTime, error } = await this.timeRequest(async () => { @@ -405,7 +460,7 @@ class NetworkService { reject(new Error("Connection timeout")); }); - socket.on("error", (err) => { + socket.on("error", (err: any) => { socket.destroy(); reject(err); }); @@ -429,18 +484,18 @@ class NetworkService { } return portResponse; - } catch (error) { + } catch (error: any) { error.service = this.SERVICE_NAME; error.method = "requestTCP"; throw error; } } - async requestGame(monitor) { + async requestGame(monitor: Monitor) { try { const { url, port, gameId } = monitor; - const gameResponse = { + const gameResponse: Record = { code: 200, status: true, message: "Success", @@ -452,7 +507,7 @@ class NetworkService { type: gameId, host: url, port: port, - }).catch((error) => { + }).catch((error: any) => { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, @@ -471,21 +526,21 @@ class NetworkService { gameResponse.responseTime = state.ping; gameResponse.payload = state; return gameResponse; - } catch (error) { + } catch (error: any) { error.service = this.SERVICE_NAME; error.method = "requestPing"; throw error; } } - async handleUnsupportedType(type) { - const err = new Error(`Unsupported type: ${type}`); + async handleUnsupportedType(type: string) { + const err: any = new Error(`Unsupported type: ${type}`); err.service = this.SERVICE_NAME; err.method = "getStatus"; throw err; } // Other network requests unrelated to monitoring: - async requestWebhook(type, url, body) { + async requestWebhook(type: string, url: string, body: any) { try { const response = await this.axios.post(url, body, { headers: { @@ -500,7 +555,7 @@ class NetworkService { message: `Successfully sent ${type} notification`, payload: response.data, }; - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, @@ -517,7 +572,7 @@ class NetworkService { } } - async requestPagerDuty({ message, routingKey, monitorUrl }) { + async requestPagerDuty({ message, routingKey, monitorUrl }: { message: string; routingKey: string; monitorUrl: string }) { try { const response = await this.axios.post(`https://events.pagerduty.com/v2/enqueue`, { routing_key: routingKey, @@ -532,7 +587,7 @@ class NetworkService { if (response?.data?.status !== "success") return false; return true; - } catch (error) { + } catch (error: any) { error.details = error.response?.data; error.service = this.SERVICE_NAME; error.method = "requestPagerDuty"; @@ -540,7 +595,17 @@ class NetworkService { } } - async requestMatrix({ homeserverUrl, accessToken, roomId, message }) { + async requestMatrix({ + homeserverUrl, + accessToken, + roomId, + message, + }: { + homeserverUrl: string; + accessToken: string; + roomId: string; + message: string; + }) { try { const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; const body = { @@ -560,7 +625,7 @@ class NetworkService { code: response.status, message: "Successfully sent Matrix notification", }; - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.ts similarity index 78% rename from server/src/service/infrastructure/statusService.js rename to server/src/service/infrastructure/statusService.ts index 763b6d1fa..d68ec345a 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.ts @@ -1,11 +1,30 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; import MonitorStats from "../../db/models/MonitorStats.js"; import { CheckModel } from "@/db/models/index.js"; +import type { Monitor } from "@/types/index.js"; const SERVICE_NAME = "StatusService"; class StatusService { static SERVICE_NAME = SERVICE_NAME; + private db: any; + private logger: any; + private buffer: any; + private incidentService: any; + private monitorsRepository: IMonitorsRepository; - constructor({ db, logger, buffer, incidentService, monitorsRepository }) { + constructor({ + db, + logger, + buffer, + incidentService, + monitorsRepository, + }: { + db: any; + logger: any; + buffer: any; + incidentService: any; + monitorsRepository: IMonitorsRepository; + }) { this.db = db; this.logger = logger; this.buffer = buffer; @@ -17,7 +36,7 @@ class StatusService { return StatusService.SERVICE_NAME; } - async updateRunningStats({ monitor, networkResponse }) { + async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }) { try { const monitorId = monitor.id; const { responseTime, status } = networkResponse; @@ -78,9 +97,9 @@ class StatusService { await stats.save(); return true; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, message: error.message, method: "updateRunningStats", stack: error.stack, @@ -89,23 +108,13 @@ class StatusService { } } - getStatusString = (status) => { + getStatusString = (status: boolean | undefined) => { if (status === true) return "up"; if (status === false) return "down"; return "unknown"; }; - /** - * Saves check if needed and adds to incident buffer - * Removes check from checks buffer if it was saved immediately - * - * @param {Object} check - The check object - * @param {Object} monitor - The monitor object - * @param {string} action - The incident action ("create" or "resolve") - * @param {string} errorContext - Context for error messages - * @returns {Promise} - */ - handleIncidentForCheck = async (check, monitor, action, errorContext = "incident handling") => { + handleIncidentForCheck = async (check: any, monitor: Monitor, action: any, errorContext = "incident handling") => { try { let savedCheck = check; @@ -115,9 +124,9 @@ class StatusService { savedCheck = await checkModel.save(); this.buffer.removeCheckFromBuffer(check); - } catch (checkError) { + } catch (checkError: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "handleIncidentForCheck", message: `Failed to save check immediately for ${errorContext}: ${checkError.message}`, monitorId: monitor.id, @@ -130,9 +139,9 @@ class StatusService { if (savedCheck && savedCheck._id) { try { this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action }); - } catch (incidentError) { + } catch (incidentError: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "handleIncidentForCheck", message: `Failed to add incident to buffer for ${errorContext}: ${incidentError.message}`, monitorId: monitor.id, @@ -141,9 +150,9 @@ class StatusService { }); } } - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "handleIncidentForCheck", message: `Error in ${errorContext}: ${error.message}`, monitorId: monitor?.id, @@ -151,19 +160,8 @@ class StatusService { }); } }; - /** - * Updates the status of a monitor based on the network response. - * - * @param {Object} networkResponse - The network response containing monitorId and status. - * @param {string} networkResponse.monitorId - The ID of the monitor. - * @param {string} networkResponse.status - The new status of the monitor. - * @returns {Promise} - A promise that resolves to an object containinfg the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred. - * @returns {Promise} returnObject - The object returned by the function. - * @returns {Object} returnObject.monitor - The monitor object. - * @returns {boolean} returnObject.statusChanged - Flag indicating if the status has changed. - * @returns {boolean} returnObject.prevStatus - The previous status of the monitor - */ - updateStatus = async (networkResponse) => { + + updateStatus = async (networkResponse: any) => { const check = this.buildCheck(networkResponse); await this.insertCheck(check); try { @@ -221,7 +219,7 @@ class StatusService { if (statusChanged) { this.logger.info({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`, prevStatus, newStatus, @@ -242,7 +240,7 @@ class StatusService { if (lastManuallyResolvedIncident && lastManuallyResolvedIncident.endTime) { try { - const checksAfterResolution = await Check.find({ + const checksAfterResolution = await CheckModel.find({ monitorId: monitor.id, createdAt: { $gt: lastManuallyResolvedIncident.endTime }, }) @@ -258,9 +256,9 @@ class StatusService { } else { calculatedFailureRate = 0; } - } catch (checkQueryError) { + } catch (checkQueryError: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "updateStatus", message: `Failed to query checks after manual resolution: ${checkQueryError.message}`, monitorId: monitor.id, @@ -272,9 +270,9 @@ class StatusService { if (calculatedFailureRate >= monitor.statusWindowThreshold) { await this.handleIncidentForCheck(check, monitor, "create", "threshold check without status change"); } - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "updateStatus", message: `Error handling threshold check without status change: ${error.message}`, monitorId: monitor.id, @@ -293,27 +291,14 @@ class StatusService { code, timestamp: new Date().getTime(), }; - } catch (error) { - error.service = this.SERVICE_NAME; + } catch (error: any) { + error.service = SERVICE_NAME; error.method = "updateStatus"; throw error; } }; - /** - * Builds a check object from the network response. - * - * @param {Object} networkResponse - The network response object. - * @param {string} networkResponse.monitorId - The monitor ID. - * @param {string} networkResponse.type - The type of the response. - * @param {string} networkResponse.status - The status of the response. - * @param {number} networkResponse.responseTime - The response time. - * @param {number} networkResponse.code - The status code. - * @param {string} networkResponse.message - The message. - * @param {Object} networkResponse.payload - The payload of the response. - * @returns {Object} The check object. - */ - buildCheck = (networkResponse) => { + buildCheck = (networkResponse: any) => { const { monitorId, teamId, @@ -355,7 +340,7 @@ class StatusService { if (typeof payload === "undefined") { this.logger.warn({ message: "Failed to build check", - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "buildCheck", details: "empty payload", }); @@ -363,7 +348,7 @@ class StatusService { } const categories = payload?.lighthouseResult?.categories ?? {}; const audits = payload?.lighthouseResult?.audits ?? {}; - const mapAudit = (audit) => { + const mapAudit = (audit: any) => { if (!audit || typeof audit !== "object") { return undefined; } @@ -416,22 +401,22 @@ class StatusService { * @param {Object} networkResponse.payload - The payload of the response. * @returns {Promise} A promise that resolves when the check is inserted. */ - insertCheck = async (check) => { + insertCheck = async (check: any) => { try { if (typeof check === "undefined") { this.logger.warn({ message: "Failed to build check", - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "insertCheck", }); return false; } this.buffer.addToBuffer({ check }); return true; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, - service: error.service || this.SERVICE_NAME, + service: error.service || SERVICE_NAME, method: error.method || "insertCheck", details: error.details || `Error inserting check for monitor: ${check?.monitorId}`, stack: error.stack, diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts index 056173c82..997eb9e83 100644 --- a/server/test/superSimpleQueueHelper.test.ts +++ b/server/test/superSimpleQueueHelper.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, jest } from "@jest/globals"; import SuperSimpleQueueHelper from "../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts"; import type { Monitor } from "../src/types/monitor.ts"; -const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn() }); +const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }); const createHelper = (overrides?: Partial[0]>) => { const maintenanceWindowModule = { @@ -27,7 +27,7 @@ describe("SuperSimpleQueueHelper", () => { const job = helper.getMonitorJob(); await job({ id: "m1", teamId: "team", interval: 60000 } as Monitor); expect(helper["networkService"].requestStatus).not.toHaveBeenCalled(); - expect(helper["logger"].info).toHaveBeenCalledWith( + expect(helper["logger"].debug).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Monitor m1 is in maintenance window") }) ); spy.mockRestore(); From 8b2ee8d937094dd284a044ae567d789e716abb73 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 15 Jan 2026 17:30:15 -0500 Subject: [PATCH 034/103] Simplify theme and fix Redux state bugs ## Changes - Remove unused semantic color groups (successSecondary, map, chatbot) - Remove unused properties from primary and tertiary groups - Inline accent.darker usage in globalTheme.js - Fix greeting component crash with persisted Redux state - Fix MonitorGrid key prop warning ## Details Theme simplification: - Reduced constants.js from 355 to 291 lines - Removed 3 unused semantic color groups - Removed 6 unused properties (contrastBorder, contrastBorderDisabled, etc.) - Inlined darken() call for accent hover states Bug fixes: - Added null-safe selectors in greeting.jsx for state.ui.greeting - Added initialization check in setGreeting reducer for older persisted state - Fixed key prop in MonitorGrid to handle both _id and id - Removed unused console.log and Grid2 import --- client/src/Components/LanguageSelector.jsx | 2 +- client/src/Components/v1/I18nLoader/index.jsx | 2 +- client/src/Components/v1/Sidebar/index.jsx | 2 +- client/src/Features/UI/uiSlice.js | 3 + .../Monitors/Components/MonitorGrid/index.jsx | 9 +-- client/src/Pages/Settings/index.jsx | 2 +- client/src/Utils/Theme/constants.js | 66 +------------------ client/src/Utils/Theme/globalTheme.js | 4 +- client/src/Utils/greeting.jsx | 4 +- 9 files changed, 17 insertions(+), 77 deletions(-) diff --git a/client/src/Components/LanguageSelector.jsx b/client/src/Components/LanguageSelector.jsx index 9b46204e3..fdda73f4a 100644 --- a/client/src/Components/LanguageSelector.jsx +++ b/client/src/Components/LanguageSelector.jsx @@ -16,7 +16,7 @@ const langMap = { const LanguageSelector = () => { const { i18n } = useTranslation(); const theme = useTheme(); - const { language } = useSelector((state) => state.ui); + const { language = "en" } = useSelector((state) => state.ui); const dispatch = useDispatch(); const handleChange = (event) => { const newLang = event.target.value; diff --git a/client/src/Components/v1/I18nLoader/index.jsx b/client/src/Components/v1/I18nLoader/index.jsx index 2a6a4966d..676e63d7b 100644 --- a/client/src/Components/v1/I18nLoader/index.jsx +++ b/client/src/Components/v1/I18nLoader/index.jsx @@ -2,7 +2,7 @@ import i18n from "../../../Utils/i18n.js"; import { useSelector } from "react-redux"; import { useEffect } from "react"; const I18nLoader = () => { - const language = useSelector((state) => state.ui.language); + const language = useSelector((state) => state.ui.language ?? "en"); useEffect(() => { if (language && i18n.language !== language) { diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx index eb38b9129..80022c7e3 100644 --- a/client/src/Components/v1/Sidebar/index.jsx +++ b/client/src/Components/v1/Sidebar/index.jsx @@ -84,7 +84,7 @@ const Sidebar = () => { const { t } = useTranslation(); const navigate = useNavigate(); // Redux state - const collapsed = useSelector((state) => state.ui.sidebar.collapsed); + const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); const menu = getMenu(t); const otherMenuItems = getOtherMenuItems(t); diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js index 63f7ae90e..af9731e9b 100644 --- a/client/src/Features/UI/uiSlice.js +++ b/client/src/Features/UI/uiSlice.js @@ -58,6 +58,9 @@ const uiSlice = createSlice({ state.showURL = action.payload; }, setGreeting(state, action) { + if (!state.greeting) { + state.greeting = { index: 0, lastUpdate: null }; + } state.greeting.index = action.payload.index; state.greeting.lastUpdate = action.payload.lastUpdate; }, diff --git a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx index 8708502cd..9df5f7577 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx @@ -1,8 +1,9 @@ -import { Grid, Grid2 } from "@mui/material"; +import { Grid } from "@mui/material"; import Card from "../Card/index.jsx"; -const MonitorGrid = ({ shouldRender, monitors }) => { - console.log(monitors); +const MonitorGrid = ({ monitors, shouldRender = true }) => { + if (!shouldRender) return null; + return ( { {monitors?.map((monitor) => ( ))} diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 27cc6bb21..3e753ab60 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -33,7 +33,7 @@ const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; const Settings = () => { // Redux state - const { mode, language, timezone, showURL } = useSelector((state) => state.ui); + const { mode, language = "en", timezone, showURL } = useSelector((state) => state.ui); // Local state const [settingsData, setSettingsData] = useState({}); diff --git a/client/src/Utils/Theme/constants.js b/client/src/Utils/Theme/constants.js index 45217fdeb..2664c6126 100644 --- a/client/src/Utils/Theme/constants.js +++ b/client/src/Utils/Theme/constants.js @@ -1,4 +1,4 @@ -import { lighten, darken } from "@mui/material/styles"; // CAIO_REVIEW +import { lighten } from "@mui/material/styles"; // CAIO_REVIEW const typographyBase = 13; @@ -133,12 +133,6 @@ const newSemanticColors = { light: lighten(newColors.blueBlueWave, 0.2), //CAIO_REVIEW dark: lighten(newColors.blueBlueWave, 0.2), //CAIO_REVIEW }, - darker: { - // CAIO_REVIEW - light: darken(newColors.blueBlueWave, 0.2), // CAIO_REVIEW - dark: darken(newColors.blueBlueWave, 0.2), // CAIO_REVIEW - }, - contrastText: { light: newColors.offWhite, dark: newColors.offWhite, @@ -165,14 +159,6 @@ const newSemanticColors = { light: newColors.blueGray500, dark: newColors.gray500, }, - contrastBorder: { - light: newColors.gray500, - dark: newColors.blueGray600, - }, - contrastBorderDisabled: { - light: newColors.gray100, - dark: newColors.blueGray800, - }, lowContrast: { light: newColors.gray250, dark: newColors.blueGray600, @@ -207,18 +193,6 @@ const newSemanticColors = { light: newColors.blueGray800, dark: newColors.gray100, }, - background: { - light: newColors.gray50, - dark: newColors.offBlack, - }, - border: { - light: newColors.gray500, - dark: newColors.blueGray600, - }, - cardShadow: { - light: "0 0 0 1px rgba(0, 0, 0, 0.04), 0 12px 24px rgba(0, 0, 0, 0.08)", - dark: "0 2px 10px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.04)", - }, }, success: { main: { @@ -234,20 +208,6 @@ const newSemanticColors = { dark: newColors.green200, }, }, - successSecondary: { - main: { - light: undefined, - dark: "#073C2D", - }, - contrastText: { - light: undefined, - dark: "#34D399", - }, - lowContrast: { - light: undefined, - dark: "#094434", - }, - }, warning: { main: { light: newColors.orange700, @@ -316,30 +276,6 @@ const newSemanticColors = { }, }, - map: { - main: { - light: newColors.offWhite, - dark: undefined, - }, - lowContrast: { - light: newColors.gray200, - dark: undefined, - }, - highContrast: { - light: newColors.gray500, - dark: undefined, - }, - }, - chatbot: { - background: { - light: newColors.gray200, - dark: "#112B2B", - }, - textAccent: { - light: "#C084FC", - dark: "#C084FC", - }, - }, alert: { main: { light: newColors.gray200, diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index e65baf4c0..58e1a9b63 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -64,7 +64,7 @@ const baseTheme = (palette) => ({ letterSpacing: "0.5px", textShadow: "0 0 1px rgba(0, 0, 0, 0.15)", "&:hover": { - backgroundColor: darken(theme.palette.accent.darker, 0.05), + backgroundColor: darken(theme.palette.accent.main, 0.25), boxShadow: `0 2px 6px rgba(0, 0, 0, 0.1)`, transition: "all 0.2s ease-in-out", }, @@ -74,7 +74,7 @@ const baseTheme = (palette) => ({ props: (props) => props.color === "accent", style: { "&:hover": { - backgroundColor: theme.palette.accent.darker, + backgroundColor: darken(theme.palette.accent.main, 0.2), }, }, }, diff --git a/client/src/Utils/greeting.jsx b/client/src/Utils/greeting.jsx index d2b7fa033..c4afbf88c 100644 --- a/client/src/Utils/greeting.jsx +++ b/client/src/Utils/greeting.jsx @@ -136,8 +136,8 @@ const Greeting = ({ type = "" }) => { const dispatch = useDispatch(); const { t } = useTranslation(); const { firstName } = useSelector((state) => state.auth.user); - const index = useSelector((state) => state.ui.greeting.index); - const lastUpdate = useSelector((state) => state.ui.greeting.lastUpdate); + const index = useSelector((state) => state.ui.greeting?.index ?? 0); + const lastUpdate = useSelector((state) => state.ui.greeting?.lastUpdate ?? null); const now = new Date(); const hour = now.getHours(); From 0f764ca0841ab2910851d31549b49a30d17d0b62 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 23:45:40 +0000 Subject: [PATCH 035/103] typing --- server/src/config/services.ts | 2 +- server/src/db/models/Check.ts | 7 +- .../repositories/checks/IChecksRepository.ts | 3 +- .../checks/MongoChecksRepistory.ts | 111 ++++++++- server/src/service/index.ts | 1 + .../SuperSimpleQueueHelper.ts | 15 +- .../service/infrastructure/networkService.ts | 220 +++++++++++++----- .../service/infrastructure/statusService.ts | 37 +-- server/src/types/check.ts | 27 +-- server/src/types/index.ts | 1 + server/src/types/network.ts | 112 +++++++++ server/test/superSimpleQueueHelper.test.ts | 11 +- 12 files changed, 431 insertions(+), 116 deletions(-) create mode 100644 server/src/types/network.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 90d6bb518..57a223ebf 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -155,7 +155,7 @@ export const initializeServices = async ({ // Repositories const monitorsRepository = new MongoMonitorsRepository(); - const checksRepository = new MongoChecksRepository(); + const checksRepository = new MongoChecksRepository(logger); const monitorStatsRepository = new MongoMonitorStatsRepository(); const statusPagesRepository = new MongoStatusPagesRepository(); diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index c176aa3bb..94dfa873c 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -11,8 +11,7 @@ import type { CheckMemoryInfo, CheckMetadata, CheckNetworkInterfaceInfo, - CheckTimings, - CheckTimingPhases, + GotTimings, ILighthouseAudit, } from "@/types/check.js"; @@ -35,7 +34,7 @@ interface CheckDocument extends CheckDocumentBase { _id: Types.ObjectId; } -const timingPhasesSchema = new Schema( +const timingPhasesSchema = new Schema( { wait: { type: Number, default: 0 }, dns: { type: Number, default: 0 }, @@ -49,7 +48,7 @@ const timingPhasesSchema = new Schema( { _id: false } ); -const timingsSchema = new Schema( +const timingsSchema = new Schema( { start: { type: Number, default: 0 }, socket: { type: Number, default: 0 }, diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 19faa0159..5737daff6 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -1,4 +1,4 @@ -import type { Check, CheckAudits, MonitorType } from "@/types/index.js"; +import type { Check, CheckAudits, MonitorStatusResponse, MonitorType } from "@/types/index.js"; import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; export interface PageSpeedChecksResult { @@ -55,6 +55,7 @@ export interface UptimeChecksResult { export interface IChecksRepository { // create + buildCheck(status: MonitorStatusResponse, type: MonitorType): Check; // single fetch // collection fetch findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 3816d8cc5..225aec193 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -10,18 +10,29 @@ import type { CheckMemoryInfo, CheckMetadata, CheckNetworkInterfaceInfo, - CheckTimings, + GotTimings, + MonitorStatusResponse, MonitorType, } from "@/types/index.js"; +import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; +const SERVICE_NAME = "StatusService"; + export type LatestChecksMap = Record; type DateRange = { start: Date; end: Date }; type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number }; type HardwareUpChecks = { totalChecks: number }; class MongoChecksRepository implements IChecksRepository { + static SERVICE_NAME = SERVICE_NAME; + + private logger: any; + constructor(logger: any) { + this.logger = logger; + } + private toEntity = (doc: CheckDocument): Check => { const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => { if (!value) { @@ -44,7 +55,7 @@ class MongoChecksRepository implements IChecksRepository { return toDateString(value); }; - const mapTimings = (timings?: CheckTimings): CheckTimings => { + const mapTimings = (timings?: GotTimings): GotTimings => { const phases = timings?.phases ?? { wait: 0, dns: 0, @@ -176,6 +187,102 @@ class MongoChecksRepository implements IChecksRepository { }; }; + buildCheck = ( + statusResponse: MonitorStatusResponse + ) => { + const { + monitorId, + teamId, + type, + status, + responseTime, + code, + message, + payload, + first_byte_took, + body_read_took, + dns_took, + conn_took, + connect_took, + tls_took, + timings, + } = statusResponse; + + const check: any = { + metadata: { + monitorId, + teamId, + type, + }, + status, + statusCode: code, + responseTime, + timings: timings || {}, + message, + first_byte_took, + body_read_took, + dns_took, + conn_took, + connect_took, + tls_took, + }; + + if (type === "pagespeed") { + const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; + if (!pageSpeedPayload) { + this.logger.warn({ + message: "Failed to build check", + service: SERVICE_NAME, + method: "buildCheck", + details: "empty payload", + }); + return undefined; + } + const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; + const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; + const mapAudit = (audit: any) => { + if (!audit || typeof audit !== "object") { + return undefined; + } + return { + id: audit.id, + title: audit.title, + score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), + displayValue: audit.displayValue, + numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, + numericUnit: audit.numericUnit, + }; + }; + check.accessibility = (categories?.accessibility?.score || 0) * 100; + check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100; + check.seo = (categories?.seo?.score || 0) * 100; + check.performance = (categories?.performance?.score || 0) * 100; + check.audits = { + cls: mapAudit(audits?.["cumulative-layout-shift"]), + si: mapAudit(audits?.["speed-index"]), + fcp: mapAudit(audits?.["first-contentful-paint"]), + lcp: mapAudit(audits?.["largest-contentful-paint"]), + tbt: mapAudit(audits?.["total-blocking-time"]), + }; + } + + if (type === "hardware") { + const hardwarePayload = payload as HardwareStatusPayload | undefined; + const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; + const errorsSource = Array.isArray(hardwarePayload?.errors) + ? hardwarePayload?.errors + : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; + check.cpu = cpu ?? {}; + check.memory = memory ?? {}; + check.disk = disk ?? {}; + check.host = host ?? {}; + check.errors = errorsSource ?? []; + check.capture = hardwarePayload?.capture ?? {}; + check.net = net ?? {}; + } + return check; + }; + findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => { if (monitorIds.length === 0) { return {}; diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 4793f055b..a094f8b01 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -1 +1,2 @@ export * from "@/service/business/monitorService.js"; +export * from "@/service/infrastructure/networkService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index b025a65c6..aa4c4ab10 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,13 +1,13 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; - +import { INetworkService } from "@/service/index.js"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; private db: any; private logger: any; - private networkService: any; + private networkService: INetworkService; private statusService: any; private notificationService: any; @@ -20,7 +20,7 @@ class SuperSimpleQueueHelper { }: { db: any; logger: any; - networkService: any; + networkService: INetworkService; statusService: any; notificationService: any; }) { @@ -61,14 +61,13 @@ class SuperSimpleQueueHelper { throw new Error("No network response"); } - const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(status); - + const statusChangeResult = await this.statusService.updateMonitorStatus(status); this.notificationService .handleNotifications({ ...status, - monitor: updatedMonitor, - prevStatus, - statusChanged, + monitor: statusChangeResult.monitor, + prevStatus: statusChangeResult.prevStatus, + statusChanged: statusChangeResult.statusChanged, }) .catch((error: any) => { this.logger.error({ diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index b9f02e7c9..e4e5972af 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,10 +1,38 @@ -import { Got } from "got"; -import type { Monitor } from "@/types/index.js"; +import { HTTPError, RequestError } from "got"; +import type { Got, Response } from "got"; +import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; import CacheableLookup from "cacheable-lookup"; const SERVICE_NAME = "NetworkService"; -class NetworkService { +type MonitorStatusResponseOverrides = Partial, "monitorId" | "teamId" | "type">>; + +interface BuildStatusResponseArgs { + monitor: Monitor; + response?: Response | null; + error?: Error | RequestError | HTTPError | null; + payload?: T | null; + jsonPath?: string; + matchMethod?: MonitorStatusResponse["matchMethod"]; + expectedValue?: string; + extracted?: unknown; + overrides?: MonitorStatusResponseOverrides; +} + +export interface INetworkService { + readonly serviceName: string; + requestStatus(monitor: Monitor): Promise; + requestWebhook(type: string, url: string, body: any): Promise<{ type: string; status: boolean; code: number; message: string; payload?: unknown }>; + requestPagerDuty(args: { message: string; routingKey: string; monitorUrl: string }): Promise; + requestMatrix(args: { homeserverUrl: string; accessToken: string; roomId: string; message: string }): Promise<{ + status: boolean; + code: number; + message: string; + payload?: unknown; + }>; +} + +class NetworkService implements INetworkService { static SERVICE_NAME = SERVICE_NAME; private TYPE_PING: string; @@ -31,6 +59,60 @@ class NetworkService { private stringService: any; private settingsService: any; + private buildStatusResponse = ({ + monitor, + response, + error, + payload, + jsonPath, + matchMethod, + expectedValue, + extracted, + overrides, + }: BuildStatusResponseArgs): MonitorStatusResponse => { + if (error) { + const statusResponse: MonitorStatusResponse = { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: this.NETWORK_ERROR, + message: error.message ?? "Network error", + responseTime: 0, + timings: undefined, + jsonPath, + matchMethod, + expectedValue, + extracted, + payload, + }; + if (error instanceof HTTPError || error instanceof RequestError) { + statusResponse.code = error?.response?.statusCode ?? this.NETWORK_ERROR; + statusResponse.message = error.message; + statusResponse.responseTime = error.timings?.phases?.total ?? 0; + statusResponse.timings = error.timings; + } + return { ...statusResponse, ...(overrides ?? {}) }; + } + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: response?.ok ?? false, + code: response?.statusCode ?? this.NETWORK_ERROR, + message: response?.statusMessage ?? "", + responseTime: response?.timings?.phases?.total ?? 0, + timings: response?.timings, + payload: payload ?? response?.body, + jsonPath, + matchMethod, + expectedValue, + extracted, + ...(overrides ?? {}), + }; + }; + constructor({ axios, got, @@ -91,13 +173,17 @@ class NetworkService { }); } + get serviceName(): string { + return NetworkService.SERVICE_NAME; + } + // Helper functions - async timeRequest(operation: any) { + private async timeRequest(operation: () => Promise): Promise<{ response: T | null; responseTime: number; error: unknown }> { const start = process.hrtime.bigint(); try { const response = await operation(); const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); - return { response, responseTime: elapsedMs }; + return { response, responseTime: elapsedMs, error: null }; } catch (error) { const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); return { response: null, responseTime: elapsedMs, error }; @@ -105,7 +191,7 @@ class NetworkService { } // Main entry point - async requestStatus(monitor: Monitor) { + async requestStatus(monitor: Monitor): Promise { const type = monitor?.type || "unknown"; switch (type) { case this.TYPE_PING: @@ -123,11 +209,11 @@ class NetworkService { case this.TYPE_GAME: return await this.requestGame(monitor); default: - return await this.handleUnsupportedType(type); + return this.handleUnsupportedType(type); } } - async requestPing(monitor: Monitor) { + private async requestPing(monitor: Monitor): Promise { try { if (!monitor?.url) { throw new Error("Monitor URL is required"); @@ -147,19 +233,21 @@ class NetworkService { throw new Error("Ping failed - no result returned"); } - const pingResponse = { - monitorId: monitor.id, - type: "ping", - status: response.alive, - code: 200, - responseTime: response.time, - message: "Success", + const pingResponse = this.buildStatusResponse({ + monitor, payload: response, - }; + overrides: { + status: (response as { alive?: boolean })?.alive ?? false, + code: 200, + message: "Success", + responseTime: (response as { time?: number })?.time ?? 0, + payload: response, + }, + }); if (error) { pingResponse.status = false; - pingResponse.code = 200; + pingResponse.code = this.PING_ERROR; pingResponse.message = "Ping failed"; return pingResponse; } @@ -172,13 +260,16 @@ class NetworkService { } } - async requestHttp(monitor: Monitor) { + private async requestHttp(monitor: Monitor): Promise { const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; - const httpResponse: Record = { - monitorId: id, - teamId: teamId, - type, - }; + const httpResponse = this.buildStatusResponse({ + monitor, + overrides: { + status: false, + code: this.NETWORK_ERROR, + message: "Request not executed", + }, + }); try { if (!url) { @@ -211,12 +302,14 @@ class NetworkService { payload = response.body; } - httpResponse.code = response.statusCode; - httpResponse.status = response.ok; - httpResponse.message = response.statusMessage; - httpResponse.responseTime = response.timings.phases.total || 0; - httpResponse.payload = payload; - httpResponse.timings = response.timings || {}; + Object.assign(httpResponse, { + code: response.statusCode, + status: response.ok, + message: response.statusMessage ?? "", + responseTime: response.timings.phases.total || 0, + payload, + timings: response.timings, + }); if (!expectedValue && !jsonPath) { return httpResponse; @@ -300,7 +393,7 @@ class NetworkService { } } - async requestPageSpeed(monitor: Monitor) { + private async requestPageSpeed(monitor: Monitor): Promise { try { const url = monitor.url; if (!url) { @@ -329,7 +422,7 @@ class NetworkService { } } - async requestHardware(monitor: Monitor) { + private async requestHardware(monitor: Monitor): Promise { try { return await this.requestHttp(monitor); } catch (err: any) { @@ -339,7 +432,7 @@ class NetworkService { } } - async requestDocker(monitor: Monitor) { + private async requestDocker(monitor: Monitor): Promise { try { if (!monitor.url) { throw new Error("Monitor URL is required"); @@ -350,10 +443,14 @@ class NetworkService { handleError: true, // Enable error handling }); - const dockerResponse: Record = { - monitorId: monitor.id, - type: monitor.type, - }; + const dockerResponse = this.buildStatusResponse({ + monitor, + overrides: { + status: false, + code: this.NETWORK_ERROR, + message: "No response", + }, + }); const containers = await docker.listContainers({ all: true }); @@ -406,7 +503,6 @@ class NetworkService { method: "requestDocker", details: { url: monitor.url }, }); - dockerResponse.status = 404; dockerResponse.status = false; dockerResponse.message = `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`; return dockerResponse; @@ -437,7 +533,7 @@ class NetworkService { } } - async requestPort(monitor: Monitor) { + private async requestPort(monitor: Monitor): Promise { try { const { url, port } = monitor; const { response, responseTime, error } = await this.timeRequest(async () => { @@ -467,14 +563,15 @@ class NetworkService { }); }); - const portResponse = { - code: 200, - status: response.success, - message: this.stringService.portSuccess, - monitorId: monitor.id, - type: monitor.type, - responseTime: responseTime, - }; + const portResponse = this.buildStatusResponse({ + monitor, + overrides: { + code: 200, + status: (response as { success?: boolean })?.success ?? false, + message: this.stringService.portSuccess, + responseTime, + }, + }); if (error) { portResponse.code = this.NETWORK_ERROR; @@ -491,17 +588,18 @@ class NetworkService { } } - async requestGame(monitor: Monitor) { + private async requestGame(monitor: Monitor): Promise { try { const { url, port, gameId } = monitor; - const gameResponse: Record = { - code: 200, - status: true, - message: "Success", - monitorId: monitor.id, - type: "game", - }; + const gameResponse = this.buildStatusResponse({ + monitor, + overrides: { + code: 200, + status: true, + message: "Success", + }, + }); const state = await this.GameDig.query({ type: gameId, @@ -532,11 +630,15 @@ class NetworkService { throw error; } } - async handleUnsupportedType(type: string) { - const err: any = new Error(`Unsupported type: ${type}`); - err.service = this.SERVICE_NAME; - err.method = "getStatus"; - throw err; + private async handleUnsupportedType(type: string): Promise { + return { + monitorId: "unknown", + teamId: "unknown", + type: "unknown", + status: false, + code: this.NETWORK_ERROR, + message: `Unsupported type: ${type}`, + }; } // Other network requests unrelated to monitoring: diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index d68ec345a..23b691e0e 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -1,7 +1,8 @@ import { IMonitorsRepository } from "@/repositories/index.js"; import MonitorStats from "../../db/models/MonitorStats.js"; import { CheckModel } from "@/db/models/index.js"; -import type { Monitor } from "@/types/index.js"; +import type { CheckErrorInfo, Monitor, MonitorStatusResponse, StatusChangeResult } from "@/types/index.js"; +import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; const SERVICE_NAME = "StatusService"; class StatusService { @@ -161,15 +162,17 @@ class StatusService { } }; - updateStatus = async (networkResponse: any) => { - const check = this.buildCheck(networkResponse); + updateMonitorStatus = async ( + statusResponse: MonitorStatusResponse + ): Promise => { + const check = this.buildCheck(statusResponse); await this.insertCheck(check); try { - const { monitorId, teamId, status, code } = networkResponse; + const { monitorId, teamId, status, code } = statusResponse; const monitor = await this.monitorsRepository.findById(monitorId, teamId); // Update running stats - this.updateRunningStats({ monitor, networkResponse }); + this.updateRunningStats({ monitor, networkResponse: statusResponse }); // If the status window size has changed, empty while (monitor.statusWindow.length > monitor.statusWindowSize) { @@ -298,7 +301,9 @@ class StatusService { } }; - buildCheck = (networkResponse: any) => { + buildCheck = ( + networkResponse: MonitorStatusResponse + ) => { const { monitorId, teamId, @@ -317,7 +322,7 @@ class StatusService { timings, } = networkResponse; - const check = { + const check: any = { metadata: { monitorId, teamId, @@ -337,7 +342,8 @@ class StatusService { }; if (type === "pagespeed") { - if (typeof payload === "undefined") { + const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; + if (!pageSpeedPayload) { this.logger.warn({ message: "Failed to build check", service: SERVICE_NAME, @@ -346,8 +352,8 @@ class StatusService { }); return undefined; } - const categories = payload?.lighthouseResult?.categories ?? {}; - const audits = payload?.lighthouseResult?.audits ?? {}; + const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; + const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; const mapAudit = (audit: any) => { if (!audit || typeof audit !== "object") { return undefined; @@ -375,14 +381,17 @@ class StatusService { } if (type === "hardware") { - const { cpu, memory, disk, host, net } = payload?.data ?? {}; - const { errors } = payload?.errors ?? []; + const hardwarePayload = payload as HardwareStatusPayload | undefined; + const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; + const errorsSource = Array.isArray(hardwarePayload?.errors) + ? hardwarePayload?.errors + : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; check.cpu = cpu ?? {}; check.memory = memory ?? {}; check.disk = disk ?? {}; check.host = host ?? {}; - check.errors = errors ?? []; - check.capture = payload?.capture ?? {}; + check.errors = errorsSource ?? []; + check.capture = hardwarePayload?.capture ?? {}; check.net = net ?? {}; } return check; diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 28353497e..e3f32280f 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -1,4 +1,6 @@ import type { MonitorType } from "@/types/index.js"; +import type { Response } from "got"; +export type GotTimings = Response["timings"]; export interface CheckMetadata { monitorId: string; @@ -6,29 +8,6 @@ export interface CheckMetadata { type: MonitorType; } -export interface CheckTimingPhases { - wait: number; - dns: number; - tcp: number; - tls: number; - request: number; - firstByte: number; - download: number; - total: number; -} - -export interface CheckTimings { - start: number; - socket: number; - lookup: number; - connect: number; - secureConnect: number; - upload: number; - response: number; - end: number; - phases: CheckTimingPhases; -} - export interface CheckCpuInfo { physical_core: number; logical_core: number; @@ -107,7 +86,7 @@ export interface Check { metadata: CheckMetadata; status: boolean; responseTime: number; - timings: CheckTimings; + timings: GotTimings; statusCode: number; message: string; ack: boolean; diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 81620da03..4f71788f2 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -2,3 +2,4 @@ export * from "@/types/check.js"; export * from "@/types/monitor.js"; export * from "@/types/monitorStats.js"; export * from "@/types/statusPage.js"; +export * from "@/types/network.js"; diff --git a/server/src/types/network.ts b/server/src/types/network.ts new file mode 100644 index 000000000..097cb928d --- /dev/null +++ b/server/src/types/network.ts @@ -0,0 +1,112 @@ +import type { + CheckCaptureInfo, + CheckCpuInfo, + CheckDiskInfo, + CheckErrorInfo, + CheckHostInfo, + CheckMemoryInfo, + CheckNetworkInterfaceInfo, + GotTimings, + ILighthouseAudit, + Monitor, + MonitorMatchMethod, + MonitorType, +} from "@/types/index.js"; + +export interface MonitorStatusResponse { + monitorId: string; + teamId: string; + type: MonitorType | "unknown"; + status: boolean; + code: number; + message: string; + responseTime?: number; + payload?: T | null; + timings?: GotTimings; + first_byte_took?: number; + body_read_took?: number; + dns_took?: number; + conn_took?: number; + connect_took?: number; + tls_took?: number; + jsonPath?: string; + matchMethod?: MonitorMatchMethod; + expectedValue?: string; + extracted?: unknown; +} + +export interface PingStatusPayload { + host: string; + numeric_host?: string; + alive: boolean; + time: number; + times?: number[]; + output?: string; + min?: string; + max?: string; + avg?: string; + stddev?: string; + packetLoss?: string; +} + +export type HttpStatusPayload = unknown; + +export interface PageSpeedCategoryScore { + score?: number | null; +} + +export interface PageSpeedStatusPayload { + lighthouseResult?: { + categories?: { + accessibility?: PageSpeedCategoryScore; + "best-practices"?: PageSpeedCategoryScore; + performance?: PageSpeedCategoryScore; + seo?: PageSpeedCategoryScore; + [key: string]: PageSpeedCategoryScore | undefined; + }; + audits?: Record; + }; + [key: string]: unknown; +} + +export interface HardwareStatusMetrics { + cpu?: CheckCpuInfo; + memory?: CheckMemoryInfo; + disk?: CheckDiskInfo[]; + host?: CheckHostInfo; + net?: CheckNetworkInterfaceInfo[]; +} + +export interface HardwareStatusPayload { + data?: HardwareStatusMetrics; + errors?: CheckErrorInfo[] | { errors?: CheckErrorInfo[] }; + capture?: CheckCaptureInfo; + [key: string]: unknown; +} + +export type DockerStatusPayload = Record; + +export interface PortStatusPayload { + success: boolean; +} + +export type GameStatusPayload = Record; + +export interface MonitorPayloadMap { + ping: PingStatusPayload; + http: HttpStatusPayload; + pagespeed: PageSpeedStatusPayload; + hardware: HardwareStatusPayload; + docker: DockerStatusPayload; + port: PortStatusPayload; + game: GameStatusPayload; + default: unknown; +} + +export type StatusChangeResult = { + monitor: Monitor; + statusChanged: boolean; + prevStatus: boolean | undefined; + code: number; + timestamp: number; +}; diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts index 997eb9e83..6fba58eda 100644 --- a/server/test/superSimpleQueueHelper.test.ts +++ b/server/test/superSimpleQueueHelper.test.ts @@ -8,11 +8,14 @@ const createHelper = (overrides?: Partial { const { helper } = createHelper({ networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) }, statusService: { - updateStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), + updateMonitorStatus: jest + .fn() + .mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), }, notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, }); @@ -48,7 +53,7 @@ describe("SuperSimpleQueueHelper", () => { const monitor = { id: "m1", teamId: "team" } as Monitor; await job(monitor); expect(helper["networkService"].requestStatus).toHaveBeenCalledWith(monitor); - expect(helper["statusService"].updateStatus).toHaveBeenCalledWith(networkResponse); + expect(helper["statusService"].updateMonitorStatus).toHaveBeenCalledWith(networkResponse); expect(helper["notificationService"].handleNotifications).toHaveBeenCalledWith( expect.objectContaining({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }) ); From 1ab31d19f3f483bbfc245dd148735f244d15abd6 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 15 Jan 2026 20:44:51 -0500 Subject: [PATCH 036/103] Improve UI consistency for Logs and Incidents pages ## Changes ### Logs page - Convert server logs from monospace text to DataTable with pagination - Add table columns: Timestamp, Level, Service, Method, Message - Add colored level badges (info=green, warn=yellow, error=red, debug=blue) - Add pagination with configurable rows per page - Align diagnostics gauge cards with 16px gap and responsive widths ### Incidents page - Redesign summary cards with consistent styling - Remove shadows and use 1px border matching other sections - Reduce icon sizes to 18-24px (sidebar-style) - Reduce font sizes (13px titles, 18-32px values) - Add titles to all three summary cards - Fix "Most Affected Monitor" showing "Unknown Monitor" when no incidents - Show "N/A" for avg resolution time when no incidents - Remove cluttered "Resolutions" progress bar section - Remove horizontal dividers between statistics rows - Standardize card padding and gap to 16px ### Uptime page - Fix oversized monitor names in table (removed h6 variant) --- client/src/Components/v1/Host/index.jsx | 10 +- .../Components/ActiveIncidentsPanel/index.jsx | 46 ++--- .../IncidentsSummaryPanel/index.jsx | 2 +- .../Components/LatestIncidentsPanel/index.jsx | 40 +++- .../Components/StatisticsPanel/index.jsx | 103 ++--------- .../Components/SummaryCard/index.jsx | 24 +-- .../Diagnostics/components/gauges/index.jsx | 15 +- client/src/Pages/Logs/Logs/index.jsx | 172 +++++++++++++----- client/src/locales/en.json | 14 +- 9 files changed, 223 insertions(+), 203 deletions(-) diff --git a/client/src/Components/v1/Host/index.jsx b/client/src/Components/v1/Host/index.jsx index 935224994..6670722c1 100644 --- a/client/src/Components/v1/Host/index.jsx +++ b/client/src/Components/v1/Host/index.jsx @@ -23,12 +23,12 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => { direction="row" position="relative" alignItems="center" - gap={theme.spacing(5)} + gap={theme.spacing(4)} > {title} @@ -38,9 +38,9 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => { <> diff --git a/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx b/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx index b6024ce53..8ed80d037 100644 --- a/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx +++ b/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx @@ -43,39 +43,36 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null if (!totalCount || totalCount === 0) { return ( - + {t("incidentsPage.allSystemsAreOperational")} @@ -86,21 +83,20 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null } return ( - + {totalCount} - - {t("incidentsPage.incidentsActivePanelTitle")} - ); diff --git a/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx b/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx index 43071fa03..33ba21cd1 100644 --- a/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx +++ b/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx @@ -53,7 +53,7 @@ const IncidentsSummaryPanel = ({ updateTrigger }) => { <> {!incidents || incidents.length === 0 ? ( - + + + + {t("incidentsPage.incidentsLatestPanelEmpty")} - + ) : ( {incidents.map((incident, index) => ( diff --git a/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx b/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx index ede9c619a..c8559d7a2 100644 --- a/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx +++ b/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx @@ -3,10 +3,8 @@ import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx"; import { useTranslation } from "react-i18next"; -import { Divider } from "@mui/material"; import Clock from "@/assets/icons/maintenance.svg?react"; import Incidents from "@/assets/icons/incidents.svg?react"; -import ResolutionItem from "@/assets/icons/interval-check.svg?react"; import NotificationIcon from "@/assets/icons/notifications.svg?react"; import SummaryCard from "../SummaryCard/index.jsx"; @@ -43,10 +41,15 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => { const iconWrapperStyle = { display: "flex", justifyContent: "center", - mx: theme.spacing(3), + mx: theme.spacing(2), color: theme.palette.primary.contrastTextTertiary, + "& svg": { + width: 18, + height: 18, + }, "& svg path": { stroke: "currentColor", + strokeWidth: 1.5, }, }; if (isLoading) { @@ -76,6 +79,13 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => { ); } + const getMostAffectedMonitor = () => { + if (!summary.total || summary.total === 0) { + return t("incidentsPage.none"); + } + return summary.topMonitor?.monitorName || t("incidentsPage.none"); + }; + return ( @@ -99,11 +109,10 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => { lineHeight: 1.2, }} > - {t("incidentsPage.totalIncidents")} : {summary.total || 0} + {t("incidentsPage.totalIncidents")}: {summary.total || 0} - { lineHeight: 1.2, }} > - {t("incidentsPage.mostAffectedMonitor")} :{" "} - {summary.topMonitor?.monitorName || t("incidentsPage.unknownMonitor")} + {t("incidentsPage.mostAffectedMonitor")}: {getMostAffectedMonitor()} - { - {t("incidentsPage.avgResolutionTime")} :{" "} - {summary.avgResolutionTimeHours || 0} {t("incidentsPage.hours")} + {t("incidentsPage.avgResolutionTime")}:{" "} + {summary.total > 0 ? `${summary.avgResolutionTimeHours || 0} ${t("incidentsPage.hours")}` : "N/A"} - - - - - - - - - - - {t("incidentsPage.resolutions")}: {totalResolutions} - - - - - - - - - {summary.totalAutomaticResolutions > 0 && ( - - {t("incidentsPage.automatic")} ({summary.totalAutomaticResolutions}) - - )} - {summary.totalManualResolutions > 0 && ( - - {t("incidentsPage.manual")} ({summary.totalManualResolutions}) - - )} - - ); diff --git a/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx b/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx index cb895dc50..3a8589dc1 100644 --- a/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx +++ b/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx @@ -8,17 +8,14 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null } {title} @@ -49,9 +45,9 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null } )} {children} diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx index b38837cf2..be6dc99aa 100644 --- a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -12,13 +12,16 @@ import { Box } from "@mui/material"; const BaseContainer = ({children}) => { const theme = useTheme() return( - {children} @@ -99,7 +102,7 @@ const Gauges = ({ diagnostics, isLoading }) => { return ( { +const LevelBadge = ({ level }) => { + const theme = useTheme(); + const levelColors = { info: theme.palette.success.main, warn: theme.palette.warning.main, @@ -17,31 +20,38 @@ const formatLog = (theme, log, idx) => { debug: theme.palette.accent.main, }; - const color = levelColors[log.level] || theme.palette.primary.contrastText; + const color = levelColors[level] || theme.palette.primary.contrastText; return ( - - [{log.timestamp}]{" "} - {log.level.toUpperCase()} - {": "} - {`(${log.service})`} - {`(${log.method})`} - {": "} - {log.message} -
-
+ + {level} + ); }; -const Logs = () => { - // Local state - const [logLevel, setLogLevel] = useState("all"); +const formatTimestamp = (timestamp) => { + if (!timestamp) return "-"; + const date = new Date(timestamp); + return date.toLocaleString(); +}; + +const Logs = () => { + const [logLevel, setLogLevel] = useState("all"); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(15); - // Hooks const theme = useTheme(); const { t } = useTranslation(); const [logs, isLoading, error] = useFetchLogs(); - // Setup + const LOG_LEVELS = [ { _id: "all", name: t("logsPage.logLevelSelect.values.all") }, { _id: "info", name: t("logsPage.logLevelSelect.values.info") }, @@ -49,52 +59,116 @@ const Logs = () => { { _id: "error", name: t("logsPage.logLevelSelect.values.error") }, { _id: "debug", name: t("logsPage.logLevelSelect.values.debug") }, ]; + + const headers = [ + { + id: "timestamp", + content: t("logsPage.table.timestamp"), + render: (row) => ( + + {formatTimestamp(row.timestamp)} + + ), + }, + { + id: "level", + content: t("logsPage.table.level"), + render: (row) => , + }, + { + id: "service", + content: t("logsPage.table.service"), + render: (row) => ( + {row.service || "-"} + ), + }, + { + id: "method", + content: t("logsPage.table.method"), + render: (row) => ( + {row.method || "-"} + ), + }, + { + id: "message", + content: t("logsPage.table.message"), + render: (row) => ( + + {row.message || "-"} + + ), + }, + ]; + + const filteredLogs = logs + ?.filter((log) => { + if (logLevel === "all") return true; + return log.level === logLevel; + }) + .reverse() + .map((log, idx) => ({ ...log, id: idx })); + + const paginatedLogs = filteredLogs?.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const handleLogLevelChange = (e) => { + setLogLevel(e.target.value); + setPage(0); + }; + return ( {t("logsPage.logLevelSelect.title")} ( - )} displayEmpty diff --git a/client/src/Components/v1/Icon/iconMap.js b/client/src/Components/v1/Icon/iconMap.js new file mode 100644 index 000000000..58da7304b --- /dev/null +++ b/client/src/Components/v1/Icon/iconMap.js @@ -0,0 +1,120 @@ +/** + * Maps legacy icon names to Lucide icon names + * Use: import { iconMap } from './iconMap'; LucideIcons[iconMap['monitors']] + */ +export const iconMap = { + // Navigation/Sidebar icons + monitors: "Globe", + incidents: "AlertTriangle", + integrations: "Link", + "page-speed": "Gauge", + settings: "Settings", + notifications: "Bell", + maintenance: "Wrench", + "status-pages": "Wifi", + docs: "FileText", + discussions: "MessageCircle", + support: "HelpCircle", + changeLog: "Code", + logs: "Database", + + // User icons + user: "User", + "user-two": "Users", + "user-edit": "UserPen", + groups: "Users", + lock: "Lock", + key: "Key", + logout: "LogOut", + + // Action icons + edit: "Pencil", + "trash-bin": "Trash2", + search: "Search", + "close-icon": "X", + check: "Check", + "check-icon": "CheckCircle", + "check-outlined": "CheckCircle", + + // Arrow icons + "left-arrow": "ChevronLeft", + "right-arrow": "ChevronRight", + "up-arrow": "ChevronUp", + "down-arrow": "ChevronDown", + "left-arrow-double": "ChevronsLeft", + "right-arrow-double": "ChevronsRight", + "left-arrow-long": "ArrowLeft", + "top-right-arrow": "ArrowUpRight", + "open-in-new-page": "ExternalLink", + "selector-vertical": "ChevronsUpDown", + + // Status/Alert icons + "alert-icon": "Bell", + "warning-icon": "AlertCircle", + "history-icon": "History", + + // Data/Chart icons + dashboard: "LayoutGrid", + "monitor-graph-line": "TrendingUp", + "response-time-icon": "TrendingUp", + "uptime-icon": "Activity", + "speedometer-icon": "Gauge", + "spedometer-icon": "Gauge", + "ruler-icon": "Ruler", + "performance-report": "Layers", + "average-response-icon": "BarChart3", + "distributed-uptime": "Network", + + // Misc icons + calendar: "Calendar", + "calendar-check": "CalendarCheck", + "clock-snooze": "Clock", + mail: "Mail", + email: "Mail", + folder: "Folder", + "dots-vertical": "MoreVertical", + "pause-icon": "PauseCircle", + "resume-icon": "PlayCircle", + "checkbox-filled": "CheckSquare", + "checkbox-outline": "Square", + "radio-checked": "Circle", + "cpu-chip": "Cpu", + certificate: "Award", + "interval-check": "RefreshCw", +}; + +/** + * MUI icon to Lucide mapping + */ +export const muiToLucideMap = { + AddCircleOutline: "PlusCircle", + ArrowDownwardRounded: "ArrowDown", + ArrowUpwardRounded: "ArrowUp", + ArrowDropDown: "ChevronDown", + ArrowOutward: "ExternalLink", + CheckCircle: "CheckCircle2", + Clear: "X", + Close: "X", + CloseRounded: "X", + CloudUpload: "Upload", + Delete: "Trash2", + DeleteOutlineRounded: "Trash2", + Email: "Mail", + ErrorOutline: "AlertTriangle", + ErrorOutlineOutlined: "AlertTriangle", + Image: "ImageIcon", + InfoOutlined: "Info", + KeyboardArrowDown: "ChevronDown", + Menu: "Menu", + PauseCircleOutline: "PauseCircle", + PauseOutlined: "Pause", + PlayArrowOutlined: "Play", + PlayCircleOutlineRounded: "PlayCircle", + RadioButtonChecked: "Circle", + ReorderRounded: "GripVertical", + SettingsOutlined: "Settings", + Share: "Share2", + Visibility: "Eye", + VisibilityOff: "EyeOff", + WarningAmberOutlined: "AlertTriangle", +}; diff --git a/client/src/Components/v1/Icon/index.jsx b/client/src/Components/v1/Icon/index.jsx new file mode 100644 index 000000000..4fa82a686 --- /dev/null +++ b/client/src/Components/v1/Icon/index.jsx @@ -0,0 +1,63 @@ +import { useTheme } from "@mui/material"; +import * as LucideIcons from "lucide-react"; +import PropTypes from "prop-types"; + +/** + * Theme-aware icon component wrapping Lucide icons + * + * @param {string} name - Lucide icon name (e.g., "Bell", "Check", "ArrowLeft") + * @param {number} size - Icon size in pixels (default: 20) + * @param {string} color - Direct color value OR theme path like "primary.contrastText" + * @param {number} strokeWidth - Stroke width (default: 1.5) + * @param {string} fill - Fill color (default: "none") + */ +const Icon = ({ name, size = 20, color, strokeWidth = 1.5, fill = "none", ...props }) => { + const theme = useTheme(); + + // Resolve color from theme path or use direct value + const resolveColor = (colorValue) => { + if (!colorValue) { + return theme.palette.primary.contrastTextTertiary; // Default icon color + } + + // If it's a theme path like "primary.contrastText" + if (typeof colorValue === "string" && colorValue.includes(".")) { + const parts = colorValue.split("."); + let resolved = theme.palette; + for (const part of parts) { + resolved = resolved?.[part]; + } + return resolved || colorValue; + } + + // Direct color value + return colorValue; + }; + + const LucideIcon = LucideIcons[name]; + + if (!LucideIcon) { + console.warn(`Icon "${name}" not found in Lucide icons`); + return null; + } + + return ( + + ); +}; + +Icon.propTypes = { + name: PropTypes.string.isRequired, + size: PropTypes.number, + color: PropTypes.string, + strokeWidth: PropTypes.number, + fill: PropTypes.string, +}; + +export default Icon; diff --git a/client/src/Components/v1/Inputs/Checkbox/index.jsx b/client/src/Components/v1/Inputs/Checkbox/index.jsx index 64f3cc3db..caa6830e7 100644 --- a/client/src/Components/v1/Inputs/Checkbox/index.jsx +++ b/client/src/Components/v1/Inputs/Checkbox/index.jsx @@ -1,8 +1,7 @@ import PropTypes from "prop-types"; import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material"; import { useTheme } from "@emotion/react"; -import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react"; -import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react"; +import Icon from "../../Icon"; /** * Checkbox Component @@ -65,8 +64,8 @@ const Checkbox = ({ name={name} value={value} onChange={onChange} - icon={} - checkedIcon={} + icon={} + checkedIcon={} inputProps={{ "aria-label": "controlled checkbox", id: id, diff --git a/client/src/Components/v1/Inputs/ImageUpload/index.jsx b/client/src/Components/v1/Inputs/ImageUpload/index.jsx index e7415bf8f..5b2be991a 100644 --- a/client/src/Components/v1/Inputs/ImageUpload/index.jsx +++ b/client/src/Components/v1/Inputs/ImageUpload/index.jsx @@ -1,11 +1,10 @@ // Components import { Box, Stack, Typography } from "@mui/material"; -import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import Icon from "../../Icon"; import Image from "../../Image/index.jsx"; import TextField from "@mui/material/TextField"; import IconButton from "@mui/material/IconButton"; import ProgressUpload from "../../ProgressBars/index.jsx"; -import ImageIcon from "@mui/icons-material/Image"; // Utils import PropTypes from "prop-types"; @@ -176,7 +175,7 @@ const ImageUpload = ({ boxShadow: theme.shape.boxShadow, }} > - + {(localError || progress.isLoading || progress.value !== 0) && ( } + icon={} label={file?.name || "Upload failed"} size={file?.size} progress={progress.value} diff --git a/client/src/Components/v1/Inputs/Radio/index.jsx b/client/src/Components/v1/Inputs/Radio/index.jsx index 575c1af74..79a9e64f3 100644 --- a/client/src/Components/v1/Inputs/Radio/index.jsx +++ b/client/src/Components/v1/Inputs/Radio/index.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import RadioChecked from "../../../../assets/icons/radio-checked.svg?react"; +import Icon from "../../Icon"; import "./index.css"; /** @@ -47,7 +47,7 @@ const Radio = ({ } + checkedIcon={} sx={{ color: "transparent", width: 16, diff --git a/client/src/Components/v1/Inputs/Search/index.jsx b/client/src/Components/v1/Inputs/Search/index.jsx index 8138ee6f4..4371ef971 100644 --- a/client/src/Components/v1/Inputs/Search/index.jsx +++ b/client/src/Components/v1/Inputs/Search/index.jsx @@ -8,7 +8,7 @@ import { Checkbox, } from "@mui/material"; import { useTheme } from "@emotion/react"; -import SearchIcon from "../../../../assets/icons/search.svg?react"; +import Icon from "../../Icon"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import FieldWrapper from "../FieldWrapper/index.jsx"; @@ -31,20 +31,13 @@ import FieldWrapper from "../FieldWrapper/index.jsx"; const SearchAdornment = () => { const theme = useTheme(); return ( - - + + ); }; diff --git a/client/src/Components/v1/Inputs/Select/index.jsx b/client/src/Components/v1/Inputs/Select/index.jsx index f952b6818..b292d6896 100644 --- a/client/src/Components/v1/Inputs/Select/index.jsx +++ b/client/src/Components/v1/Inputs/Select/index.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import Icon from "../../Icon"; import FieldWrapper from "../FieldWrapper/index.jsx"; import "./index.css"; @@ -98,7 +98,7 @@ const Select = ({ error={error} name={name} inputProps={{ id: id }} - IconComponent={KeyboardArrowDownIcon} + IconComponent={(props) => } MenuProps={{ disableScrollLock: true }} sx={{ fontSize: 13, diff --git a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx index 4e4bbd2af..8df4ee3b1 100644 --- a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx +++ b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx @@ -1,10 +1,7 @@ import { Stack, Typography, InputAdornment, IconButton } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -import VisibilityOff from "@mui/icons-material/VisibilityOff"; -import Visibility from "@mui/icons-material/Visibility"; -import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded"; -import DeleteIcon from "../../../../../assets/icons/trash-bin.svg?react"; +import Icon from "../../../Icon"; export const HttpAdornment = ({ https }) => { const theme = useTheme(); @@ -56,7 +53,11 @@ export const PasswordEndAdornment = ({ fieldType, setFieldType }) => { }, }} > - {fieldType === "password" ? : } + {fieldType === "password" ? ( + + ) : ( + + )} ); diff --git a/client/src/Components/v1/MonitorActions/index.jsx b/client/src/Components/v1/MonitorActions/index.jsx index 624d34b9a..d5bab53ba 100644 --- a/client/src/Components/v1/MonitorActions/index.jsx +++ b/client/src/Components/v1/MonitorActions/index.jsx @@ -1,7 +1,7 @@ import * as React from "react"; import Button from "@mui/material/Button"; import ButtonGroup from "@mui/material/ButtonGroup"; -import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import Icon from "../Icon"; import ClickAwayListener from "@mui/material/ClickAwayListener"; import Grow from "@mui/material/Grow"; import Paper from "@mui/material/Paper"; @@ -70,7 +70,7 @@ const MonitorActions = ({ isLoading }) => { aria-haspopup="menu" onClick={handleToggle} > - + } + startIcon={} disabled={isTestNotificationsDisabled} onClick={() => { testAllNotifications({ monitorId: monitor?.id }); @@ -103,7 +100,7 @@ const MonitorDetailsControlHeader = ({ color="secondary" loading={isPausing} startIcon={ - monitor?.isActive ? : + monitor?.isActive ? : } onClick={() => { pauseMonitor({ @@ -119,7 +116,7 @@ const MonitorDetailsControlHeader = ({ @@ -111,7 +110,7 @@ const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => { }} > {t("publicLink")} - + )}
diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx index 1898ff3d7..44cfbaca3 100644 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx @@ -1,7 +1,6 @@ // Components import { Stack, Typography } from "@mui/material"; -import CheckCircleIcon from "@mui/icons-material/CheckCircle"; -import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import Icon from "@/Components/v1/Icon"; // Utils import { useTheme } from "@mui/material/styles"; @@ -10,18 +9,14 @@ import PropTypes from "prop-types"; const getMonitorStatus = (monitors, theme) => { const monitorsStatus = { icon: ( - + ), }; if (monitors.every((monitor) => monitor.status === true)) { monitorsStatus.msg = "All systems operational"; monitorsStatus.color = theme.palette.success.lowContrast; monitorsStatus.icon = ( - + ); } diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx index 1afe25409..57f7235e6 100644 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx @@ -2,7 +2,7 @@ import DataTable from "@/Components/v1/Table/index.jsx"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; +import Icon from "@/Components/v1/Icon"; import { Stack, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; const StatusPagesTable = ({ data }) => { @@ -48,7 +48,7 @@ const StatusPagesTable = ({ data }) => { }} > {content} - {row.isPublished && } + {row.isPublished && }
); }, diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 2c7821571..70be3bc00 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -28,10 +28,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { monitorValidation } from "../../../Validation/validation.js"; import { createToast } from "../../../Utils/toastUtils.jsx"; -import { - PauseOutlined as PauseOutlinedIcon, - PlayArrowOutlined as PlayArrowOutlinedIcon, -} from "@mui/icons-material"; +import Icon from "@/Components/v1/Icon"; import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; import { useParams } from "react-router-dom"; @@ -429,7 +426,7 @@ const UptimeCreate = ({ isClone = false }) => { color="secondary" loading={isBusy} startIcon={ - monitor?.isActive ? : + monitor?.isActive ? : } onClick={handlePause} > diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx index 582c7891f..6449946f3 100644 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx +++ b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx @@ -1,9 +1,7 @@ // Components import { Stack, Typography, Box } from "@mui/material"; import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import UptimeIcon from "@/assets/icons/uptime-icon.svg?react"; -import IncidentsIcon from "@/assets/icons/incidents.svg?react"; -import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react"; +import Icon from "@/Components/v1/Icon"; import UpBarChart from "../Charts/UpBarChart.jsx"; import DownBarChart from "../Charts/DownBarChart.jsx"; import ResponseGaugeChart from "../Charts/ResponseGaugeChart.jsx"; @@ -38,7 +36,7 @@ const ChartBoxes = ({ gap={theme.spacing(8)} > } + icon={} header="Uptime" isEmpty={monitorData?.groupedUpChecks?.length === 0} > @@ -89,7 +87,7 @@ const ChartBoxes = ({ /> } + icon={} header="Incidents" noDataMessage={noIncidentsMessage} isEmpty={monitorData?.groupedDownChecks?.length === 0} @@ -123,7 +121,7 @@ const ChartBoxes = ({ /> } + icon={} header="Average Response Time" > diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx index 861b600d4..365324b4d 100644 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx @@ -1,6 +1,6 @@ import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; import MonitorDetailsAreaChart from "@/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx"; -import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react"; +import Icon from "@/Components/v1/Icon"; import SkeletonLayout from "./ResponseTimeChartSkeleton.jsx"; import PropTypes from "prop-types"; @@ -11,7 +11,7 @@ const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange }) return ( } + icon={} header="Response Times" > } + icon={} header="Response Times" height="100%" > diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx index b9a6824c1..003617046 100644 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import FilterHeader from "@/Components/v1/FilterHeader/index.jsx"; import { useMemo } from "react"; import { Box, Button } from "@mui/material"; -import ClearIcon from "@mui/icons-material/Clear"; +import Icon from "@/Components/v1/Icon"; import { useTranslation } from "react-i18next"; /** @@ -131,7 +131,7 @@ const Filter = ({ color={theme.palette.primary.contrastText} onClick={handleReset} variant="contained" - endIcon={} + endIcon={} sx={{ visibility: isFilterActive ? "visible" : "hidden", }} diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx index c9015816a..ee9a5d037 100644 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx @@ -1,9 +1,8 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography } from "@mui/material"; -import Arrow from "@/assets/icons/top-right-arrow.svg?react"; +import Icon from "@/Components/v1/Icon"; import Background from "@/assets/Images/background-grid.svg?react"; -import ClockSnooze from "@/assets/icons/clock-snooze.svg?react"; const StatusBox = ({ title, value, status }) => { const theme = useTheme(); @@ -20,21 +19,21 @@ const StatusBox = ({ title, value, status }) => { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); } diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx index fe220251d..43a467405 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx @@ -1,8 +1,7 @@ // Components import { Box, Stack } from "@mui/material"; import DataTable from "@/Components/v1/Table/index.jsx"; -import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; -import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; +import Icon from "@/Components/v1/Icon"; import Host from "@/Components/v1/Host/index.jsx"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import BarChart from "@/Components/v1/Charts/BarChart/index.jsx"; @@ -92,9 +91,9 @@ const UptimeDataTable = ({ }} > {sort?.order === "asc" ? ( - + ) : ( - + )} @@ -128,9 +127,9 @@ const UptimeDataTable = ({ }} > {sort?.order === "asc" ? ( - + ) : ( - + )} diff --git a/client/src/assets/icons/alert-icon.svg b/client/src/assets/icons/alert-icon.svg deleted file mode 100644 index 1dc7fc2b7..000000000 --- a/client/src/assets/icons/alert-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/average-response-icon.svg b/client/src/assets/icons/average-response-icon.svg deleted file mode 100644 index 572e2002f..000000000 --- a/client/src/assets/icons/average-response-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/bwu-icon.svg b/client/src/assets/icons/bwu-icon.svg deleted file mode 100644 index 80ab50ad9..000000000 --- a/client/src/assets/icons/bwu-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/calendar-check.svg b/client/src/assets/icons/calendar-check.svg deleted file mode 100644 index 07d96dad9..000000000 --- a/client/src/assets/icons/calendar-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/calendar.svg b/client/src/assets/icons/calendar.svg deleted file mode 100644 index be023a4de..000000000 --- a/client/src/assets/icons/calendar.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/certificate.svg b/client/src/assets/icons/certificate.svg deleted file mode 100644 index 2bc6c7e74..000000000 --- a/client/src/assets/icons/certificate.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/changeLog.svg b/client/src/assets/icons/changeLog.svg deleted file mode 100644 index d12f6c10e..000000000 --- a/client/src/assets/icons/changeLog.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/check-icon.svg b/client/src/assets/icons/check-icon.svg deleted file mode 100644 index 9a2cfac5a..000000000 --- a/client/src/assets/icons/check-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/check-outlined.svg b/client/src/assets/icons/check-outlined.svg deleted file mode 100644 index 1e779d547..000000000 --- a/client/src/assets/icons/check-outlined.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/check.svg b/client/src/assets/icons/check.svg deleted file mode 100644 index 357378f40..000000000 --- a/client/src/assets/icons/check.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/checkbox-filled.svg b/client/src/assets/icons/checkbox-filled.svg deleted file mode 100644 index 8a5be81f0..000000000 --- a/client/src/assets/icons/checkbox-filled.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/checkbox-green.svg b/client/src/assets/icons/checkbox-green.svg deleted file mode 100644 index dd5676298..000000000 --- a/client/src/assets/icons/checkbox-green.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/src/assets/icons/checkbox-outline.svg b/client/src/assets/icons/checkbox-outline.svg deleted file mode 100644 index bf5a7218c..000000000 --- a/client/src/assets/icons/checkbox-outline.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/checkbox-red.svg b/client/src/assets/icons/checkbox-red.svg deleted file mode 100644 index 3c43a2042..000000000 --- a/client/src/assets/icons/checkbox-red.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/src/assets/icons/clock-snooze.svg b/client/src/assets/icons/clock-snooze.svg deleted file mode 100644 index 708e7ba91..000000000 --- a/client/src/assets/icons/clock-snooze.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/close-icon.svg b/client/src/assets/icons/close-icon.svg deleted file mode 100644 index e1b6b690a..000000000 --- a/client/src/assets/icons/close-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/cpu-chip.svg b/client/src/assets/icons/cpu-chip.svg deleted file mode 100644 index 5645840b1..000000000 --- a/client/src/assets/icons/cpu-chip.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/dashboard.svg b/client/src/assets/icons/dashboard.svg deleted file mode 100644 index 8958e90ed..000000000 --- a/client/src/assets/icons/dashboard.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/discussions.svg b/client/src/assets/icons/discussions.svg deleted file mode 100644 index 293cd51f2..000000000 --- a/client/src/assets/icons/discussions.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/client/src/assets/icons/distributed-uptime.svg b/client/src/assets/icons/distributed-uptime.svg deleted file mode 100644 index e612081e4..000000000 --- a/client/src/assets/icons/distributed-uptime.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/docs.svg b/client/src/assets/icons/docs.svg deleted file mode 100644 index d36458de2..000000000 --- a/client/src/assets/icons/docs.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/dots-vertical.svg b/client/src/assets/icons/dots-vertical.svg deleted file mode 100644 index e720066c4..000000000 --- a/client/src/assets/icons/dots-vertical.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/src/assets/icons/down-arrow.svg b/client/src/assets/icons/down-arrow.svg deleted file mode 100644 index 59b4b6bc5..000000000 --- a/client/src/assets/icons/down-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/edit.svg b/client/src/assets/icons/edit.svg deleted file mode 100644 index be2c78408..000000000 --- a/client/src/assets/icons/edit.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/email.svg b/client/src/assets/icons/email.svg deleted file mode 100644 index 01f55e961..000000000 --- a/client/src/assets/icons/email.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/folder.svg b/client/src/assets/icons/folder.svg deleted file mode 100644 index 15941e5b5..000000000 --- a/client/src/assets/icons/folder.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/client/src/assets/icons/groups.svg b/client/src/assets/icons/groups.svg deleted file mode 100644 index fa2cae03c..000000000 --- a/client/src/assets/icons/groups.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/assets/icons/history-icon.svg b/client/src/assets/icons/history-icon.svg deleted file mode 100644 index 7343455ec..000000000 --- a/client/src/assets/icons/history-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/incidents.svg b/client/src/assets/icons/incidents.svg deleted file mode 100644 index ffe04d09f..000000000 --- a/client/src/assets/icons/incidents.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/integrations.svg b/client/src/assets/icons/integrations.svg deleted file mode 100644 index 50e817f00..000000000 --- a/client/src/assets/icons/integrations.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/interval-check.svg b/client/src/assets/icons/interval-check.svg deleted file mode 100644 index 923475c8b..000000000 --- a/client/src/assets/icons/interval-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/jupiter_ag_logo.svg b/client/src/assets/icons/jupiter_ag_logo.svg deleted file mode 100644 index b344961c8..000000000 --- a/client/src/assets/icons/jupiter_ag_logo.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/jupiter_logo_banner_dark.svg b/client/src/assets/icons/jupiter_logo_banner_dark.svg deleted file mode 100644 index 3d3e23b14..000000000 --- a/client/src/assets/icons/jupiter_logo_banner_dark.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/jupiter_logo_banner_light.svg b/client/src/assets/icons/jupiter_logo_banner_light.svg deleted file mode 100644 index 4744e0bcb..000000000 --- a/client/src/assets/icons/jupiter_logo_banner_light.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/key.svg b/client/src/assets/icons/key.svg deleted file mode 100644 index 5db8d5f16..000000000 --- a/client/src/assets/icons/key.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/left-arrow-double.svg b/client/src/assets/icons/left-arrow-double.svg deleted file mode 100644 index 4cb2a5186..000000000 --- a/client/src/assets/icons/left-arrow-double.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/left-arrow-long.svg b/client/src/assets/icons/left-arrow-long.svg deleted file mode 100644 index 56a774730..000000000 --- a/client/src/assets/icons/left-arrow-long.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/left-arrow.svg b/client/src/assets/icons/left-arrow.svg deleted file mode 100644 index 649d1681c..000000000 --- a/client/src/assets/icons/left-arrow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/lock.svg b/client/src/assets/icons/lock.svg deleted file mode 100644 index a87535d54..000000000 --- a/client/src/assets/icons/lock.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/logout.svg b/client/src/assets/icons/logout.svg deleted file mode 100644 index 3f3b69c49..000000000 --- a/client/src/assets/icons/logout.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/logs.svg b/client/src/assets/icons/logs.svg deleted file mode 100644 index 556c3ba33..000000000 --- a/client/src/assets/icons/logs.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/mail.svg b/client/src/assets/icons/mail.svg deleted file mode 100644 index 75c22b111..000000000 --- a/client/src/assets/icons/mail.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/maintenance.svg b/client/src/assets/icons/maintenance.svg deleted file mode 100644 index 9486f6086..000000000 --- a/client/src/assets/icons/maintenance.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/monitor-graph-line.svg b/client/src/assets/icons/monitor-graph-line.svg deleted file mode 100644 index 86049eb9e..000000000 --- a/client/src/assets/icons/monitor-graph-line.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/monitors.svg b/client/src/assets/icons/monitors.svg deleted file mode 100644 index f40f36433..000000000 --- a/client/src/assets/icons/monitors.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/notifications.svg b/client/src/assets/icons/notifications.svg deleted file mode 100644 index c09ac5746..000000000 --- a/client/src/assets/icons/notifications.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/open-in-new-page.svg b/client/src/assets/icons/open-in-new-page.svg deleted file mode 100644 index 70599f390..000000000 --- a/client/src/assets/icons/open-in-new-page.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/page-speed.svg b/client/src/assets/icons/page-speed.svg deleted file mode 100644 index 30a1c8a53..000000000 --- a/client/src/assets/icons/page-speed.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/pause-icon.svg b/client/src/assets/icons/pause-icon.svg deleted file mode 100644 index c73abfc1d..000000000 --- a/client/src/assets/icons/pause-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/performance-report.svg b/client/src/assets/icons/performance-report.svg deleted file mode 100644 index 8b7073ac3..000000000 --- a/client/src/assets/icons/performance-report.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/radio-checked.svg b/client/src/assets/icons/radio-checked.svg deleted file mode 100644 index 1c584b79a..000000000 --- a/client/src/assets/icons/radio-checked.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/response-time-icon.svg b/client/src/assets/icons/response-time-icon.svg deleted file mode 100644 index db5b2af38..000000000 --- a/client/src/assets/icons/response-time-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/resume-icon.svg b/client/src/assets/icons/resume-icon.svg deleted file mode 100644 index 1da787081..000000000 --- a/client/src/assets/icons/resume-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/right-arrow-double.svg b/client/src/assets/icons/right-arrow-double.svg deleted file mode 100644 index 6ad4882ad..000000000 --- a/client/src/assets/icons/right-arrow-double.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/right-arrow.svg b/client/src/assets/icons/right-arrow.svg deleted file mode 100644 index 77d022798..000000000 --- a/client/src/assets/icons/right-arrow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/ruler-icon.svg b/client/src/assets/icons/ruler-icon.svg deleted file mode 100644 index 7266d2876..000000000 --- a/client/src/assets/icons/ruler-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/search.svg b/client/src/assets/icons/search.svg deleted file mode 100644 index a1bc74224..000000000 --- a/client/src/assets/icons/search.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/selector-vertical.svg b/client/src/assets/icons/selector-vertical.svg deleted file mode 100644 index 29d9a4998..000000000 --- a/client/src/assets/icons/selector-vertical.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/settings-bold.svg b/client/src/assets/icons/settings-bold.svg deleted file mode 100644 index d8057286c..000000000 --- a/client/src/assets/icons/settings-bold.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/settings.svg b/client/src/assets/icons/settings.svg deleted file mode 100644 index eb86f1d19..000000000 --- a/client/src/assets/icons/settings.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/solana_logo.svg b/client/src/assets/icons/solana_logo.svg deleted file mode 100644 index bd349af26..000000000 --- a/client/src/assets/icons/solana_logo.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/solana_logo_banner.svg b/client/src/assets/icons/solana_logo_banner.svg deleted file mode 100644 index 08ec87cb1..000000000 --- a/client/src/assets/icons/solana_logo_banner.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/spedometer-icon.svg b/client/src/assets/icons/spedometer-icon.svg deleted file mode 100644 index 19b13f44e..000000000 --- a/client/src/assets/icons/spedometer-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/speedometer-icon.svg b/client/src/assets/icons/speedometer-icon.svg deleted file mode 100644 index 19b13f44e..000000000 --- a/client/src/assets/icons/speedometer-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/status-pages.svg b/client/src/assets/icons/status-pages.svg deleted file mode 100644 index 97cea88fb..000000000 --- a/client/src/assets/icons/status-pages.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/support.svg b/client/src/assets/icons/support.svg deleted file mode 100644 index 7de16379a..000000000 --- a/client/src/assets/icons/support.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/top-right-arrow.svg b/client/src/assets/icons/top-right-arrow.svg deleted file mode 100644 index d569cdd7e..000000000 --- a/client/src/assets/icons/top-right-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/trash-bin.svg b/client/src/assets/icons/trash-bin.svg deleted file mode 100644 index 83bca0c6f..000000000 --- a/client/src/assets/icons/trash-bin.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/up-arrow.svg b/client/src/assets/icons/up-arrow.svg deleted file mode 100644 index 0f8f3bc34..000000000 --- a/client/src/assets/icons/up-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/uptime-icon.svg b/client/src/assets/icons/uptime-icon.svg deleted file mode 100644 index 55706ae71..000000000 --- a/client/src/assets/icons/uptime-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/user-edit.svg b/client/src/assets/icons/user-edit.svg deleted file mode 100644 index cbd1a252f..000000000 --- a/client/src/assets/icons/user-edit.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/user-two.svg b/client/src/assets/icons/user-two.svg deleted file mode 100644 index 031b22787..000000000 --- a/client/src/assets/icons/user-two.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/user.svg b/client/src/assets/icons/user.svg deleted file mode 100644 index 231c89d40..000000000 --- a/client/src/assets/icons/user.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/warning-icon.svg b/client/src/assets/icons/warning-icon.svg deleted file mode 100644 index 468611758..000000000 --- a/client/src/assets/icons/warning-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - From 7140d436a54c1bfbda12477d9a6ea0ab91c393d5 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 15 Jan 2026 23:39:26 -0500 Subject: [PATCH 043/103] style: apply Prettier formatting --- .../src/Components/v1/ActionsMenu/index.jsx | 5 ++- client/src/Components/v1/Alert/index.jsx | 26 +++++++++++++--- client/src/Components/v1/Check/Check.jsx | 10 ++++-- client/src/Components/v1/Common/AppBar.jsx | 12 +++++-- .../v1/Inputs/ImageUpload/index.jsx | 12 +++++-- .../src/Components/v1/Inputs/Radio/index.jsx | 7 ++++- .../src/Components/v1/Inputs/Select/index.jsx | 8 ++++- .../v1/Inputs/TextInput/Adornments/index.jsx | 10 ++++-- .../Components/v1/MonitorActions/index.jsx | 5 ++- .../v1/MonitorDetailsControlHeader/index.jsx | 26 ++++++++++++++-- .../src/Components/v1/ProgressBars/index.jsx | 10 ++++-- .../Components/v1/ProgressStepper/index.jsx | 12 +++++-- .../Components/v1/ShareComponent/index.jsx | 8 ++++- client/src/Components/v1/StarPrompt/index.jsx | 5 ++- .../v1/Table/TablePagination/index.jsx | 7 ++++- client/src/Components/v1/Toast/index.jsx | 31 ++++++++++++++++--- .../components/AddMemberMenu/index.jsx | 8 ++++- .../Components/StatusBoxes/StatusBox.jsx | 20 +++++++++--- .../Components/ActiveIncidentsPanel/index.jsx | 10 ++++-- .../LatestIncidentsPanel/IncidentItem.jsx | 10 ++++-- .../Components/StatisticsPanel/index.jsx | 20 +++++++++--- .../Components/MonitorActionButtons.jsx | 10 ++++-- .../Monitors/Components/Filters/index.jsx | 7 ++++- .../Components/MonitorsTable/index.jsx | 5 ++- .../Components/MonitorsTableMenu/index.jsx | 5 ++- .../Components/MonitorList/index.jsx | 4 ++- .../Maintenance/CreateMaintenance/index.jsx | 9 +++++- .../MaintenanceTable/ActionsMenu/index.jsx | 5 ++- .../Maintenance/MaintenanceTable/index.jsx | 20 +++++++++--- .../Notifications/components/ActionMenu.jsx | 5 ++- client/src/Pages/PageSpeed/Create/index.jsx | 10 ++++-- .../Components/Charts/AreaChartLegend.jsx | 7 ++++- .../Components/Charts/PieChartLegend.jsx | 7 ++++- .../Components/PageSpeedAreaChart/index.jsx | 7 ++++- .../Components/PerformanceReport/index.jsx | 7 ++++- .../Monitors/Components/Card/index.jsx | 5 ++- .../Create/Components/MonitorList/index.jsx | 9 ++++-- .../Create/Components/Progress/index.jsx | 7 ++++- .../Components/ControlsHeader/index.jsx | 12 +++++-- .../Status/Components/StatusBar/index.jsx | 12 +++++-- .../Components/StatusPagesTable/index.jsx | 7 ++++- client/src/Pages/Uptime/Create/index.jsx | 12 ++++++- .../Details/Components/ChartBoxes/index.jsx | 21 +++++++++++-- .../Components/Charts/ResponseTimeChart.jsx | 7 ++++- .../Components/ResponseTable/index.jsx | 7 ++++- .../Monitors/Components/Filter/index.jsx | 7 ++++- .../Components/StatusBoxes/statusBox.jsx | 15 +++++++-- .../Components/UptimeDataTable/index.jsx | 20 +++++++++--- 48 files changed, 425 insertions(+), 86 deletions(-) diff --git a/client/src/Components/v1/ActionsMenu/index.jsx b/client/src/Components/v1/ActionsMenu/index.jsx index 8fcc77f5d..b469e6764 100644 --- a/client/src/Components/v1/ActionsMenu/index.jsx +++ b/client/src/Components/v1/ActionsMenu/index.jsx @@ -84,7 +84,10 @@ const ActionsMenu = ({ }, }} > - + , - error: , - warning: , + info: ( + + ), + error: ( + + ), + warning: ( + + ), }; /** @@ -94,7 +109,10 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { }, }} > - + )} diff --git a/client/src/Components/v1/Check/Check.jsx b/client/src/Components/v1/Check/Check.jsx index 5e25569be..01a41879e 100644 --- a/client/src/Components/v1/Check/Check.jsx +++ b/client/src/Components/v1/Check/Check.jsx @@ -34,7 +34,10 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => alignItems="center" > {outlined ? ( - + ) : ( "& svg > path": { fill: colors[variant] }, }} > - + )} { aria-label="Menu button" onClick={toggleDrawer(true)} > - + { }} > - + diff --git a/client/src/Components/v1/Inputs/ImageUpload/index.jsx b/client/src/Components/v1/Inputs/ImageUpload/index.jsx index 5b2be991a..d224418e0 100644 --- a/client/src/Components/v1/Inputs/ImageUpload/index.jsx +++ b/client/src/Components/v1/Inputs/ImageUpload/index.jsx @@ -175,7 +175,10 @@ const ImageUpload = ({ boxShadow: theme.shape.boxShadow, }} > - + {(localError || progress.isLoading || progress.value !== 0) && ( } + icon={ + + } label={file?.name || "Upload failed"} size={file?.size} progress={progress.value} diff --git a/client/src/Components/v1/Inputs/Radio/index.jsx b/client/src/Components/v1/Inputs/Radio/index.jsx index 79a9e64f3..b6bd4e69c 100644 --- a/client/src/Components/v1/Inputs/Radio/index.jsx +++ b/client/src/Components/v1/Inputs/Radio/index.jsx @@ -47,7 +47,12 @@ const Radio = ({ } + checkedIcon={ + + } sx={{ color: "transparent", width: 16, diff --git a/client/src/Components/v1/Inputs/Select/index.jsx b/client/src/Components/v1/Inputs/Select/index.jsx index b292d6896..0f96a2ff6 100644 --- a/client/src/Components/v1/Inputs/Select/index.jsx +++ b/client/src/Components/v1/Inputs/Select/index.jsx @@ -98,7 +98,13 @@ const Select = ({ error={error} name={name} inputProps={{ id: id }} - IconComponent={(props) => } + IconComponent={(props) => ( + + )} MenuProps={{ disableScrollLock: true }} sx={{ fontSize: 13, diff --git a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx index 8df4ee3b1..078684405 100644 --- a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx +++ b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx @@ -54,9 +54,15 @@ export const PasswordEndAdornment = ({ fieldType, setFieldType }) => { }} > {fieldType === "password" ? ( - + ) : ( - + )} diff --git a/client/src/Components/v1/MonitorActions/index.jsx b/client/src/Components/v1/MonitorActions/index.jsx index d5bab53ba..f63fc23fe 100644 --- a/client/src/Components/v1/MonitorActions/index.jsx +++ b/client/src/Components/v1/MonitorActions/index.jsx @@ -70,7 +70,10 @@ const MonitorActions = ({ isLoading }) => { aria-haspopup="menu" onClick={handleToggle} > - + } + startIcon={ + + } disabled={isTestNotificationsDisabled} onClick={() => { testAllNotifications({ monitorId: monitor?.id }); @@ -100,7 +105,17 @@ const MonitorDetailsControlHeader = ({ color="secondary" loading={isPausing} startIcon={ - monitor?.isActive ? : + monitor?.isActive ? ( + + ) : ( + + ) } onClick={() => { pauseMonitor({ @@ -116,7 +131,12 @@ const MonitorDetailsControlHeader = ({ @@ -110,7 +115,10 @@ const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => { }} > {t("publicLink")} - + )} diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx index 44cfbaca3..7b9460068 100644 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx @@ -9,14 +9,22 @@ import PropTypes from "prop-types"; const getMonitorStatus = (monitors, theme) => { const monitorsStatus = { icon: ( - + ), }; if (monitors.every((monitor) => monitor.status === true)) { monitorsStatus.msg = "All systems operational"; monitorsStatus.color = theme.palette.success.lowContrast; monitorsStatus.icon = ( - + ); } diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx index 57f7235e6..9882239bd 100644 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx @@ -48,7 +48,12 @@ const StatusPagesTable = ({ data }) => { }} > {content} - {row.isPublished && } + {row.isPublished && ( + + )} ); }, diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 70be3bc00..b1460dba6 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -426,7 +426,17 @@ const UptimeCreate = ({ isClone = false }) => { color="secondary" loading={isBusy} startIcon={ - monitor?.isActive ? : + monitor?.isActive ? ( + + ) : ( + + ) } onClick={handlePause} > diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx index 6449946f3..b8496f5ea 100644 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx +++ b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx @@ -36,7 +36,12 @@ const ChartBoxes = ({ gap={theme.spacing(8)} > } + icon={ + + } header="Uptime" isEmpty={monitorData?.groupedUpChecks?.length === 0} > @@ -87,7 +92,12 @@ const ChartBoxes = ({ /> } + icon={ + + } header="Incidents" noDataMessage={noIncidentsMessage} isEmpty={monitorData?.groupedDownChecks?.length === 0} @@ -121,7 +131,12 @@ const ChartBoxes = ({ /> } + icon={ + + } header="Average Response Time" > diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx index 365324b4d..7e7a940e0 100644 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx @@ -11,7 +11,12 @@ const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange }) return ( } + icon={ + + } header="Response Times" > } + icon={ + + } header="Response Times" height="100%" > diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx index 003617046..62ba0be13 100644 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx @@ -131,7 +131,12 @@ const Filter = ({ color={theme.palette.primary.contrastText} onClick={handleReset} variant="contained" - endIcon={} + endIcon={ + + } sx={{ visibility: isFilterActive ? "visible" : "hidden", }} diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx index ee9a5d037..136c3575d 100644 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx @@ -19,21 +19,30 @@ const StatusBox = ({ title, value, status }) => { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); } diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx index 43a467405..8c3239dd3 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx @@ -91,9 +91,15 @@ const UptimeDataTable = ({ }} > {sort?.order === "asc" ? ( - + ) : ( - + )} @@ -127,9 +133,15 @@ const UptimeDataTable = ({ }} > {sort?.order === "asc" ? ( - + ) : ( - + )} From 7874439e4c87a41db14f103e2a14ccd6cd72709e Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 07:31:23 -0500 Subject: [PATCH 044/103] fix: address PR review feedback for icon migration - Use AlertCircle for error and AlertTriangle for warning icons - Use Icon color prop instead of CSS fill for Lucide stroke-based icons - Replace wildcard import with explicit named imports for tree-shaking - Replace sx prop with style prop on Icon components - Add Trash2 to Icon component imports --- client/src/Components/v1/Alert/index.jsx | 2 +- client/src/Components/v1/Check/Check.jsx | 16 +- client/src/Components/v1/Icon/index.jsx | 138 +++++++++++++++++- client/src/Components/v1/Toast/index.jsx | 2 +- .../Components/MonitorList/index.jsx | 2 +- .../Create/Components/MonitorList/index.jsx | 2 +- 6 files changed, 144 insertions(+), 18 deletions(-) diff --git a/client/src/Components/v1/Alert/index.jsx b/client/src/Components/v1/Alert/index.jsx index 89416f5b1..dd3b2f9f2 100644 --- a/client/src/Components/v1/Alert/index.jsx +++ b/client/src/Components/v1/Alert/index.jsx @@ -18,7 +18,7 @@ const icons = { ), error: ( ), diff --git a/client/src/Components/v1/Check/Check.jsx b/client/src/Components/v1/Check/Check.jsx index 01a41879e..cc3749fda 100644 --- a/client/src/Components/v1/Check/Check.jsx +++ b/client/src/Components/v1/Check/Check.jsx @@ -39,17 +39,11 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => size={20} /> ) : ( - path": { fill: colors[variant] }, - }} - > - - + )} ), diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx index 3f606e4a2..2bf7a991f 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx @@ -17,7 +17,7 @@ const MonitorListItem = ({ monitor, onDelete }) => { onDelete(monitor)} /> diff --git a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx b/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx index 81e6df6c5..349571fe8 100644 --- a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx +++ b/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx @@ -35,7 +35,7 @@ const MonitorListItem = ({ { onDelete(monitor); }} From f5aa24d77e6864ba67a8f18149fc4b3e287ed931 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 07:34:03 -0500 Subject: [PATCH 045/103] fix: rename Image import to ImageIcon to avoid conflicts --- client/src/Components/v1/Icon/index.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/Components/v1/Icon/index.jsx b/client/src/Components/v1/Icon/index.jsx index 2f4ff968d..5828601fd 100644 --- a/client/src/Components/v1/Icon/index.jsx +++ b/client/src/Components/v1/Icon/index.jsx @@ -34,7 +34,7 @@ import { GripVertical, HelpCircle, History, - Image, + Image as ImageIcon, Info, Key, Layers, @@ -67,7 +67,8 @@ import { /** * Map of icon names to Lucide icon components. - * Only includes icons that are actually used in the application. + * Only icons explicitly imported here will be included in the bundle (tree-shaking). + * To add a new icon: import it above and add it to this object. */ const iconComponents = { Activity, @@ -103,7 +104,7 @@ const iconComponents = { GripVertical, HelpCircle, History, - Image, + Image: ImageIcon, Info, Key, Layers, From 95cd33ac065d53940873e6cfe1e796b2e60994ce Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 18:11:04 +0000 Subject: [PATCH 046/103] queue and buffer refactor --- server/src/config/services.ts | 22 +-- .../repositories/checks/IChecksRepository.ts | 5 +- .../checks/MongoChecksRepistory.ts | 108 +-------------- server/src/service/business/checkService.ts | 90 +++++++++++- .../SuperSimpleQueueHelper.ts | 18 ++- .../{bufferService.js => bufferService.ts} | 59 +++++--- .../service/infrastructure/statusService.ts | 129 ++---------------- server/src/types/check.ts | 70 +++++----- server/src/types/monitor.ts | 2 +- server/src/types/network.ts | 2 +- 10 files changed, 218 insertions(+), 287 deletions(-) rename server/src/service/infrastructure/{bufferService.js => bufferService.ts} (72%) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 57a223ebf..02a3d2345 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -183,7 +183,17 @@ export const initializeServices = async ({ stringService, }); - const bufferService = new BufferService({ db, logger, envSettings, incidentService }); + const checkService = new CheckService({ + db, + settingsService, + stringService, + errorService, + monitorsRepository, + logger, + checksRepository, + }); + + const bufferService = new BufferService({ db, logger, envSettings, incidentService, checkService }); const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService, monitorsRepository }); @@ -208,6 +218,8 @@ export const initializeServices = async ({ networkService, statusService, notificationService, + checkService, + buffer: bufferService, }); const superSimpleQueue = await SuperSimpleQueue.create({ @@ -229,13 +241,7 @@ export const initializeServices = async ({ jobQueue: superSimpleQueue, monitorsRepository, }); - const checkService = new CheckService({ - db, - settingsService, - stringService, - errorService, - monitorsRepository, - }); + const diagnosticService = new DiagnosticService(); const inviteService = new InviteService({ db, diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 5737daff6..7b98ae7b6 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -1,4 +1,4 @@ -import type { Check, CheckAudits, MonitorStatusResponse, MonitorType } from "@/types/index.js"; +import type { Check, MonitorType } from "@/types/index.js"; import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; export interface PageSpeedChecksResult { @@ -55,7 +55,8 @@ export interface UptimeChecksResult { export interface IChecksRepository { // create - buildCheck(status: MonitorStatusResponse, type: MonitorType): Check; + createChecks(checks: Check[]): Promise; + // single fetch // collection fetch findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 225aec193..7707a7b17 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -11,10 +11,8 @@ import type { CheckMetadata, CheckNetworkInterfaceInfo, GotTimings, - MonitorStatusResponse, MonitorType, } from "@/types/index.js"; -import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; @@ -144,11 +142,11 @@ class MongoChecksRepository implements IChecksRepository { return undefined; } return { - cls: audits.cls ?? 0, - si: audits.si ?? 0, - fcp: audits.fcp ?? 0, - lcp: audits.lcp ?? 0, - tbt: audits.tbt ?? 0, + cls: audits.cls, + si: audits.si, + fcp: audits.fcp, + lcp: audits.lcp, + tbt: audits.tbt, }; }; @@ -187,100 +185,8 @@ class MongoChecksRepository implements IChecksRepository { }; }; - buildCheck = ( - statusResponse: MonitorStatusResponse - ) => { - const { - monitorId, - teamId, - type, - status, - responseTime, - code, - message, - payload, - first_byte_took, - body_read_took, - dns_took, - conn_took, - connect_took, - tls_took, - timings, - } = statusResponse; - - const check: any = { - metadata: { - monitorId, - teamId, - type, - }, - status, - statusCode: code, - responseTime, - timings: timings || {}, - message, - first_byte_took, - body_read_took, - dns_took, - conn_took, - connect_took, - tls_took, - }; - - if (type === "pagespeed") { - const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; - if (!pageSpeedPayload) { - this.logger.warn({ - message: "Failed to build check", - service: SERVICE_NAME, - method: "buildCheck", - details: "empty payload", - }); - return undefined; - } - const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; - const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; - const mapAudit = (audit: any) => { - if (!audit || typeof audit !== "object") { - return undefined; - } - return { - id: audit.id, - title: audit.title, - score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), - displayValue: audit.displayValue, - numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, - numericUnit: audit.numericUnit, - }; - }; - check.accessibility = (categories?.accessibility?.score || 0) * 100; - check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100; - check.seo = (categories?.seo?.score || 0) * 100; - check.performance = (categories?.performance?.score || 0) * 100; - check.audits = { - cls: mapAudit(audits?.["cumulative-layout-shift"]), - si: mapAudit(audits?.["speed-index"]), - fcp: mapAudit(audits?.["first-contentful-paint"]), - lcp: mapAudit(audits?.["largest-contentful-paint"]), - tbt: mapAudit(audits?.["total-blocking-time"]), - }; - } - - if (type === "hardware") { - const hardwarePayload = payload as HardwareStatusPayload | undefined; - const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; - const errorsSource = Array.isArray(hardwarePayload?.errors) - ? hardwarePayload?.errors - : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; - check.cpu = cpu ?? {}; - check.memory = memory ?? {}; - check.disk = disk ?? {}; - check.host = host ?? {}; - check.errors = errorsSource ?? []; - check.capture = hardwarePayload?.capture ?? {}; - check.net = net ?? {}; - } - return check; + createChecks = async (checks: Check[]) => { + return await CheckModel.insertMany(checks); }; findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => { diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 8aea221f6..3e6c727b5 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,5 +1,6 @@ -import { IMonitorsRepository } from "@/repositories/index.js"; -import type { MonitorType } from "@/types/index.js"; +import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; +import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; +import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; const SERVICE_NAME = "checkService"; @@ -11,32 +12,113 @@ class CheckService { private stringService: any; private errorService: any; private monitorsRepository: IMonitorsRepository; - + private checksRepository: IChecksRepository; + private logger: any; constructor({ db, settingsService, stringService, errorService, monitorsRepository, + logger, + checksRepository, }: { db: any; settingsService: any; stringService: any; errorService: any; monitorsRepository: IMonitorsRepository; + logger: any; + checksRepository: IChecksRepository; }) { this.db = db; this.settingsService = settingsService; this.stringService = stringService; this.errorService = errorService; this.monitorsRepository = monitorsRepository; + this.logger = logger; + this.checksRepository = checksRepository; } get serviceName() { return CheckService.SERVICE_NAME; } - buildCheck = async (statusResponse: any, type: MonitorType) => {}; + createChecks = async (checks: Check[]) => { + return this.checksRepository.createChecks(checks); + }; + + buildCheck = (statusResponse: MonitorStatusResponse) => { + const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse; + + const check: Partial = { + metadata: { + monitorId, + teamId, + type, + }, + status, + statusCode: code, + responseTime: responseTime || 0, + timings: timings, + message, + }; + + if (type === "pagespeed") { + const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; + if (!pageSpeedPayload) { + this.logger.warn({ + message: "Failed to build check", + service: SERVICE_NAME, + method: "buildCheck", + details: "empty payload", + }); + return undefined; + } + const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; + const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; + const mapAudit = (audit: any) => { + if (!audit || typeof audit !== "object") { + return undefined; + } + return { + id: audit.id, + title: audit.title, + score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), + displayValue: audit.displayValue, + numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, + numericUnit: audit.numericUnit, + }; + }; + check.accessibility = (categories?.accessibility?.score || 0) * 100; + check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100; + check.seo = (categories?.seo?.score || 0) * 100; + check.performance = (categories?.performance?.score || 0) * 100; + check.audits = { + cls: mapAudit(audits?.["cumulative-layout-shift"]), + si: mapAudit(audits?.["speed-index"]), + fcp: mapAudit(audits?.["first-contentful-paint"]), + lcp: mapAudit(audits?.["largest-contentful-paint"]), + tbt: mapAudit(audits?.["total-blocking-time"]), + }; + } + + if (type === "hardware") { + const hardwarePayload = payload as HardwareStatusPayload | undefined; + const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; + const errorsSource = Array.isArray(hardwarePayload?.errors) + ? hardwarePayload?.errors + : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; + check.cpu = cpu; + check.memory = memory; + check.disk = disk; + check.host = host; + check.errors = errorsSource; + check.capture = hardwarePayload?.capture; + check.net = net; + } + return check; + }; getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { if (!monitorId) { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index aa4c4ab10..891635d56 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -10,6 +10,8 @@ class SuperSimpleQueueHelper { private networkService: INetworkService; private statusService: any; private notificationService: any; + private checkService: any; + private buffer: any; constructor({ db, @@ -17,18 +19,24 @@ class SuperSimpleQueueHelper { networkService, statusService, notificationService, + checkService, + buffer, }: { db: any; logger: any; networkService: INetworkService; statusService: any; notificationService: any; + checkService: any; + buffer: any; }) { this.db = db; this.logger = logger; this.networkService = networkService; this.statusService = statusService; this.notificationService = notificationService; + this.checkService = checkService; + this.buffer = buffer; } get serviceName() { @@ -61,7 +69,15 @@ class SuperSimpleQueueHelper { throw new Error("No network response"); } - const statusChangeResult = await this.statusService.updateMonitorStatus(status); + // Step 3. Build check + const check = await this.checkService.buildCheck(status); + + // Step 4 Add check to buffer + this.buffer.addToBuffer({ check }); + + // Step 4. Update monitor status + const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); + this.notificationService .handleNotifications({ ...status, diff --git a/server/src/service/infrastructure/bufferService.js b/server/src/service/infrastructure/bufferService.ts similarity index 72% rename from server/src/service/infrastructure/bufferService.js rename to server/src/service/infrastructure/bufferService.ts index a218f56a0..e64a07077 100755 --- a/server/src/service/infrastructure/bufferService.js +++ b/server/src/service/infrastructure/bufferService.ts @@ -1,15 +1,38 @@ import { config } from "@/config/index.js"; +import type { Check } from "@/types/index.js"; const SERVICE_NAME = "BufferService"; class BufferService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, logger, envSettings, incidentService }) { - console.log(envSettings); + private BUFFER_TIMEOUT: number; + private db: any; + private logger: any; + private incidentService: any; + private SERVICE_NAME: string; + private buffer: any[]; + private incidentBuffer: any[]; + private bufferTimer: NodeJS.Timeout; + private checksService: any; + + constructor({ + db, + logger, + envSettings, + incidentService, + checkService, + }: { + db: any; + logger: any; + envSettings: any; + incidentService: any; + checkService: any; + }) { this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute this.db = db; this.logger = logger; this.incidentService = incidentService; + this.checksService = checkService; this.SERVICE_NAME = SERVICE_NAME; this.buffer = []; this.incidentBuffer = []; @@ -25,10 +48,10 @@ class BufferService { return BufferService.SERVICE_NAME; } - addToBuffer({ check }) { + addToBuffer({ check }: { check: Check }) { try { this.buffer.push(check); - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: this.SERVICE_NAME, @@ -38,7 +61,7 @@ class BufferService { } } - addIncidentToBuffer({ monitor, check, action = "create" }) { + addIncidentToBuffer({ monitor, check, action = "create" }: { monitor: any; check: Check; action?: string }) { try { if (!monitor || !check) { this.logger.warn({ @@ -50,7 +73,7 @@ class BufferService { } this.incidentBuffer.push({ monitor, check, action }); - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: this.SERVICE_NAME, @@ -60,20 +83,20 @@ class BufferService { } } - removeCheckFromBuffer(checkToRemove) { + removeCheckFromBuffer(checkToRemove: Check) { try { if (!checkToRemove) { return false; } const index = this.buffer.findIndex((check) => { - if (checkToRemove._id && check._id) { - return check._id.toString() === checkToRemove._id.toString(); + if (checkToRemove.id && check.id) { + return check.id.toString() === checkToRemove.id.toString(); } return ( - check.monitorId?.toString() === checkToRemove.monitorId?.toString() && - check.teamId?.toString() === checkToRemove.teamId?.toString() && - check.type === checkToRemove.type && + check.monitorId?.toString() === checkToRemove.metadata.monitorId && + check.teamId?.toString() === checkToRemove.metadata.teamId && + check.type === checkToRemove.metadata.type && check.status === checkToRemove.status && check.statusCode === checkToRemove.statusCode && check.responseTime === checkToRemove.responseTime && @@ -87,7 +110,7 @@ class BufferService { } return false; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: this.SERVICE_NAME, @@ -102,7 +125,7 @@ class BufferService { this.bufferTimer = setTimeout(async () => { try { await this.flushBuffer(); - } catch (error) { + } catch (error: any) { this.logger.error({ message: `Error in flush cycle: ${error.message}`, service: this.SERVICE_NAME, @@ -118,9 +141,9 @@ class BufferService { async flushBuffer() { try { if (this.buffer.length > 0) { - await this.db.checkModule.createChecks(this.buffer); + await this.checksService.createChecks(this.buffer); } - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: this.SERVICE_NAME, @@ -133,7 +156,7 @@ class BufferService { if (this.incidentBuffer.length > 0 && this.incidentService) { await this.flushIncidentBuffer(); } - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: this.SERVICE_NAME, @@ -154,7 +177,7 @@ class BufferService { try { const itemsToProcess = [...this.incidentBuffer]; await this.incidentService.processIncidentsFromBuffer(itemsToProcess); - } catch (error) { + } catch (error: any) { this.logger.error({ message: `Error flushing incident buffer: ${error.message}`, service: this.SERVICE_NAME, diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 23b691e0e..7d34017a5 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -1,8 +1,15 @@ import { IMonitorsRepository } from "@/repositories/index.js"; import MonitorStats from "../../db/models/MonitorStats.js"; import { CheckModel } from "@/db/models/index.js"; -import type { CheckErrorInfo, Monitor, MonitorStatusResponse, StatusChangeResult } from "@/types/index.js"; -import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; +import type { + CheckErrorInfo, + Monitor, + MonitorStatusResponse, + StatusChangeResult, + Check, + HardwareStatusPayload, + PageSpeedStatusPayload, +} from "@/types/index.js"; const SERVICE_NAME = "StatusService"; class StatusService { @@ -163,10 +170,9 @@ class StatusService { }; updateMonitorStatus = async ( - statusResponse: MonitorStatusResponse + statusResponse: MonitorStatusResponse, + check: Check ): Promise => { - const check = this.buildCheck(statusResponse); - await this.insertCheck(check); try { const { monitorId, teamId, status, code } = statusResponse; const monitor = await this.monitorsRepository.findById(monitorId, teamId); @@ -301,116 +307,7 @@ class StatusService { } }; - buildCheck = ( - networkResponse: MonitorStatusResponse - ) => { - const { - monitorId, - teamId, - type, - status, - responseTime, - code, - message, - payload, - first_byte_took, - body_read_took, - dns_took, - conn_took, - connect_took, - tls_took, - timings, - } = networkResponse; - - const check: any = { - metadata: { - monitorId, - teamId, - type, - }, - status, - statusCode: code, - responseTime, - timings: timings || {}, - message, - first_byte_took, - body_read_took, - dns_took, - conn_took, - connect_took, - tls_took, - }; - - if (type === "pagespeed") { - const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; - if (!pageSpeedPayload) { - this.logger.warn({ - message: "Failed to build check", - service: SERVICE_NAME, - method: "buildCheck", - details: "empty payload", - }); - return undefined; - } - const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; - const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; - const mapAudit = (audit: any) => { - if (!audit || typeof audit !== "object") { - return undefined; - } - return { - id: audit.id, - title: audit.title, - score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), - displayValue: audit.displayValue, - numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, - numericUnit: audit.numericUnit, - }; - }; - check.accessibility = (categories?.accessibility?.score || 0) * 100; - check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100; - check.seo = (categories?.seo?.score || 0) * 100; - check.performance = (categories?.performance?.score || 0) * 100; - check.audits = { - cls: mapAudit(audits?.["cumulative-layout-shift"]), - si: mapAudit(audits?.["speed-index"]), - fcp: mapAudit(audits?.["first-contentful-paint"]), - lcp: mapAudit(audits?.["largest-contentful-paint"]), - tbt: mapAudit(audits?.["total-blocking-time"]), - }; - } - - if (type === "hardware") { - const hardwarePayload = payload as HardwareStatusPayload | undefined; - const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; - const errorsSource = Array.isArray(hardwarePayload?.errors) - ? hardwarePayload?.errors - : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; - check.cpu = cpu ?? {}; - check.memory = memory ?? {}; - check.disk = disk ?? {}; - check.host = host ?? {}; - check.errors = errorsSource ?? []; - check.capture = hardwarePayload?.capture ?? {}; - check.net = net ?? {}; - } - return check; - }; - - /** - * Inserts a check into the database based on the network response. - * - * @param {Object} networkResponse - The network response object. - * @param {string} networkResponse.monitorId - The monitor ID. - * @param {string} networkResponse.type - The type of the response. - * @param {string} networkResponse.status - The status of the response. - * @param {number} networkResponse.responseTime - The response time. - * @param {number} networkResponse.code - The status code. - * @param {string} networkResponse.message - The message. - * @param {Object} networkResponse.payload - The payload of the response. - * @returns {Promise} A promise that resolves when the check is inserted. - */ - insertCheck = async (check: any) => { + insertCheck = async (check: Check) => { try { if (typeof check === "undefined") { this.logger.warn({ @@ -427,7 +324,7 @@ class StatusService { message: error.message, service: error.service || SERVICE_NAME, method: error.method || "insertCheck", - details: error.details || `Error inserting check for monitor: ${check?.monitorId}`, + details: error.details || `Error inserting check for monitor: ${check?.metadata.monitorId}`, stack: error.stack, }); } diff --git a/server/src/types/check.ts b/server/src/types/check.ts index e3f32280f..96c6f6e7f 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -9,40 +9,40 @@ export interface CheckMetadata { } export interface CheckCpuInfo { - physical_core: number; - logical_core: number; - frequency: number; - temperature: number[]; - free_percent: number; - usage_percent: number; + physical_core?: number; + logical_core?: number; + frequency?: number; + temperature?: number[]; + free_percent?: number; + usage_percent?: number; } export interface CheckMemoryInfo { - total_bytes: number; - available_bytes: number; - used_bytes: number; - usage_percent: number; + total_bytes?: number; + available_bytes?: number; + used_bytes?: number; + usage_percent?: number; } export interface CheckHostInfo { - os: string; - platform: string; - kernel_version: string; + os?: string; + platform?: string; + kernel_version?: string; } export interface CheckCaptureInfo { - version: string; - mode: string; + version?: string; + mode?: string; } export interface CheckDiskInfo { - device: string; - mountpoint: string; - read_speed_bytes: number; - write_speed_bytes: number; - total_bytes: number; - free_bytes: number; - usage_percent: number; + device?: string; + mountpoint?: string; + read_speed_bytes?: number; + write_speed_bytes?: number; + total_bytes?: number; + free_bytes?: number; + usage_percent?: number; } export interface CheckErrorInfo { @@ -65,11 +65,11 @@ export interface CheckNetworkInterfaceInfo { } export interface CheckAudits { - cls: ILighthouseAudit; - si: ILighthouseAudit; - fcp: ILighthouseAudit; - lcp: ILighthouseAudit; - tbt: ILighthouseAudit; + cls?: ILighthouseAudit; + si?: ILighthouseAudit; + fcp?: ILighthouseAudit; + lcp?: ILighthouseAudit; + tbt?: ILighthouseAudit; } export interface ILighthouseAudit { @@ -86,19 +86,19 @@ export interface Check { metadata: CheckMetadata; status: boolean; responseTime: number; - timings: GotTimings; + timings?: GotTimings; statusCode: number; message: string; ack: boolean; ackAt?: string | null; expiry: string; - cpu: CheckCpuInfo; - memory: CheckMemoryInfo; - disk: CheckDiskInfo[]; - host: CheckHostInfo; - errors: CheckErrorInfo[]; - capture: CheckCaptureInfo; - net: CheckNetworkInterfaceInfo[]; + cpu?: CheckCpuInfo; + memory?: CheckMemoryInfo; + disk?: CheckDiskInfo[]; + host?: CheckHostInfo; + errors?: CheckErrorInfo[]; + capture?: CheckCaptureInfo; + net?: CheckNetworkInterfaceInfo[]; accessibility?: number; bestPractices?: number; seo?: number; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 4c8dc7528..dc3bd8c09 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -1,4 +1,4 @@ -export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"] as const; +export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const; export type MonitorType = (typeof MonitorTypes)[number]; export interface MonitorThresholds { diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 097cb928d..10f89842f 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -16,7 +16,7 @@ import type { export interface MonitorStatusResponse { monitorId: string; teamId: string; - type: MonitorType | "unknown"; + type: MonitorType; status: boolean; code: number; message: string; From 2055943876a8ea870c85d91c654bd0859312b08d Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 13:10:45 -0500 Subject: [PATCH 047/103] fix: make dateRange optional for hardware details endpoint The Joi validation marks dateRange as optional, but the controller was using requireString() which throws a 400 error when dateRange is missing. Changed to optionalString() with "recent" as the default value, matching the behavior of other similar endpoints and the Joi validation schema. --- server/src/controllers/monitorController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index d46845026..56a18b6b3 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -97,7 +97,7 @@ class MonitorController { await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = requireString(req?.query?.dateRange, "dateRange"); + const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent"; const teamId = requireTeamId(req?.user?.teamId); const monitor = await this.monitorService.getHardwareDetailsById({ From 7436c2b5df6623b784a170e4c7c2cd8590458b9f Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 13:12:00 -0500 Subject: [PATCH 048/103] fix: remove unnecessary dateRange param now that backend defaults it --- client/src/Pages/Infrastructure/Create/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index e5cdbcd63..06a6a5716 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -44,7 +44,6 @@ const CreateInfrastructureMonitor = () => { // Fetch monitor details if editing const [monitor, isLoading] = useFetchHardwareMonitorById({ monitorId, - dateRange: "day", updateTrigger, }); const [deleteMonitor, isDeleting] = useDeleteMonitor(); From 5784fec3d126db67be1da05ce543ae117460783f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 18:17:55 +0000 Subject: [PATCH 049/103] format --- .../SuperSimpleQueue/SuperSimpleQueue.ts | 36 +++++++++---------- server/test/superSimpleQueueHelper.test.ts | 4 +-- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index daf7e130f..8fd55ff70 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -217,24 +217,24 @@ class SuperSimpleQueue implements ISuperSimpleQueue { }; getJobs = async () => { - const jobs = await this.scheduler.getJobs(); - return jobs.map((job) => { - return { - monitorId: job.id, - monitorUrl: job?.data?.url || null, - monitorType: job?.data?.type || null, - monitorInterval: job?.data?.interval || null, - active: job.active, - lockedAt: job.lockedAt ?? null, - runCount: job.runCount ?? 0, - failCount: job.failCount ?? 0, - failReason: job.lastFailReason ?? null, - lastRunAt: job.lastRunAt ?? null, - lastFinishedAt: job.lastFinishedAt ?? null, - lastRunTook: job.lockedAt ? null : (job.lastFinishedAt ?? 0) - (job.lastRunAt ?? 0), - lastFailedAt: job.lastFailedAt ?? null, - }; - }); + const jobs = await this.scheduler.getJobs(); + return jobs.map((job) => { + return { + monitorId: job.id, + monitorUrl: job?.data?.url || null, + monitorType: job?.data?.type || null, + monitorInterval: job?.data?.interval || null, + active: job.active, + lockedAt: job.lockedAt ?? null, + runCount: job.runCount ?? 0, + failCount: job.failCount ?? 0, + failReason: job.lastFailReason ?? null, + lastRunAt: job.lastRunAt ?? null, + lastFinishedAt: job.lastFinishedAt ?? null, + lastRunTook: job.lockedAt ? null : (job.lastFinishedAt ?? 0) - (job.lastRunAt ?? 0), + lastFailedAt: job.lastFailedAt ?? null, + }; + }); }; flushQueues = async () => { diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts index 6fba58eda..19caef876 100644 --- a/server/test/superSimpleQueueHelper.test.ts +++ b/server/test/superSimpleQueueHelper.test.ts @@ -42,9 +42,7 @@ describe("SuperSimpleQueueHelper", () => { const { helper } = createHelper({ networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) }, statusService: { - updateMonitorStatus: jest - .fn() - .mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), + updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), }, notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, }); From af141ec772d810dc155300b04a0f6c73cd6f0fb8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 18:25:28 +0000 Subject: [PATCH 050/103] build fix --- server/src/service/infrastructure/bufferService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index e64a07077..5a809eea5 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -12,7 +12,7 @@ class BufferService { private SERVICE_NAME: string; private buffer: any[]; private incidentBuffer: any[]; - private bufferTimer: NodeJS.Timeout; + private bufferTimer: NodeJS.Timeout | null = null; private checksService: any; constructor({ @@ -122,6 +122,9 @@ class BufferService { } scheduleNextFlush() { + if (this.bufferTimer) { + clearTimeout(this.bufferTimer); + } this.bufferTimer = setTimeout(async () => { try { await this.flushBuffer(); From d633fc5ccbf04d7e35bcd4d466da0d7c18d171ae Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 18:37:14 +0000 Subject: [PATCH 051/103] infra fix --- client/package-lock.json | 55 ++++++++++++------- .../Create/hooks/useInfrastructureSubmit.jsx | 2 +- .../src/Pages/Infrastructure/Create/index.jsx | 13 +++-- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 176b2c5b4..7f5397b7f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -97,6 +97,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -408,6 +409,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -451,6 +453,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1347,6 +1350,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz", "integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.26.0", "@mui/core-downloads-tracker": "^6.4.11", @@ -1511,7 +1515,6 @@ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz", "integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.1", "@mui/utils": "^7.1.0", @@ -1539,7 +1542,6 @@ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.1", "@mui/types": "^7.4.2", @@ -1570,7 +1572,6 @@ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz", "integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.1", "@emotion/cache": "^11.13.5", @@ -1646,7 +1647,6 @@ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.1", "@mui/types": "^7.4.2", @@ -2024,9 +2024,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -2712,6 +2712,7 @@ "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2733,6 +2734,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2796,6 +2798,7 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3102,6 +3105,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -3514,7 +3518,8 @@ "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.1", @@ -3921,6 +3926,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4728,6 +4734,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5264,9 +5271,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6004,6 +6011,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6017,6 +6025,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -6103,12 +6112,12 @@ } }, "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -6118,13 +6127,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -6220,7 +6229,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-persist": { "version": "6.0.0", @@ -6358,6 +6368,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -6993,6 +7004,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7112,6 +7124,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx index c5c7aacf2..04279cac4 100644 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx @@ -61,7 +61,7 @@ const useInfrastructureSubmit = () => { }; const finalForm = { - ...(isCreate ? {} : { _id: monitorId }), + ...(isCreate ? {} : { id: monitorId }), ...rest, description: form.name, type: "hardware", diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index b27c11cd4..21d70720d 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -22,9 +22,9 @@ import { useTranslation } from "react-i18next"; import { useDeleteMonitor, useFetchGlobalSettings, - useFetchHardwareMonitorById, + useFetchMonitorById, usePauseMonitor, -} from "../../../Hooks/monitorHooks.js"; +} from "@/Hooks/monitorHooks.js"; import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx"; import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx"; import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx"; @@ -37,14 +37,16 @@ const CreateInfrastructureMonitor = () => { const { t } = useTranslation(); // State + const [monitor, setMonitor] = useState(null); const [https, setHttps] = useState(false); const [updateTrigger, setUpdateTrigger] = useState(false); const [availableDisks, setAvailableDisks] = useState([]); // Fetch monitor details if editing - const [monitor, isLoading] = useFetchHardwareMonitorById({ + const [isLoading] = useFetchMonitorById({ monitorId, - updateTrigger, + setMonitor, + updateTrigger: true, }); const [deleteMonitor, isDeleting] = useDeleteMonitor(); const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings(); @@ -150,8 +152,7 @@ const CreateInfrastructureMonitor = () => { const handleRemove = async (event) => { event.preventDefault(); - const TEMP_MONITOR = { id: monitor._id }; - await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/infrastructure" }); + await deleteMonitor({ monitor, redirect: "/infrastructure" }); }; const isBusy = From 62ea7b0e5b836d9aaeea0018eb39bcac681d4c71 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 14:09:37 -0500 Subject: [PATCH 052/103] Fix horizontal overflow causing white space on narrow screens Add overflow-x: hidden and width: 100% to .home-layout to prevent horizontal scrollbar. Add min-width: 0 to .home-content-wrapper to allow flexbox child to shrink below content size at narrow widths. --- client/src/Components/v1/Layouts/HomeLayout/index.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.css b/client/src/Components/v1/Layouts/HomeLayout/index.css index 56606a935..953a2c682 100644 --- a/client/src/Components/v1/Layouts/HomeLayout/index.css +++ b/client/src/Components/v1/Layouts/HomeLayout/index.css @@ -3,6 +3,8 @@ min-height: 100vh; margin: 0 auto; padding: 0; + overflow-x: hidden; + width: 100%; } /* TODO go for this approach for responsiveness. The aside needs to be taken care of */ @@ -30,4 +32,6 @@ max-width: 1400px; margin: 0 auto; flex: 1; + min-width: 0; + overflow-x: hidden; } From af82dec2ad97fd10001e50cdd2e56b3e83f92910 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 19:12:21 +0000 Subject: [PATCH 053/103] separate user from token --- .../src/Components/v1/ActionsMenu/index.jsx | 1 - client/src/Features/Auth/authSlice.js | 29 ++++++++++--------- client/src/Hooks/monitorHooks.js | 1 - client/src/Pages/Auth/Register/index.jsx | 2 +- client/src/Pages/Uptime/Monitors/index.jsx | 1 - client/src/Utils/NetworkService.js | 13 ++------- 6 files changed, 19 insertions(+), 28 deletions(-) diff --git a/client/src/Components/v1/ActionsMenu/index.jsx b/client/src/Components/v1/ActionsMenu/index.jsx index b469e6764..ea0d029b8 100644 --- a/client/src/Components/v1/ActionsMenu/index.jsx +++ b/client/src/Components/v1/ActionsMenu/index.jsx @@ -32,7 +32,6 @@ const ActionsMenu = ({ event.preventDefault(); event.stopPropagation(); let monitor = { id: actions.id }; - console.log(monitor); await deleteMonitor({ monitor }); updateRowCallback(); }; diff --git a/client/src/Features/Auth/authSlice.js b/client/src/Features/Auth/authSlice.js index 5ee31944a..b78a8b47e 100644 --- a/client/src/Features/Auth/authSlice.js +++ b/client/src/Features/Auth/authSlice.js @@ -10,21 +10,24 @@ const initialState = { msg: null, }; -export const register = createAsyncThunk("auth/register", async (form, thunkApi) => { - try { - const res = await networkService.registerUser(form); - return res.data; - } catch (error) { - if (error.response.data) { - return thunkApi.rejectWithValue(error.response.data); +export const register = createAsyncThunk( + "auth/register", + async ({ user, token }, thunkApi) => { + try { + const res = await networkService.registerUser(user, token); + return res.data; + } catch (error) { + if (error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); } -}); +); export const login = createAsyncThunk("auth/login", async (form, thunkApi) => { try { diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index d865ffab8..4fdedbcff 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -342,7 +342,6 @@ export const useDeleteMonitor = () => { const deleteMonitor = async ({ monitor, redirect }) => { try { setIsLoading(true); - console.log(monitor); await networkService.deleteMonitorById({ monitorId: monitor.id }); createToast({ body: "Monitor deleted successfully!" }); if (redirect) { diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/Auth/Register/index.jsx index 3345d8aa0..821c56427 100644 --- a/client/src/Pages/Auth/Register/index.jsx +++ b/client/src/Pages/Auth/Register/index.jsx @@ -176,7 +176,7 @@ const Register = ({ superAdminExists }) => { delete toSubmit.confirm; - const action = await dispatch(register(toSubmit)); + const action = await dispatch(register({ user: toSubmit, token })); if (action.payload.success) { navigate("/uptime"); createToast({ diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index 101020eea..67019362f 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -143,7 +143,6 @@ const UptimeMonitors = () => { type: ["http", "ping", "docker", "port", "game"], }); - console.log(monitors); useEffect(() => { if (isSearching) { setPage(undefined); diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index bd0e37b30..5b28281fe 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -347,17 +347,8 @@ class NetworkService { return this.axiosInstance.get(`/monitors/certificate/${config.monitorId}`); } - /** - * ************************************ - * Registers a new user - * ************************************ - * - * @async - * @param {Object} form - The form data for the new user to be registered. - * @returns {Promise} The response from the axios POST request. - */ - async registerUser(form) { - return this.axiosInstance.post(`/auth/register`, form); + async registerUser(user, token) { + return this.axiosInstance.post(`/auth/register`, { user, token }); } /** From 64dc381e0f527c7ec6f7214983d7c3a1674447c9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 19:14:03 +0000 Subject: [PATCH 054/103] separate user from token --- server/src/controllers/authController.ts | 10 +- server/src/db/models/User.js | 105 ------------------ server/src/db/models/User.ts | 95 ++++++++++++++++ server/src/db/models/index.ts | 3 + server/src/repositories/index.ts | 3 + .../src/repositories/users/IUserRepository.ts | 9 ++ .../repositories/users/MongoUserRepository.ts | 4 + server/src/service/business/userService.ts | 5 +- server/src/types/index.ts | 1 + server/src/types/user.ts | 24 ++++ 10 files changed, 148 insertions(+), 111 deletions(-) delete mode 100755 server/src/db/models/User.js create mode 100644 server/src/db/models/User.ts create mode 100644 server/src/repositories/users/IUserRepository.ts create mode 100644 server/src/repositories/users/MongoUserRepository.ts create mode 100644 server/src/types/user.ts diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index d033aa39d..435813a36 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -32,11 +32,13 @@ class AuthController { registerUser = async (req: Request, res: Response, next: NextFunction) => { try { - if (req.body?.email) { - req.body.email = req.body.email?.toLowerCase(); + const newUser = req.body.user; + const newUserToken = req.body.token; + if (newUser?.email) { + newUser.email = newUser.email.toLowerCase(); } - await registrationBodyValidation.validateAsync(req.body); - const { user, token } = await this.userService.registerUser(req.body, req.file); + await registrationBodyValidation.validateAsync(newUser); + const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file); res.status(200).json({ success: true, msg: "User registered successfully", diff --git a/server/src/db/models/User.js b/server/src/db/models/User.js deleted file mode 100755 index bf3012b26..000000000 --- a/server/src/db/models/User.js +++ /dev/null @@ -1,105 +0,0 @@ -import mongoose from "mongoose"; -import bcrypt from "bcryptjs"; -import Monitor from "./Monitor.js"; -import Team from "./Team.js"; -import Notification from "./Notification.js"; - -const UserSchema = mongoose.Schema( - { - firstName: { - type: String, - required: true, - }, - lastName: { - type: String, - required: true, - }, - email: { - type: String, - required: true, - unique: true, - }, - password: { - type: String, - required: true, - }, - avatarImage: { - type: String, - }, - profileImage: { - data: Buffer, - contentType: String, - }, - isActive: { - type: Boolean, - default: true, - }, - isVerified: { - type: Boolean, - default: false, - }, - role: { - type: [String], - default: "user", - enum: ["user", "admin", "superadmin", "demo"], - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - }, - checkTTL: { - type: Number, - }, - }, - { - timestamps: true, - } -); - -UserSchema.pre("save", function (next) { - if (!this.isModified("password")) { - return next(); - } - const salt = bcrypt.genSaltSync(10); - this.password = bcrypt.hashSync(this.password, salt); - next(); -}); - -UserSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if ("password" in update) { - const salt = bcrypt.genSaltSync(10); - update.password = bcrypt.hashSync(update.password, salt); - } - - next(); -}); - -UserSchema.pre("findOneAndDelete", async function (next) { - try { - const userToDelete = await this.model.findOne(this.getFilter()); - if (!userToDelete) return next(); - if (userToDelete.role.includes("superadmin")) { - await Team.deleteOne({ _id: userToDelete.teamId }); - await Monitor.deleteMany({ userId: userToDelete._id }); - await this.model.deleteMany({ - teamId: userToDelete.teamId, - _id: { $ne: userToDelete._id }, - }); - await Notification.deleteMany({ teamId: userToDelete.teamId }); - } - next(); - } catch (error) { - next(error); - } -}); - -UserSchema.methods.comparePassword = async function (submittedPassword) { - const res = await bcrypt.compare(submittedPassword, this.password); - return res; -}; - -const User = mongoose.model("User", UserSchema); - -export default User; diff --git a/server/src/db/models/User.ts b/server/src/db/models/User.ts new file mode 100644 index 000000000..e00250340 --- /dev/null +++ b/server/src/db/models/User.ts @@ -0,0 +1,95 @@ +import { Schema, model, type Types } from "mongoose"; +import bcrypt from "bcryptjs"; +import type { User, UserProfileImage, UserRole } from "@/types/index.js"; +import { MonitorModel } from "@/db/models/index.js"; +import Team from "./Team.js"; +import Notification from "./Notification.js"; + +type UserDocumentBase = Omit & { + teamId?: Types.ObjectId; + profileImage?: Required; +}; + +interface UserDocument extends UserDocumentBase { + _id: Types.ObjectId; + teamId?: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const profileImageSchema = new Schema>( + { + data: { type: Buffer }, + contentType: { type: String }, + }, + { _id: false } +); + +const UserSchema = new Schema( + { + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + avatarImage: { type: String }, + profileImage: { type: profileImageSchema }, + isActive: { type: Boolean, default: true }, + isVerified: { type: Boolean, default: false }, + role: { + type: [String], + enum: ["user", "admin", "superadmin", "demo" satisfies UserRole], + default: ["user"], + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + }, + checkTTL: { type: Number }, + }, + { timestamps: true } +); + +UserSchema.pre("save", function (next) { + if (!this.isModified("password")) { + return next(); + } + const salt = bcrypt.genSaltSync(10); + this.password = bcrypt.hashSync(this.password, salt); + next(); +}); + +UserSchema.pre("findOneAndUpdate", function (next) { + const update = this.getUpdate(); + if (update && "password" in update) { + const salt = bcrypt.genSaltSync(10); + (update as any).password = bcrypt.hashSync((update as any).password, salt); + } + next(); +}); + +UserSchema.pre("findOneAndDelete", async function (next) { + try { + const userToDelete = await this.model.findOne(this.getFilter()); + if (!userToDelete) return next(); + if (userToDelete.role.includes("superadmin")) { + await Team.deleteOne({ _id: userToDelete.teamId }); + await MonitorModel.deleteMany({ userId: userToDelete._id }); + await this.model.deleteMany({ teamId: userToDelete.teamId, _id: { $ne: userToDelete._id } }); + await Notification.deleteMany({ teamId: userToDelete.teamId }); + } + next(); + } catch (error) { + next(error as Error); + } +}); + +UserSchema.methods.comparePassword = async function (submittedPassword: string) { + return bcrypt.compare(submittedPassword, this.password); +}; + +const UserModel = model("User", UserSchema); + +export type { UserDocument }; +export { UserModel }; +export default UserModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index b92ae18b7..d91aa0778 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -9,3 +9,6 @@ export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js"; export * from "@/db/models/StatusPage.js"; export { default as StatusPageModel } from "@/db/models/StatusPage.js"; + +export * from "@/db/models/User.js"; +export { default as UserModel } from "@/db/models/User.js"; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 2c27591f1..92d9b2371 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -9,3 +9,6 @@ export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-s export * from "@/repositories/status-pages/IStatusPagesRepository.js"; export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js"; + +export * from "@/repositories/users/IUserRepository.js"; +export { default as MongoUserRepository } from "@/repositories/users/MongoUserRepository.js"; diff --git a/server/src/repositories/users/IUserRepository.ts b/server/src/repositories/users/IUserRepository.ts new file mode 100644 index 000000000..259ba781b --- /dev/null +++ b/server/src/repositories/users/IUserRepository.ts @@ -0,0 +1,9 @@ +import type { User } from "@/types/index.js"; + +export interface IUsersRepository { + // create + // fetch + // update + // delete + // other +} diff --git a/server/src/repositories/users/MongoUserRepository.ts b/server/src/repositories/users/MongoUserRepository.ts new file mode 100644 index 000000000..82c5eb5b6 --- /dev/null +++ b/server/src/repositories/users/MongoUserRepository.ts @@ -0,0 +1,4 @@ +import { IUsersRepository } from "@/repositories/index.js"; +class MongoUserRepository implements IUsersRepository {} + +export default MongoUserRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 129ed1e59..e409c9f98 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,4 +1,5 @@ import { IMonitorsRepository } from "@/repositories/index.js"; +import type { User } from "@/types/index.js"; const SERVICE_NAME = "userService"; @@ -62,12 +63,12 @@ class UserService { return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); }; - registerUser = async (user: any, file: any) => { + registerUser = async (user: Partial, inviteToken: string, file: any) => { // Create a new user // If superAdmin exists, a token should be attached to all further register requests const superAdminExists = await this.db.userModule.checkSuperadmin(); if (superAdminExists) { - const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(user.inviteToken); + const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(inviteToken); user.role = invitedUser.role; user.teamId = invitedUser.teamId; } else { diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 4f71788f2..b6495497d 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -3,3 +3,4 @@ export * from "@/types/monitor.js"; export * from "@/types/monitorStats.js"; export * from "@/types/statusPage.js"; export * from "@/types/network.js"; +export * from "@/types/user.js"; diff --git a/server/src/types/user.ts b/server/src/types/user.ts new file mode 100644 index 000000000..41c009791 --- /dev/null +++ b/server/src/types/user.ts @@ -0,0 +1,24 @@ +export const UserRoles = ["user", "admin", "superadmin", "demo"] as const; +export type UserRole = (typeof UserRoles)[number]; + +export interface UserProfileImage { + data?: Buffer; + contentType?: string; +} + +export interface User { + id: string; + firstName: string; + lastName: string; + email: string; + password: string; + avatarImage?: string; + profileImage?: UserProfileImage; + isActive: boolean; + isVerified: boolean; + role: UserRole[]; + teamId: string; + checkTTL?: number; + createdAt: string; + updatedAt: string; +} From 5c52a208b9d33676cbcead52bdb8f4339c17abd3 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 14:44:28 -0500 Subject: [PATCH 055/103] Fix build errors and formatting - Replace CheckIcon SVG import with Lucide Icon component in LatestIncidentsPanel/index.jsx - Format IncidentItem.jsx and StatisticsPanel.jsx with Prettier --- CLAUDE.md | 145 ++++++++++++++++++ .../LatestIncidentsPanel/IncidentItem.jsx | 5 +- .../Components/LatestIncidentsPanel/index.jsx | 21 +-- .../Components/StatisticsPanel/index.jsx | 15 +- 4 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1535e84cf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,145 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature). + +## Development Commands + +### Client (React/Vite) +```bash +cd client +npm install +npm run dev # Start dev server at http://localhost:5173 +npm run build # TypeScript check + production build +npm run lint # ESLint (strict, max-warnings 0) +npm run format # Prettier formatting +npm run format-check # Check formatting +``` + +### Server (Node.js/Express) +```bash +cd server +npm install +npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345 +npm run build # TypeScript compile + path alias resolution +npm run test # Run Mocha tests with c8 coverage +npm run lint # ESLint v9 +npm run lint-fix # Auto-fix lint issues +npm run format # Prettier formatting +``` + +### Docker Development +```bash +cd docker/dev +./build_images.sh +docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0 +``` + +## Environment Setup + +### Server `.env` (minimum required) +```env +CLIENT_HOST="http://localhost:5173" +JWT_SECRET="my_secret_key_change_this" +DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db" +TOKEN_TTL="99d" +ORIGIN="localhost" +LOG_LEVEL="debug" +``` + +### Client `.env` +```env +VITE_APP_API_BASE_URL="http://localhost:52345/api/v1" +VITE_APP_LOG_LEVEL="debug" +``` + +## Architecture + +### Monorepo Structure +- `/client` - React 18 + TypeScript + Vite + MUI frontend +- `/server` - Node.js 20+ + Express + TypeScript backend +- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono) + +### Backend Layers +``` +server/src/ +├── controllers/ # Route handlers (authController, monitorController, etc.) +├── service/ # Business logic +│ ├── business/ # Core monitoring logic +│ ├── infrastructure/ # Server/system utilities +│ └── system/ # App-level settings +├── db/ +│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.) +│ ├── migration/ # Database migrations (run on startup) +│ └── modules/ # Database-specific modules +├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler +├── routes/v1/ # API route definitions +├── validation/ # Joi input validation schemas +└── repositories/ # Data access layer +``` + +### Frontend Structure +``` +client/src/ +├── Components/ # Reusable UI components +├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.) +├── Features/ # Redux slices (Auth, UI) +├── Hooks/ # Custom React hooks +├── Utils/ # Utilities (NetworkService.js is main API client) +├── Validation/ # Input validation +└── locales/ # i18n translations +``` + +### API +- Base URL: `/api/v1` +- Documentation: `http://localhost:52345/api-docs` (Swagger UI) +- OpenAPI spec: `/server/openapi.json` + +### Key Technologies +- **State Management**: Redux Toolkit + Redux-Persist +- **Data Fetching**: SWR + Axios +- **Database**: MongoDB with Mongoose ODM +- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling) +- **i18n**: i18next + react-i18next (translations via PoEditor) + +## Code Conventions + +### Internationalization +All user-facing strings must use the translation function: +```javascript +t('your.key') // Never hardcode UI strings +``` + +### Branching +- Always branch from `develop` (not master) +- Use descriptive names: `feat/add-alerts`, `fix/login-error` +- PRs target `develop` branch + +### Formatting +- **Client**: Prettier with `printWidth: 90`, tabs, double quotes +- **Server**: Prettier with `printWidth: 150`, tabs, double quotes +- Both use ESLint with strict settings + +### Testing +Server tests use Mocha + Chai + Sinon: +```bash +npm test # Run all tests with coverage +npm test -- --grep "pattern" # Run specific tests +``` +Test files: `server/tests/**/*.test.js` + +## Database Models + +Key Mongoose models in `/server/src/db/models/`: +- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.) +- **Check** - Individual monitoring check results +- **Incident** - Downtime incidents +- **User** - User accounts +- **Team** - Team/workspace management +- **StatusPage** - Public status pages +- **Notification** - Alert configuration (email, Discord, Slack, webhooks) +- **MaintenanceWindow** - Scheduled maintenance periods +- **AppSettings** - Global application settings diff --git a/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx index 67254f548..0ecb34791 100644 --- a/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx +++ b/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx @@ -67,7 +67,10 @@ const IncidentItem = ({ incident }) => { gap={theme.spacing(2)} > - + - - + + { sx={rowStyle} > - + { sx={rowStyle} > - + { sx={rowStyle} > - + Date: Fri, 16 Jan 2026 20:29:54 +0000 Subject: [PATCH 056/103] update user refactor --- server/src/config/services.ts | 17 ++- server/src/controllers/authController.ts | 1 + server/src/db/models/Invite.ts | 30 +++++ server/src/db/models/InviteToken.js | 35 ----- server/src/db/models/index.ts | 3 + server/src/repositories/index.ts | 7 +- .../invites/IInvitesRepository.ts | 11 ++ .../invites/MongoInviteRepository.ts | 45 +++++++ .../src/repositories/users/IUserRepository.ts | 9 -- .../repositories/users/IUsersRepository.ts | 12 ++ .../repositories/users/MongoUserRepository.ts | 4 - .../users/MongoUsersRepository.ts | 125 ++++++++++++++++++ server/src/service/business/userService.ts | 56 +++++--- server/src/types/index.ts | 1 + server/src/types/invite.ts | 12 ++ server/src/utils/utils.js | 5 - 16 files changed, 293 insertions(+), 80 deletions(-) create mode 100755 server/src/db/models/Invite.ts delete mode 100755 server/src/db/models/InviteToken.js create mode 100644 server/src/repositories/invites/IInvitesRepository.ts create mode 100644 server/src/repositories/invites/MongoInviteRepository.ts delete mode 100644 server/src/repositories/users/IUserRepository.ts create mode 100644 server/src/repositories/users/IUsersRepository.ts delete mode 100644 server/src/repositories/users/MongoUserRepository.ts create mode 100644 server/src/repositories/users/MongoUsersRepository.ts create mode 100644 server/src/types/invite.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index af49cc2d4..66fb18f47 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -37,9 +37,6 @@ import crypto from "crypto"; import { games, GameDig } from "gamedig"; import jmespath from "jmespath"; -import { fileURLToPath } from "url"; -import { ObjectId } from "mongodb"; - // DB Modules import { NormalizeData, NormalizeDataUptimeDetails } from "../utils/dataUtils.js"; import { GenerateAvatarImage } from "../utils/imageProcessing.js"; @@ -48,7 +45,7 @@ import { ParseBoolean } from "../utils/utils.js"; // Models import Monitor from "../db/models/Monitor.js"; import User from "../db/models/User.js"; -import InviteToken from "../db/models/InviteToken.js"; +import InviteToken from "../db/models/Invite.js"; import StatusPage from "../db/models/StatusPage.js"; import Team from "../db/models/Team.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; @@ -74,10 +71,14 @@ import { MongoChecksRepository, MongoMonitorStatsRepository, MongoStatusPagesRepository, + MongoUsersRepository, + MongoInvitesRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, IStatusPagesRepository, + IUsersRepository, + IInvitesRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -107,6 +108,8 @@ export type InitializedSerivces = { checksRepository: IChecksRepository; monitorStatsRepository: IMonitorStatsRepository; statusPagesRepository: IStatusPagesRepository; + usersRepository: IUsersRepository; + invitesRepository: IInvitesRepository; }; export const initializeServices = async ({ @@ -158,6 +161,8 @@ export const initializeServices = async ({ const checksRepository = new MongoChecksRepository(logger); const monitorStatsRepository = new MongoMonitorStatsRepository(); const statusPagesRepository = new MongoStatusPagesRepository(); + const usersRepository = new MongoUsersRepository(); + const invitesRepository = new MongoInvitesRepository(); const networkService = new NetworkService({ axios, @@ -240,6 +245,8 @@ export const initializeServices = async ({ errorService, jobQueue: superSimpleQueue, monitorsRepository, + usersRepository, + invitesRepository, }); const diagnosticService = new DiagnosticService(); @@ -298,6 +305,8 @@ export const initializeServices = async ({ checksRepository, monitorStatsRepository, statusPagesRepository, + usersRepository, + invitesRepository, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 435813a36..1b76b249e 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -38,6 +38,7 @@ class AuthController { newUser.email = newUser.email.toLowerCase(); } await registrationBodyValidation.validateAsync(newUser); + const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file); res.status(200).json({ success: true, diff --git a/server/src/db/models/Invite.ts b/server/src/db/models/Invite.ts new file mode 100755 index 000000000..25ddd9a72 --- /dev/null +++ b/server/src/db/models/Invite.ts @@ -0,0 +1,30 @@ +import { Schema, model, type Types } from "mongoose"; +import type { Invite } from "@/types/invite.js"; + +type InviteDocumentBase = Omit & { + teamId: Types.ObjectId; + expiry: Date; +}; + +interface InviteDocument extends InviteDocumentBase { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const InviteSchema = new Schema( + { + email: { type: String, required: true, unique: true }, + teamId: { type: Schema.Types.ObjectId, ref: "Team", immutable: true, required: true }, + role: { type: [String], required: true, default: ["user"] }, + token: { type: String, required: true }, + expiry: { type: Date, default: Date.now, expires: 3600 }, + }, + { timestamps: true } +); + +const InviteModel = model("Invite", InviteSchema); + +export type { InviteDocument }; +export { InviteModel }; +export default InviteModel; diff --git a/server/src/db/models/InviteToken.js b/server/src/db/models/InviteToken.js deleted file mode 100755 index 079600647..000000000 --- a/server/src/db/models/InviteToken.js +++ /dev/null @@ -1,35 +0,0 @@ -import mongoose from "mongoose"; -const InviteTokenSchema = mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - role: { - type: Array, - required: true, - default: ["user"], - }, - token: { - type: String, - required: true, - }, - expiry: { - type: Date, - default: Date.now, - expires: 3600, - }, - }, - { - timestamps: true, - } -); - -export default mongoose.model("InviteToken", InviteTokenSchema); diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index d91aa0778..d378c6750 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -12,3 +12,6 @@ export { default as StatusPageModel } from "@/db/models/StatusPage.js"; export * from "@/db/models/User.js"; export { default as UserModel } from "@/db/models/User.js"; + +export * from "@/db/models/Invite.js"; +export { default as InviteModel } from "@/db/models/Invite.js"; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 92d9b2371..7afe3a9f4 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -10,5 +10,8 @@ export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-s export * from "@/repositories/status-pages/IStatusPagesRepository.js"; export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js"; -export * from "@/repositories/users/IUserRepository.js"; -export { default as MongoUserRepository } from "@/repositories/users/MongoUserRepository.js"; +export * from "@/repositories/users/IUsersRepository.js"; +export { default as MongoUsersRepository } from "@/repositories/users/MongoUsersRepository.js"; + +export * from "@/repositories/invites/IInvitesRepository.js"; +export { default as MongoInvitesRepository } from "@/repositories/invites/MongoInviteRepository.js"; diff --git a/server/src/repositories/invites/IInvitesRepository.ts b/server/src/repositories/invites/IInvitesRepository.ts new file mode 100644 index 000000000..066a51a0f --- /dev/null +++ b/server/src/repositories/invites/IInvitesRepository.ts @@ -0,0 +1,11 @@ +import type { Invite } from "@/types/index.js"; + +export interface IInvitesRepository { + // create + // fetch + findByTokenAndDelete(token: string): Promise; + // update + + // delete + // other +} diff --git a/server/src/repositories/invites/MongoInviteRepository.ts b/server/src/repositories/invites/MongoInviteRepository.ts new file mode 100644 index 000000000..16e56440b --- /dev/null +++ b/server/src/repositories/invites/MongoInviteRepository.ts @@ -0,0 +1,45 @@ +import { IInvitesRepository } from "@/repositories/index.js"; +import type { Invite } from "@/types/index.js"; +import { type InviteDocument, InviteModel } from "@/db/models/index.js"; +import { AppError } from "@/utils/AppError.js"; +import mongoose from "mongoose"; + +class MongoInvitesRepository implements IInvitesRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: InviteDocument): Invite => { + return { + id: this.toStringId(doc._id), + email: doc.email, + teamId: this.toStringId(doc.teamId), + role: doc.role ?? [], + token: doc.token, + expiry: this.toDateString(doc.expiry), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + findByTokenAndDelete = async (token: string) => { + const invite = await InviteModel.findOneAndDelete({ + token, + }); + if (invite === null) { + throw new AppError({ message: "Invite not found", status: 404 }); + } + return this.toEntity(invite); + }; +} +export default MongoInvitesRepository; diff --git a/server/src/repositories/users/IUserRepository.ts b/server/src/repositories/users/IUserRepository.ts deleted file mode 100644 index 259ba781b..000000000 --- a/server/src/repositories/users/IUserRepository.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { User } from "@/types/index.js"; - -export interface IUsersRepository { - // create - // fetch - // update - // delete - // other -} diff --git a/server/src/repositories/users/IUsersRepository.ts b/server/src/repositories/users/IUsersRepository.ts new file mode 100644 index 000000000..9678de47a --- /dev/null +++ b/server/src/repositories/users/IUsersRepository.ts @@ -0,0 +1,12 @@ +import type { User } from "@/types/index.js"; +export interface IUsersRepository { + // create + create(user: Partial, imageFile?: Express.Multer.File | null): Promise; + // fetch + findByEmail(email: string): Promise; + // update + updateById(id: string, patch: Partial, file?: Express.Multer.File | null): Promise; + // delete + // other + findSuperAdmin(): Promise; +} diff --git a/server/src/repositories/users/MongoUserRepository.ts b/server/src/repositories/users/MongoUserRepository.ts deleted file mode 100644 index 82c5eb5b6..000000000 --- a/server/src/repositories/users/MongoUserRepository.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { IUsersRepository } from "@/repositories/index.js"; -class MongoUserRepository implements IUsersRepository {} - -export default MongoUserRepository; diff --git a/server/src/repositories/users/MongoUsersRepository.ts b/server/src/repositories/users/MongoUsersRepository.ts new file mode 100644 index 000000000..b2687ff4a --- /dev/null +++ b/server/src/repositories/users/MongoUsersRepository.ts @@ -0,0 +1,125 @@ +import mongoose from "mongoose"; +import { IUsersRepository } from "@/repositories/index.js"; +import { UserModel, type UserDocument } from "@/db/models/index.js"; +import type { User, UserProfileImage } from "@/types/index.js"; +import { GenerateAvatarImage } from "@/utils/imageProcessing.js"; +import { ParseBoolean } from "@/utils/utils.js"; +import { AppError } from "@/utils/AppError.js"; +const SERVICE_NAME = "MongoUsersRepository"; + +class MongoUsersRepository implements IUsersRepository { + static SERVICE_NAME = SERVICE_NAME; + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + private mapProfileImage = (image?: (UserProfileImage & { data?: Buffer }) | null) => { + if (!image) { + return undefined; + } + return { + data: image.data, + contentType: image.contentType, + }; + }; + + protected toEntity = (doc: UserDocument): User => { + return { + id: this.toStringId(doc._id), + firstName: doc.firstName, + lastName: doc.lastName, + email: doc.email, + password: doc.password, + avatarImage: doc.avatarImage ?? undefined, + profileImage: this.mapProfileImage(doc.profileImage), + isActive: doc.isActive ?? false, + isVerified: doc.isVerified ?? false, + role: doc.role ?? [], + teamId: this.toStringId(doc.teamId), + checkTTL: doc.checkTTL ?? undefined, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (user: Partial, imageFile: Express.Multer.File | null) => { + if (imageFile) { + // 1. Save the full size image + user.profileImage = { + data: imageFile.buffer, + contentType: imageFile.mimetype, + }; + + // 2. Get the avatar sized image + const avatar = await GenerateAvatarImage(imageFile); + user.avatarImage = avatar; + } + + const newUser = new UserModel(user); + await newUser.save(); + const sanitizedUser = await UserModel.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); + if (!sanitizedUser) { + throw new AppError({ message: "Failed to create user", service: SERVICE_NAME, status: 500 }); + } + return this.toEntity(sanitizedUser); + }; + + findByEmail = async (email: string) => { + const user = await UserModel.findOne({ email: email }).select("-profileImage"); + if (!user) { + throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(user); + }; + + updateById = async (id: string, patch: Partial, file?: Express.Multer.File | null): Promise => { + const candidateUser = { ...patch }; + + if (ParseBoolean(candidateUser.deleteProfileImage) === true) { + candidateUser.profileImage = undefined; + candidateUser.avatarImage = undefined; + } else if (file) { + // 1. Save the full size image + candidateUser.profileImage = { + data: file.buffer, + contentType: file.mimetype, + }; + + // 2. Get the avatar sized image + const avatar = await GenerateAvatarImage(file); + candidateUser.avatarImage = avatar; + } + + const updatedUser = await UserModel.findOneAndUpdate( + { _id: id }, + candidateUser, + { new: true } // Returns updated user instead of pre-update user + ) + .select("-password") + .select("-profileImage"); + if (!updatedUser) { + throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(updatedUser); + }; + + findSuperAdmin = async () => { + const superAdmin = await UserModel.findOne({ role: "superadmin" }); + if (superAdmin !== null) { + return true; + } + return false; + }; +} + +export default MongoUsersRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index e409c9f98..dca362ebe 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,5 +1,7 @@ -import { IMonitorsRepository } from "@/repositories/index.js"; +import { IInvitesRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; +import Team from "@/db/models/Team.js"; import type { User } from "@/types/index.js"; +import bcrypt from "bcryptjs"; const SERVICE_NAME = "userService"; @@ -16,6 +18,8 @@ class UserService { private jobQueue: any; private crypto: any; private monitorsRepository: IMonitorsRepository; + private usersRepository: IUsersRepository; + private invitesRepository: IInvitesRepository; constructor({ crypto, @@ -28,6 +32,8 @@ class UserService { errorService, jobQueue, monitorsRepository, + usersRepository, + invitesRepository, }: { crypto: any; db: any; @@ -39,6 +45,8 @@ class UserService { errorService: any; jobQueue: any; monitorsRepository: IMonitorsRepository; + usersRepository: IUsersRepository; + invitesRepository: IInvitesRepository; }) { this.db = db; this.emailService = emailService; @@ -50,6 +58,8 @@ class UserService { this.jobQueue = jobQueue; this.crypto = crypto; this.monitorsRepository = monitorsRepository; + this.usersRepository = usersRepository; + this.invitesRepository = invitesRepository; } get serviceName() { @@ -66,33 +76,37 @@ class UserService { registerUser = async (user: Partial, inviteToken: string, file: any) => { // Create a new user // If superAdmin exists, a token should be attached to all further register requests - const superAdminExists = await this.db.userModule.checkSuperadmin(); + const superAdminExists = await this.usersRepository.findSuperAdmin(); if (superAdminExists) { - const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(inviteToken); - user.role = invitedUser.role; - user.teamId = invitedUser.teamId; + const invite = await this.invitesRepository.findByTokenAndDelete(inviteToken); + user.role = invite.role; + user.teamId = invite.teamId; } else { // This is the first account, create JWT secret to use if one is not supplied by env const jwtSecret = this.crypto.randomBytes(64).toString("hex"); await this.db.settingsModule.updateAppSettings({ jwtSecret }); + // Create a new team + const team = new Team({ + email: user.email, + }); + user.teamId = team._id; } - const newUser = await this.db.userModule.insertUser({ ...user }, file); + const newUser = await this.usersRepository.create({ ...user }, file); this.logger.debug({ message: "New user created", service: SERVICE_NAME, method: "registerUser", - details: newUser._id, + details: newUser.id, }); - const userForToken = { ...newUser._doc }; - delete userForToken.profileImage; - delete userForToken.avatarImage; + delete newUser.profileImage; + delete newUser.avatarImage; const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(userForToken, appSettings); + const token = this.issueToken(newUser, appSettings); try { const html = await this.emailService.buildEmail("welcomeEmailTemplate", { @@ -120,17 +134,18 @@ class UserService { loginUser = async (email: string, password: string) => { // Check if user exists - const user = await this.db.userModule.getUserByEmail(email); + const user = await this.usersRepository.findByEmail(email); // Compare password - const match = await user.comparePassword(password); + const match = await bcrypt.compare(password, user.password); + if (match !== true) { throw this.errorService.createAuthenticationError(this.stringService.authIncorrectPassword); } // Remove password from user object. Should this be abstracted to DB layer? - const userWithoutPassword = { ...user._doc }; - delete userWithoutPassword.password; - delete userWithoutPassword.avatarImage; + const userWithoutPassword = { ...user }; + userWithoutPassword.password = ""; + userWithoutPassword.avatarImage = ""; // Happy path, return token const appSettings = await this.settingsService.getSettings(); @@ -140,16 +155,16 @@ class UserService { return { user: userWithoutPassword, token }; }; - editUser = async (updates: any, file: any, currentUser: any) => { + editUser = async (updates: Partial, file: any, currentUser: any) => { // Change Password check if (updates?.password && updates?.newPassword) { // Get user's email // Add user email to body for DB operation updates.email = currentUser.email; // Get user - const user = await this.db.userModule.getUserByEmail(currentUser.email); + const user = await this.usersRepository.findByEmail(currentUser.email); // Compare passwords - const match = await user.comparePassword(updates?.password); + const match = await bcrypt.compare(updates?.password, user.password); // If not a match, throw a 403 // 403 instead of 401 to avoid triggering axios interceptor if (!match) { @@ -159,8 +174,7 @@ class UserService { updates.password = updates.newPassword; } - const updatedUser = await this.db.userModule.updateUser({ userId: currentUser?._id, user: updates, file: file }); - return updatedUser; + return await this.usersRepository.updateById(currentUser.id, updates, file); }; checkSuperadminExists = async () => { diff --git a/server/src/types/index.ts b/server/src/types/index.ts index b6495497d..9416de9fa 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -4,3 +4,4 @@ export * from "@/types/monitorStats.js"; export * from "@/types/statusPage.js"; export * from "@/types/network.js"; export * from "@/types/user.js"; +export * from "@/types/invite.js"; diff --git a/server/src/types/invite.ts b/server/src/types/invite.ts new file mode 100644 index 000000000..8e65d54e9 --- /dev/null +++ b/server/src/types/invite.ts @@ -0,0 +1,12 @@ +import { UserRole } from "./user.js"; + +export interface Invite { + id: string; + email: string; + teamId: string; + role: UserRole[]; + token: string; + expiry: string; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/utils/utils.js b/server/src/utils/utils.js index bcb4d8e5e..ee4ee1c6d 100755 --- a/server/src/utils/utils.js +++ b/server/src/utils/utils.js @@ -1,8 +1,3 @@ -/** - * Converts a request body parameter to a boolean. - * @param {string | boolean} value - * @returns {boolean} - */ const ParseBoolean = (value) => { if (value === true || value === "true") { return true; From 6447c315ce28175cef7698b880c440878c1aa138 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 21:04:41 +0000 Subject: [PATCH 057/103] stats fixes --- client/src/Hooks/monitorHooks.js | 6 ++++-- .../Components/PageSpeedStatusBoxes/index.jsx | 20 +++++++++++++++---- client/src/Pages/PageSpeed/Details/index.jsx | 4 ++-- .../Components/UptimeStatusBoxes/index.jsx | 5 ++++- server/src/controllers/monitorController.ts | 4 ++-- server/src/service/business/monitorService.ts | 10 ++++++++-- 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index d865ffab8..997eae249 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -243,6 +243,7 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg const [isLoading, setIsLoading] = useState(true); const [networkError, setNetworkError] = useState(false); const [monitor, setMonitor] = useState(undefined); + const [monitorStats, setMonitorStats] = useState(undefined); useEffect(() => { const fetchMonitor = async () => { @@ -254,7 +255,8 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg monitorId: monitorId, dateRange: dateRange, }); - setMonitor(response.data.data); + setMonitor(response.data.data.monitor); + setMonitorStats(response.data.data.monitorStats); } catch (error) { setNetworkError(true); } finally { @@ -263,7 +265,7 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg }; fetchMonitor(); }, [monitorId, dateRange, updateTrigger]); - return [monitor, isLoading, networkError]; + return [monitor, monitorStats, isLoading, networkError]; }; export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => { diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx index 493d1ece6..872b1e293 100644 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx @@ -4,9 +4,21 @@ import { Typography } from "@mui/material"; import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js"; import { useTranslation } from "react-i18next"; -const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => { - const uptimeDuration = getHumanReadableDuration(monitor?.uptimeDuration); - const time = getHumanReadableDuration(monitor?.lastChecked); +const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => { + const lastChecked = monitorStats?.lastCheckTimestamp + ? Date.now() - monitorStats.lastCheckTimestamp + : 0; + + // Determine time since last failure + const timeOfLastFailure = monitorStats?.timeOfLastFailure; + const timeSinceLastFailure = + timeOfLastFailure > 0 + ? Date.now() - timeOfLastFailure + : Date.now() - new Date(monitorStats?.createdAt); + + const streakTime = getHumanReadableDuration(timeSinceLastFailure); + + const time = getHumanReadableDuration(lastChecked); const { t } = useTranslation(); @@ -16,7 +28,7 @@ const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => { heading="checks since" subHeading={ <> - {uptimeDuration} + {streakTime} {t("ago")} } diff --git a/client/src/Pages/PageSpeed/Details/index.jsx b/client/src/Pages/PageSpeed/Details/index.jsx index df411048a..3190787b3 100644 --- a/client/src/Pages/PageSpeed/Details/index.jsx +++ b/client/src/Pages/PageSpeed/Details/index.jsx @@ -35,7 +35,7 @@ const PageSpeedDetails = () => { }); const [trigger, setTrigger] = useState(false); // Network - const [monitor, isLoading, networkError] = useFetchPageSpeedMonitorById({ + const [monitor, monitorStats, isLoading, networkError] = useFetchPageSpeedMonitorById({ monitorId, dateRange: "day", updateTrigger: trigger, @@ -95,7 +95,7 @@ const PageSpeedDetails = () => { /> 0 ? Date.now() - timeOfLastFailure : 0; + const timeSinceLastFailure = + timeOfLastFailure > 0 + ? Date.now() - timeOfLastFailure + : Date.now() - new Date(monitorStats?.createdAt); // Determine time since last check const timeOfLastCheck = monitorStats?.lastCheckTimestamp; diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index d46845026..32c4310f3 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -124,7 +124,7 @@ class MonitorController { const dateRange = requireString(req?.query?.dateRange, "dateRange"); const teamId = requireTeamId(req?.user?.teamId); - const monitor = await this.monitorService.getPageSpeedDetailsById({ + const data = await this.monitorService.getPageSpeedDetailsById({ teamId, monitorId, dateRange, @@ -133,7 +133,7 @@ class MonitorController { return res.status(200).json({ success: true, msg: "Page speed details retrieved successfully", - data: monitor, + data, }); } catch (error) { next(error); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index ecdf8ca02..36f6b34d9 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -330,9 +330,15 @@ export class MonitorService implements IMonitorService { if (checksData.monitorType !== "pagespeed") { throw new AppError({ message: "Unable to load pagespeed stats for this monitor", status: 500 }); } + + const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id); + return { - ...monitor, - checks: checksData.checks, + monitor: { + ...monitor, + checks: checksData.checks, + }, + monitorStats, }; }; getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { From 11dcccc085c1da9c4c57ebe8d68c8c4f884ebf46 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 21:16:05 +0000 Subject: [PATCH 058/103] remove pagespeed from snapshot array, full check list required --- server/src/service/business/monitorService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 36f6b34d9..7c8ad199e 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -388,7 +388,7 @@ export class MonitorService implements IMonitorService { }); const monitorsList = (monitors ?? []) as Monitor[]; - const snapshotTypes: MonitorType[] = ["hardware", "pagespeed"]; + const snapshotTypes: MonitorType[] = ["hardware"]; const requestedTypes = Array.isArray(type) ? type : type ? [type] : []; const snapshotOnlyRequest = requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType)); From 9c92542653a0e040e1a771e20e777ff7f8ed9732 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 22:03:35 +0000 Subject: [PATCH 059/103] recovery --- server/src/config/services.ts | 6 ++ server/src/db/models/RecoveryToken.js | 25 -------- server/src/db/models/RecoveryToken.ts | 27 ++++++++ server/src/db/models/index.ts | 3 + server/src/repositories/index.ts | 3 + .../IRecoveryTokensRepository.ts | 12 ++++ .../MongoRecoveryTokensRepository.ts | 61 +++++++++++++++++++ server/src/service/business/userService.ts | 40 +++++++++--- .../service/infrastructure/emailService.js | 2 +- server/src/types/index.ts | 1 + server/src/types/invite.ts | 2 +- server/src/types/recoveryToken.ts | 8 +++ 12 files changed, 154 insertions(+), 36 deletions(-) delete mode 100755 server/src/db/models/RecoveryToken.js create mode 100644 server/src/db/models/RecoveryToken.ts create mode 100644 server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts create mode 100644 server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts create mode 100644 server/src/types/recoveryToken.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 66fb18f47..aef32440f 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -73,12 +73,14 @@ import { MongoStatusPagesRepository, MongoUsersRepository, MongoInvitesRepository, + MongoRecoveryTokensRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, IStatusPagesRepository, IUsersRepository, IInvitesRepository, + IRecoveryTokensRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -110,6 +112,7 @@ export type InitializedSerivces = { statusPagesRepository: IStatusPagesRepository; usersRepository: IUsersRepository; invitesRepository: IInvitesRepository; + recoveryTokensRepository: IRecoveryTokensRepository; }; export const initializeServices = async ({ @@ -163,6 +166,7 @@ export const initializeServices = async ({ const statusPagesRepository = new MongoStatusPagesRepository(); const usersRepository = new MongoUsersRepository(); const invitesRepository = new MongoInvitesRepository(); + const recoveryTokensRepository = new MongoRecoveryTokensRepository(); const networkService = new NetworkService({ axios, @@ -247,6 +251,7 @@ export const initializeServices = async ({ monitorsRepository, usersRepository, invitesRepository, + recoveryTokensRepository, }); const diagnosticService = new DiagnosticService(); @@ -307,6 +312,7 @@ export const initializeServices = async ({ statusPagesRepository, usersRepository, invitesRepository, + recoveryTokensRepository, }; Object.values(services).forEach((service) => { diff --git a/server/src/db/models/RecoveryToken.js b/server/src/db/models/RecoveryToken.js deleted file mode 100755 index 2219a4bca..000000000 --- a/server/src/db/models/RecoveryToken.js +++ /dev/null @@ -1,25 +0,0 @@ -import mongoose from "mongoose"; - -const RecoveryTokenSchema = mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true, - }, - token: { - type: String, - required: true, - }, - expiry: { - type: Date, - default: Date.now, - expires: 600, - }, - }, - { - timestamps: true, - } -); - -export default mongoose.model("RecoveryToken", RecoveryTokenSchema); diff --git a/server/src/db/models/RecoveryToken.ts b/server/src/db/models/RecoveryToken.ts new file mode 100644 index 000000000..07b697d9e --- /dev/null +++ b/server/src/db/models/RecoveryToken.ts @@ -0,0 +1,27 @@ +import { Schema, model, type Types } from "mongoose"; +import type { RecoveryToken as RecoveryTokenEntity } from "@/types/recoveryToken.js"; + +type RecoveryTokenDocumentBase = Omit & { + expiry: Date; +}; + +interface RecoveryTokenDocument extends RecoveryTokenDocumentBase { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const RecoveryTokenSchema = new Schema( + { + email: { type: String, required: true, unique: true }, + token: { type: String, required: true }, + expiry: { type: Date, default: Date.now, expires: 600 }, + }, + { timestamps: true } +); + +const RecoveryTokenModel = model("RecoveryToken", RecoveryTokenSchema); + +export type { RecoveryTokenDocument }; +export { RecoveryTokenModel }; +export default RecoveryTokenModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index d378c6750..1e5024b1c 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -15,3 +15,6 @@ export { default as UserModel } from "@/db/models/User.js"; export * from "@/db/models/Invite.js"; export { default as InviteModel } from "@/db/models/Invite.js"; + +export * from "@/db/models/RecoveryToken.js"; +export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 7afe3a9f4..7a3c08c81 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -15,3 +15,6 @@ export { default as MongoUsersRepository } from "@/repositories/users/MongoUsers export * from "@/repositories/invites/IInvitesRepository.js"; export { default as MongoInvitesRepository } from "@/repositories/invites/MongoInviteRepository.js"; + +export * from "@/repositories/recovery-tokens/IRecoveryTokensRepository.js"; +export { default as MongoRecoveryTokensRepository } from "@/repositories/recovery-tokens/MongoRecoveryTokensRepository.js"; diff --git a/server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts b/server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts new file mode 100644 index 000000000..9df1fa793 --- /dev/null +++ b/server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts @@ -0,0 +1,12 @@ +import type { RecoveryToken } from "@/types/recoveryToken.js"; + +export interface IRecoveryTokensRepository { + // create + create(email: string): Promise; + // fetch + findByToken(token: string): Promise; + // update + // delete + deleteManyByEmail(email: string): Promise; + // other +} diff --git a/server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts b/server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts new file mode 100644 index 000000000..4c9f9ebb6 --- /dev/null +++ b/server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts @@ -0,0 +1,61 @@ +import type { RecoveryToken } from "@/types/index.js"; +import type { IRecoveryTokensRepository } from "./IRecoveryTokensRepository.js"; +import type { RecoveryTokenDocument } from "@/db/models/RecoveryToken.js"; +import { RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; +import mongoose from "mongoose"; +import crypto from "crypto"; +import { AppError } from "@/utils/AppError.js"; +const SERVICE_NAME = "MongoRecoveryTokensRepository"; + +class MongoRecoveryTokensRepository implements IRecoveryTokensRepository { + static SERVICE_NAME = SERVICE_NAME; + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: RecoveryTokenDocument): RecoveryToken => { + return { + id: this.toStringId(doc._id), + email: doc.email, + token: doc.token, + expiry: this.toDateString(doc.expiry), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (email: string): Promise => { + const token = await RecoveryTokenModel.create({ + email, + token: crypto.randomBytes(32).toString("hex"), + }); + return this.toEntity(token); + }; + + findByToken = async (token: string): Promise => { + const recoveryToken = await RecoveryTokenModel.findOne({ token }); + if (!recoveryToken) { + throw new AppError({ message: "Recovery token not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(recoveryToken); + }; + + deleteManyByEmail = async (email: string) => { + const result = await RecoveryTokenModel.deleteMany({ + email, + }); + return Promise.resolve(result.deletedCount || 0); + }; +} + +export default MongoRecoveryTokensRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index dca362ebe..efd599299 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,7 +1,8 @@ -import { IInvitesRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; +import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository } from "@/repositories/index.js"; import Team from "@/db/models/Team.js"; import type { User } from "@/types/index.js"; import bcrypt from "bcryptjs"; +import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "userService"; @@ -20,6 +21,7 @@ class UserService { private monitorsRepository: IMonitorsRepository; private usersRepository: IUsersRepository; private invitesRepository: IInvitesRepository; + private recoveryTokensRepository: IRecoveryTokensRepository; constructor({ crypto, @@ -34,6 +36,7 @@ class UserService { monitorsRepository, usersRepository, invitesRepository, + recoveryTokensRepository, }: { crypto: any; db: any; @@ -47,6 +50,7 @@ class UserService { monitorsRepository: IMonitorsRepository; usersRepository: IUsersRepository; invitesRepository: IInvitesRepository; + recoveryTokensRepository: IRecoveryTokensRepository; }) { this.db = db; this.emailService = emailService; @@ -60,6 +64,7 @@ class UserService { this.monitorsRepository = monitorsRepository; this.usersRepository = usersRepository; this.invitesRepository = invitesRepository; + this.recoveryTokensRepository = recoveryTokensRepository; } get serviceName() { @@ -178,13 +183,15 @@ class UserService { }; checkSuperadminExists = async () => { - const superAdminExists = await this.db.userModule.checkSuperadmin(); - return superAdminExists; + return await this.usersRepository.findSuperAdmin(); }; requestRecovery = async (email: string) => { const user = await this.db.userModule.getUserByEmail(email); - const recoveryToken = await this.db.recoveryModule.requestRecoveryToken(email); + + // Delete existing tokens + await this.recoveryTokensRepository.deleteManyByEmail(email); + const recoveryToken = await this.recoveryTokensRepository.create(email); const name = user.firstName; const { clientHost } = this.settingsService.getSettings(); const url = `${clientHost}/set-new-password/${recoveryToken.token}`; @@ -199,14 +206,29 @@ class UserService { }; validateRecovery = async (recoveryToken: string) => { - await this.db.recoveryModule.validateRecoveryToken(recoveryToken); + // Throws if token not found, validating + await this.recoveryTokensRepository.findByToken(recoveryToken); }; resetPassword = async (password: string, recoveryToken: string) => { - const user = await this.db.recoveryModule.resetPassword(password, recoveryToken); - const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(user._doc, appSettings); - return { user, token }; + const existingToken = await this.recoveryTokensRepository.findByToken(recoveryToken); + const existingUser = await this.usersRepository.findByEmail(existingToken.email); + + const match = await bcrypt.compare(password, existingUser.password); + if (match === true) { + throw new AppError({ message: "New password cannot be same as old password", service: SERVICE_NAME, status: 400 }); + } + + existingUser.password = password; + await this.usersRepository.updateById(existingUser.id, existingUser, null); + await this.recoveryTokensRepository.deleteManyByEmail(existingUser.email); + + existingUser.password = ""; + existingUser.profileImage = undefined; + + const token = this.issueToken(existingUser, await this.settingsService.getSettings()); + + return { user: existingUser, token }; }; deleteUser = async (user: any) => { diff --git a/server/src/service/infrastructure/emailService.js b/server/src/service/infrastructure/emailService.js index ed661ff16..bc040d51a 100755 --- a/server/src/service/infrastructure/emailService.js +++ b/server/src/service/infrastructure/emailService.js @@ -46,7 +46,7 @@ class EmailService { */ this.loadTemplate = (templateName) => { try { - const templatePath = this.path.join(__dirname, `../../../templates/${templateName}.mjml`); + const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`); const templateContent = this.fs.readFileSync(templatePath, "utf8"); return this.compile(templateContent); } catch (error) { diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 9416de9fa..1a43d4849 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -5,3 +5,4 @@ export * from "@/types/statusPage.js"; export * from "@/types/network.js"; export * from "@/types/user.js"; export * from "@/types/invite.js"; +export * from "@/types/recoveryToken.js"; diff --git a/server/src/types/invite.ts b/server/src/types/invite.ts index 8e65d54e9..cdbdb37e5 100644 --- a/server/src/types/invite.ts +++ b/server/src/types/invite.ts @@ -1,4 +1,4 @@ -import { UserRole } from "./user.js"; +import type { UserRole } from "@/types/user.js"; export interface Invite { id: string; diff --git a/server/src/types/recoveryToken.ts b/server/src/types/recoveryToken.ts new file mode 100644 index 000000000..488e1f4c1 --- /dev/null +++ b/server/src/types/recoveryToken.ts @@ -0,0 +1,8 @@ +export interface RecoveryToken { + id: string; + email: string; + token: string; + expiry: string; + createdAt: string; + updatedAt: string; +} From b47f3710fe6dac3abb27d2bf7e51ea2bad78009b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 22:41:12 +0000 Subject: [PATCH 060/103] refactor user-service --- server/src/controllers/authController.ts | 7 +++-- server/src/controllers/incidentController.ts | 2 +- .../maintenanceWindowController.ts | 4 +-- server/src/controllers/monitorController.ts | 8 ++--- .../src/controllers/notificationController.ts | 2 +- .../src/controllers/statusPageController.ts | 6 ++-- .../v1/{verifyJWT.js => verifyJWT.ts} | 18 ++++-------- .../repositories/users/IUsersRepository.ts | 3 ++ .../users/MongoUsersRepository.ts | 29 +++++++++++++++++++ server/src/service/business/userService.ts | 24 ++++++++------- server/src/types/express.d.ts | 4 +-- 11 files changed, 69 insertions(+), 38 deletions(-) rename server/src/middleware/v1/{verifyJWT.js => verifyJWT.ts} (74%) diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 1b76b249e..23ea6e42e 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -192,7 +192,8 @@ class AuthController { editUserById = async (req: Request, res: Response, next: NextFunction) => { try { const roles = req?.user?.role; - if (!roles.includes("superadmin")) { + + if (!roles || !roles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } @@ -201,7 +202,7 @@ class AuthController { await editUserByIdParamValidation.validateAsync(req.params); // If this is superadmin self edit, allow "superadmin" role - if (userId === req.user._id) { + if (userId === req.user?.id) { await editSuperadminUserByIdBodyValidation.validateAsync(req.body); } else { await editUserByIdBodyValidation.validateAsync(req.body); @@ -217,7 +218,7 @@ class AuthController { editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { try { const roles = req?.user?.role; - if (!roles.includes("superadmin")) { + if (!roles || !roles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index 8a56b5ef8..b17788d80 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -69,7 +69,7 @@ class IncidentController { try { const resolvedIncident = await this.incidentService.resolveIncidentManually({ incidentId: req?.params?.incidentId, - userId: req?.user?._id, + userId: req?.user?.id, teamId: req?.user?.teamId, comment: req?.body?.comment, }); diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index afeba9219..3d212769f 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -46,7 +46,7 @@ class MaintenanceWindowController { try { await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); - const teamId = req.user.teamId; + const teamId = req.user?.teamId; if (!teamId) { throw new AppError({ message: "Team ID is required", status: 400 }); } @@ -130,7 +130,7 @@ class MaintenanceWindowController { await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); - const teamId = req.user.teamId; + const teamId = req.user?.teamId; if (!teamId) { throw new AppError({ message: "Team ID is required", status: 400 }); } diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index d46845026..0b0184bae 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -164,7 +164,7 @@ class MonitorController { try { await createMonitorBodyValidation.validateAsync(req.body); - const userId = requireString(req?.user?._id, "User ID"); + const userId = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); const monitor = await this.monitorService.createMonitor(teamId, userId, req.body); @@ -193,7 +193,7 @@ class MonitorController { throw new AppError({ message: "File is empty", status: 400 }); } - const userId = requireString(req?.user?._id, "User ID"); + const userId = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); const fileData = req?.file?.buffer?.toString("utf-8"); @@ -286,9 +286,9 @@ class MonitorController { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const _id = requireString(req?.user?._id, "User ID"); + const id = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); - const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId }); + const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); return res.status(200).json({ success: true, diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index defae2fd3..6f4a7f164 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -53,7 +53,7 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const userId = req?.user?._id; + const userId = req?.user?.id; if (!userId) { throw new AppError({ message: "User ID is required", status: 400 }); } diff --git a/server/src/controllers/statusPageController.ts b/server/src/controllers/statusPageController.ts index 105beb230..b63e925c0 100644 --- a/server/src/controllers/statusPageController.ts +++ b/server/src/controllers/statusPageController.ts @@ -21,11 +21,11 @@ class StatusPageController { await createStatusPageBodyValidation.validateAsync(req.body); await imageValidation.validateAsync(req.file); - const { _id, teamId } = req.user; + const { id, teamId } = req.user ?? {}; const statusPage = await this.db.statusPageModule.createStatusPage({ statusPageData: req.body, image: req.file, - userId: _id, + userId: id, teamId, }); return res.status(200).json({ @@ -87,7 +87,7 @@ class StatusPageController { }; getStatusPagesByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = req.user.teamId; + const teamId = req.user?.teamId; const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId); return res.status(200).json({ diff --git a/server/src/middleware/v1/verifyJWT.js b/server/src/middleware/v1/verifyJWT.ts similarity index 74% rename from server/src/middleware/v1/verifyJWT.js rename to server/src/middleware/v1/verifyJWT.ts index cf983a6c8..b3211d7b6 100755 --- a/server/src/middleware/v1/verifyJWT.js +++ b/server/src/middleware/v1/verifyJWT.ts @@ -1,3 +1,5 @@ +import { NextFunction, Request, Response } from "express"; + import jwt from "jsonwebtoken"; import ServiceRegistry from "../../service/system/serviceRegistry.js"; import SettingsService from "../../service/system/settingsService.js"; @@ -5,20 +7,12 @@ import StringService from "../../service/system/stringService.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; -/** - * Verifies the JWT token - * @function - * @param {express.Request} req - * @param {express.Response} res - * @param {express.NextFunction} next - * @returns {express.Response} - */ -const verifyJWT = (req, res, next) => { +const verifyJWT = (req: Request, res: Response, next: NextFunction) => { const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); const token = req.headers["authorization"]; // Make sure a token is provided if (!token) { - const error = new Error(stringService.noAuthToken); + const error: any = new Error(stringService.noAuthToken); error.status = 401; error.service = SERVICE_NAME; next(error); @@ -26,7 +20,7 @@ const verifyJWT = (req, res, next) => { } // Make sure it is properly formatted if (!token.startsWith(TOKEN_PREFIX)) { - const error = new Error(stringService.invalidAuthToken); // Instantiate a new Error object for improperly formatted token + const error: any = new Error(stringService.invalidAuthToken); // Instantiate a new Error object for improperly formatted token error.status = 401; error.service = SERVICE_NAME; error.method = "verifyJWT"; @@ -37,7 +31,7 @@ const verifyJWT = (req, res, next) => { const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); // Verify the token's authenticity const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings(); - jwt.verify(parsedToken, jwtSecret, (err, decoded) => { + jwt.verify(parsedToken, jwtSecret, (err: any, decoded: any) => { if (err) { const errorMessage = err.name === "TokenExpiredError" ? stringService.expiredAuthToken : stringService.invalidAuthToken; err.details = { msg: errorMessage }; diff --git a/server/src/repositories/users/IUsersRepository.ts b/server/src/repositories/users/IUsersRepository.ts index 9678de47a..3fa094c54 100644 --- a/server/src/repositories/users/IUsersRepository.ts +++ b/server/src/repositories/users/IUsersRepository.ts @@ -4,9 +4,12 @@ export interface IUsersRepository { create(user: Partial, imageFile?: Express.Multer.File | null): Promise; // fetch findByEmail(email: string): Promise; + findById(id: string): Promise; + findAll(): Promise; // update updateById(id: string, patch: Partial, file?: Express.Multer.File | null): Promise; // delete + deleteById(id: string): Promise; // other findSuperAdmin(): Promise; } diff --git a/server/src/repositories/users/MongoUsersRepository.ts b/server/src/repositories/users/MongoUsersRepository.ts index b2687ff4a..023e8aefb 100644 --- a/server/src/repositories/users/MongoUsersRepository.ts +++ b/server/src/repositories/users/MongoUsersRepository.ts @@ -82,6 +82,20 @@ class MongoUsersRepository implements IUsersRepository { return this.toEntity(user); }; + findById = async (id: string) => { + const user = await UserModel.findById(id).select("-password").select("-profileImage"); + if (!user) { + throw new Error("User not found"); + } + + return this.toEntity(user); + }; + + findAll = async () => { + const users = await UserModel.find().select("-password").select("-profileImage"); + return this.mapDocuments(users); + }; + updateById = async (id: string, patch: Partial, file?: Express.Multer.File | null): Promise => { const candidateUser = { ...patch }; @@ -113,6 +127,14 @@ class MongoUsersRepository implements IUsersRepository { return this.toEntity(updatedUser); }; + deleteById = async (id: string) => { + const deletedUser = await UserModel.findByIdAndDelete(id); + if (!deletedUser) { + throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(deletedUser); + }; + findSuperAdmin = async () => { const superAdmin = await UserModel.findOne({ role: "superadmin" }); if (superAdmin !== null) { @@ -120,6 +142,13 @@ class MongoUsersRepository implements IUsersRepository { } return false; }; + + private mapDocuments = (documents: UserDocument[]): User[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; } export default MongoUsersRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index efd599299..7d9e5811e 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -187,7 +187,7 @@ class UserService { }; requestRecovery = async (email: string) => { - const user = await this.db.userModule.getUserByEmail(email); + const user = await this.usersRepository.findByEmail(email); // Delete existing tokens await this.recoveryTokensRepository.deleteManyByEmail(email); @@ -231,14 +231,14 @@ class UserService { return { user: existingUser, token }; }; - deleteUser = async (user: any) => { + deleteUser = async (user: User) => { const email = user?.email; if (!email) { throw this.errorService.createBadRequestError("No email in request"); } const teamId = user?.teamId; - const userId = user?._id; + const userId = user?.id; if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); @@ -267,24 +267,28 @@ class UserService { )); } // 6. Delete the user by id - await this.db.userModule.deleteUser(userId); + await this.usersRepository.deleteById(userId); }; getAllUsers = async () => { - const users = await this.db.userModule.getAllUsers(); - return users; + return await this.usersRepository.findAll(); }; getUserById = async (roles: any, userId: any) => { - const user = await this.db.userModule.getUserById(roles, userId); + if (!roles.includes("superadmin")) { + throw new AppError({ message: "User is not a superadmin", service: SERVICE_NAME, status: 403 }); + } + const user = await this.usersRepository.findById(userId); + return user; }; - editUserById = async (userId: any, user: any) => { - await this.db.userModule.editUserById(userId, user); + editUserById = async (userId: any, patch: Partial) => { + await this.usersRepository.updateById(userId, patch, null); }; + setPasswordByUserId = async (userId: any, password: string) => { - const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: { password: password }, file: null }); + const updatedUser = await this.usersRepository.updateById(userId, { password }, null); return updatedUser; }; } diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts index b9f7386f6..66755659c 100644 --- a/server/src/types/express.d.ts +++ b/server/src/types/express.d.ts @@ -1,10 +1,10 @@ -import { ITokenizedUser } from "../db/models/index.ts"; +import type { User } from "@/types/index.js"; declare global { namespace Express { interface Request { file?: Multer.File; - user?: ITokenizedUser; + user?: User | undefined; resource?: any; } } From 3a77c6d7fad51e06a860328c7730ce62c0db989b Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 22:26:43 -0500 Subject: [PATCH 061/103] Fix sidebar gap when scrolling on pages with tall content The sidebar was showing a visible gap at the bottom when scrolling on pages with tall tables (e.g., /checks). This occurred because the parent container's overflow-x: hidden was breaking sticky positioning. Changes: - Changed sidebar from position: sticky to position: fixed - Added a spacer Box element to maintain layout spacing - Moved border and background styles to the sidebar component - Cleaned up unused CSS selectors --- .../Components/v1/Layouts/HomeLayout/index.css | 10 +--------- .../Components/v1/Layouts/HomeLayout/index.jsx | 16 ++++++++++++++-- client/src/Components/v1/Sidebar/index.jsx | 7 +++++-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.css b/client/src/Components/v1/Layouts/HomeLayout/index.css index 953a2c682..61808aa57 100644 --- a/client/src/Components/v1/Layouts/HomeLayout/index.css +++ b/client/src/Components/v1/Layouts/HomeLayout/index.css @@ -14,15 +14,7 @@ } } */ -/* .home-layout aside { - position: sticky; - top: 0; - left: 0; - height: 100vh; - max-width: var(--env-var-side-bar-width); -} */ - -.home-layout > div { +.home-layout > .home-content-wrapper { min-height: calc(100vh - var(--env-var-spacing-2) * 2); flex: 1; } diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.jsx b/client/src/Components/v1/Layouts/HomeLayout/index.jsx index 3e63aff37..825157333 100644 --- a/client/src/Components/v1/Layouts/HomeLayout/index.jsx +++ b/client/src/Components/v1/Layouts/HomeLayout/index.jsx @@ -1,17 +1,29 @@ import Sidebar from "../../Sidebar/index.jsx"; import { Outlet } from "react-router"; -import { Stack } from "@mui/material"; +import { Box, Stack } from "@mui/material"; +import { useSelector } from "react-redux"; import "./index.css"; const HomeLayout = () => { + const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); + return ( + {/* Spacer for fixed sidebar */} + diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx index b66f081fb..f39121987 100644 --- a/client/src/Components/v1/Sidebar/index.jsx +++ b/client/src/Components/v1/Sidebar/index.jsx @@ -81,14 +81,17 @@ const Sidebar = () => { : "var(--env-var-side-bar-width)" } component="aside" - position="sticky" + position="fixed" top={0} - borderRight={`1px solid ${theme.palette.primary.lowContrast}`} + left={0} paddingTop={theme.spacing(6)} paddingBottom={theme.spacing(6)} gap={theme.spacing(6)} sx={{ transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)", + backgroundColor: "#000000", + borderRight: "1px solid #344054", + zIndex: 1000, }} > From a4cf53ee1549f510787d71d0233e99f517a19022 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 22:52:11 -0500 Subject: [PATCH 062/103] Set html/body background to black and disable overscroll bounce - Added background-color: #000000 to prevent white flash on overscroll - Added overscroll-behavior: none to disable elastic bounce effect --- client/src/index.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/index.css b/client/src/index.css index d58a7e872..b29c17960 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -4,6 +4,12 @@ box-sizing: border-box; } +html, +body { + background-color: #000000; + overscroll-behavior: none; +} + html { scroll-behavior: smooth; } From e498bd60b17e9404cd51bc251d7b37eb73c2c06e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 17 Jan 2026 05:41:52 +0000 Subject: [PATCH 063/103] team panel fix --- client/src/Pages/Account/components/TeamPanel.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/Account/components/TeamPanel.jsx b/client/src/Pages/Account/components/TeamPanel.jsx index 03a6145a5..bb40a5e4f 100644 --- a/client/src/Pages/Account/components/TeamPanel.jsx +++ b/client/src/Pages/Account/components/TeamPanel.jsx @@ -104,7 +104,7 @@ const TeamPanel = () => { team = team.map((member) => ({ ...member, - id: member._id, + id: member.id, role: member.role.map((role) => ROLE_MAP[role]).join(","), })); setData(team); @@ -268,6 +268,7 @@ const TeamPanel = () => { cursor: isSuperAdmin ? "pointer" : "default", }, onRowClick: (row) => { + console.log(row); if (isSuperAdmin) { navigate(`/account/team/${row.id}`); } From 048e53ea1556fe45434c8b52ab68e7583639b17b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 17 Jan 2026 18:29:42 +0000 Subject: [PATCH 064/103] setting service --- server/src/db/models/AppSettings.js | 84 ------------------- server/src/db/models/AppSettings.ts | 49 +++++++++++ server/src/db/models/index.ts | 3 + server/src/repositories/index.ts | 3 + .../settings/ISettingsRepository.ts | 8 ++ .../settings/MongoSettingsRepository.ts | 74 ++++++++++++++++ server/src/types/index.ts | 1 + server/src/types/settings.ts | 31 +++++++ 8 files changed, 169 insertions(+), 84 deletions(-) delete mode 100755 server/src/db/models/AppSettings.js create mode 100644 server/src/db/models/AppSettings.ts create mode 100644 server/src/repositories/settings/ISettingsRepository.ts create mode 100644 server/src/repositories/settings/MongoSettingsRepository.ts create mode 100644 server/src/types/settings.ts diff --git a/server/src/db/models/AppSettings.js b/server/src/db/models/AppSettings.js deleted file mode 100755 index a1a9964f2..000000000 --- a/server/src/db/models/AppSettings.js +++ /dev/null @@ -1,84 +0,0 @@ -import mongoose from "mongoose"; - -const AppSettingsSchema = mongoose.Schema( - { - checkTTL: { - type: Number, - default: 30, - }, - language: { - type: String, - default: "gb", - }, - pagespeedApiKey: { - type: String, - }, - systemEmailHost: { - type: String, - }, - systemEmailPort: { - type: Number, - }, - systemEmailAddress: { - type: String, - }, - systemEmailPassword: { - type: String, - }, - systemEmailUser: { - type: String, - }, - systemEmailConnectionHost: { - type: String, - default: "localhost", - }, - systemEmailTLSServername: { - type: String, - }, - systemEmailSecure: { - type: Boolean, - default: false, - }, - systemEmailPool: { - type: Boolean, - default: false, - }, - systemEmailIgnoreTLS: { - type: Boolean, - default: false, - }, - systemEmailRequireTLS: { - type: Boolean, - default: false, - }, - systemEmailRejectUnauthorized: { - type: Boolean, - default: true, - }, - showURL: { - type: Boolean, - default: false, - }, - singleton: { - type: Boolean, - required: true, - unique: true, - default: true, - }, - version: { - type: Number, - default: 1, - }, - globalThresholds: { - cpu: { type: Number }, - memory: { type: Number }, - disk: { type: Number }, - temperature: { type: Number }, - }, - }, - { - timestamps: true, - } -); - -export default mongoose.model("AppSettings", AppSettingsSchema); diff --git a/server/src/db/models/AppSettings.ts b/server/src/db/models/AppSettings.ts new file mode 100644 index 000000000..22d1369e5 --- /dev/null +++ b/server/src/db/models/AppSettings.ts @@ -0,0 +1,49 @@ +import { Schema, model, type Types } from "mongoose"; +import type { Settings, SettingsThresholds } from "@/types/settings.js"; + +interface AppSettingsDocument extends Omit { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const thresholdsSchema = new Schema( + { + cpu: { type: Number }, + memory: { type: Number }, + disk: { type: Number }, + temperature: { type: Number }, + }, + { _id: false } +); + +const AppSettingsSchema = new Schema( + { + checkTTL: { type: Number, default: 30 }, + language: { type: String, default: "gb" }, + pagespeedApiKey: { type: String }, + systemEmailHost: { type: String }, + systemEmailPort: { type: Number }, + systemEmailAddress: { type: String }, + systemEmailPassword: { type: String }, + systemEmailUser: { type: String }, + systemEmailConnectionHost: { type: String, default: "localhost" }, + systemEmailTLSServername: { type: String }, + systemEmailSecure: { type: Boolean, default: false }, + systemEmailPool: { type: Boolean, default: false }, + systemEmailIgnoreTLS: { type: Boolean, default: false }, + systemEmailRequireTLS: { type: Boolean, default: false }, + systemEmailRejectUnauthorized: { type: Boolean, default: true }, + showURL: { type: Boolean, default: false }, + singleton: { type: Boolean, required: true, unique: true, default: true }, + version: { type: Number, default: 1 }, + globalThresholds: { type: thresholdsSchema }, + }, + { timestamps: true } +); + +const AppSettingsModel = model("AppSettings", AppSettingsSchema); + +export type { AppSettingsDocument }; +export { AppSettingsModel }; +export default AppSettingsModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 1e5024b1c..59ed8125d 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -16,5 +16,8 @@ export { default as UserModel } from "@/db/models/User.js"; export * from "@/db/models/Invite.js"; export { default as InviteModel } from "@/db/models/Invite.js"; +export * from "@/db/models/AppSettings.js"; +export { default as AppSettingsModel } from "@/db/models/AppSettings.js"; + export * from "@/db/models/RecoveryToken.js"; export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 7a3c08c81..09a686595 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -18,3 +18,6 @@ export { default as MongoInvitesRepository } from "@/repositories/invites/MongoI export * from "@/repositories/recovery-tokens/IRecoveryTokensRepository.js"; export { default as MongoRecoveryTokensRepository } from "@/repositories/recovery-tokens/MongoRecoveryTokensRepository.js"; + +export * from "@/repositories/settings/ISettingsRepository.js"; +export { default as MongoSettingsRepository } from "@/repositories/settings/MongoSettingsRepository.js"; diff --git a/server/src/repositories/settings/ISettingsRepository.ts b/server/src/repositories/settings/ISettingsRepository.ts new file mode 100644 index 000000000..111a92a55 --- /dev/null +++ b/server/src/repositories/settings/ISettingsRepository.ts @@ -0,0 +1,8 @@ +import type { Settings } from "@/types/index.js"; +export interface ISettingsRepository { + // create + // fetch + // update + update(settings: Partial): Promise; + // delete +} diff --git a/server/src/repositories/settings/MongoSettingsRepository.ts b/server/src/repositories/settings/MongoSettingsRepository.ts new file mode 100644 index 000000000..f4d71d56e --- /dev/null +++ b/server/src/repositories/settings/MongoSettingsRepository.ts @@ -0,0 +1,74 @@ +import mongoose from "mongoose"; +import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js"; +import type { Settings } from "@/types/index.js"; +import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/AppSettings.js"; +import { AppError } from "@/utils/AppError.js"; + +class MongoSettingsRepository implements ISettingsRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: AppSettingsDocument): Settings => { + return { + id: this.toStringId(doc._id), + checkTTL: doc.checkTTL, + language: doc.language, + pagespeedApiKey: doc.pagespeedApiKey ?? undefined, + systemEmailHost: doc.systemEmailHost ?? undefined, + systemEmailPort: doc.systemEmailPort ?? undefined, + systemEmailAddress: doc.systemEmailAddress ?? undefined, + systemEmailPassword: doc.systemEmailPassword ?? undefined, + systemEmailUser: doc.systemEmailUser ?? undefined, + systemEmailConnectionHost: doc.systemEmailConnectionHost ?? undefined, + systemEmailTLSServername: doc.systemEmailTLSServername ?? undefined, + systemEmailSecure: doc.systemEmailSecure ?? false, + systemEmailPool: doc.systemEmailPool ?? false, + systemEmailIgnoreTLS: doc.systemEmailIgnoreTLS ?? false, + systemEmailRequireTLS: doc.systemEmailRequireTLS ?? false, + systemEmailRejectUnauthorized: doc.systemEmailRejectUnauthorized ?? true, + showURL: doc.showURL ?? false, + singleton: doc.singleton, + version: doc.version ?? 1, + globalThresholds: doc.globalThresholds ?? undefined, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + update = async (settings: Partial) => { + const update: Record = { $set: { ...settings } }; + + if (settings.pagespeedApiKey === "") { + update.$unset = { pagespeedApiKey: "" }; + delete update.$set.pagespeedApiKey; + } + + if (settings.systemEmailPassword === "") { + update.$unset = { systemEmailPassword: "" }; + delete update.$set.systemEmailPassword; + } + + await AppSettingsModel.findOneAndUpdate({}, update, { + upsert: true, + }); + + const currentSettings = await AppSettingsModel.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); + if (!currentSettings) { + throw new AppError({ message: "Settings not found after update", status: 500 }); + } + return this.toEntity(currentSettings); + }; +} + +export default MongoSettingsRepository; diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 1a43d4849..fcfb6fc37 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -6,3 +6,4 @@ export * from "@/types/network.js"; export * from "@/types/user.js"; export * from "@/types/invite.js"; export * from "@/types/recoveryToken.js"; +export * from "@/types/settings.js"; diff --git a/server/src/types/settings.ts b/server/src/types/settings.ts new file mode 100644 index 000000000..78284a451 --- /dev/null +++ b/server/src/types/settings.ts @@ -0,0 +1,31 @@ +export interface SettingsThresholds { + cpu?: number; + memory?: number; + disk?: number; + temperature?: number; +} + +export interface Settings { + id: string; + checkTTL: number; + language: string; + pagespeedApiKey?: string; + systemEmailHost?: string; + systemEmailPort?: number; + systemEmailAddress?: string; + systemEmailPassword?: string; + systemEmailUser?: string; + systemEmailConnectionHost?: string; + systemEmailTLSServername?: string; + systemEmailSecure: boolean; + systemEmailPool: boolean; + systemEmailIgnoreTLS: boolean; + systemEmailRequireTLS: boolean; + systemEmailRejectUnauthorized: boolean; + showURL: boolean; + singleton: boolean; + version: number; + globalThresholds?: SettingsThresholds; + createdAt: string; + updatedAt: string; +} From ba352309195b4458bed20b0fcc2e7b7d3a8d46e5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 17 Jan 2026 19:08:12 +0000 Subject: [PATCH 065/103] remove db references from userService --- server/src/config/services.ts | 7 ++++++- server/src/db/models/AppSettings.ts | 1 + .../settings/MongoSettingsRepository.ts | 14 ++++++++------ server/src/service/business/userService.ts | 12 ++++++------ server/src/types/settings.ts | 1 + 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index aef32440f..6b91bcf63 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -74,6 +74,7 @@ import { MongoUsersRepository, MongoInvitesRepository, MongoRecoveryTokensRepository, + MongoSettingsRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, @@ -81,6 +82,7 @@ import { IUsersRepository, IInvitesRepository, IRecoveryTokensRepository, + ISettingsRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -113,6 +115,7 @@ export type InitializedSerivces = { usersRepository: IUsersRepository; invitesRepository: IInvitesRepository; recoveryTokensRepository: IRecoveryTokensRepository; + settingsRepository: ISettingsRepository; }; export const initializeServices = async ({ @@ -167,6 +170,7 @@ export const initializeServices = async ({ const usersRepository = new MongoUsersRepository(); const invitesRepository = new MongoInvitesRepository(); const recoveryTokensRepository = new MongoRecoveryTokensRepository(); + const settingsRepository = new MongoSettingsRepository(); const networkService = new NetworkService({ axios, @@ -240,7 +244,6 @@ export const initializeServices = async ({ // Business services const userService = new UserService({ crypto, - db, emailService, settingsService, logger, @@ -252,6 +255,7 @@ export const initializeServices = async ({ usersRepository, invitesRepository, recoveryTokensRepository, + settingsRepository, }); const diagnosticService = new DiagnosticService(); @@ -313,6 +317,7 @@ export const initializeServices = async ({ usersRepository, invitesRepository, recoveryTokensRepository, + settingsRepository, }; Object.values(services).forEach((service) => { diff --git a/server/src/db/models/AppSettings.ts b/server/src/db/models/AppSettings.ts index 22d1369e5..9eeaa640a 100644 --- a/server/src/db/models/AppSettings.ts +++ b/server/src/db/models/AppSettings.ts @@ -21,6 +21,7 @@ const AppSettingsSchema = new Schema( { checkTTL: { type: Number, default: 30 }, language: { type: String, default: "gb" }, + jwtSecret: { type: String }, pagespeedApiKey: { type: String }, systemEmailHost: { type: String }, systemEmailPort: { type: Number }, diff --git a/server/src/repositories/settings/MongoSettingsRepository.ts b/server/src/repositories/settings/MongoSettingsRepository.ts index f4d71d56e..2cc700525 100644 --- a/server/src/repositories/settings/MongoSettingsRepository.ts +++ b/server/src/repositories/settings/MongoSettingsRepository.ts @@ -2,7 +2,6 @@ import mongoose from "mongoose"; import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js"; import type { Settings } from "@/types/index.js"; import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/AppSettings.js"; -import { AppError } from "@/utils/AppError.js"; class MongoSettingsRepository implements ISettingsRepository { private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { @@ -24,6 +23,7 @@ class MongoSettingsRepository implements ISettingsRepository { id: this.toStringId(doc._id), checkTTL: doc.checkTTL, language: doc.language, + jwtSecret: doc.jwtSecret ?? undefined, pagespeedApiKey: doc.pagespeedApiKey ?? undefined, systemEmailHost: doc.systemEmailHost ?? undefined, systemEmailPort: doc.systemEmailPort ?? undefined, @@ -63,11 +63,13 @@ class MongoSettingsRepository implements ISettingsRepository { upsert: true, }); - const currentSettings = await AppSettingsModel.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); - if (!currentSettings) { - throw new AppError({ message: "Settings not found after update", status: 500 }); - } - return this.toEntity(currentSettings); + const updatedSettings = await AppSettingsModel.findOneAndUpdate({}, update, { + upsert: true, + new: true, + projection: "-__v -_id -createdAt -updatedAt -singleton", + }); + + return this.toEntity(updatedSettings); }; } diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 7d9e5811e..5f73508dd 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,4 +1,4 @@ -import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository } from "@/repositories/index.js"; +import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository, ISettingsRepository } from "@/repositories/index.js"; import Team from "@/db/models/Team.js"; import type { User } from "@/types/index.js"; import bcrypt from "bcryptjs"; @@ -9,7 +9,6 @@ const SERVICE_NAME = "userService"; class UserService { static SERVICE_NAME = SERVICE_NAME; - private db: any; private emailService: any; private settingsService: any; private logger: any; @@ -22,10 +21,10 @@ class UserService { private usersRepository: IUsersRepository; private invitesRepository: IInvitesRepository; private recoveryTokensRepository: IRecoveryTokensRepository; + private settingsRepository: ISettingsRepository; constructor({ crypto, - db, emailService, settingsService, logger, @@ -37,9 +36,9 @@ class UserService { usersRepository, invitesRepository, recoveryTokensRepository, + settingsRepository, }: { crypto: any; - db: any; emailService: any; settingsService: any; logger: any; @@ -51,8 +50,8 @@ class UserService { usersRepository: IUsersRepository; invitesRepository: IInvitesRepository; recoveryTokensRepository: IRecoveryTokensRepository; + settingsRepository: ISettingsRepository; }) { - this.db = db; this.emailService = emailService; this.settingsService = settingsService; this.logger = logger; @@ -65,6 +64,7 @@ class UserService { this.usersRepository = usersRepository; this.invitesRepository = invitesRepository; this.recoveryTokensRepository = recoveryTokensRepository; + this.settingsRepository = settingsRepository; } get serviceName() { @@ -89,7 +89,7 @@ class UserService { } else { // This is the first account, create JWT secret to use if one is not supplied by env const jwtSecret = this.crypto.randomBytes(64).toString("hex"); - await this.db.settingsModule.updateAppSettings({ jwtSecret }); + await this.settingsRepository.update({ jwtSecret }); // Create a new team const team = new Team({ email: user.email, diff --git a/server/src/types/settings.ts b/server/src/types/settings.ts index 78284a451..108cdcb72 100644 --- a/server/src/types/settings.ts +++ b/server/src/types/settings.ts @@ -9,6 +9,7 @@ export interface Settings { id: string; checkTTL: number; language: string; + jwtSecret?: string; pagespeedApiKey?: string; systemEmailHost?: string; systemEmailPort?: number; From 3e8d4cb49bb47123ee4390feec240cc12863d26e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 05:38:50 +0000 Subject: [PATCH 066/103] convert to ts --- server/src/config/services.ts | 3 +- .../invites/IInvitesRepository.ts | 2 + .../invites/MongoInviteRepository.ts | 18 ++++++ server/src/service/business/inviteService.ts | 60 +++++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 server/src/service/business/inviteService.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 6b91bcf63..bbb42984e 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -260,10 +260,9 @@ export const initializeServices = async ({ const diagnosticService = new DiagnosticService(); const inviteService = new InviteService({ - db, + invitesRepository, settingsService, emailService, - stringService, errorService, }); const maintenanceWindowService = new MaintenanceWindowService({ diff --git a/server/src/repositories/invites/IInvitesRepository.ts b/server/src/repositories/invites/IInvitesRepository.ts index 066a51a0f..74a7ad68c 100644 --- a/server/src/repositories/invites/IInvitesRepository.ts +++ b/server/src/repositories/invites/IInvitesRepository.ts @@ -2,7 +2,9 @@ import type { Invite } from "@/types/index.js"; export interface IInvitesRepository { // create + create(invite: Partial): Promise; // fetch + findByToken(token: string): Promise; findByTokenAndDelete(token: string): Promise; // update diff --git a/server/src/repositories/invites/MongoInviteRepository.ts b/server/src/repositories/invites/MongoInviteRepository.ts index 16e56440b..e9243e96e 100644 --- a/server/src/repositories/invites/MongoInviteRepository.ts +++ b/server/src/repositories/invites/MongoInviteRepository.ts @@ -3,6 +3,7 @@ import type { Invite } from "@/types/index.js"; import { type InviteDocument, InviteModel } from "@/db/models/index.js"; import { AppError } from "@/utils/AppError.js"; import mongoose from "mongoose"; +import crypto from "crypto"; class MongoInvitesRepository implements IInvitesRepository { private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { @@ -32,6 +33,23 @@ class MongoInvitesRepository implements IInvitesRepository { }; }; + create = async (invite: Partial) => { + await InviteModel.deleteMany({ email: invite.email }); + invite.token = crypto.randomBytes(32).toString("hex"); + let inviteToken = await InviteModel.create(invite); + return this.toEntity(inviteToken); + }; + + findByToken = async (token: string) => { + const invite = await InviteModel.findOne({ + token, + }); + if (invite === null) { + throw new AppError({ message: "Invite not found", status: 404 }); + } + return this.toEntity(invite); + }; + findByTokenAndDelete = async (token: string) => { const invite = await InviteModel.findOneAndDelete({ token, diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts new file mode 100644 index 000000000..cdb860fba --- /dev/null +++ b/server/src/service/business/inviteService.ts @@ -0,0 +1,60 @@ +import type { Invite } from "@/types/index.js"; +import type { IInvitesRepository } from "@/repositories/index.js"; + +const SERVICE_NAME = "inviteService"; + +class InviteService { + static SERVICE_NAME = SERVICE_NAME; + + private settingsService: any; + private emailService: any; + private errorService: any; + private invitesRepository: IInvitesRepository; + + constructor({ + invitesRepository, + settingsService, + emailService, + errorService, + }: { + invitesRepository: IInvitesRepository; + settingsService: any; + emailService: any; + errorService: any; + }) { + this.invitesRepository = invitesRepository; + this.settingsService = settingsService; + this.emailService = emailService; + this.errorService = errorService; + } + + get serviceName() { + return InviteService.SERVICE_NAME; + } + + getInviteToken = async ({ invite, teamId }: { invite: Partial; teamId: string }) => { + invite.teamId = teamId; + const inviteToken = await this.invitesRepository.create(invite); + return inviteToken; + }; + + sendInviteEmail = async ({ invite, firstName }: { invite: Partial; firstName: any }) => { + const inviteToken = await this.invitesRepository.create(invite); + const { clientHost } = this.settingsService.getSettings(); + + const html = await this.emailService.buildEmail("employeeActivationTemplate", { + name: firstName, + link: `${clientHost}/register/${inviteToken.token}`, + }); + const result = await this.emailService.sendEmail(invite.email, "Welcome to Uptime Monitor", html); + if (!result) { + throw this.errorService.createServerError("Failed to send invite e-mail... Please verify your settings."); + } + }; + + verifyInviteToken = async ({ inviteToken }: { inviteToken: string }) => { + return await this.invitesRepository.findByToken(inviteToken); + }; +} + +export default InviteService; From a0ebf7d224bd06c2561a446c1f6e043293145104 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 06:02:07 +0000 Subject: [PATCH 067/103] initial commit --- server/src/db/models/Notification.js | 74 ------------------- server/src/db/models/Notification.ts | 50 +++++++++++++ server/src/db/models/User.ts | 4 +- server/src/db/models/index.ts | 3 + .../INotificationProvider.ts | 4 + .../infrastructure/notificationsService.ts | 4 + server/src/types/index.ts | 1 + server/src/types/notification.ts | 17 +++++ 8 files changed, 81 insertions(+), 76 deletions(-) delete mode 100755 server/src/db/models/Notification.js create mode 100755 server/src/db/models/Notification.ts create mode 100644 server/src/service/infrastructure/notificationProviders/INotificationProvider.ts create mode 100644 server/src/service/infrastructure/notificationsService.ts create mode 100644 server/src/types/notification.ts diff --git a/server/src/db/models/Notification.js b/server/src/db/models/Notification.js deleted file mode 100755 index 72c716a26..000000000 --- a/server/src/db/models/Notification.js +++ /dev/null @@ -1,74 +0,0 @@ -import mongoose from "mongoose"; - -const NotificationSchema = mongoose.Schema( - { - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - immutable: true, - required: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - type: { - type: String, - enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"], - }, - notificationName: { - type: String, - required: true, - }, - address: { - type: String, - }, - phone: { - type: String, - }, - // Matrix-specific fields - homeserverUrl: { - type: String, - }, - roomId: { - type: String, - }, - accessToken: { - type: String, - }, - }, - { - timestamps: true, - } -); - -NotificationSchema.pre("save", function (next) { - if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { - this.cpuAlertThreshold = this.alertThreshold; - } - if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) { - this.memoryAlertThreshold = this.alertThreshold; - } - if (!this.diskAlertThreshold || this.isModified("alertThreshold")) { - this.diskAlertThreshold = this.alertThreshold; - } - if (!this.tempAlertThreshold || this.isModified("alertThreshold")) { - this.tempAlertThreshold = this.alertThreshold; - } - next(); -}); - -NotificationSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if (update.alertThreshold) { - update.cpuAlertThreshold = update.alertThreshold; - update.memoryAlertThreshold = update.alertThreshold; - update.diskAlertThreshold = update.alertThreshold; - update.tempAlertThreshold = update.alertThreshold; - } - next(); -}); - -export default mongoose.model("Notification", NotificationSchema); diff --git a/server/src/db/models/Notification.ts b/server/src/db/models/Notification.ts new file mode 100755 index 000000000..97e2d43fb --- /dev/null +++ b/server/src/db/models/Notification.ts @@ -0,0 +1,50 @@ +import { Schema, model, type Types } from "mongoose"; +import type { Notification, NotificationChannel } from "@/types/notification.js"; + +interface NotificationDocument extends Omit { + _id: Types.ObjectId; + userId: Types.ObjectId; + teamId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const NotificationSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + immutable: true, + required: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, + type: { + type: String, + enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[], + required: true, + }, + notificationName: { + type: String, + required: true, + }, + address: { type: String }, + phone: { type: String }, + homeserverUrl: { type: String }, + roomId: { type: String }, + accessToken: { type: String }, + }, + { + timestamps: true, + } +); + +const NotificationModel = model("Notification", NotificationSchema); + +export type { NotificationDocument }; +export { NotificationModel }; +export default NotificationModel; diff --git a/server/src/db/models/User.ts b/server/src/db/models/User.ts index e00250340..6aa3835a6 100644 --- a/server/src/db/models/User.ts +++ b/server/src/db/models/User.ts @@ -3,7 +3,7 @@ import bcrypt from "bcryptjs"; import type { User, UserProfileImage, UserRole } from "@/types/index.js"; import { MonitorModel } from "@/db/models/index.js"; import Team from "./Team.js"; -import Notification from "./Notification.js"; +import NotificationModel from "./Notification.js"; type UserDocumentBase = Omit & { teamId?: Types.ObjectId; @@ -76,7 +76,7 @@ UserSchema.pre("findOneAndDelete", async function (next) { await Team.deleteOne({ _id: userToDelete.teamId }); await MonitorModel.deleteMany({ userId: userToDelete._id }); await this.model.deleteMany({ teamId: userToDelete.teamId, _id: { $ne: userToDelete._id } }); - await Notification.deleteMany({ teamId: userToDelete.teamId }); + await NotificationModel.deleteMany({ teamId: userToDelete.teamId }); } next(); } catch (error) { diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 59ed8125d..18a3a3b19 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -21,3 +21,6 @@ export { default as AppSettingsModel } from "@/db/models/AppSettings.js"; export * from "@/db/models/RecoveryToken.js"; export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; + +export * from "@/db/models/Notification.js"; +export { default as NotificationModel } from "@/db/models/Notification.js"; diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts new file mode 100644 index 000000000..c9a20ff5f --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -0,0 +1,4 @@ +import type { Monitor } from "@/types/index.js"; + +export interface IAlert {} +export interface IMessageProvider {} diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts new file mode 100644 index 000000000..e9a6e8e2d --- /dev/null +++ b/server/src/service/infrastructure/notificationsService.ts @@ -0,0 +1,4 @@ +import type { Monitor } from "@/types/index.js"; +interface INotificationSerivce { + handleNotifications: (moniotr: Monitor) => Promise; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts index fcfb6fc37..9bffb3816 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -7,3 +7,4 @@ export * from "@/types/user.js"; export * from "@/types/invite.js"; export * from "@/types/recoveryToken.js"; export * from "@/types/settings.js"; +export * from "@/types/notification.js"; diff --git a/server/src/types/notification.ts b/server/src/types/notification.ts new file mode 100644 index 000000000..669c840ca --- /dev/null +++ b/server/src/types/notification.ts @@ -0,0 +1,17 @@ +export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const; +export type NotificationChannel = (typeof NotificationChannels)[number]; + +export interface Notification { + id: string; + userId: string; + teamId: string; + type: NotificationChannel; + notificationName: string; + address?: string; + phone?: string; + homeserverUrl?: string; + roomId?: string; + accessToken?: string; + createdAt: string; + updatedAt: string; +} From cff5508614deb9a4270683092d85a8d4104a3dee Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 16:34:20 +0000 Subject: [PATCH 068/103] notification provider --- server/src/config/services.ts | 4 +-- server/src/db/modules/notificationModule.js | 2 -- .../INotificationProvider.ts | 9 ++++-- server/src/types/alert.ts | 29 +++++++++++++++++++ server/src/types/index.ts | 1 + 5 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 server/src/types/alert.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index bbb42984e..49e6f3d5e 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -50,7 +50,7 @@ import StatusPage from "../db/models/StatusPage.js"; import Team from "../db/models/Team.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; import MonitorStats from "../db/models/MonitorStats.js"; -import Notification from "../db/models/Notification.js"; +import NotificationModel from "../db/models/Notification.js"; import RecoveryToken from "../db/models/RecoveryToken.js"; import AppSettings from "../db/models/AppSettings.js"; import Incident from "../db/models/Incident.js"; @@ -141,7 +141,7 @@ export const initializeServices = async ({ const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); - const notificationModule = new NotificationModule({ Notification, Monitor }); + const notificationModule = new NotificationModule({ Notification: NotificationModel, Monitor }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); const settingsModule = new SettingsModule({ AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); diff --git a/server/src/db/modules/notificationModule.js b/server/src/db/modules/notificationModule.js index 3c6edb54a..f45900184 100755 --- a/server/src/db/modules/notificationModule.js +++ b/server/src/db/modules/notificationModule.js @@ -1,5 +1,3 @@ -// import Notification from "../../models/Notification.js"; -// import Monitor from "../../models/Monitor.js"; const SERVICE_NAME = "notificationModule"; class NotificationModule { diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index c9a20ff5f..12b28d907 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,4 +1,7 @@ -import type { Monitor } from "@/types/index.js"; +import type { Monitor, Notification, Alert } from "@/types/index.js"; -export interface IAlert {} -export interface IMessageProvider {} +export interface IMessageProvider { + buildAlert: (monitor: Monitor) => Alert; + sendAlert: (alert: Alert, notification: Notification) => Promise; + sendTestAlert(notification: Notification): Promise; +} diff --git a/server/src/types/alert.ts b/server/src/types/alert.ts new file mode 100644 index 000000000..4725ebee8 --- /dev/null +++ b/server/src/types/alert.ts @@ -0,0 +1,29 @@ +import { NotificationChannel } from "@/types/index.js"; +export interface AlertWebhookPayload { + body: Record; +} + +export interface AlertPagerDutyPayload { + routingKey: string; + monitorUrl?: string; +} + +export interface AlertMatrixPayload { + friendlyName?: string; + homeserverUrl: string; + accessToken: string; + roomId: string; + monitorName: string; +} + +export interface Alert { + channel: NotificationChannel; + address?: string; + subject: string; + message: string; + html?: string; + discordContent?: Record | null; + webhook?: AlertWebhookPayload | null; + pagerDuty?: AlertPagerDutyPayload | null; + matrix?: AlertMatrixPayload | null; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 9bffb3816..fda48f0c6 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -8,3 +8,4 @@ export * from "@/types/invite.js"; export * from "@/types/recoveryToken.js"; export * from "@/types/settings.js"; export * from "@/types/notification.js"; +export * from "@/types/alert.js"; From 391f42ee53dd62e2a093122236df320dc8cf2151 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 16:59:38 +0000 Subject: [PATCH 069/103] add webhook provider and utils --- server/src/service/index.ts | 2 + .../INotificationProvider.ts | 6 +- .../notificationProviders/utils.ts | 91 +++++++++++++++++++ .../notificationProviders/webhook.ts | 15 +++ 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 server/src/service/infrastructure/notificationProviders/utils.ts create mode 100644 server/src/service/infrastructure/notificationProviders/webhook.ts diff --git a/server/src/service/index.ts b/server/src/service/index.ts index a094f8b01..4dba8f6f4 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -1,2 +1,4 @@ export * from "@/service/business/monitorService.js"; export * from "@/service/infrastructure/networkService.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 12b28d907..2743c40bb 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,7 +1,7 @@ -import type { Monitor, Notification, Alert } from "@/types/index.js"; +import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js"; -export interface IMessageProvider { - buildAlert: (monitor: Monitor) => Alert; +export interface INotificationProvider { + buildAlert: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Alert; sendAlert: (alert: Alert, notification: Notification) => Promise; sendTestAlert(notification: Notification): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts new file mode 100644 index 000000000..9a1b2e7f8 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -0,0 +1,91 @@ +import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; + +const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, networkResponse: MonitorStatusResponse) => { + const thresholds = monitor.thresholds || {}; + const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; + + const payload = networkResponse?.payload as HardwareStatusPayload; + const metrics = payload.data || {}; + const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; + + const alerts = { + cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, + memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, + disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, + }; + + const alertsToSend = []; + const discordEmbeds = []; + const monitorInfoFields = [ + { name: "Monitor", value: monitor.name, inline: true }, + { name: "URL", value: monitor.url, inline: false }, + ]; + const goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor.id}` }; + const formatDiscordAlert = { + cpu: () => ({ + title: "CPU alert", + description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, + color: 15548997, + fields: [...monitorInfoFields, goToIncidentField], + footer: { text: "Checkmate" }, + }), + + memory: () => ({ + title: "Memory alert", + description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, + color: 15548997, + fields: [...monitorInfoFields, goToIncidentField], + footer: { text: "Checkmate" }, + }), + + disk: () => ({ + title: "Disk alert", + description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + color: 15548997, + footer: { text: "Checkmate" }, + fields: [ + ...monitorInfoFields, + ...(Array.isArray(disk) ? disk : []).map((d: any, idx: number) => ({ + name: `Disk ${idx}`, + value: `${(d?.usage_percent * 100).toFixed(0)}%`, + inline: true, + })), + goToIncidentField, + ], + }), + }; + const alertTypes = ["cpu", "memory", "disk"]; + for (const type of alertTypes) { + // Iterate over each alert type to see if any need to be decremented + if (alerts[type] === true) { + monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered + + if (monitor[`${type}AlertThreshold`] <= 0) { + // If threshold drops below 0, reset and send notification + monitor[`${type}AlertThreshold`] = monitor.alertThreshold; + + const formatAlert = { + cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, + memory: () => + `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, + disk: () => + `Your current disk usage: ${disk + .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) + .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + }; + alertsToSend.push(formatAlert[type]()); + discordEmbeds.push(formatDiscordAlert[type]()); + } + } + } + const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; + return [alertsToSend, discordPayload]; +}; + +const buildHardwareNotificationMessage = (alerts: any, monitor: Monitor) => { + const { clientHost } = this.settingsService.getSettings(); + const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; + const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor._id}`]; + const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; + return alertText.map((alert) => alert).join("\n"); +}; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts new file mode 100644 index 000000000..4266bbf3b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -0,0 +1,15 @@ +import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +export class WebhookProvider implements INotificationProvider { + buildAlert = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + return null; + }; + + sendAlert = async (alert: Alert) => { + return false; + }; + + sendTestAlert(notification: Notification): Promise { + return false; + } +} From 487f6b222d7ad0e919435348f21eb24067f6def6 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Sun, 18 Jan 2026 14:11:05 -0500 Subject: [PATCH 070/103] refactor: extract sidebar width logic into useSidebar hook Create shared useSidebar hook to centralize sidebar width and transition logic, addressing code review feedback about duplicated logic between Sidebar and HomeLayout components. Changes: - Add useSidebar hook with collapsed state, width, and transition values - Update Sidebar to use the hook instead of inline selectors and values - Update HomeLayout to use the hook for spacer element styling This ensures sidebar width logic is defined in one place and stays in sync. --- .../v1/Layouts/HomeLayout/index.jsx | 10 +++---- client/src/Components/v1/Sidebar/index.jsx | 13 +++------ client/src/Hooks/useSidebar.js | 29 +++++++++++++++++++ 3 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 client/src/Hooks/useSidebar.js diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.jsx b/client/src/Components/v1/Layouts/HomeLayout/index.jsx index 825157333..b078fe048 100644 --- a/client/src/Components/v1/Layouts/HomeLayout/index.jsx +++ b/client/src/Components/v1/Layouts/HomeLayout/index.jsx @@ -1,12 +1,12 @@ import Sidebar from "../../Sidebar/index.jsx"; import { Outlet } from "react-router"; import { Box, Stack } from "@mui/material"; -import { useSelector } from "react-redux"; +import { useSidebar } from "@/Hooks/useSidebar.js"; import "./index.css"; const HomeLayout = () => { - const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); + const { width, transition } = useSidebar(); return ( { {/* Spacer for fixed sidebar */} diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx index f39121987..06caede5a 100644 --- a/client/src/Components/v1/Sidebar/index.jsx +++ b/client/src/Components/v1/Sidebar/index.jsx @@ -12,9 +12,9 @@ import Icon from "../Icon"; // Utils import { useTheme } from "@mui/material/styles"; -import { useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; +import { useSidebar } from "@/Hooks/useSidebar.js"; const URL_MAP = { support: "https://discord.com/invite/NAb6H3UTjK", @@ -65,8 +65,7 @@ const Sidebar = () => { const theme = useTheme(); const { t } = useTranslation(); const navigate = useNavigate(); - // Redux state - const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); + const { collapsed, width, transition } = useSidebar(); const menu = getMenu(t); const otherMenuItems = getOtherMenuItems(t); @@ -75,11 +74,7 @@ const Sidebar = () => { return ( { paddingBottom={theme.spacing(6)} gap={theme.spacing(6)} sx={{ - transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)", + transition, backgroundColor: "#000000", borderRight: "1px solid #344054", zIndex: 1000, diff --git a/client/src/Hooks/useSidebar.js b/client/src/Hooks/useSidebar.js new file mode 100644 index 000000000..97724b417 --- /dev/null +++ b/client/src/Hooks/useSidebar.js @@ -0,0 +1,29 @@ +import { useSelector } from "react-redux"; + +// CSS variable names for sidebar widths +const SIDEBAR_WIDTH_VAR = "var(--env-var-side-bar-width)"; +const SIDEBAR_COLLAPSED_WIDTH_VAR = "var(--env-var-side-bar-collapsed-width)"; + +// Transition timing for sidebar width changes +const SIDEBAR_TRANSITION = "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)"; + +/** + * Hook to get sidebar state and computed width + * Centralizes sidebar width logic to avoid duplication between Sidebar and HomeLayout + * + * @returns {Object} Sidebar state and styles + * @returns {boolean} collapsed - Whether the sidebar is collapsed + * @returns {string} width - CSS width value based on collapsed state + * @returns {string} transition - CSS transition for width changes + */ +export const useSidebar = () => { + const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); + + return { + collapsed, + width: collapsed ? SIDEBAR_COLLAPSED_WIDTH_VAR : SIDEBAR_WIDTH_VAR, + transition: SIDEBAR_TRANSITION, + }; +}; + +export default useSidebar; From 4e9839e2e5a3bd27968660c660f497d2e69193ea Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 22:52:59 +0000 Subject: [PATCH 071/103] notificationsService --- server/src/config/services.ts | 9 +++- server/src/service/index.ts | 1 + .../SuperSimpleQueueHelper.ts | 43 +++++++++++-------- .../service/infrastructure/bufferService.ts | 16 +------ .../infrastructure/notificationsService.ts | 13 ++++-- .../service/infrastructure/statusService.ts | 16 +------ 6 files changed, 46 insertions(+), 52 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 49e6f3d5e..92acd8868 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -8,6 +8,7 @@ import BufferService from "../service/infrastructure/bufferService.js"; import StatusService from "../service/infrastructure/statusService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import NotificationService from "../service/infrastructure/notificationService.js"; +import { NotificationsService } from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; @@ -206,9 +207,9 @@ export const initializeServices = async ({ checksRepository, }); - const bufferService = new BufferService({ db, logger, envSettings, incidentService, checkService }); + const bufferService = new BufferService({ logger, incidentService, checkService }); - const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService, monitorsRepository }); + const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); const notificationUtils = new NotificationUtils({ stringService, @@ -225,12 +226,15 @@ export const initializeServices = async ({ notificationUtils, }); + const notificationsService = new NotificationsService(); + const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, logger, networkService, statusService, notificationService, + notificationsService, checkService, buffer: bufferService, }); @@ -317,6 +321,7 @@ export const initializeServices = async ({ invitesRepository, recoveryTokensRepository, settingsRepository, + notificationsService, }; Object.values(services).forEach((service) => { diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 4dba8f6f4..08c02f397 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -2,3 +2,4 @@ export * from "@/service/business/monitorService.js"; export * from "@/service/infrastructure/networkService.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +export * from "@/service/infrastructure/notificationsService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 891635d56..c2a9e1267 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,7 +1,7 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { INetworkService } from "@/service/index.js"; +import { INetworkService, INotificationsService } from "@/service/index.js"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; @@ -10,6 +10,7 @@ class SuperSimpleQueueHelper { private networkService: INetworkService; private statusService: any; private notificationService: any; + private notificationsService: INotificationsService; private checkService: any; private buffer: any; @@ -19,6 +20,7 @@ class SuperSimpleQueueHelper { networkService, statusService, notificationService, + notificationsService, checkService, buffer, }: { @@ -27,6 +29,7 @@ class SuperSimpleQueueHelper { networkService: INetworkService; statusService: any; notificationService: any; + notificationsService: INotificationsService; checkService: any; buffer: any; }) { @@ -37,6 +40,7 @@ class SuperSimpleQueueHelper { this.notificationService = notificationService; this.checkService = checkService; this.buffer = buffer; + this.notificationsService = notificationsService; } get serviceName() { @@ -78,22 +82,27 @@ class SuperSimpleQueueHelper { // Step 4. Update monitor status const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); - this.notificationService - .handleNotifications({ - ...status, - monitor: statusChangeResult.monitor, - prevStatus: statusChangeResult.prevStatus, - statusChanged: statusChangeResult.statusChanged, - }) - .catch((error: any) => { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "getMonitorJob", - details: `Error sending notifications for job ${monitor.id}: ${error.message}`, - stack: error.stack, - }); - }); + // Step 5 handle incidents and notifications + if (statusChangeResult.statusChanged === true) { + this.notificationsService.handleNotifications(monitor, status); + } + + // this.notificationService + // .handleNotifications({ + // ...status, + // monitor: statusChangeResult.monitor, + // prevStatus: statusChangeResult.prevStatus, + // statusChanged: statusChangeResult.statusChanged, + // }) + // .catch((error: any) => { + // this.logger.error({ + // message: error.message, + // service: SERVICE_NAME, + // method: "getMonitorJob", + // details: `Error sending notifications for job ${monitor.id}: ${error.message}`, + // stack: error.stack, + // }); + // }); } catch (error: any) { this.logger.warn({ message: error.message, diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 5a809eea5..0363aa362 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -6,7 +6,6 @@ const SERVICE_NAME = "BufferService"; class BufferService { static SERVICE_NAME = SERVICE_NAME; private BUFFER_TIMEOUT: number; - private db: any; private logger: any; private incidentService: any; private SERVICE_NAME: string; @@ -15,21 +14,8 @@ class BufferService { private bufferTimer: NodeJS.Timeout | null = null; private checksService: any; - constructor({ - db, - logger, - envSettings, - incidentService, - checkService, - }: { - db: any; - logger: any; - envSettings: any; - incidentService: any; - checkService: any; - }) { + constructor({ logger, incidentService, checkService }: { logger: any; incidentService: any; checkService: any }) { this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute - this.db = db; this.logger = logger; this.incidentService = incidentService; this.checksService = checkService; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index e9a6e8e2d..f9fdbfea8 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,4 +1,11 @@ -import type { Monitor } from "@/types/index.js"; -interface INotificationSerivce { - handleNotifications: (moniotr: Monitor) => Promise; +import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; +export interface INotificationsService { + handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; +} + +export class NotificationsService implements INotificationsService { + handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + console.log(JSON.stringify(monitor, null, 2)); + console.log(JSON.stringify(monitorStatusResponse, null, 2)); + }; } diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 7d34017a5..c9aab706b 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -17,26 +17,12 @@ class StatusService { private db: any; private logger: any; private buffer: any; - private incidentService: any; private monitorsRepository: IMonitorsRepository; - constructor({ - db, - logger, - buffer, - incidentService, - monitorsRepository, - }: { - db: any; - logger: any; - buffer: any; - incidentService: any; - monitorsRepository: IMonitorsRepository; - }) { + constructor({ db, logger, buffer, monitorsRepository }: { db: any; logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) { this.db = db; this.logger = logger; this.buffer = buffer; - this.incidentService = incidentService; this.monitorsRepository = monitorsRepository; } From a86588f080b03ce88664b86517e1dc9d380dc7d0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 23:38:40 +0000 Subject: [PATCH 072/103] refactoring notifications --- server/src/config/services.ts | 3 +- server/src/service/index.ts | 1 + .../SuperSimpleQueueHelper.ts | 17 ++++--- .../infrastructure/notificationsService.ts | 48 +++++++++++++++++-- .../service/infrastructure/statusService.ts | 31 +++++++----- server/src/types/network.ts | 2 +- 6 files changed, 74 insertions(+), 28 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 92acd8868..ed1224ec7 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -5,10 +5,9 @@ import MongoDB from "../db/MongoDB.js"; import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; -import StatusService from "../service/infrastructure/statusService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import NotificationService from "../service/infrastructure/notificationService.js"; -import { NotificationsService } from "@/service/index.js"; +import { NotificationsService, StatusService } from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 08c02f397..fbba7714d 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -3,3 +3,4 @@ export * from "@/service/infrastructure/networkService.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; export * from "@/service/infrastructure/notificationsService.js"; +export * from "@/service/infrastructure/statusService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index c2a9e1267..21ee6e388 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,18 +1,19 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { INetworkService, INotificationsService } from "@/service/index.js"; +import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; private db: any; private logger: any; private networkService: INetworkService; - private statusService: any; + private statusService: IStatusService; private notificationService: any; private notificationsService: INotificationsService; private checkService: any; private buffer: any; + private incidentService: any; constructor({ db, @@ -23,15 +24,17 @@ class SuperSimpleQueueHelper { notificationsService, checkService, buffer, + incidentService, }: { db: any; logger: any; networkService: INetworkService; - statusService: any; + statusService: IStatusService; notificationService: any; notificationsService: INotificationsService; checkService: any; buffer: any; + incidentService: any; }) { this.db = db; this.logger = logger; @@ -41,6 +44,7 @@ class SuperSimpleQueueHelper { this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; + this.incidentService = incidentService; } get serviceName() { @@ -82,10 +86,9 @@ class SuperSimpleQueueHelper { // Step 4. Update monitor status const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); - // Step 5 handle incidents and notifications - if (statusChangeResult.statusChanged === true) { - this.notificationsService.handleNotifications(monitor, status); - } + // Step 5 handle notifications + + this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); // this.notificationService // .handleNotifications({ diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index f9fdbfea8..3dc5fe65e 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,11 +1,49 @@ -import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; +import type { HardwareStatusPayload, Monitor, MonitorStatusResponse } from "@/types/index.js"; export interface INotificationsService { - handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; + handleNotifications: ( + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + prevStatus: boolean | undefined, + statusChanged: boolean + ) => Promise; } export class NotificationsService implements INotificationsService { - handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - console.log(JSON.stringify(monitor, null, 2)); - console.log(JSON.stringify(monitorStatusResponse, null, 2)); + handleNotifications = async ( + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + prevStatus: boolean | undefined, + statusChanged: boolean + ) => { + const { type } = monitor; + const payload = monitorStatusResponse.payload as HardwareStatusPayload; + // If this is a non-hardeware type monitor and status did not change, we're done + if (type !== "hardware" && statusChanged === false) return false; + // if prevStatus is undefined, monitor is resuming, we're done + if (type !== "hardware" && prevStatus === undefined) return false; + + // Deal with hardware thresholds + if (type === "hardware") { + const thresholds = monitor.thresholds; + + if (thresholds === undefined) return false; // No thresholds set, we're done + const metrics = payload?.data ?? null; + if (metrics === null) return false; // No metrics, we're done + + // We shoul dsend a notificaiton + + console.log(JSON.stringify(monitor, null, 2)); + console.log(JSON.stringify(monitorStatusResponse, null, 2)); + // const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); + // if (alerts.length === 0) return false; + + // const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); + // const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); + // const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); + // const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); + return true; + } + + return false; }; } diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index c9aab706b..3d9591c02 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -12,7 +12,17 @@ import type { } from "@/types/index.js"; const SERVICE_NAME = "StatusService"; -class StatusService { +export interface IStatusService { + updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise; + getStatusString(status: boolean | undefined): string; + handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise; + updateMonitorStatus( + statusResponse: MonitorStatusResponse, + check: Check + ): Promise; +} + +export class StatusService implements IStatusService { static SERVICE_NAME = SERVICE_NAME; private db: any; private logger: any; @@ -108,11 +118,10 @@ class StatusService { return "unknown"; }; - handleIncidentForCheck = async (check: any, monitor: Monitor, action: any, errorContext = "incident handling") => { + handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => { try { - let savedCheck = check; - - if (!check._id) { + let savedCheck; + if (!check.id) { try { const checkModel = new CheckModel(check); savedCheck = await checkModel.save(); @@ -130,7 +139,7 @@ class StatusService { } } - if (savedCheck && savedCheck._id) { + if (savedCheck && savedCheck.id) { try { this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action }); } catch (incidentError: any) { @@ -167,13 +176,10 @@ class StatusService { this.updateRunningStats({ monitor, networkResponse: statusResponse }); // If the status window size has changed, empty - while (monitor.statusWindow.length > monitor.statusWindowSize) { - monitor.statusWindow.shift(); - } - // Update status sliding window + monitor.statusWindow = monitor.statusWindow || []; monitor.statusWindow.push(status); - if (monitor.statusWindow.length > monitor.statusWindowSize) { + while (monitor.statusWindow.length > monitor.statusWindowSize) { monitor.statusWindow.shift(); } @@ -181,9 +187,9 @@ class StatusService { monitor.status = status; } + const prevStatus = monitor.status; let newStatus = monitor.status; let statusChanged = false; - const prevStatus = monitor.status; // Return early if not enough data points if (monitor.statusWindow.length < monitor.statusWindowSize) { @@ -316,4 +322,3 @@ class StatusService { } }; } -export default StatusService; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 10f89842f..6bed40316 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -13,7 +13,7 @@ import type { MonitorType, } from "@/types/index.js"; -export interface MonitorStatusResponse { +export interface MonitorStatusResponse { monitorId: string; teamId: string; type: MonitorType; From c2487f013437b1c6e85adedf2d029c40eb3fb78f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 23:54:42 +0000 Subject: [PATCH 073/103] alerts --- .../infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts | 3 --- .../src/service/infrastructure/notificationProviders/utils.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 21ee6e388..d1315e151 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -24,7 +24,6 @@ class SuperSimpleQueueHelper { notificationsService, checkService, buffer, - incidentService, }: { db: any; logger: any; @@ -34,7 +33,6 @@ class SuperSimpleQueueHelper { notificationsService: INotificationsService; checkService: any; buffer: any; - incidentService: any; }) { this.db = db; this.logger = logger; @@ -44,7 +42,6 @@ class SuperSimpleQueueHelper { this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; - this.incidentService = incidentService; } get serviceName() { diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 9a1b2e7f8..ad8dc14ae 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -8,7 +8,7 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network const metrics = payload.data || {}; const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; - const alerts = { + const alerts: Record = { cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, From 7d6bc4b2dc80a25ef31840c9595974a9f1345029 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 00:48:20 +0000 Subject: [PATCH 074/103] notificaitons provider --- server/src/config/services.ts | 7 ++- server/src/repositories/index.ts | 3 + .../notifications/INotificationsRepository.ts | 7 +++ .../MongoNotificationsRepository.ts | 40 +++++++++++++ .../src/service/business/incidentService.js | 12 ++-- .../SuperSimpleQueueHelper.ts | 1 - .../notificationProviders/utils.ts | 56 +++++++++++++++---- .../infrastructure/notificationsService.ts | 33 +++++++++-- 8 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 server/src/repositories/notifications/INotificationsRepository.ts create mode 100644 server/src/repositories/notifications/MongoNotificationsRepository.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index ed1224ec7..e25ee1706 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -75,6 +75,7 @@ import { MongoInvitesRepository, MongoRecoveryTokensRepository, MongoSettingsRepository, + MongoNotificationsRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, @@ -83,6 +84,7 @@ import { IInvitesRepository, IRecoveryTokensRepository, ISettingsRepository, + INotificationsRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -116,6 +118,7 @@ export type InitializedSerivces = { invitesRepository: IInvitesRepository; recoveryTokensRepository: IRecoveryTokensRepository; settingsRepository: ISettingsRepository; + notificationsRepository: INotificationsRepository; }; export const initializeServices = async ({ @@ -171,7 +174,7 @@ export const initializeServices = async ({ const invitesRepository = new MongoInvitesRepository(); const recoveryTokensRepository = new MongoRecoveryTokensRepository(); const settingsRepository = new MongoSettingsRepository(); - + const notificationsRepository = new MongoNotificationsRepository(); const networkService = new NetworkService({ axios, got, @@ -225,7 +228,7 @@ export const initializeServices = async ({ notificationUtils, }); - const notificationsService = new NotificationsService(); + const notificationsService = new NotificationsService(notificationsRepository); const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 09a686595..1a3199081 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -21,3 +21,6 @@ export { default as MongoRecoveryTokensRepository } from "@/repositories/recover export * from "@/repositories/settings/ISettingsRepository.js"; export { default as MongoSettingsRepository } from "@/repositories/settings/MongoSettingsRepository.js"; + +export * from "@/repositories/notifications/INotificationsRepository.js"; +export { default as MongoNotificationsRepository } from "@/repositories/notifications/MongoNotificationsRepository.js"; diff --git a/server/src/repositories/notifications/INotificationsRepository.ts b/server/src/repositories/notifications/INotificationsRepository.ts new file mode 100644 index 000000000..0244972e4 --- /dev/null +++ b/server/src/repositories/notifications/INotificationsRepository.ts @@ -0,0 +1,7 @@ +import type { Notification } from "@/types/index.js"; +export interface INotificationsRepository { + // create + // fetch + // update + // delete +} diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts new file mode 100644 index 000000000..51839c125 --- /dev/null +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -0,0 +1,40 @@ +import mongoose from "mongoose"; +import { NotificationModel, type NotificationDocument } from "@/db/models/index.js"; +import { INotificationsRepository } from "@/repositories/index.js"; +import type { Notification } from "@/types/index.js"; + +class MongoNotificationsRepository implements INotificationsRepository { + private mapDocuments = (documents: NotificationDocument[]): Notification[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; + + private toEntity = (doc: NotificationDocument): Notification => { + const toStringId = (value: mongoose.Types.ObjectId | string): string => { + return value instanceof mongoose.Types.ObjectId ? value.toString() : value; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + type: doc.type, + notificationName: doc.notificationName, + address: doc.address ?? undefined, + phone: doc.phone ?? undefined, + homeserverUrl: doc.homeserverUrl ?? undefined, + roomId: doc.roomId ?? undefined, + accessToken: doc.accessToken ?? undefined, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + }; +} + +export default MongoNotificationsRepository; diff --git a/server/src/service/business/incidentService.js b/server/src/service/business/incidentService.js index 71e60d279..db89caa68 100644 --- a/server/src/service/business/incidentService.js +++ b/server/src/service/business/incidentService.js @@ -305,7 +305,7 @@ class IncidentService { const groupedByMonitor = {}; for (const item of createItems) { - if (!item.monitor || !item.monitor._id || !item.check || !item.check._id) { + if (!item.monitor || !item.monitor.id || !item.check || !item.check.id) { this.logger.warn({ service: this.SERVICE_NAME, method: "processIncidentsFromBuffer", @@ -315,7 +315,7 @@ class IncidentService { continue; } - const monitorId = item.monitor._id.toString(); + const monitorId = item.monitor.id.toString(); if (!groupedByMonitor[monitorId]) { groupedByMonitor[monitorId] = []; } @@ -341,20 +341,20 @@ class IncidentService { for (const item of items) { checksToAddToIncidents.push({ incidentId, - checkId: item.check._id.toString(), + checkId: item.check.id.toString(), }); } } else { const firstItem = items[0]; const incidentData = { - monitorId: firstItem.monitor._id, + monitorId: firstItem.monitor.id, teamId: firstItem.monitor.teamId, type: firstItem.monitor.type, startTime: new Date(), status: true, message: firstItem.check.message || null, statusCode: firstItem.check.statusCode || null, - checks: [firstItem.check._id], + checks: [firstItem.check.id], }; newIncidentsToCreate.push({ @@ -380,7 +380,7 @@ class IncidentService { for (const remainingItem of item.remainingChecks) { checksToAddToIncidents.push({ incidentId, - checkId: remainingItem.check._id.toString(), + checkId: remainingItem.check.id, }); } } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index d1315e151..41885a146 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -84,7 +84,6 @@ class SuperSimpleQueueHelper { const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); // Step 5 handle notifications - this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); // this.notificationService diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index ad8dc14ae..7cba00a5d 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -1,6 +1,10 @@ import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; -const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, networkResponse: MonitorStatusResponse) => { +export const buildHardwareAlerts = ( + clientHost: string, + monitor: Monitor, + networkResponse: MonitorStatusResponse +): { alertsToSend: string[]; discordPayload: any } => { const thresholds = monitor.thresholds || {}; const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; @@ -54,15 +58,22 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network ], }), }; - const alertTypes = ["cpu", "memory", "disk"]; + const alertTypes = ["cpu", "memory", "disk"] as const; + const alertThresholdKeyMap: Record<(typeof alertTypes)[number], "cpuAlertThreshold" | "memoryAlertThreshold" | "diskAlertThreshold"> = { + cpu: "cpuAlertThreshold", + memory: "memoryAlertThreshold", + disk: "diskAlertThreshold", + }; for (const type of alertTypes) { + const thresholdKey = alertThresholdKeyMap[type]; // Iterate over each alert type to see if any need to be decremented if (alerts[type] === true) { - monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered + const nextValue = ((monitor[thresholdKey] ?? monitor.alertThreshold) as number) - 1; + monitor[thresholdKey] = nextValue; // Decrement threshold if an alert is triggered - if (monitor[`${type}AlertThreshold`] <= 0) { + if (monitor[thresholdKey] <= 0) { // If threshold drops below 0, reset and send notification - monitor[`${type}AlertThreshold`] = monitor.alertThreshold; + monitor[thresholdKey] = monitor.alertThreshold; const formatAlert = { cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, @@ -70,7 +81,7 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, disk: () => `Your current disk usage: ${disk - .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) + .map((d, idx) => `(Disk${idx}: ${(d?.usage_percent ?? 0 * 100).toFixed(0)}%)`) .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, }; alertsToSend.push(formatAlert[type]()); @@ -79,13 +90,38 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network } } const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; - return [alertsToSend, discordPayload]; + return { alertsToSend, discordPayload }; }; -const buildHardwareNotificationMessage = (alerts: any, monitor: Monitor) => { - const { clientHost } = this.settingsService.getSettings(); +const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => { const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; - const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor._id}`]; + const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor.id}`]; const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; return alertText.map((alert) => alert).join("\n"); }; + +export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { + const thresholds = monitor.thresholds || {}; + const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; + + const payload = networkResponse?.payload as HardwareStatusPayload; + const metrics = payload.data || {}; + const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; + + const cpuBreach = cpuThreshold !== -1 && cpuUsage > cpuThreshold; + if (cpuBreach && ((monitor.cpuAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + const memoryBreach = memoryThreshold !== -1 && memoryUsage > memoryThreshold; + if (memoryBreach && ((monitor.memoryAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + const diskBreach = disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold); + if (diskBreach && ((monitor.diskAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + return false; +}; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 3dc5fe65e..2070b0c4f 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,4 +1,7 @@ -import type { HardwareStatusPayload, Monitor, MonitorStatusResponse } from "@/types/index.js"; +import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; +import { NotificationModel } from "@/db/models/index.js"; +import { buildHardwareAlerts, shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; +import { INotificationsRepository } from "@/repositories/index.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -9,6 +12,24 @@ export interface INotificationsService { } export class NotificationsService implements INotificationsService { + private notificationsRepository: INotificationsRepository; + constructor(notificationsRepository: INotificationsRepository) { + this.notificationsRepository = notificationsRepository; + } + + private send = (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + console.log(notification); + }; + + private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const notifications = await this.notificationsRepository.findByMonitorId(monitor.id); + console.log({ notifications }); + const tasks = notifications.map((notification) => { + this.send(notification, monitor, monitorStatusResponse); + }); + return true; + }; + handleNotifications = async ( monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, @@ -32,18 +53,18 @@ export class NotificationsService implements INotificationsService { // We shoul dsend a notificaiton + const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse); + if (shouldSend === false) return false; console.log(JSON.stringify(monitor, null, 2)); console.log(JSON.stringify(monitorStatusResponse, null, 2)); - // const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); - // if (alerts.length === 0) return false; - // const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); // const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); // const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); // const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); - return true; + return await this.sendNotifications(monitor, monitorStatusResponse); } - return false; + // We should send a notification for non-hardware monitor status change + return await this.sendNotifications(monitor, monitorStatusResponse); }; } From 3b0326457ffd100999a867fbb398ab5ea77df8e6 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Mon, 19 Jan 2026 13:49:08 -0500 Subject: [PATCH 075/103] docs: update OpenAPI specification to match current API ## Changes ### New Endpoints Added - `/incidents/team` - Get incidents by team with filtering - `/incidents/team/summary` - Get incident summary statistics - `/incidents/{incidentId}` - Get incident by ID - `/incidents/{incidentId}/resolve` - Manually resolve incident - `/queue/jobs` (GET) - List queue jobs (admin only) - `/queue/jobs` (POST) - Add job to queue (admin only) - `/queue/metrics` - Get queue metrics (admin only) - `/monitors/team/groups` - Get monitor groups by team - `/monitors/pagespeed/details/{monitorId}` - Get PageSpeed details - `/checks/{action}` - Acknowledge all checks for all monitors - `/checks/{action}/{monitorId}` - Acknowledge checks for specific monitor ### Endpoints Fixed - `/monitors/export` changed to `/monitors/export/json` (matches actual route) - Removed non-existent `/monitors/stats/{monitorId}` endpoint - Removed non-existent `/monitors/team/summary` endpoint - Split optional path parameter into two valid endpoints for checks ### New Schemas Added - `Incident` - Complete incident model - `IncidentSummary` - Incident statistics summary - `QueueJob` - Queue job information - `QueueMetrics` - Queue metrics data - `Check` - Complete check result model - `CheckTimings` - HTTP timing information - `CheckCpuInfo` - CPU metrics for infrastructure checks - `CheckMemoryInfo` - Memory metrics for infrastructure checks - `CheckDiskInfo` - Disk metrics for infrastructure checks - `CheckHostInfo` - Host information for infrastructure checks - `CheckErrorInfo` - Error information from checks - `CheckCaptureInfo` - Capture agent information - `CheckNetworkInfo` - Network interface metrics - `CheckAudits` - PageSpeed audit results - `LighthouseAudit` - Individual Lighthouse audit result ### Responses Added - `BadRequest` - 400 error response ### Other Fixes - Simplified server variables to fix OpenAPI 3.1.0 validation - Added request body to `/checks/check/{checkId}` endpoint - Added "incidents" tag to tags array ## Benefits - OpenAPI specification now accurately reflects the actual API - All endpoints have proper request/response schemas - Validation passes with swagger-cli - Better API documentation for consumers --- server/openapi.json | 1327 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 1293 insertions(+), 34 deletions(-) diff --git a/server/openapi.json b/server/openapi.json index 6cc2e7de0..d104501d0 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -21,13 +21,9 @@ "description": "Local Development Server", "variables": { "PORT": { - "description": "API Port", - "enum": ["52345"], "default": "52345" }, "API_PATH": { - "description": "API Base Path", - "enum": ["api/v1"], "default": "api/v1" } } @@ -37,8 +33,6 @@ "description": "Checkmate Demo Server", "variables": { "API_PATH": { - "description": "API Base Path", - "enum": ["api/v1"], "default": "api/v1" } } @@ -88,6 +82,10 @@ { "name": "diagnostic", "description": "System health and performance diagnostics" + }, + { + "name": "incidents", + "description": "Incident management and resolution" } ], "paths": { @@ -731,28 +729,54 @@ ] } }, - "/monitors/team/summary": { + "/monitors/team/groups": { "get": { "tags": ["monitors"], - "summary": "Get monitors summary", - "description": "Get team monitors with summary statistics", - "parameters": [ - { - "name": "type", - "in": "query", - "description": "Filter by monitor type", - "schema": { - "type": "array", - "items": { - "type": "string", - "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"] - } - } - } - ], + "summary": "Get monitor groups by team", + "description": "Get all monitor groups for the authenticated user's team", "responses": { "200": { - "description": "Monitor summary retrieved successfully" + "description": "Monitor groups retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Monitor groups retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "monitors": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -992,24 +1016,78 @@ ] } }, - "/monitors/stats/{monitorId}": { + "/monitors/pagespeed/details/{monitorId}": { "get": { "tags": ["monitors"], - "summary": "Get monitor statistics", - "description": "Get comprehensive statistics for a monitor", + "summary": "Get PageSpeed monitor details", + "description": "Get detailed PageSpeed monitoring data including performance metrics and Core Web Vitals", "parameters": [ { "name": "monitorId", "in": "path", "required": true, + "description": "The PageSpeed monitor ID", "schema": { "type": "string" } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort order for results", + "schema": { + "type": "string", + "enum": ["asc", "desc"], + "default": "desc" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of results to return", + "schema": { + "type": "integer", + "default": 50 + } } ], "responses": { "200": { - "description": "Monitor statistics retrieved successfully" + "description": "PageSpeed details retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "PageSpeed details retrieved successfully" + }, + "data": { + "type": "object", + "properties": { + "monitor": { + "$ref": "#/components/schemas/Monitor" + }, + "checks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Check" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" @@ -1078,22 +1156,44 @@ ] } }, - "/monitors/export": { + "/monitors/export/json": { "get": { "tags": ["monitors"], - "summary": "Export monitors to CSV", - "description": "Export all monitors to CSV format", + "summary": "Export monitors to JSON", + "description": "Export all monitors to JSON format (admin/superadmin only)", "responses": { "200": { - "description": "CSV file generated successfully", + "description": "JSON file generated successfully", "content": { - "text/csv": { + "application/json": { "schema": { - "type": "string" + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Monitors exported successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Monitor" + } + } + } } } } }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -2030,6 +2130,21 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ack": { + "type": "boolean", + "description": "Acknowledgement status" + } + } + } + } + } + }, "responses": { "200": { "$ref": "#/components/responses/Success" @@ -2048,6 +2163,164 @@ ] } }, + "/checks/{action}": { + "put": { + "tags": ["checks"], + "summary": "Acknowledge all checks for all monitors", + "description": "Acknowledge all checks across all monitors for the team", + "parameters": [ + { + "name": "action", + "in": "path", + "required": true, + "description": "Action to perform (e.g., 'ack')", + "schema": { + "type": "string", + "enum": ["ack"] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["ack"], + "properties": { + "ack": { + "type": "boolean", + "description": "Acknowledgement status to set" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Checks acknowledged successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "All checks acknowledged successfully" + }, + "data": { + "type": "object", + "description": "Updated checks information" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/checks/{action}/{monitorId}": { + "put": { + "tags": ["checks"], + "summary": "Acknowledge all checks for a specific monitor", + "description": "Acknowledge all checks for a specific monitor", + "parameters": [ + { + "name": "action", + "in": "path", + "required": true, + "description": "Action to perform (e.g., 'ack')", + "schema": { + "type": "string", + "enum": ["ack"] + } + }, + { + "name": "monitorId", + "in": "path", + "required": true, + "description": "Monitor ID to acknowledge checks for", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["ack"], + "properties": { + "ack": { + "type": "boolean", + "description": "Acknowledgement status to set" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Checks acknowledged successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "All checks acknowledged successfully" + }, + "data": { + "type": "object", + "description": "Updated checks information" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/maintenance-window": { "post": { "tags": ["maintenance-window"], @@ -2291,6 +2564,142 @@ ] } }, + "/queue/jobs": { + "get": { + "tags": ["queue"], + "summary": "Get queue jobs", + "description": "Retrieve all jobs in the queue (admin/superadmin only)", + "responses": { + "200": { + "description": "Queue jobs retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Queue jobs fetched successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueJob" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": ["queue"], + "summary": "Add job to queue", + "description": "Add a new job to the queue (admin/superadmin only)", + "responses": { + "200": { + "description": "Job added successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Job added to queue successfully" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/queue/metrics": { + "get": { + "tags": ["queue"], + "summary": "Get queue metrics", + "description": "Retrieve queue metrics (admin/superadmin only)", + "responses": { + "200": { + "description": "Queue metrics retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Queue metrics fetched successfully" + }, + "data": { + "$ref": "#/components/schemas/QueueMetrics" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/queue/health": { "get": { "tags": ["queue"], @@ -2618,6 +3027,286 @@ } ] } + }, + "/incidents/team": { + "get": { + "tags": ["incidents"], + "summary": "Get incidents by team", + "description": "Retrieve all incidents for the authenticated user's team with optional filtering", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter by incident status (true = ongoing, false = resolved)", + "schema": { + "type": "boolean" + } + }, + { + "name": "monitorId", + "in": "query", + "description": "Filter by monitor ID", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "Page number for pagination", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of items per page", + "schema": { + "type": "integer", + "default": 10 + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort order for results", + "schema": { + "type": "string", + "enum": ["asc", "desc"], + "default": "desc" + } + } + ], + "responses": { + "200": { + "description": "Incidents retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incidents retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Incident" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/incidents/team/summary": { + "get": { + "tags": ["incidents"], + "summary": "Get incident summary", + "description": "Retrieve a summary of incidents for the authenticated user's team", + "parameters": [ + { + "name": "dateRange", + "in": "query", + "description": "Date range for summary", + "schema": { + "type": "string", + "enum": ["day", "week", "month"], + "default": "week" + } + } + ], + "responses": { + "200": { + "description": "Incident summary retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incident summary retrieved successfully" + }, + "data": { + "$ref": "#/components/schemas/IncidentSummary" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/incidents/{incidentId}": { + "get": { + "tags": ["incidents"], + "summary": "Get incident by ID", + "description": "Retrieve a specific incident by its ID", + "parameters": [ + { + "name": "incidentId", + "in": "path", + "required": true, + "description": "The incident ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Incident retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incident retrieved successfully" + }, + "data": { + "$ref": "#/components/schemas/Incident" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/incidents/{incidentId}/resolve": { + "put": { + "tags": ["incidents"], + "summary": "Resolve incident manually", + "description": "Manually resolve an ongoing incident. Requires admin or superadmin role.", + "parameters": [ + { + "name": "incidentId", + "in": "path", + "required": true, + "description": "The incident ID to resolve", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "comment": { + "type": "string", + "description": "Optional comment about the resolution", + "example": "Issue resolved by restarting the service" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Incident resolved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incident resolved successfully" + }, + "data": { + "$ref": "#/components/schemas/Incident" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } } }, "components": { @@ -2680,6 +3369,16 @@ } } }, + "BadRequest": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "InternalServerError": { "description": "Internal server error", "content": { @@ -3375,6 +4074,566 @@ } } } + }, + "Incident": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "description": "Unique incident identifier" + }, + "monitorId": { + "type": "string", + "description": "ID of the associated monitor" + }, + "teamId": { + "type": "string", + "description": "ID of the team" + }, + "startTime": { + "type": "string", + "format": "date-time", + "description": "When the incident started" + }, + "endTime": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the incident was resolved (null if ongoing)" + }, + "status": { + "type": "boolean", + "description": "Incident status (true = ongoing, false = resolved)" + }, + "message": { + "type": "string", + "nullable": true, + "description": "Error message or description" + }, + "statusCode": { + "type": "integer", + "nullable": true, + "description": "HTTP status code if applicable" + }, + "resolutionType": { + "type": "string", + "enum": ["automatic", "manual"], + "nullable": true, + "description": "How the incident was resolved" + }, + "resolvedBy": { + "type": "string", + "nullable": true, + "description": "User ID who resolved the incident (for manual resolution)" + }, + "comment": { + "type": "string", + "nullable": true, + "description": "Resolution comment" + }, + "checks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of check IDs associated with this incident" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "IncidentSummary": { + "type": "object", + "properties": { + "totalIncidents": { + "type": "integer", + "description": "Total number of incidents" + }, + "activeIncidents": { + "type": "integer", + "description": "Number of ongoing incidents" + }, + "resolvedIncidents": { + "type": "integer", + "description": "Number of resolved incidents" + }, + "averageResolutionTime": { + "type": "number", + "description": "Average time to resolve incidents in milliseconds" + }, + "incidentsByMonitor": { + "type": "array", + "items": { + "type": "object", + "properties": { + "monitorId": { + "type": "string" + }, + "monitorName": { + "type": "string" + }, + "count": { + "type": "integer" + } + } + } + } + } + }, + "QueueJob": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Job ID" + }, + "name": { + "type": "string", + "description": "Job name" + }, + "data": { + "type": "object", + "description": "Job payload data" + }, + "status": { + "type": "string", + "enum": ["waiting", "active", "completed", "failed", "delayed"], + "description": "Current job status" + }, + "progress": { + "type": "number", + "description": "Job progress percentage" + }, + "attemptsMade": { + "type": "integer", + "description": "Number of attempts made" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the job was created" + }, + "processedOn": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the job started processing" + }, + "finishedOn": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the job finished" + } + } + }, + "QueueMetrics": { + "type": "object", + "properties": { + "waiting": { + "type": "integer", + "description": "Number of jobs waiting" + }, + "active": { + "type": "integer", + "description": "Number of jobs currently processing" + }, + "completed": { + "type": "integer", + "description": "Number of completed jobs" + }, + "failed": { + "type": "integer", + "description": "Number of failed jobs" + }, + "delayed": { + "type": "integer", + "description": "Number of delayed jobs" + } + } + }, + "Check": { + "type": "object", + "description": "A monitoring check result", + "properties": { + "_id": { + "type": "string", + "description": "Check ID" + }, + "metadata": { + "type": "object", + "properties": { + "monitorId": { + "type": "string", + "description": "Associated monitor ID" + }, + "teamId": { + "type": "string", + "description": "Associated team ID" + }, + "type": { + "type": "string", + "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"], + "description": "Monitor type" + } + }, + "required": ["monitorId", "teamId", "type"] + }, + "status": { + "type": "boolean", + "description": "Check status (true = up, false = down)" + }, + "responseTime": { + "type": "number", + "description": "Response time in milliseconds" + }, + "statusCode": { + "type": "integer", + "description": "HTTP status code (for HTTP monitors)" + }, + "message": { + "type": "string", + "description": "Status message or error description" + }, + "timings": { + "$ref": "#/components/schemas/CheckTimings" + }, + "cpu": { + "$ref": "#/components/schemas/CheckCpuInfo" + }, + "memory": { + "$ref": "#/components/schemas/CheckMemoryInfo" + }, + "disk": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CheckDiskInfo" + } + }, + "host": { + "$ref": "#/components/schemas/CheckHostInfo" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CheckErrorInfo" + } + }, + "capture": { + "$ref": "#/components/schemas/CheckCaptureInfo" + }, + "net": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CheckNetworkInfo" + } + }, + "accessibility": { + "type": "number", + "description": "PageSpeed accessibility score (0-100)" + }, + "bestPractices": { + "type": "number", + "description": "PageSpeed best practices score (0-100)" + }, + "seo": { + "type": "number", + "description": "PageSpeed SEO score (0-100)" + }, + "performance": { + "type": "number", + "description": "PageSpeed performance score (0-100)" + }, + "audits": { + "$ref": "#/components/schemas/CheckAudits" + }, + "ack": { + "type": "boolean", + "description": "Whether the check has been acknowledged" + }, + "ackAt": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the check was acknowledged" + }, + "expiry": { + "type": "string", + "format": "date-time", + "description": "When the check expires" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "CheckTimings": { + "type": "object", + "description": "Detailed timing information for HTTP checks", + "properties": { + "start": { + "type": "number" + }, + "socket": { + "type": "number" + }, + "lookup": { + "type": "number" + }, + "connect": { + "type": "number" + }, + "secureConnect": { + "type": "number" + }, + "upload": { + "type": "number" + }, + "response": { + "type": "number" + }, + "end": { + "type": "number" + }, + "phases": { + "type": "object", + "properties": { + "wait": { + "type": "number" + }, + "dns": { + "type": "number" + }, + "tcp": { + "type": "number" + }, + "tls": { + "type": "number" + }, + "request": { + "type": "number" + }, + "firstByte": { + "type": "number" + }, + "download": { + "type": "number" + }, + "total": { + "type": "number" + } + } + } + } + }, + "CheckCpuInfo": { + "type": "object", + "description": "CPU information for infrastructure checks", + "properties": { + "physical_core": { + "type": "integer" + }, + "logical_core": { + "type": "integer" + }, + "frequency": { + "type": "number" + }, + "temperature": { + "type": "array", + "items": { + "type": "number" + } + }, + "free_percent": { + "type": "number" + }, + "usage_percent": { + "type": "number" + } + } + }, + "CheckMemoryInfo": { + "type": "object", + "description": "Memory information for infrastructure checks", + "properties": { + "total_bytes": { + "type": "number" + }, + "available_bytes": { + "type": "number" + }, + "used_bytes": { + "type": "number" + }, + "usage_percent": { + "type": "number" + } + } + }, + "CheckDiskInfo": { + "type": "object", + "description": "Disk information for infrastructure checks", + "properties": { + "device": { + "type": "string" + }, + "mountpoint": { + "type": "string" + }, + "read_speed_bytes": { + "type": "number" + }, + "write_speed_bytes": { + "type": "number" + }, + "total_bytes": { + "type": "number" + }, + "free_bytes": { + "type": "number" + }, + "usage_percent": { + "type": "number" + } + } + }, + "CheckHostInfo": { + "type": "object", + "description": "Host information for infrastructure checks", + "properties": { + "os": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "kernel_version": { + "type": "string" + } + } + }, + "CheckErrorInfo": { + "type": "object", + "description": "Error information from checks", + "properties": { + "metric": { + "type": "array", + "items": { + "type": "string" + } + }, + "err": { + "type": "string" + } + } + }, + "CheckCaptureInfo": { + "type": "object", + "description": "Capture agent information", + "properties": { + "version": { + "type": "string" + }, + "mode": { + "type": "string" + } + } + }, + "CheckNetworkInfo": { + "type": "object", + "description": "Network interface information", + "properties": { + "name": { + "type": "string" + }, + "bytes_sent": { + "type": "number" + }, + "bytes_recv": { + "type": "number" + }, + "packets_sent": { + "type": "number" + }, + "packets_recv": { + "type": "number" + }, + "err_in": { + "type": "number" + }, + "err_out": { + "type": "number" + }, + "drop_in": { + "type": "number" + }, + "drop_out": { + "type": "number" + }, + "fifo_in": { + "type": "number" + }, + "fifo_out": { + "type": "number" + } + } + }, + "CheckAudits": { + "type": "object", + "description": "PageSpeed audit results", + "properties": { + "cls": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "si": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "fcp": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "lcp": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "tbt": { + "$ref": "#/components/schemas/LighthouseAudit" + } + } + }, + "LighthouseAudit": { + "type": "object", + "description": "Individual Lighthouse audit result", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "score": { + "type": "number" + }, + "displayValue": { + "type": "string" + }, + "numericValue": { + "type": "number" + }, + "numericUnit": { + "type": "string" + } + } } } } From f7f764fd68ea828e6ed74dc8b09c6aa8524ce536 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 18:58:17 +0000 Subject: [PATCH 076/103] providers --- .../notifications/INotificationsRepository.ts | 1 + .../MongoNotificationsRepository.ts | 6 +++ server/src/service/index.ts | 6 ++- .../SuperSimpleQueueHelper.ts | 2 +- .../INotificationProvider.ts | 3 +- .../notificationProviders/discord.ts | 10 +++++ .../notificationProviders/email.ts | 12 +++++ .../notificationProviders/slack.ts | 12 +++++ .../notificationProviders/utils.ts | 14 +++++- .../notificationProviders/webhook.ts | 44 ++++++++++++++++--- .../infrastructure/notificationsService.ts | 40 +++++++++++------ 11 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 server/src/service/infrastructure/notificationProviders/discord.ts create mode 100644 server/src/service/infrastructure/notificationProviders/email.ts create mode 100644 server/src/service/infrastructure/notificationProviders/slack.ts diff --git a/server/src/repositories/notifications/INotificationsRepository.ts b/server/src/repositories/notifications/INotificationsRepository.ts index 0244972e4..8bb65769a 100644 --- a/server/src/repositories/notifications/INotificationsRepository.ts +++ b/server/src/repositories/notifications/INotificationsRepository.ts @@ -2,6 +2,7 @@ import type { Notification } from "@/types/index.js"; export interface INotificationsRepository { // create // fetch + findNotificationsByIds(ids: string[]): Promise; // update // delete } diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts index 51839c125..01a514f1c 100644 --- a/server/src/repositories/notifications/MongoNotificationsRepository.ts +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -35,6 +35,12 @@ class MongoNotificationsRepository implements INotificationsRepository { updatedAt: toDateString(doc.updatedAt), }; }; + + findNotificationsByIds = async (ids: string[]) => { + const mongoIds = ids.map((id) => new mongoose.Types.ObjectId(id)); + const documents = await NotificationModel.find({ _id: { $in: mongoIds } }); + return this.mapDocuments(documents); + }; } export default MongoNotificationsRepository; diff --git a/server/src/service/index.ts b/server/src/service/index.ts index fbba7714d..f46e6d3d8 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -1,6 +1,10 @@ export * from "@/service/business/monitorService.js"; export * from "@/service/infrastructure/networkService.js"; -export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/discord.js"; +export * from "@/service/infrastructure/notificationProviders/email.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +export * from "@/service/infrastructure/notificationProviders/slack.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationsService.js"; export * from "@/service/infrastructure/statusService.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 41885a146..449386102 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -83,7 +83,7 @@ class SuperSimpleQueueHelper { // Step 4. Update monitor status const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); - // Step 5 handle notifications + // Step 5 handle notifications (best effort, continue even in event of failure, don't wait) this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); // this.notificationService diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 2743c40bb..2d363d6b3 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,7 +1,6 @@ import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js"; export interface INotificationProvider { - buildAlert: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Alert; - sendAlert: (alert: Alert, notification: Notification) => Promise; + sendAlert: (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; sendTestAlert(notification: Notification): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts new file mode 100644 index 000000000..18b1522be --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -0,0 +1,10 @@ +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +export class DiscordProvider implements INotificationProvider { + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + return false; + }; + sendTestAlert = async (notification: Notification) => { + return false; + }; +} diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts new file mode 100644 index 000000000..dda7338a0 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -0,0 +1,12 @@ +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; + +export class EmailProvider implements INotificationProvider { + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + return false; + } + + async sendTestAlert(notification: Notification): Promise { + return false; + } +} diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts new file mode 100644 index 000000000..bd2836d2b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -0,0 +1,12 @@ +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; + +export class SlackProvider implements INotificationProvider { + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + return false; + } + + async sendTestAlert(notification: Notification): Promise { + return false; + } +} diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 7cba00a5d..9a10e668b 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -93,13 +93,19 @@ export const buildHardwareAlerts = ( return { alertsToSend, discordPayload }; }; -const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => { +export const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => { const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor.id}`]; const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; return alertText.map((alert) => alert).join("\n"); }; +export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): { text: string; name: string; url: string } => { + const content = alerts.map((alert) => alert).join("\n"); + const body = { text: content, name: monitor.name, url: monitor.url }; + return body; +}; + export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { const thresholds = monitor.thresholds || {}; const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; @@ -125,3 +131,9 @@ export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: Monit return false; }; + +export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { status, code } = monitorStatusResponse; + const formattedTime = Date.now().toLocaleString(); + return `Monitor: ${monitor.name}\nTime: ${formattedTime}\nStatus: ${status ? "UP" : "DOWN"}\n Status Code: ${code}\n\u200B\n`; +}; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 4266bbf3b..cf56db89b 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,15 +1,47 @@ import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; + export class WebhookProvider implements INotificationProvider { - buildAlert = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - return null; + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; }; - sendAlert = async (alert: Alert) => { - return false; + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; }; - sendTestAlert(notification: Notification): Promise { + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } + }; + + sendTestAlert = async (notification: Notification) => { return false; - } + }; } diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 2070b0c4f..e170dd3c4 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,7 +1,7 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; -import { NotificationModel } from "@/db/models/index.js"; -import { buildHardwareAlerts, shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; +import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; import { INotificationsRepository } from "@/repositories/index.js"; +import { WebhookProvider } from "@/service/index.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -13,21 +13,36 @@ export interface INotificationsService { export class NotificationsService implements INotificationsService { private notificationsRepository: INotificationsRepository; - constructor(notificationsRepository: INotificationsRepository) { + private webhookProvider: WebhookProvider; + + constructor(notificationsRepository: INotificationsRepository, webhookProvider: WebhookProvider) { this.notificationsRepository = notificationsRepository; + this.webhookProvider = webhookProvider; } - private send = (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - console.log(notification); + private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { + switch (notification.type) { + case " email": { + } + case "webhook": { + return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); + } + } + return false; }; private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - const notifications = await this.notificationsRepository.findByMonitorId(monitor.id); - console.log({ notifications }); - const tasks = notifications.map((notification) => { - this.send(notification, monitor, monitorStatusResponse); - }); - return true; + const notificationIds = monitor.notifications ?? []; + const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); + const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse)); + const outcomes = await Promise.all(tasks); + const succeeded = outcomes.filter(Boolean).length; + const failed = outcomes.length - succeeded; + if (failed > 0) { + // logger.warn(`Notification send completed with ${succeeded} success, ${failed} failure(s)`); + } + // Return true if all notificaitons succeeded + return succeeded === notifications.length; }; handleNotifications = async ( @@ -51,12 +66,11 @@ export class NotificationsService implements INotificationsService { const metrics = payload?.data ?? null; if (metrics === null) return false; // No metrics, we're done - // We shoul dsend a notificaiton + // We should send a notificaiton const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse); if (shouldSend === false) return false; console.log(JSON.stringify(monitor, null, 2)); - console.log(JSON.stringify(monitorStatusResponse, null, 2)); // const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); // const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); // const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); From ab2553175817ef59263ce93d5fa4e467b1404d2f Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Mon, 19 Jan 2026 14:13:18 -0500 Subject: [PATCH 077/103] docs: fix OpenAPI spec accuracy and warnings ## Changes ### Route Corrections - Remove non-existent `/auth/refresh` endpoint - Add missing `/auth/users/{userId}/password` PUT endpoint - Fix `/notifications` GET -> `/notifications/team` GET (correct path) ### OpenAPI 3.1.0 Compliance - Fix `nullable` warnings by using `type: ["string", "null"]` syntax - Updated Incident schema nullable fields (endTime, message, statusCode, etc.) - Updated QueueJob schema nullable fields (processedOn, finishedOn) - Updated Check schema nullable field (ackAt) ### Verified Against Codebase - Audited all 76 API endpoints across 12 route files - Confirmed DELETE /checks/team and DELETE /checks/{monitorId} exist - All routes now match actual codebase implementation ## Validation - Passes `swagger-cli validate` - Only remaining warnings are cosmetic `example` deprecations --- server/openapi.json | 196 +++++++++++++++++++++++--------------------- 1 file changed, 101 insertions(+), 95 deletions(-) diff --git a/server/openapi.json b/server/openapi.json index d104501d0..d56c619bf 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -162,51 +162,6 @@ } } }, - "/auth/refresh": { - "post": { - "tags": ["auth"], - "summary": "Refresh access token", - "description": "Generate new access token using refresh token", - "parameters": [ - { - "name": "x-refresh-token", - "in": "header", - "description": "Refresh token", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "authorization", - "in": "header", - "description": "Bearer token (old access token)", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Token refreshed successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthResponse" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, "/auth/user/{userId}": { "put": { "tags": ["auth"], @@ -1362,6 +1317,39 @@ } }, "/notifications": { + "post": { + "tags": ["notifications"], + "summary": "Create notification", + "description": "Create a new notification integration", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNotificationRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Notification created successfully" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications/team": { "get": { "tags": ["notifications"], "summary": "Get team notifications", @@ -1401,37 +1389,6 @@ "bearerAuth": [] } ] - }, - "post": { - "tags": ["notifications"], - "summary": "Create notification", - "description": "Create a new notification integration", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateNotificationRequest" - } - } - } - }, - "responses": { - "201": { - "description": "Notification created successfully" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] } }, "/notifications/{id}": { @@ -1822,6 +1779,64 @@ ] } }, + "/auth/users/{userId}/password": { + "put": { + "tags": ["auth"], + "summary": "Update user password by ID", + "description": "Update a specific user's password by ID (superadmin only)", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["password"], + "properties": { + "password": { + "type": "string", + "format": "password", + "minLength": 8, + "description": "New password for the user" + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/invite": { "post": { "tags": ["invite"], @@ -4096,9 +4111,8 @@ "description": "When the incident started" }, "endTime": { - "type": "string", + "type": ["string", "null"], "format": "date-time", - "nullable": true, "description": "When the incident was resolved (null if ongoing)" }, "status": { @@ -4106,29 +4120,24 @@ "description": "Incident status (true = ongoing, false = resolved)" }, "message": { - "type": "string", - "nullable": true, + "type": ["string", "null"], "description": "Error message or description" }, "statusCode": { - "type": "integer", - "nullable": true, + "type": ["integer", "null"], "description": "HTTP status code if applicable" }, "resolutionType": { - "type": "string", - "enum": ["automatic", "manual"], - "nullable": true, + "type": ["string", "null"], + "enum": ["automatic", "manual", null], "description": "How the incident was resolved" }, "resolvedBy": { - "type": "string", - "nullable": true, + "type": ["string", "null"], "description": "User ID who resolved the incident (for manual resolution)" }, "comment": { - "type": "string", - "nullable": true, + "type": ["string", "null"], "description": "Resolution comment" }, "checks": { @@ -4220,15 +4229,13 @@ "description": "When the job was created" }, "processedOn": { - "type": "string", + "type": ["string", "null"], "format": "date-time", - "nullable": true, "description": "When the job started processing" }, "finishedOn": { - "type": "string", + "type": ["string", "null"], "format": "date-time", - "nullable": true, "description": "When the job finished" } } @@ -4358,9 +4365,8 @@ "description": "Whether the check has been acknowledged" }, "ackAt": { - "type": "string", + "type": ["string", "null"], "format": "date-time", - "nullable": true, "description": "When the check was acknowledged" }, "expiry": { From fd33b903edee65b807452c0dc1eef43256d6ee74 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 19:30:46 +0000 Subject: [PATCH 078/103] email provider --- server/src/config/services.ts | 15 ++++++-- .../notificationProviders/email.ts | 28 ++++++++++++++- .../notificationProviders/utils.ts | 32 +++++++++++++++-- .../infrastructure/notificationsService.ts | 34 ++++++++++++++----- 4 files changed, 96 insertions(+), 13 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index e25ee1706..3a740549a 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -7,7 +7,7 @@ import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import NotificationService from "../service/infrastructure/notificationService.js"; -import { NotificationsService, StatusService } from "@/service/index.js"; +import { NotificationsService, StatusService, WebhookProvider, SlackProvider, EmailProvider, DiscordProvider } from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; @@ -228,7 +228,17 @@ export const initializeServices = async ({ notificationUtils, }); - const notificationsService = new NotificationsService(notificationsRepository); + const webhookProvider = new WebhookProvider(); + const slackProvider = new SlackProvider(); + const emailProvider = new EmailProvider(emailService); + const discordProvider = new DiscordProvider(); + + const notificationsService = new NotificationsService(notificationsRepository, { + webhookProvider, + slackProvider, + emailProvider, + discordProvider, + }); const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, @@ -323,6 +333,7 @@ export const initializeServices = async ({ invitesRepository, recoveryTokensRepository, settingsRepository, + notificationsRepository, notificationsService, }; diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index dda7338a0..e60775982 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,9 +1,35 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareEmail, buildEmail } from "@/service/infrastructure/notificationProviders/utils.js"; export class EmailProvider implements INotificationProvider { + private emailService: any; + constructor(emailService: any) { + this.emailService = emailService; + } + + private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const html = buildHardwareEmail(this.emailService, monitor, alertsToSend); + return html; + }; + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { - return false; + const subject = `Monitor ${monitor.name} infrastructure alerts`; + let html; + if (monitor.type === "hardware") { + html = this.buildHardwareEmail(monitor, monitorStatusResponse); + } else { + html = await buildEmail(this.emailService, monitor); + } + + if (!notification.address) { + return false; + } + + const messageId = await this.emailService.sendEmail(notification.address, subject, html); + if (!messageId) return false; + return true; } async sendTestAlert(notification: Notification): Promise { diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 9a10e668b..0d25543ea 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -134,6 +134,34 @@ export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: Monit export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { status, code } = monitorStatusResponse; - const formattedTime = Date.now().toLocaleString(); - return `Monitor: ${monitor.name}\nTime: ${formattedTime}\nStatus: ${status ? "UP" : "DOWN"}\n Status Code: ${code}\n\u200B\n`; + const now = new Date(); + const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const localTime = new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + timeZone: localTimeZone, + timeZoneName: "short", + }).format(now); + const utcTime = now.toUTCString(); + return `Monitor: ${monitor.name}\nLocal Time (${localTimeZone}): ${localTime}\nUTC Time: ${utcTime}\nStatus: ${status ? "UP" : "DOWN"}\nStatus Code: ${code}\n\u200B\n`; +}; + +export const buildHardwareEmail = async (emailService: any, monitor: Monitor, alerts: string[]) => { + const template = "hardwareIncidentTemplate"; + const context = { monitor: monitor.name, url: monitor.url, alerts }; + const html = await emailService.buildEmail(template, context); + return html; +}; + +export const buildEmail = async (emailService: any, monitor: Monitor): Promise => { + const template = monitor.status === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; + const context = { monitor: monitor.name, url: monitor.url }; + const html = await emailService.buildEmail(template, context); + console.log({ html: typeof html }); + return html; }; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index e170dd3c4..8b44d4f9c 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,7 +1,7 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; import { INotificationsRepository } from "@/repositories/index.js"; -import { WebhookProvider } from "@/service/index.js"; +import { DiscordProvider, EmailProvider, SlackProvider, WebhookProvider } from "@/service/index.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -14,21 +14,39 @@ export interface INotificationsService { export class NotificationsService implements INotificationsService { private notificationsRepository: INotificationsRepository; private webhookProvider: WebhookProvider; + private emailProvider: EmailProvider; + private slackProvider: SlackProvider; + private discordProvider: DiscordProvider; - constructor(notificationsRepository: INotificationsRepository, webhookProvider: WebhookProvider) { + constructor( + notificationsRepository: INotificationsRepository, + providers: { + webhookProvider: WebhookProvider; + emailProvider: EmailProvider; + slackProvider: SlackProvider; + discordProvider: DiscordProvider; + } + ) { this.notificationsRepository = notificationsRepository; - this.webhookProvider = webhookProvider; + this.webhookProvider = providers.webhookProvider; + this.emailProvider = providers.emailProvider; + this.slackProvider = providers.slackProvider; + this.discordProvider = providers.discordProvider; } private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { switch (notification.type) { - case " email": { - } - case "webhook": { + case "email": + return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "slack": + return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "discord": + return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "webhook": return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); - } + default: + return false; } - return false; }; private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { From be82a7ee2dd5c2708022ab857a572f06c33144eb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 19:58:15 +0000 Subject: [PATCH 079/103] discord --- .../notificationProviders/discord.ts | 34 +++++++++++- .../notificationProviders/utils.ts | 55 ++++++++++++++----- .../notificationProviders/webhook.ts | 3 +- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 18b1522be..fcb126455 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,8 +1,40 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildDiscordBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import got, { HTTPError } from "got"; + export class DiscordProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + return discordPayload; + }; + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - return false; + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = buildDiscordBody(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + console.log({ body }); + + try { + await got.post(notification.address, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error: any) { + return false; + } }; sendTestAlert = async (notification: Notification) => { return false; diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 0d25543ea..3c6737263 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -1,5 +1,23 @@ import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; +const getTime = (): { localTimeZone: string; localTime: string; utcTime: string } => { + const now = new Date(); + const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const localTime = new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + timeZone: localTimeZone, + timeZoneName: "short", + }).format(now); + const utcTime = now.toUTCString(); + return { localTimeZone, localTime, utcTime }; +}; + export const buildHardwareAlerts = ( clientHost: string, monitor: Monitor, @@ -134,20 +152,7 @@ export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: Monit export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { status, code } = monitorStatusResponse; - const now = new Date(); - const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const localTime = new Intl.DateTimeFormat(undefined, { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - timeZone: localTimeZone, - timeZoneName: "short", - }).format(now); - const utcTime = now.toUTCString(); + const { localTimeZone, localTime, utcTime } = getTime(); return `Monitor: ${monitor.name}\nLocal Time (${localTimeZone}): ${localTime}\nUTC Time: ${utcTime}\nStatus: ${status ? "UP" : "DOWN"}\nStatus Code: ${code}\n\u200B\n`; }; @@ -165,3 +170,25 @@ export const buildEmail = async (emailService: any, monitor: Monitor): Promise { + const { localTime, utcTime } = getTime(); + let body = { + embeds: [ + { + title: `Monitor ${monitor.name}`, + color: monitor.status ? 5763719 : 15548997, + + fields: [ + { name: "Monitor", value: monitor.name, inline: true }, + { name: "Status", value: monitor.status ? "Up" : "Down", inline: true }, + { name: "Status Code", value: String(monitorStatusResponse.code), inline: true }, + { name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: true }, + { name: "URL", value: monitor.url, inline: false }, + ], + footer: { text: "Checkmate" }, + }, + ], + }; + return body; +}; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index cf56db89b..d5ae3bcd3 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -15,7 +15,8 @@ export class WebhookProvider implements INotificationProvider { return body; }; - sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + console.log("WebhookProvider.sendAlert called"); let body; if (monitor.type === "hardware") { body = this.getHardwareContent(monitor, monitorStatusResponse); From 9006707e57ee97902e21857ee96876ff675877b8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 20:03:20 +0000 Subject: [PATCH 080/103] discord --- .../infrastructure/notificationProviders/discord.ts | 2 -- .../infrastructure/notificationProviders/utils.ts | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index fcb126455..8ac042a92 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -21,8 +21,6 @@ export class DiscordProvider implements INotificationProvider { return false; } - console.log({ body }); - try { await got.post(notification.address, { json: body, diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 3c6737263..fff682246 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -176,14 +176,14 @@ export const buildDiscordBody = (monitor: Monitor, monitorStatusResponse: Monito let body = { embeds: [ { - title: `Monitor ${monitor.name}`, - color: monitor.status ? 5763719 : 15548997, + title: `Monitor name: ${monitor.name}`, + color: monitorStatusResponse.status ? 5763719 : 15548997, fields: [ - { name: "Monitor", value: monitor.name, inline: true }, - { name: "Status", value: monitor.status ? "Up" : "Down", inline: true }, - { name: "Status Code", value: String(monitorStatusResponse.code), inline: true }, - { name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: true }, + { name: "Monitor", value: monitor.name, inline: false }, + { name: "Status", value: monitorStatusResponse.status ? "Up" : "Down", inline: false }, + { name: "Status Code", value: String(monitorStatusResponse.code), inline: false }, + { name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: false }, { name: "URL", value: monitor.url, inline: false }, ], footer: { text: "Checkmate" }, From 1d36f40caf983a8c7470982ed54a689791812f61 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 20:12:54 +0000 Subject: [PATCH 081/103] slack --- .../notificationProviders/discord.ts | 2 +- .../notificationProviders/slack.ts | 37 ++++++++++++++++++- .../notificationProviders/utils.ts | 6 +-- .../notificationProviders/webhook.ts | 1 - 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 8ac042a92..5aa8dd1d4 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,7 +1,7 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { buildHardwareAlerts, buildDiscordBody } from "@/service/infrastructure/notificationProviders/utils.js"; -import got, { HTTPError } from "got"; +import got from "got"; export class DiscordProvider implements INotificationProvider { private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index bd2836d2b..4f4c9df18 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,9 +1,44 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; export class SlackProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { - return false; + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: { text: body }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } } async sendTestAlert(notification: Notification): Promise { diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index fff682246..7d7d25f49 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -118,10 +118,9 @@ export const buildHardwareNotificationMessage = (clientHost: string, alerts: any return alertText.map((alert) => alert).join("\n"); }; -export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): { text: string; name: string; url: string } => { +export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): string => { const content = alerts.map((alert) => alert).join("\n"); - const body = { text: content, name: monitor.name, url: monitor.url }; - return body; + return content; }; export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { @@ -167,7 +166,6 @@ export const buildEmail = async (emailService: any, monitor: Monitor): Promise { - console.log("WebhookProvider.sendAlert called"); let body; if (monitor.type === "hardware") { body = this.getHardwareContent(monitor, monitorStatusResponse); From 7c9410f9d73453ce48923b64f3c9643d3699288c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 21:39:02 +0000 Subject: [PATCH 082/103] test alerts --- server/src/config/controllers.ts | 2 +- server/src/config/services.ts | 40 +++++---- .../src/controllers/notificationController.ts | 15 ++-- server/src/service/index.ts | 3 + .../SuperSimpleQueueHelper.ts | 33 +++---- .../notificationProviders/email.ts | 8 +- .../notificationProviders/matrix.js | 34 ------- .../notificationProviders/matrix.ts | 56 ++++++++++++ .../notificationProviders/pagerduty.ts | 50 +++++++++++ .../notificationProviders/slack.ts | 24 ++++- .../notificationProviders/utils.ts | 10 +++ .../infrastructure/notificationsService.ts | 90 ++++++++++++++----- 12 files changed, 258 insertions(+), 107 deletions(-) delete mode 100644 server/src/service/infrastructure/notificationProviders/matrix.js create mode 100644 server/src/service/infrastructure/notificationProviders/matrix.ts create mode 100644 server/src/service/infrastructure/notificationProviders/pagerduty.ts diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 44b051580..5ddbf2a06 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -26,7 +26,7 @@ export const initializeControllers = (services: InitializedSerivces) => { controllers.queueController = new QueueController(services.jobQueue); controllers.logController = new LogController(services.logger); controllers.statusPageController = new StatusPageController(services.db); - controllers.notificationController = new NotificationController(services.notificationService, services.db, services.monitorsRepository); + controllers.notificationController = new NotificationController(services.notificationsService, services.db, services.monitorsRepository); controllers.diagnosticController = new DiagnosticController(services.diagnosticService); controllers.incidentController = new IncidentController(services.incidentService); diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 3a740549a..035a0782f 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -6,8 +6,17 @@ import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; -import NotificationService from "../service/infrastructure/notificationService.js"; -import { NotificationsService, StatusService, WebhookProvider, SlackProvider, EmailProvider, DiscordProvider } from "@/service/index.js"; +import { + NotificationsService, + StatusService, + WebhookProvider, + SlackProvider, + EmailProvider, + DiscordProvider, + PagerDutyProvider, + MatrixProvider, + INotificationsService, +} from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; @@ -97,7 +106,6 @@ export type InitializedSerivces = { emailService: any; bufferService: any; statusService: any; - notificationService: any; jobQueue: any; userService: any; checkService: any; @@ -108,6 +116,7 @@ export type InitializedSerivces = { incidentService: any; errorService: any; logger: any; + notificationsService: INotificationsService; // Repositories monitorsRepository: IMonitorsRepository; @@ -219,33 +228,29 @@ export const initializeServices = async ({ settingsService, }); - const notificationService = new NotificationService({ - emailService, - db, - logger, - networkService, - stringService, - notificationUtils, - }); - const webhookProvider = new WebhookProvider(); const slackProvider = new SlackProvider(); const emailProvider = new EmailProvider(emailService); const discordProvider = new DiscordProvider(); + const pagerDutyProvider = new PagerDutyProvider(); + const matrixProvider = new MatrixProvider(); - const notificationsService = new NotificationsService(notificationsRepository, { + const notificationsService = new NotificationsService( + notificationsRepository, webhookProvider, - slackProvider, emailProvider, + slackProvider, discordProvider, - }); + pagerDutyProvider, + matrixProvider, + logger + ); const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, logger, networkService, statusService, - notificationService, notificationsService, checkService, buffer: bufferService, @@ -312,7 +317,6 @@ export const initializeServices = async ({ emailService, bufferService, statusService, - notificationService, jobQueue: superSimpleQueue, userService, checkService, @@ -323,6 +327,7 @@ export const initializeServices = async ({ incidentService, errorService, logger, + notificationsService, // Repositories monitorsRepository, @@ -334,7 +339,6 @@ export const initializeServices = async ({ recoveryTokensRepository, settingsRepository, notificationsRepository, - notificationsService, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 6f4a7f164..1569f0986 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -1,18 +1,20 @@ import { Request, Response, NextFunction } from "express"; +import { Notification } from "@/types/index.js"; import { createNotificationBodyValidation } from "@/validation/joi.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorsRepository } from "@/repositories/index.js"; +import { INotificationsService } from "@/service/index.js"; const SERVICE_NAME = "NotificationController"; class NotificationController { static SERVICE_NAME = SERVICE_NAME; private db: any; - private notificationService: any; + private notificationsService: INotificationsService; private monitorsRepository: IMonitorsRepository; - constructor(notificationService: any, db: any, monitorsRepository: IMonitorsRepository) { - this.notificationService = notificationService; + constructor(notificationsService: INotificationsService, db: any, monitorsRepository: IMonitorsRepository) { + this.notificationsService = notificationsService; this.db = db; this.monitorsRepository = monitorsRepository; } @@ -23,9 +25,8 @@ class NotificationController { testNotification = async (req: Request, res: Response, next: NextFunction) => { try { - const notification = req.body; - - const success = await this.notificationService.sendTestNotification(notification); + const notification: Notification = req.body; + const success = await this.notificationsService.sendTestNotification(notification); if (!success) { throw new AppError({ message: "Sending notification failed", status: 500 }); @@ -177,7 +178,7 @@ class NotificationController { throw new AppError({ message: "No notifications", status: 400 }); } - const result = await this.notificationService.testAllNotifications(notifications); + const result = await this.notificationsService.testAllNotifications(notifications); if (!result) { throw new AppError({ message: "Failed to send all notifications", status: 500 }); diff --git a/server/src/service/index.ts b/server/src/service/index.ts index f46e6d3d8..7f7d0f14a 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -3,8 +3,11 @@ export * from "@/service/infrastructure/networkService.js"; export * from "@/service/infrastructure/notificationProviders/discord.js"; export * from "@/service/infrastructure/notificationProviders/email.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +export * from "@/service/infrastructure/notificationProviders/pagerduty.js"; +export * from "@/service/infrastructure/notificationProviders/matrix.js"; export * from "@/service/infrastructure/notificationProviders/slack.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationsService.js"; export * from "@/service/infrastructure/statusService.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/matrix.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 449386102..ebb0949b0 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -9,11 +9,9 @@ class SuperSimpleQueueHelper { private logger: any; private networkService: INetworkService; private statusService: IStatusService; - private notificationService: any; private notificationsService: INotificationsService; private checkService: any; private buffer: any; - private incidentService: any; constructor({ db, @@ -29,7 +27,6 @@ class SuperSimpleQueueHelper { logger: any; networkService: INetworkService; statusService: IStatusService; - notificationService: any; notificationsService: INotificationsService; checkService: any; buffer: any; @@ -38,7 +35,6 @@ class SuperSimpleQueueHelper { this.logger = logger; this.networkService = networkService; this.statusService = statusService; - this.notificationService = notificationService; this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; @@ -84,24 +80,17 @@ class SuperSimpleQueueHelper { const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); // Step 5 handle notifications (best effort, continue even in event of failure, don't wait) - this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); - - // this.notificationService - // .handleNotifications({ - // ...status, - // monitor: statusChangeResult.monitor, - // prevStatus: statusChangeResult.prevStatus, - // statusChanged: statusChangeResult.statusChanged, - // }) - // .catch((error: any) => { - // this.logger.error({ - // message: error.message, - // service: SERVICE_NAME, - // method: "getMonitorJob", - // details: `Error sending notifications for job ${monitor.id}: ${error.message}`, - // stack: error.stack, - // }); - // }); + this.notificationsService + .handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged) + .catch((error: any) => { + this.logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "getMonitorJob", + details: `Error sending notifications for job ${monitor.id}: ${error.message}`, + stack: error.stack, + }); + }); } catch (error: any) { this.logger.warn({ message: error.message, diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index e60775982..0a405a078 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,6 +1,6 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareEmail, buildEmail } from "@/service/infrastructure/notificationProviders/utils.js"; +import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js"; export class EmailProvider implements INotificationProvider { private emailService: any; @@ -33,6 +33,10 @@ export class EmailProvider implements INotificationProvider { } async sendTestAlert(notification: Notification): Promise { - return false; + const subject = "Test notification"; + const html = await buildTestEmail(this.emailService); + const messageId = await this.emailService.sendEmail(notification.address, subject, html); + if (!messageId) return false; + return true; } } diff --git a/server/src/service/infrastructure/notificationProviders/matrix.js b/server/src/service/infrastructure/notificationProviders/matrix.js deleted file mode 100644 index a8b33d4b8..000000000 --- a/server/src/service/infrastructure/notificationProviders/matrix.js +++ /dev/null @@ -1,34 +0,0 @@ -const SERVICE_NAME = "Matrix"; - -class Matrix { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ networkService, logger }) { - this.networkService = networkService; - this.logger = logger; - } - - get serviceName() { - return Matrix.SERVICE_NAME; - } - - async send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName }) { - const title = `Checkmate status for ${monitorName}`; - const formattedMessage = `## ${title}\n${message}`; - try { - await this.networkService.requestMatrix({ - homeserverUrl, - accessToken, - roomId, - message: formattedMessage, - }); - this.logger.info(`Successfully sent Matrix notification for ${friendlyName}`); - return true; - } catch (error) { - this.logger.error(`Failed to send Matrix notification for ${friendlyName}: ${error.message}`); - return false; - } - } -} - -export default Matrix; diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts new file mode 100644 index 000000000..8985ac10b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -0,0 +1,56 @@ +import got from "got"; +import type { INotificationProvider } from "@/service/index.js"; +import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; + +export class MatrixProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { homeserverUrl, accessToken, roomId } = notification; + + let content; + if (monitor.type === "hardware") { + content = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + content = this.getContent(monitor, monitorStatusResponse); + } + + const title = `Checkmate status for ${monitor.name}`; + + const formattedMessage = `## ${title}\n${content}`; + + const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; + const body = { + msgtype: "m.text", + body: formattedMessage, + format: "org.matrix.custom.html", + formatted_body: formattedMessage, + }; + try { + await got.post(url, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } + }; + + sendTestAlert = async (notification: Notification) => { + return false; + }; +} diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts new file mode 100644 index 000000000..d0309550b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -0,0 +1,50 @@ +import got from "got"; +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; + +export class PagerDutyProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + try { + await got.post("https://events.pagerduty.com/v2/enqueue", { + json: { + routing_key: notification.address, + event_action: "trigger", + payload: { + summary: body, + severity: "critical", + source: monitor.url, + timestamp: new Date().toISOString(), + }, + }, + responseType: "json", + }); + + return true; + } catch (error) { + return false; + } + } + + async sendTestAlert(notification: Notification): Promise { + return false; + } +} diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index 4f4c9df18..c5745ce05 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,6 +1,11 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class SlackProvider implements INotificationProvider { @@ -42,6 +47,21 @@ export class SlackProvider implements INotificationProvider { } async sendTestAlert(notification: Notification): Promise { - return false; + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: { text: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } } } diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 7d7d25f49..a7e954e55 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -190,3 +190,13 @@ export const buildDiscordBody = (monitor: Monitor, monitorStatusResponse: Monito }; return body; }; + +export const buildTestEmail = async (emailService: any) => { + const context = { testName: "Monitoring System" }; + const html = await emailService.buildEmail("testEmailTemplate", context); + return html; +}; + +export const getTestMessage = () => { + return "This is a test notification from Checkmate"; +}; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 8b44d4f9c..7ca52945e 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,7 +1,7 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; import { INotificationsRepository } from "@/repositories/index.js"; -import { DiscordProvider, EmailProvider, SlackProvider, WebhookProvider } from "@/service/index.js"; +import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -9,29 +9,42 @@ export interface INotificationsService { prevStatus: boolean | undefined, statusChanged: boolean ) => Promise; + sendTestNotification: (notification: Notification) => Promise; + testAllNotifications: (notificationIds: string[]) => Promise; } +const SERVICE_NAME = "NotificationsService"; + export class NotificationsService implements INotificationsService { + static SERVICE_NAME = SERVICE_NAME; + private notificationsRepository: INotificationsRepository; - private webhookProvider: WebhookProvider; - private emailProvider: EmailProvider; - private slackProvider: SlackProvider; - private discordProvider: DiscordProvider; + private webhookProvider: INotificationProvider; + private emailProvider: INotificationProvider; + private slackProvider: INotificationProvider; + private discordProvider: INotificationProvider; + private pagerDutyProvider: INotificationProvider; + private matrixProvider: INotificationProvider; + private logger: any; constructor( notificationsRepository: INotificationsRepository, - providers: { - webhookProvider: WebhookProvider; - emailProvider: EmailProvider; - slackProvider: SlackProvider; - discordProvider: DiscordProvider; - } + webhookProvider: INotificationProvider, + emailProvider: INotificationProvider, + slackProvider: INotificationProvider, + discordProvider: INotificationProvider, + pagerDutyProvider: INotificationProvider, + matrixProvider: INotificationProvider, + logger: any ) { this.notificationsRepository = notificationsRepository; - this.webhookProvider = providers.webhookProvider; - this.emailProvider = providers.emailProvider; - this.slackProvider = providers.slackProvider; - this.discordProvider = providers.discordProvider; + this.webhookProvider = webhookProvider; + this.emailProvider = emailProvider; + this.slackProvider = slackProvider; + this.discordProvider = discordProvider; + this.pagerDutyProvider = pagerDutyProvider; + this.matrixProvider = matrixProvider; + this.logger = logger; } private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { @@ -42,6 +55,10 @@ export class NotificationsService implements INotificationsService { return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse); case "discord": return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "pager_duty": + return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "matrix": + return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse); case "webhook": return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); default: @@ -57,7 +74,11 @@ export class NotificationsService implements INotificationsService { const succeeded = outcomes.filter(Boolean).length; const failed = outcomes.length - succeeded; if (failed > 0) { - // logger.warn(`Notification send completed with ${succeeded} success, ${failed} failure(s)`); + this.logger.warn({ + message: `Notification send completed with ${succeeded} success, ${failed} failure(s)`, + service: SERVICE_NAME, + method: "getMonitorJob", + }); } // Return true if all notificaitons succeeded return succeeded === notifications.length; @@ -88,15 +109,42 @@ export class NotificationsService implements INotificationsService { const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse); if (shouldSend === false) return false; - console.log(JSON.stringify(monitor, null, 2)); - // const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); - // const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); - // const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); - // const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); + return await this.sendNotifications(monitor, monitorStatusResponse); } // We should send a notification for non-hardware monitor status change return await this.sendNotifications(monitor, monitorStatusResponse); }; + + sendTestNotification = async (notification: Notification) => { + switch (notification.type) { + case "email": + return await this.emailProvider.sendTestAlert(notification); + case "slack": + return await this.slackProvider.sendTestAlert(notification); + case "discord": + return await this.discordProvider.sendTestAlert(notification); + case "pager_duty": + return await this.pagerDutyProvider.sendTestAlert(notification); + case "matrix": + return await this.matrixProvider.sendTestAlert(notification); + case "webhook": + return await this.webhookProvider.sendTestAlert(notification); + default: + return false; + } + }; + + testAllNotifications = async (notificationIds: string[]) => { + const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); + const tasks = notifications.map((notification) => this.sendTestNotification(notification)); + const outcomes = await Promise.all(tasks); + const succeeded = outcomes.filter(Boolean).length; + const failed = outcomes.length - succeeded; + if (failed > 0) { + return false; + } + return true; + }; } From bebb5d6942a0e3ad85d0d4cf1afb735eb4c57ffe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 21:58:48 +0000 Subject: [PATCH 083/103] inject logger --- server/src/config/services.ts | 12 +++---- .../SuperSimpleQueueHelper.ts | 1 - .../notificationProviders/discord.ts | 23 +++++++++++-- .../notificationProviders/email.ts | 4 ++- .../notificationProviders/matrix.ts | 34 +++++++++++++++++-- .../notificationProviders/pagerduty.ts | 31 +++++++++++++++-- .../notificationProviders/slack.ts | 5 +++ .../notificationProviders/webhook.ts | 32 ++++++++++++++--- .../infrastructure/notificationsService.ts | 1 + 9 files changed, 125 insertions(+), 18 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 035a0782f..953fc5a1a 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -228,12 +228,12 @@ export const initializeServices = async ({ settingsService, }); - const webhookProvider = new WebhookProvider(); - const slackProvider = new SlackProvider(); - const emailProvider = new EmailProvider(emailService); - const discordProvider = new DiscordProvider(); - const pagerDutyProvider = new PagerDutyProvider(); - const matrixProvider = new MatrixProvider(); + const webhookProvider = new WebhookProvider(logger); + const slackProvider = new SlackProvider(logger); + const emailProvider = new EmailProvider(emailService, logger); + const discordProvider = new DiscordProvider(logger); + const pagerDutyProvider = new PagerDutyProvider(logger); + const matrixProvider = new MatrixProvider(logger); const notificationsService = new NotificationsService( notificationsRepository, diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index ebb0949b0..bbd3a11f0 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -18,7 +18,6 @@ class SuperSimpleQueueHelper { logger, networkService, statusService, - notificationService, notificationsService, checkService, buffer, diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 5aa8dd1d4..83d8e4ec2 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,9 +1,10 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildDiscordBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class DiscordProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); return discordPayload; @@ -22,6 +23,7 @@ export class DiscordProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending Discord alert", { address: notification.address }); await got.post(notification.address, { json: body, headers: { @@ -31,10 +33,27 @@ export class DiscordProvider implements INotificationProvider { }); return true; } catch (error: any) { + this.logger?.error?.("Discord alert failed", { error }); return false; } }; sendTestAlert = async (notification: Notification) => { - return false; + if (!notification.address) { + return false; + } + try { + this.logger?.debug?.("Sending Discord test alert", { address: notification.address }); + await got.post(notification.address, { + json: { content: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("Discord test alert failed", { error }); + return false; + } }; } diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index 0a405a078..344285e9c 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -4,7 +4,7 @@ import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } f export class EmailProvider implements INotificationProvider { private emailService: any; - constructor(emailService: any) { + constructor(emailService: any, private logger: any) { this.emailService = emailService; } @@ -27,6 +27,7 @@ export class EmailProvider implements INotificationProvider { return false; } + this.logger?.debug?.("Sending Email alert", { address: notification.address }); const messageId = await this.emailService.sendEmail(notification.address, subject, html); if (!messageId) return false; return true; @@ -35,6 +36,7 @@ export class EmailProvider implements INotificationProvider { async sendTestAlert(notification: Notification): Promise { const subject = "Test notification"; const html = await buildTestEmail(this.emailService); + this.logger?.debug?.("Sending Email test alert", { address: notification.address }); const messageId = await this.emailService.sendEmail(notification.address, subject, html); if (!messageId) return false; return true; diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index 8985ac10b..4592500a5 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -1,9 +1,15 @@ import got from "got"; import type { INotificationProvider } from "@/service/index.js"; import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; export class MatrixProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -37,6 +43,7 @@ export class MatrixProvider implements INotificationProvider { formatted_body: formattedMessage, }; try { + this.logger?.debug?.("Sending Matrix alert", { room: roomId }); await got.post(url, { json: body, headers: { @@ -46,11 +53,34 @@ export class MatrixProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Matrix alert failed", { error }); return false; } }; sendTestAlert = async (notification: Notification) => { - return false; + const { homeserverUrl, accessToken, roomId } = notification; + if (!homeserverUrl || !accessToken || !roomId) { + return false; + } + const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; + const body = { + msgtype: "m.text", + body: getTestMessage(), + }; + try { + this.logger?.debug?.("Sending Matrix test alert", { room: roomId }); + await got.post(url, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("Matrix test alert failed", { error }); + return false; + } }; } diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts index d0309550b..2d85cd4d6 100644 --- a/server/src/service/infrastructure/notificationProviders/pagerduty.ts +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -1,9 +1,15 @@ import got from "got"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; export class PagerDutyProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -24,6 +30,7 @@ export class PagerDutyProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending PagerDuty alert", { routingKey: notification.address }); await got.post("https://events.pagerduty.com/v2/enqueue", { json: { routing_key: notification.address, @@ -40,11 +47,31 @@ export class PagerDutyProvider implements INotificationProvider { return true; } catch (error) { + this.logger?.error?.("PagerDuty alert failed", { error }); return false; } } async sendTestAlert(notification: Notification): Promise { - return false; + try { + this.logger?.debug?.("Sending PagerDuty test alert", { routingKey: notification.address }); + await got.post("https://events.pagerduty.com/v2/enqueue", { + json: { + routing_key: notification.address, + event_action: "trigger", + payload: { + summary: getTestMessage(), + severity: "info", + source: "checkmate", + timestamp: new Date().toISOString(), + }, + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("PagerDuty test alert failed", { error }); + return false; + } } } diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index c5745ce05..947048e4c 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -9,6 +9,7 @@ import { import got from "got"; export class SlackProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -33,6 +34,7 @@ export class SlackProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending Slack alert", { address: notification.address }); await got.post(notification.address, { json: { text: body }, headers: { @@ -42,6 +44,7 @@ export class SlackProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Slack alert failed", { error }); return false; } } @@ -52,6 +55,7 @@ export class SlackProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending Slack test alert", { address: notification.address }); await got.post(notification.address, { json: { text: getTestMessage() }, headers: { @@ -61,6 +65,7 @@ export class SlackProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Slack test alert failed", { error }); return false; } } diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 4d102c2b7..3976fb8e5 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,16 +1,22 @@ import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class WebhookProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); return body; }; - private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): string => { const body = buildWebhookBody(monitor, monitorStatusResponse); return body; }; @@ -28,8 +34,9 @@ export class WebhookProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending webhook alert", { address: notification.address }); await got.post(notification.address, { - json: body, + json: { text: body }, headers: { "Content-Type": "application/json", }, @@ -37,11 +44,28 @@ export class WebhookProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Webhook alert failed", { error }); return false; } }; sendTestAlert = async (notification: Notification) => { - return false; + if (!notification.address) { + return false; + } + try { + this.logger?.debug?.("Sending webhook test alert", { address: notification.address }); + await got.post(notification.address, { + json: { text: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("Webhook test alert failed", { error }); + return false; + } }; } diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 7ca52945e..ad27fe99a 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -140,6 +140,7 @@ export class NotificationsService implements INotificationsService { const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); const tasks = notifications.map((notification) => this.sendTestNotification(notification)); const outcomes = await Promise.all(tasks); + console.log(outcomes); const succeeded = outcomes.filter(Boolean).length; const failed = outcomes.length - succeeded; if (failed > 0) { From 5f17520ba0e7cbda174f8bfa3ed595effec64c24 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 22:16:33 +0000 Subject: [PATCH 084/103] logging --- .../notificationProviders/discord.ts | 29 ++++++++++++++----- .../notificationProviders/email.ts | 27 +++++++++++++---- .../notificationProviders/matrix.ts | 27 ++++++++++++----- .../notificationProviders/pagerduty.ts | 26 +++++++++++++---- .../notificationProviders/slack.ts | 29 ++++++++++++++----- .../notificationProviders/webhook.ts | 27 ++++++++++++----- .../infrastructure/notificationsService.ts | 1 - 7 files changed, 124 insertions(+), 42 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 83d8e4ec2..ad1f2f28a 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,10 +1,15 @@ +const SERVICE_NAME = "DiscordProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class DiscordProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); return discordPayload; @@ -23,17 +28,21 @@ export class DiscordProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending Discord alert", { address: notification.address }); await got.post(notification.address, { json: body, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; - } catch (error: any) { - this.logger?.error?.("Discord alert failed", { error }); + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Discord alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } }; @@ -42,17 +51,21 @@ export class DiscordProvider implements INotificationProvider { return false; } try { - this.logger?.debug?.("Sending Discord test alert", { address: notification.address }); await got.post(notification.address, { json: { content: getTestMessage() }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Discord test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Discord test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } }; diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index 344285e9c..16e48f5c1 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,11 +1,15 @@ +const SERVICE_NAME = "EmailProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js"; export class EmailProvider implements INotificationProvider { private emailService: any; - constructor(emailService: any, private logger: any) { + private logger: any; + + constructor(emailService: any, logger: any) { this.emailService = emailService; + this.logger = logger; } private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { @@ -27,18 +31,31 @@ export class EmailProvider implements INotificationProvider { return false; } - this.logger?.debug?.("Sending Email alert", { address: notification.address }); const messageId = await this.emailService.sendEmail(notification.address, subject, html); - if (!messageId) return false; + if (!messageId) { + this.logger.warn({ + message: "Email alert failed", + service: SERVICE_NAME, + method: "sendAlert", + }); + return false; + } return true; } async sendTestAlert(notification: Notification): Promise { const subject = "Test notification"; const html = await buildTestEmail(this.emailService); - this.logger?.debug?.("Sending Email test alert", { address: notification.address }); + const messageId = await this.emailService.sendEmail(notification.address, subject, html); - if (!messageId) return false; + if (!messageId) { + this.logger.warn({ + message: "Email test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + }); + return false; + } return true; } } diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index 4592500a5..8914dbf80 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "MatrixProvider"; import got from "got"; import type { INotificationProvider } from "@/service/index.js"; import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; @@ -9,7 +10,11 @@ import { } from "@/service/infrastructure/notificationProviders/utils.js"; export class MatrixProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -43,17 +48,21 @@ export class MatrixProvider implements INotificationProvider { formatted_body: formattedMessage, }; try { - this.logger?.debug?.("Sending Matrix alert", { room: roomId }); await got.post(url, { json: body, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Matrix alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Matrix alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } }; @@ -69,17 +78,21 @@ export class MatrixProvider implements INotificationProvider { body: getTestMessage(), }; try { - this.logger?.debug?.("Sending Matrix test alert", { room: roomId }); await got.post(url, { json: body, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Matrix test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Matrix test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } }; diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts index 2d85cd4d6..f65e50c68 100644 --- a/server/src/service/infrastructure/notificationProviders/pagerduty.ts +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "PagerDutyProvider"; import got from "got"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; @@ -9,7 +10,11 @@ import { } from "@/service/infrastructure/notificationProviders/utils.js"; export class PagerDutyProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -30,7 +35,6 @@ export class PagerDutyProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending PagerDuty alert", { routingKey: notification.address }); await got.post("https://events.pagerduty.com/v2/enqueue", { json: { routing_key: notification.address, @@ -42,19 +46,23 @@ export class PagerDutyProvider implements INotificationProvider { timestamp: new Date().toISOString(), }, }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("PagerDuty alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "PagerDuty alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } } async sendTestAlert(notification: Notification): Promise { try { - this.logger?.debug?.("Sending PagerDuty test alert", { routingKey: notification.address }); await got.post("https://events.pagerduty.com/v2/enqueue", { json: { routing_key: notification.address, @@ -70,7 +78,13 @@ export class PagerDutyProvider implements INotificationProvider { }); return true; } catch (error) { - this.logger?.error?.("PagerDuty test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "PagerDuty test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } } diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index 947048e4c..2aeb442eb 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "SlackProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { @@ -6,10 +7,14 @@ import { buildWebhookBody, getTestMessage, } from "@/service/infrastructure/notificationProviders/utils.js"; -import got from "got"; +import got, { HTTPError } from "got"; export class SlackProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -34,17 +39,21 @@ export class SlackProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending Slack alert", { address: notification.address }); await got.post(notification.address, { json: { text: body }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Slack alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Slack alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } } @@ -55,17 +64,21 @@ export class SlackProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending Slack test alert", { address: notification.address }); await got.post(notification.address, { json: { text: getTestMessage() }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Slack test alert failed", { error }); + const err = error as HTTPError; + this.logger.warn({ + message: "Slack test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } } diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 3976fb8e5..9e743ae53 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "WebhookProvider"; import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { @@ -9,7 +10,11 @@ import { import got from "got"; export class WebhookProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -34,17 +39,21 @@ export class WebhookProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending webhook alert", { address: notification.address }); await got.post(notification.address, { json: { text: body }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Webhook alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Webhook alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } }; @@ -54,17 +63,21 @@ export class WebhookProvider implements INotificationProvider { return false; } try { - this.logger?.debug?.("Sending webhook test alert", { address: notification.address }); await got.post(notification.address, { json: { text: getTestMessage() }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Webhook test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Webhook test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } }; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index ad27fe99a..7ca52945e 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -140,7 +140,6 @@ export class NotificationsService implements INotificationsService { const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); const tasks = notifications.map((notification) => this.sendTestNotification(notification)); const outcomes = await Promise.all(tasks); - console.log(outcomes); const succeeded = outcomes.filter(Boolean).length; const failed = outcomes.length - succeeded; if (failed > 0) { From dde54a4e4540a9b057780779c617322cba3fee10 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 22:18:17 +0000 Subject: [PATCH 085/103] remove old service --- server/src/config/services.ts | 7 - .../infrastructure/notificationService.js | 128 ----------- .../infrastructure/notificationUtils.js | 205 ------------------ 3 files changed, 340 deletions(-) delete mode 100644 server/src/service/infrastructure/notificationService.js delete mode 100644 server/src/service/infrastructure/notificationUtils.js diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 953fc5a1a..9ae5ecb12 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -5,7 +5,6 @@ import MongoDB from "../db/MongoDB.js"; import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; -import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import { NotificationsService, StatusService, @@ -222,12 +221,6 @@ export const initializeServices = async ({ const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); - const notificationUtils = new NotificationUtils({ - stringService, - emailService, - settingsService, - }); - const webhookProvider = new WebhookProvider(logger); const slackProvider = new SlackProvider(logger); const emailProvider = new EmailProvider(emailService, logger); diff --git a/server/src/service/infrastructure/notificationService.js b/server/src/service/infrastructure/notificationService.js deleted file mode 100644 index 8c6fb063f..000000000 --- a/server/src/service/infrastructure/notificationService.js +++ /dev/null @@ -1,128 +0,0 @@ -const SERVICE_NAME = "NotificationService"; -import Matrix from "./notificationProviders/matrix.js"; - -class NotificationService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ emailService, db, logger, networkService, stringService, notificationUtils }) { - this.emailService = emailService; - this.db = db; - this.logger = logger; - this.networkService = networkService; - this.stringService = stringService; - this.notificationUtils = notificationUtils; - } - - get serviceName() { - return NotificationService.SERVICE_NAME; - } - - sendNotification = async ({ notification, subject, content, html, discordContent = null, webhookBody = null }) => { - const { type, address } = notification; - - if (type === "email") { - const messageId = await this.emailService.sendEmail(address, subject, html); - if (!messageId) return false; - return true; - } - - // Create a body for webhooks - let body = { text: content }; - if (type === "discord") { - body = !discordContent ? { content } : discordContent; - } - if (type === "webhook") { - body = !webhookBody ? { content } : webhookBody; - } - if (type === "slack" || type === "discord" || type === "webhook") { - const response = await this.networkService.requestWebhook(type, address, body); - return response.status; - } - if (type === "pager_duty") { - const response = await this.networkService.requestPagerDuty({ - message: content, - monitorUrl: subject, - routingKey: address, - }); - - return response; - } - if (type === "matrix") { - const { friendlyName, homeserverUrl, accessToken, roomId } = notification; - const monitorName = subject; - const message = content; - const matrix = new Matrix({ networkService: this.networkService, logger: this.logger }); - const success = await matrix.send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName }); - return success; - } - }; - - async handleNotifications(networkResponse) { - const { monitor, statusChanged, prevStatus } = networkResponse; - const { type } = monitor; - if (type !== "hardware" && statusChanged === false) return false; - // if prevStatus is undefined, monitor is resuming, we're done - if (type !== "hardware" && prevStatus === undefined) return false; - - const notificationIDs = networkResponse.monitor?.notifications ?? []; - if (notificationIDs.length === 0) return false; - if (networkResponse.monitor.type === "hardware") { - const thresholds = networkResponse?.monitor?.thresholds; - - if (thresholds === undefined) return false; // No thresholds set, we're done - const metrics = networkResponse?.payload?.data ?? null; - if (metrics === null) return false; // No metrics, we're done - - const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); - if (alerts.length === 0) return false; - - const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); - const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); - const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); - const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); - return success; - } - - // Status monitors - const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse); - const [content, discordContent] = await this.notificationUtils.buildWebhookMessage(networkResponse); - const success = this.notifyAll({ notificationIDs, subject, html, content, discordContent }); - return success; - } - - async notifyAll({ notificationIDs, subject, html, content, discordContent = null, webhookBody = null }) { - const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs); - // Map each notification to a test promise - const promises = notifications.map(async (notification) => { - try { - await this.sendNotification({ notification, subject, content, html, discordContent, webhookBody }); - return true; - } catch (err) { - return false; - } - }); - - const results = await Promise.all(promises); - return results.every((r) => r === true); - } - - async getTestNotification() { - const html = await this.notificationUtils.buildTestEmail(); - const content = "This is a test notification"; - const subject = "Test Notification"; - return { subject, html, content }; - } - - async testAllNotifications(notificationIDs) { - const { subject, html, content } = await this.getTestNotification(); - return this.notifyAll({ notificationIDs, subject, html, content }); - } - - async sendTestNotification(notification) { - const { subject, html, content } = await this.getTestNotification(); - const success = await this.sendNotification({ notification, subject, content, html }); - return success; - } -} - -export default NotificationService; diff --git a/server/src/service/infrastructure/notificationUtils.js b/server/src/service/infrastructure/notificationUtils.js deleted file mode 100644 index 106c8f349..000000000 --- a/server/src/service/infrastructure/notificationUtils.js +++ /dev/null @@ -1,205 +0,0 @@ -const SERVICE_NAME = "NotificationUtils"; - -class NotificationUtils { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ stringService, emailService, settingsService }) { - this.stringService = stringService; - this.emailService = emailService; - this.settingsService = settingsService; - } - - get serviceName() { - return NotificationUtils.SERVICE_NAME; - } - - buildTestEmail = async () => { - const context = { testName: "Monitoring System" }; - const html = await this.emailService.buildEmail("testEmailTemplate", context); - return html; - }; - - buildStatusEmail = async (networkResponse) => { - const { monitor, status, prevStatus } = networkResponse; - const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; - const context = { monitor: monitor.name, url: monitor.url }; - const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`; - const html = await this.emailService.buildEmail(template, context); - return { subject, html }; - }; - - buildWebhookMessage = (networkResponse) => { - const { monitor, status, code, timestamp } = networkResponse; - // Format timestamp using the local system timezone - const formatTime = (timestamp) => { - const date = new Date(timestamp); - - // Get timezone abbreviation and format the date - const timeZoneAbbr = date.toLocaleTimeString("en-US", { timeZoneName: "short" }).split(" ").pop(); - - // Format the date with readable format - return ( - date - .toLocaleString("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }) - .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") + - " " + - timeZoneAbbr - ); - }; - - // Get formatted time - const formattedTime = timestamp ? formatTime(timestamp) : formatTime(new Date().getTime()); - - // Create different messages based on status with extra spacing - let messageText; - if (status === true) { - messageText = this.stringService.monitorUpAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || "Unknown"); - } else { - messageText = this.stringService.monitorDownAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || "Unknown"); - } - const dn = this.stringService.discordNotification; - let discordMessageText = { - embeds: [ - { - title: status - ? dn.uptimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown) - : dn.downtimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown), - color: status ? 5763719 : 15548997, - - fields: [ - { name: dn.monitor, value: monitor?.name ?? dn.unknown, inline: true }, - { name: dn.status, value: status ? dn.up : dn.down, inline: true }, - { name: dn.statusCode, value: String(code ?? dn.unknown), inline: true }, - { name: dn.time, value: formattedTime, inline: true }, - { name: dn.url, value: monitor?.url ?? dn.unknown, inline: false }, - ], - footer: { text: dn.checkmate }, - }, - ], - }; - return [messageText, discordMessageText]; - }; - - buildHardwareAlerts = async (networkResponse) => { - const monitor = networkResponse?.monitor; - const thresholds = networkResponse?.monitor?.thresholds; - const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; - - const metrics = networkResponse?.payload?.data; - const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; - - const { clientHost } = this.settingsService.getSettings(); - - const alerts = { - cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, - memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, - disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, - }; - - const alertsToSend = []; - const discordEmbeds = []; - const monitorInfoFields = [ - { name: "Monitor", value: monitor.name, inline: true }, - { name: "URL", value: monitor.url, inline: false }, - ]; - const goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor._id}` }; - const formatDiscordAlert = { - cpu: () => ({ - title: "CPU alert", - description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, - color: 15548997, - fields: [...monitorInfoFields, goToIncidentField], - footer: { text: "Checkmate" }, - }), - - memory: () => ({ - title: "Memory alert", - description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, - color: 15548997, - fields: [...monitorInfoFields, goToIncidentField], - footer: { text: "Checkmate" }, - }), - - disk: () => ({ - title: "Disk alert", - description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, - color: 15548997, - footer: { text: "Checkmate" }, - fields: [ - ...monitorInfoFields, - ...(Array.isArray(disk) ? disk : []).map((d, idx) => ({ - name: `Disk ${idx}`, - value: `${(d?.usage_percent * 100).toFixed(0)}%`, - inline: true, - })), - goToIncidentField, - ], - }), - }; - const alertTypes = ["cpu", "memory", "disk"]; - for (const type of alertTypes) { - // Iterate over each alert type to see if any need to be decremented - if (alerts[type] === true) { - monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered - - if (monitor[`${type}AlertThreshold`] <= 0) { - // If threshold drops below 0, reset and send notification - monitor[`${type}AlertThreshold`] = monitor.alertThreshold; - - const formatAlert = { - cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, - memory: () => - `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, - disk: () => - `Your current disk usage: ${disk - .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) - .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, - }; - alertsToSend.push(formatAlert[type]()); - discordEmbeds.push(formatDiscordAlert[type]()); - } - } - } - await monitor.save(); - const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; - return [alertsToSend, discordPayload]; - }; - - buildHardwareEmail = async (networkResponse, alerts) => { - const { monitor } = networkResponse; - const template = "hardwareIncidentTemplate"; - const context = { monitor: monitor.name, url: monitor.url, alerts }; - const subject = `Monitor ${monitor.name} infrastructure alerts`; - const html = await this.emailService.buildEmail(template, context); - return { subject, html }; - }; - - buildHardwareNotificationMessage = (alerts, monitor) => { - const { clientHost } = this.settingsService.getSettings(); - const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; - const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor._id}`]; - const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; - return alertText.map((alert) => alert).join("\n"); - }; - buildHardwareWebhookBody = (alerts, monitor) => { - const content = alerts.map((alert) => alert).join("\n"); - const body = { text: content, name: monitor.name, url: monitor.url }; - return body; - }; -} - -export default NotificationUtils; From e91a54b75c33327b1cf20e0a2dd89178c2942087 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 23:41:24 +0000 Subject: [PATCH 086/103] initial commit --- server/src/config/services.ts | 7 + server/src/controllers/incidentController.ts | 12 +- .../db/models/{Incident.js => Incident.ts} | 42 +-- server/src/db/models/index.ts | 3 + .../incidents/IIncidentsRepository.ts | 22 ++ .../incidents/MongoIncidentRepository.ts | 100 +++++++ server/src/repositories/index.ts | 3 + ...{incidentService.js => incidentService.ts} | 252 +++++++----------- .../SuperSimpleQueueHelper.ts | 11 + .../service/infrastructure/statusService.ts | 64 ----- server/src/types/incident.ts | 20 ++ server/src/types/index.ts | 1 + 12 files changed, 295 insertions(+), 242 deletions(-) rename server/src/db/models/{Incident.js => Incident.ts} (55%) create mode 100644 server/src/repositories/incidents/IIncidentsRepository.ts create mode 100644 server/src/repositories/incidents/MongoIncidentRepository.ts rename server/src/service/business/{incidentService.js => incidentService.ts} (55%) create mode 100644 server/src/types/incident.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 9ae5ecb12..cbd58f2e9 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -84,6 +84,7 @@ import { MongoRecoveryTokensRepository, MongoSettingsRepository, MongoNotificationsRepository, + MongoIncidentRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, @@ -93,6 +94,7 @@ import { IRecoveryTokensRepository, ISettingsRepository, INotificationsRepository, + IIncidentsRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -127,6 +129,7 @@ export type InitializedSerivces = { recoveryTokensRepository: IRecoveryTokensRepository; settingsRepository: ISettingsRepository; notificationsRepository: INotificationsRepository; + incidentsRepository: IIncidentsRepository; }; export const initializeServices = async ({ @@ -183,6 +186,7 @@ export const initializeServices = async ({ const recoveryTokensRepository = new MongoRecoveryTokensRepository(); const settingsRepository = new MongoSettingsRepository(); const notificationsRepository = new MongoNotificationsRepository(); + const incidentsRepository = new MongoIncidentRepository(); const networkService = new NetworkService({ axios, got, @@ -205,6 +209,7 @@ export const initializeServices = async ({ logger, errorService, stringService, + incidentsRepository, }); const checkService = new CheckService({ @@ -247,6 +252,7 @@ export const initializeServices = async ({ notificationsService, checkService, buffer: bufferService, + incidentService, }); const superSimpleQueue = await SuperSimpleQueue.create({ @@ -332,6 +338,7 @@ export const initializeServices = async ({ recoveryTokensRepository, settingsRepository, notificationsRepository, + incidentsRepository, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index b17788d80..645149acd 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -67,12 +67,12 @@ class IncidentController { resolveIncidentManually = async (req: Request, res: Response, next: NextFunction) => { try { - const resolvedIncident = await this.incidentService.resolveIncidentManually({ - incidentId: req?.params?.incidentId, - userId: req?.user?.id, - teamId: req?.user?.teamId, - comment: req?.body?.comment, - }); + const resolvedIncident = await this.incidentService.resolveIncident( + req?.params?.incidentId, + req?.user?.id, + req?.user?.teamId, + req?.body?.comment + ); return res.status(200).json({ success: true, diff --git a/server/src/db/models/Incident.js b/server/src/db/models/Incident.ts similarity index 55% rename from server/src/db/models/Incident.js rename to server/src/db/models/Incident.ts index a9477301b..8de1c7c2c 100644 --- a/server/src/db/models/Incident.js +++ b/server/src/db/models/Incident.ts @@ -1,16 +1,31 @@ -import mongoose from "mongoose"; +import { Schema, model, type Types } from "mongoose"; +import { IncidentResolutionTypes, type Incident } from "@/types/incident.js"; -const IncidentSchema = mongoose.Schema( +type IncidentDocumentBase = Omit & { + monitorId: Types.ObjectId; + teamId: Types.ObjectId; + resolvedBy?: Types.ObjectId | null; + startTime: Date; + endTime: Date | null; + createdAt: Date; + updatedAt: Date; +}; + +export interface IncidentDocument extends IncidentDocumentBase { + _id: Types.ObjectId; +} + +const IncidentSchema = new Schema( { monitorId: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "Monitor", required: true, immutable: true, index: true, }, teamId: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "Team", required: true, immutable: true, @@ -18,8 +33,8 @@ const IncidentSchema = mongoose.Schema( }, startTime: { type: Date, - required: true, immutable: true, + required: true, }, endTime: { type: Date, @@ -36,16 +51,16 @@ const IncidentSchema = mongoose.Schema( }, statusCode: { type: Number, - index: true, default: null, + index: true, }, resolutionType: { type: String, - enum: ["automatic", "manual"], + enum: IncidentResolutionTypes, default: null, }, resolvedBy: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "User", default: null, }, @@ -53,12 +68,6 @@ const IncidentSchema = mongoose.Schema( type: String, default: null, }, - checks: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Check", - }, - ], }, { timestamps: true } ); @@ -71,6 +80,7 @@ IncidentSchema.index({ resolutionType: 1, status: 1 }); IncidentSchema.index({ resolvedBy: 1, status: 1 }); IncidentSchema.index({ createdAt: -1 }); -const Incident = mongoose.model("Incident", IncidentSchema); +const IncidentModel = model("Incident", IncidentSchema); -export default Incident; +export { IncidentModel }; +export default IncidentModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 18a3a3b19..4b71937bb 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -24,3 +24,6 @@ export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; export * from "@/db/models/Notification.js"; export { default as NotificationModel } from "@/db/models/Notification.js"; + +export * from "@/db/models/Incident.js"; +export { default as IncidentModel } from "@/db/models/Incident.js"; diff --git a/server/src/repositories/incidents/IIncidentsRepository.ts b/server/src/repositories/incidents/IIncidentsRepository.ts new file mode 100644 index 000000000..00235de0d --- /dev/null +++ b/server/src/repositories/incidents/IIncidentsRepository.ts @@ -0,0 +1,22 @@ +import type { Incident } from "@/types/index.js"; +export interface IIncidentsRepository { + // create + create(incident: Partial): Promise; + // fetch + findActiveByIncidentId(incidentId: string, teamId: string): Promise; + findActiveByMonitorId(monitorId: string, teamId: string): Promise; + findByTeamId( + teamId: string, + dateRange: string, + page: number, + rowsPerPage: number, + sortOrder?: string, + status?: string, + monitorId?: string, + resolutionType?: string + ): Promise; + + // update + updateById(incidentId: string, teamId: string, updateData: Partial): Promise; + // delete +} diff --git a/server/src/repositories/incidents/MongoIncidentRepository.ts b/server/src/repositories/incidents/MongoIncidentRepository.ts new file mode 100644 index 000000000..db0048fc7 --- /dev/null +++ b/server/src/repositories/incidents/MongoIncidentRepository.ts @@ -0,0 +1,100 @@ +import { IncidentModel } from "@/db/models/index.js"; +import type { IncidentDocument } from "@/db/models/Incident.js"; +import type { Incident } from "@/types/index.js"; +import type { IIncidentsRepository } from "@/repositories/index.js"; +import mongoose from "mongoose"; +import { AppError } from "@/utils/AppError.js"; + +class MongoIncidentRepository implements IIncidentsRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: IncidentDocument): Incident => { + return { + id: this.toStringId(doc._id), + monitorId: this.toStringId(doc.monitorId), + teamId: this.toStringId(doc.teamId), + startTime: this.toDateString(doc.startTime), + endTime: doc.endTime ? this.toDateString(doc.endTime) : null, + status: doc.status, + message: doc.message ?? null, + statusCode: doc.statusCode ?? null, + resolutionType: doc.resolutionType ?? null, + resolvedBy: doc.resolvedBy ? this.toStringId(doc.resolvedBy) : null, + comment: doc.comment ?? null, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + protected mapDocuments = (documents: IncidentDocument[] | IncidentDocument | null): Incident[] => { + if (!documents) { + return []; + } + if (Array.isArray(documents)) { + return documents.map((doc) => this.toEntity(doc)); + } + return [this.toEntity(documents)]; + }; + + async create(incident: Partial): Promise { + const newIncident = await IncidentModel.create(incident); + return this.toEntity(newIncident); + } + + findActiveByIncidentId = async (incidentId: string, teamId: string): Promise => { + const incident = await IncidentModel.findOne({ + _id: new mongoose.Types.ObjectId(incidentId), + teamId: new mongoose.Types.ObjectId(teamId), + status: true, + }); + if (!incident) { + return null; + } + return this.toEntity(incident); + }; + + findActiveByMonitorId = async (monitorId: string, teamId: string): Promise => { + const incident = await IncidentModel.findOne({ + monitorId: new mongoose.Types.ObjectId(monitorId), + teamId: new mongoose.Types.ObjectId(teamId), + status: true, + }); + if (!incident) { + return null; + } + return this.toEntity(incident); + }; + + findByTeamId = async (teamId: string): Promise => { + throw new Error("Method not implemented."); + }; + + updateById = async (incidentId: string, teamId: string, patch: Partial) => { + const updatedIncident = await IncidentModel.findOneAndUpdate( + { _id: new mongoose.Types.ObjectId(incidentId), teamId: new mongoose.Types.ObjectId(teamId) }, + { + $set: { + ...patch, + }, + }, + { new: true, runValidators: true } + ); + if (!updatedIncident) { + throw new AppError({ message: `Failed to update incident with id ${incidentId}`, status: 500 }); + } + return this.toEntity(updatedIncident); + }; +} +export default MongoIncidentRepository; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 1a3199081..72dfd2f40 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -24,3 +24,6 @@ export { default as MongoSettingsRepository } from "@/repositories/settings/Mong export * from "@/repositories/notifications/INotificationsRepository.js"; export { default as MongoNotificationsRepository } from "@/repositories/notifications/MongoNotificationsRepository.js"; + +export * from "@/repositories/incidents/IIncidentsRepository.js"; +export { default as MongoIncidentRepository } from "@/repositories/incidents/MongoIncidentRepository.js"; diff --git a/server/src/service/business/incidentService.js b/server/src/service/business/incidentService.ts similarity index 55% rename from server/src/service/business/incidentService.js rename to server/src/service/business/incidentService.ts index db89caa68..a96c7923d 100644 --- a/server/src/service/business/incidentService.js +++ b/server/src/service/business/incidentService.ts @@ -1,183 +1,123 @@ const SERVICE_NAME = "incidentService"; +import type { Monitor } from "@/types/monitor.js"; +import { AppError } from "@/utils/AppError.js"; +import type { IIncidentsRepository } from "@/repositories/index.js"; +import type { Incident } from "@/types/index.js"; class IncidentService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, logger, errorService, stringService }) { + private db: any; + private logger: any; + private errorService: any; + private stringService: any; + private incidentsRepository: IIncidentsRepository; + + constructor({ + db, + logger, + errorService, + stringService, + incidentsRepository, + }: { + db: any; + logger: any; + errorService: any; + stringService: any; + incidentsRepository: IIncidentsRepository; + }) { this.db = db; this.logger = logger; this.errorService = errorService; this.stringService = stringService; + this.incidentsRepository = incidentsRepository; } get serviceName() { return IncidentService.SERVICE_NAME; } - createIncident = async (monitor, check) => { - try { - if (!monitor || !monitor._id) { - throw this.errorService.createBadRequestError("Monitor is required"); - } - - if (!check || !check._id) { - throw this.errorService.createBadRequestError("Check is required"); - } - - const activeIncident = await this.db.incidentModule.getActiveIncidentByMonitor(monitor._id); - + handleIncident = async (monitor: Monitor, code: number): Promise => { + const activeIncident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId); + // Monitor is down, create an incident + if (monitor.status === false) { if (activeIncident) { - await this.db.incidentModule.addCheckToIncident(activeIncident._id, check._id); - - this.logger.info({ - service: this.SERVICE_NAME, - method: "createIncident", - message: `Check added to existing active incident for monitor ${monitor.name}`, - incidentId: activeIncident._id, - monitorId: monitor._id, - }); - return activeIncident; + } else { + const incident = { + monitorId: monitor.id, + teamId: monitor.teamId, + startTime: Date.now().toString(), + status: true, + statusCode: code, + }; + return await this.incidentsRepository.create(incident); } - - const incidentData = { - monitorId: monitor._id, - teamId: monitor.teamId, - type: monitor.type, - startTime: new Date(), - status: true, - message: check.message || null, - statusCode: check.statusCode || null, - checks: [check._id], - }; - - const incident = await this.db.incidentModule.createIncident(incidentData); - - this.logger.info({ - service: this.SERVICE_NAME, - method: "createIncident", - message: `New incident created for monitor ${monitor.name}`, - incidentId: incident._id, - monitorId: monitor._id, - }); - - return incident; - } catch (error) { - this.logger.error({ - service: this.SERVICE_NAME, - method: "createIncident", - message: error.message, - monitorId: monitor?._id, - error: error.stack, - }); - throw error; } + + // Monitor is up, resolve active incidents + if (!activeIncident) { + return null; + } + activeIncident.status = false; + activeIncident.endTime = Date.now().toString(); + activeIncident.resolutionType = "automatic"; + return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident); }; - resolveIncident = async (monitor, check) => { - try { - if (!monitor || !monitor._id) { - throw this.errorService.createBadRequestError("Monitor is required"); - } - - const activeIncident = await this.db.incidentModule.getActiveIncidentByMonitor(monitor._id); - - if (!activeIncident) { - this.logger.info({ - service: this.SERVICE_NAME, - method: "resolveIncident", - message: `No active incident found for monitor ${monitor.name}`, - monitorId: monitor._id, - }); - return null; - } - - await this.db.incidentModule.addCheckToIncident(activeIncident._id, check._id); - - const resolvedIncident = await this.db.incidentModule.resolveIncident(activeIncident._id, { - resolutionType: "automatic", - resolvedBy: null, - endTime: new Date(), - }); - - this.logger.info({ - service: this.SERVICE_NAME, - method: "resolveIncident", - message: `Incident automatically resolved for monitor ${monitor.name}`, - incidentId: resolvedIncident._id, - monitorId: monitor._id, - }); - - return resolvedIncident; - } catch (error) { - this.logger.error({ - service: this.SERVICE_NAME, - method: "resolveIncident", - message: error.message, - monitorId: monitor?._id, - error: error.stack, - }); - throw error; - } - }; - - resolveIncidentManually = async ({ incidentId, userId, teamId, comment }) => { + resolveIncident = async (incidentId: string, userId: string, teamId: string, comment?: string) => { try { if (!incidentId) { - throw this.errorService.createBadRequestError("No incident ID in request"); + throw new AppError({ message: "No incident ID in request", service: SERVICE_NAME, method: "resolveIncident" }); } if (!userId) { - throw this.errorService.createBadRequestError("No user ID in request"); + throw new AppError({ message: "No user ID in request", service: SERVICE_NAME, method: "resolveIncident" }); } if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "resolveIncident" }); } - const incident = await this.db.incidentModule.getIncidentById(incidentId); + const incident = await this.incidentsRepository.findActiveByIncidentId(incidentId, teamId); if (!incident) { - throw this.errorService.createNotFoundError("Incident not found"); - } - - if (!incident.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); + throw new AppError({ message: "Incident not found", service: SERVICE_NAME, method: "resolveIncident" }); } if (incident.status === false) { - throw this.errorService.createBadRequestError("Incident is already resolved"); + throw new AppError({ message: "Incident is already resolved", service: SERVICE_NAME, method: "resolveIncident" }); } - const resolvedIncident = await this.db.incidentModule.resolveIncident(incidentId, { - resolutionType: "manual", - resolvedBy: userId, - comment: comment || null, - endTime: new Date(), - }); + incident.resolutionType = "manual"; + incident.status = false; + incident.resolvedBy = userId; + incident.comment = comment || null; + incident.endTime = Date.now().toString(); - this.logger.info({ - service: this.SERVICE_NAME, + const resolvedIncident = await this.incidentsRepository.updateById(incident.id, teamId, incident); + + this.logger.debug({ + service: SERVICE_NAME, method: "resolveIncidentManually", message: `Incident manually resolved by user`, - incidentId: resolvedIncident._id, - userId, + details: resolvedIncident.id, }); return resolvedIncident; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, - method: "resolveIncidentManually", + service: SERVICE_NAME, + method: "resolveIncident", message: error.message, - incidentId, - error: error.stack, + details: incidentId, + stack: error.stack, }); throw error; } }; - getIncidentsByTeam = async ({ teamId, query }) => { + getIncidentsByTeam = async ({ teamId, query }: { teamId: string; query?: any }) => { try { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); @@ -197,19 +137,19 @@ class IncidentService { }); return result; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "getIncidentsByTeam", message: error.message, - teamId, - error: error.stack, + details: teamId, + stack: error.stack, }); throw error; } }; - getIncidentSummary = async ({ teamId, query }) => { + getIncidentSummary = async ({ teamId, query }: { teamId: string; query?: any }) => { try { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); @@ -223,19 +163,19 @@ class IncidentService { }); return summary; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "getIncidentSummary", message: error.message, - teamId, - error: error.stack, + details: teamId, + stack: error.stack, }); throw error; } }; - getIncidentById = async ({ incidentId, teamId }) => { + getIncidentById = async ({ incidentId, teamId }: { incidentId: string; teamId: string }) => { try { if (!incidentId) { throw this.errorService.createBadRequestError("No incident ID in request"); @@ -256,19 +196,19 @@ class IncidentService { } return incident; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "getIncidentById", message: error.message, - incidentId, - error: error.stack, + details: incidentId, + stack: error.stack, }); throw error; } }; - processIncidentsFromBuffer = async (incidentBufferItems) => { + processIncidentsFromBuffer = async (incidentBufferItems: any[]) => { try { if (!incidentBufferItems || incidentBufferItems.length === 0) { return; @@ -285,16 +225,16 @@ class IncidentService { } } - for (const item of resolveItems) { + for (const item of resolveItems as any[]) { try { - await this.resolveIncident(item.monitor, item.check); - } catch (error) { + await this.resolveIncident(item.monitor); + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "processIncidentsFromBuffer", message: `Failed to resolve incident from buffer: ${error.message}`, monitorId: item.monitor?._id, - error: error.stack, + stack: error.stack, }); } } @@ -303,11 +243,11 @@ class IncidentService { return; } - const groupedByMonitor = {}; + const groupedByMonitor: Record = {}; for (const item of createItems) { if (!item.monitor || !item.monitor.id || !item.check || !item.check.id) { this.logger.warn({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "processIncidentsFromBuffer", message: "Skipping item with missing monitor or check data", item, @@ -329,7 +269,7 @@ class IncidentService { const activeIncidents = await this.db.incidentModule.getActiveIncidentsByMonitors(monitorIds); - const incidentsCreatedInFlush = {}; + const incidentsCreatedInFlush: Record = {}; const checksToAddToIncidents = []; const newIncidentsToCreate = []; @@ -392,19 +332,19 @@ class IncidentService { } this.logger.info({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "processIncidentsFromBuffer", message: `Processed ${incidentBufferItems.length} incident buffer items`, created: newIncidentsToCreate.length, checksAdded: checksToAddToIncidents.length, resolved: resolveItems.length, }); - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "processIncidentsFromBuffer", message: error.message, - error: error.stack, + stack: error.stack, }); throw error; } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index bbd3a11f0..26d45db22 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -2,6 +2,8 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; +import IncidentService from "@/service/business/incidentService.js"; + class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; @@ -12,6 +14,7 @@ class SuperSimpleQueueHelper { private notificationsService: INotificationsService; private checkService: any; private buffer: any; + private incidentService: IncidentService; constructor({ db, @@ -21,6 +24,7 @@ class SuperSimpleQueueHelper { notificationsService, checkService, buffer, + incidentService, }: { db: any; logger: any; @@ -29,6 +33,7 @@ class SuperSimpleQueueHelper { notificationsService: INotificationsService; checkService: any; buffer: any; + incidentService: IncidentService; }) { this.db = db; this.logger = logger; @@ -37,6 +42,7 @@ class SuperSimpleQueueHelper { this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; + this.incidentService = incidentService; } get serviceName() { @@ -90,6 +96,11 @@ class SuperSimpleQueueHelper { stack: error.stack, }); }); + + // Step 6. Handle incidents + if (statusChangeResult.statusChanged) { + await this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code); + } } catch (error: any) { this.logger.warn({ message: error.message, diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 3d9591c02..3a83dbd28 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -218,70 +218,6 @@ export class StatusService implements IStatusService { statusChanged = true; } - if (statusChanged) { - this.logger.info({ - service: SERVICE_NAME, - message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`, - prevStatus, - newStatus, - }); - - if (newStatus === false) { - await this.handleIncidentForCheck(check, monitor, "create", "status change to down"); - } else if (prevStatus === false) { - await this.handleIncidentForCheck(check, monitor, "resolve", "status change to up"); - } - } - - if (monitor.status === false && !statusChanged) { - try { - const lastManuallyResolvedIncident = await this.db.incidentModule.getLastManuallyResolvedIncident(monitor.id); - - let calculatedFailureRate = failureRate; - - if (lastManuallyResolvedIncident && lastManuallyResolvedIncident.endTime) { - try { - const checksAfterResolution = await CheckModel.find({ - monitorId: monitor.id, - createdAt: { $gt: lastManuallyResolvedIncident.endTime }, - }) - .sort({ createdAt: 1 }) - .limit(monitor.statusWindowSize) - .select("status") - .lean(); - - if (checksAfterResolution.length > 0) { - const checksStatuses = checksAfterResolution.map((c) => c.status); - const failuresAfterResolution = checksStatuses.filter((s) => s === false).length; - calculatedFailureRate = (failuresAfterResolution / monitor.statusWindow.length) * 100; - } else { - calculatedFailureRate = 0; - } - } catch (checkQueryError: any) { - this.logger.error({ - service: SERVICE_NAME, - method: "updateStatus", - message: `Failed to query checks after manual resolution: ${checkQueryError.message}`, - monitorId: monitor.id, - stack: checkQueryError.stack, - }); - } - } - - if (calculatedFailureRate >= monitor.statusWindowThreshold) { - await this.handleIncidentForCheck(check, monitor, "create", "threshold check without status change"); - } - } catch (error: any) { - this.logger.error({ - service: SERVICE_NAME, - method: "updateStatus", - message: `Error handling threshold check without status change: ${error.message}`, - monitorId: monitor.id, - stack: error.stack, - }); - } - } - monitor.status = newStatus; const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); diff --git a/server/src/types/incident.ts b/server/src/types/incident.ts new file mode 100644 index 000000000..65186f08c --- /dev/null +++ b/server/src/types/incident.ts @@ -0,0 +1,20 @@ +// export type IncidentResolutionType = "automatic" | "manual" | null; + +export const IncidentResolutionTypes = ["automatic", "manual", null] as const; +export type IncidentResolutionType = (typeof IncidentResolutionTypes)[number]; + +export interface Incident { + id: string; + monitorId: string; + teamId: string; + startTime: string; + endTime: string | null; + status: boolean; + message?: string | null; + statusCode?: number | null; + resolutionType: IncidentResolutionType; + resolvedBy?: string | null; + comment?: string | null; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts index fda48f0c6..e28d75e91 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -9,3 +9,4 @@ export * from "@/types/recoveryToken.js"; export * from "@/types/settings.js"; export * from "@/types/notification.js"; export * from "@/types/alert.js"; +export * from "@/types/incident.js"; From adb2a383cfd6705473bc40b12de9eeeeb87f6190 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 00:02:27 +0000 Subject: [PATCH 087/103] refactor --- .../incidents/IIncidentsRepository.ts | 3 +- .../incidents/MongoIncidentRepository.ts | 25 ++++++++- .../src/service/business/incidentService.ts | 51 +++++++++++++++---- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/server/src/repositories/incidents/IIncidentsRepository.ts b/server/src/repositories/incidents/IIncidentsRepository.ts index 00235de0d..dbdf2b05f 100644 --- a/server/src/repositories/incidents/IIncidentsRepository.ts +++ b/server/src/repositories/incidents/IIncidentsRepository.ts @@ -7,7 +7,8 @@ export interface IIncidentsRepository { findActiveByMonitorId(monitorId: string, teamId: string): Promise; findByTeamId( teamId: string, - dateRange: string, + startDate: Date, + endDate: Date, page: number, rowsPerPage: number, sortOrder?: string, diff --git a/server/src/repositories/incidents/MongoIncidentRepository.ts b/server/src/repositories/incidents/MongoIncidentRepository.ts index db0048fc7..ead783e11 100644 --- a/server/src/repositories/incidents/MongoIncidentRepository.ts +++ b/server/src/repositories/incidents/MongoIncidentRepository.ts @@ -77,8 +77,29 @@ class MongoIncidentRepository implements IIncidentsRepository { return this.toEntity(incident); }; - findByTeamId = async (teamId: string): Promise => { - throw new Error("Method not implemented."); + findByTeamId = async ( + teamId: string, + startDate: Date, + endDate: Date, + page: number, + rowsPerPage: number, + sortOrder?: string, + status?: string, + monitorId?: string, + resolutionType?: string + ): Promise => { + const matchStage: Record = { + teamId: new mongoose.Types.ObjectId(teamId), + ...(status !== undefined && { status: status }), + ...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }), + ...(resolutionType && { resolutionType }), + createdAt: { $gte: startDate, $lte: endDate }, + }; + const incidents = await IncidentModel.find(matchStage) + .sort({ createdAt: sortOrder === "asc" ? 1 : -1 }) + .skip(page * rowsPerPage) + .limit(rowsPerPage); + return this.mapDocuments(incidents); }; updateById = async (incidentId: string, teamId: string, patch: Partial) => { diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index a96c7923d..b2ad444d3 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -4,6 +4,15 @@ import { AppError } from "@/utils/AppError.js"; import type { IIncidentsRepository } from "@/repositories/index.js"; import type { Incident } from "@/types/index.js"; +const dateRangeLookup: Record = { + recent: new Date(new Date().setDate(new Date().getDate() - 2)), + hour: new Date(new Date().setHours(new Date().getHours() - 1)), + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: undefined, +}; + class IncidentService { static SERVICE_NAME = SERVICE_NAME; @@ -125,18 +134,38 @@ class IncidentService { const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = query || {}; - const result = await this.db.incidentModule.getIncidentsByTeam({ - teamId, - sortOrder, - dateRange, - page, - rowsPerPage, - status, - monitorId, - resolutionType, - }); + const startDate = dateRangeLookup[dateRange]; - return result; + const endDate = new Date(); + + const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; + const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20; + const parsedStatus = status === "true" ? "true" : status === "false" ? "false" : undefined; + + const res = await this.incidentsRepository.findByTeamId( + teamId, + startDate, + endDate, + parsedPage, + parsedRowsPerPage, + sortOrder, + parsedStatus, + monitorId, + resolutionType + ); + + // const result = await this.db.incidentModule.getIncidentsByTeam({ + // teamId, + // sortOrder, + // dateRange, + // page, + // rowsPerPage, + // status, + // monitorId, + // resolutionType, + // }); + + return res; } catch (error: any) { this.logger.error({ service: SERVICE_NAME, From 3cf7243c93fd4fc26f1db6f141c640b41a3aaa6d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 18:56:06 +0000 Subject: [PATCH 088/103] summary --- .../Components/IncidentTable/index.jsx | 9 +- .../Incidents2/hooks/useFetchIncidents.js | 3 +- client/src/Pages/Incidents2/index.jsx | 3 +- server/src/controllers/incidentController.ts | 16 +- .../incidents/IIncidentsRepository.ts | 9 +- .../incidents/MongoIncidentRepository.ts | 166 ++++++++++++++++-- .../src/service/business/incidentService.ts | 49 +++--- server/src/types/incident.ts | 29 +++ server/src/utils/{utils.js => utils.ts} | 6 +- 9 files changed, 237 insertions(+), 53 deletions(-) rename server/src/utils/{utils.js => utils.ts} (74%) diff --git a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx index d613e15c7..670da4a81 100644 --- a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx @@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next"; import { Button, Typography, useTheme } from "@mui/material"; const IncidentTable = ({ + monitors = [], incidents = [], incidentsCount = 0, isLoading = false, @@ -44,7 +45,11 @@ const IncidentTable = ({ { id: "monitorName", content: t("incidentsTableMonitorName"), - render: (row) => row.monitorName ?? "N/A", + render: (row) => { + console.log(monitors, row); + const monitor = monitors.find((monitor) => monitor.id === row.monitorId); + return monitor ? monitor.name : "N/A"; + }, }, { id: "status", @@ -130,7 +135,7 @@ const IncidentTable = ({ lineHeight: 1.2, }} onClick={() => { - handleResolveIncident(row._id); + handleResolveIncident(row.id); }} > {t("incidentsPage.incidentsTableActionResolveManually")} diff --git a/client/src/Pages/Incidents2/hooks/useFetchIncidents.js b/client/src/Pages/Incidents2/hooks/useFetchIncidents.js index ab22c42e0..54874bae7 100644 --- a/client/src/Pages/Incidents2/hooks/useFetchIncidents.js +++ b/client/src/Pages/Incidents2/hooks/useFetchIncidents.js @@ -45,9 +45,8 @@ const useFetchIncidents = () => { setNetworkError(false); const res = await networkService.getIncidentsByTeam(config); - setIncidents(res.data?.data?.incidents || []); - setIncidentsCount(res.data?.data?.incidentsCount || 0); + setIncidentsCount(res.data?.data?.count || 0); } catch (error) { setNetworkError(true); console.error(t("incidentsPage.errorFetchingIncidents"), error); diff --git a/client/src/Pages/Incidents2/index.jsx b/client/src/Pages/Incidents2/index.jsx index 2f31864bf..4e68279c5 100644 --- a/client/src/Pages/Incidents2/index.jsx +++ b/client/src/Pages/Incidents2/index.jsx @@ -88,7 +88,7 @@ const Incidents2 = () => { useEffect(() => { const lookup = monitors?.reduce((acc, monitor) => { acc[monitor.id] = { - _id: monitor.id, + id: monitor.id, name: monitor.name, type: monitor.type, }; @@ -132,6 +132,7 @@ const Incidents2 = () => { /> { try { - const summary = await this.incidentService.getIncidentSummary({ - teamId: req?.user?.teamId, - query: req?.query, - }); + const teamId = req.user?.teamId; + + if (!teamId) { + throw new AppError({ message: "Team ID is required", service: SERVICE_NAME, status: 400 }); + } + + const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10; + + const summary = await this.incidentService.getIncidentSummary(req?.user?.teamId, limit); return res.status(200).json({ success: true, diff --git a/server/src/repositories/incidents/IIncidentsRepository.ts b/server/src/repositories/incidents/IIncidentsRepository.ts index dbdf2b05f..7c2b0179e 100644 --- a/server/src/repositories/incidents/IIncidentsRepository.ts +++ b/server/src/repositories/incidents/IIncidentsRepository.ts @@ -1,4 +1,5 @@ import type { Incident } from "@/types/index.js"; +import type { IncidentSummary } from "@/types/index.js"; export interface IIncidentsRepository { // create create(incident: Partial): Promise; @@ -7,17 +8,19 @@ export interface IIncidentsRepository { findActiveByMonitorId(monitorId: string, teamId: string): Promise; findByTeamId( teamId: string, - startDate: Date, - endDate: Date, + startDate: Date | undefined, page: number, rowsPerPage: number, sortOrder?: string, - status?: string, + status?: boolean, monitorId?: string, resolutionType?: string ): Promise; + findSummaryByTeamId(teamId: string, limit?: number): Promise; + countByTeamId(teamId: string, startDate: Date | undefined, status?: boolean, monitorId?: string, resolutionType?: string): Promise; // update updateById(incidentId: string, teamId: string, updateData: Partial): Promise; // delete + // other } diff --git a/server/src/repositories/incidents/MongoIncidentRepository.ts b/server/src/repositories/incidents/MongoIncidentRepository.ts index ead783e11..10cb47213 100644 --- a/server/src/repositories/incidents/MongoIncidentRepository.ts +++ b/server/src/repositories/incidents/MongoIncidentRepository.ts @@ -1,6 +1,6 @@ import { IncidentModel } from "@/db/models/index.js"; import type { IncidentDocument } from "@/db/models/Incident.js"; -import type { Incident } from "@/types/index.js"; +import type { Incident, IncidentSummary } from "@/types/index.js"; import type { IIncidentsRepository } from "@/repositories/index.js"; import mongoose from "mongoose"; import { AppError } from "@/utils/AppError.js"; @@ -20,6 +20,32 @@ class MongoIncidentRepository implements IIncidentsRepository { return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); }; + private buildMatchStage({ + teamId, + startDate, + status, + monitorId, + resolutionType, + }: { + teamId: string; + startDate: Date | undefined; + status?: boolean; + monitorId?: string; + resolutionType?: string; + }): Record { + const matchStage: Record = { + teamId: new mongoose.Types.ObjectId(teamId), + ...(status !== undefined && { status }), + ...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }), + ...(resolutionType && { resolutionType }), + }; + + if (startDate) { + matchStage.createdAt = { $gte: startDate }; + } + return matchStage; + } + protected toEntity = (doc: IncidentDocument): Incident => { return { id: this.toStringId(doc._id), @@ -79,22 +105,15 @@ class MongoIncidentRepository implements IIncidentsRepository { findByTeamId = async ( teamId: string, - startDate: Date, - endDate: Date, + startDate: Date | undefined, page: number, rowsPerPage: number, sortOrder?: string, - status?: string, + status?: boolean, monitorId?: string, resolutionType?: string ): Promise => { - const matchStage: Record = { - teamId: new mongoose.Types.ObjectId(teamId), - ...(status !== undefined && { status: status }), - ...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }), - ...(resolutionType && { resolutionType }), - createdAt: { $gte: startDate, $lte: endDate }, - }; + const matchStage = this.buildMatchStage({ teamId, startDate, status, monitorId, resolutionType }); const incidents = await IncidentModel.find(matchStage) .sort({ createdAt: sortOrder === "asc" ? 1 : -1 }) .skip(page * rowsPerPage) @@ -117,5 +136,130 @@ class MongoIncidentRepository implements IIncidentsRepository { } return this.toEntity(updatedIncident); }; + + countByTeamId = async ( + teamId: string, + startDate: Date | undefined, + status?: boolean, + monitorId?: string, + resolutionType?: string + ): Promise => { + const matchStage = this.buildMatchStage({ teamId, startDate, status, monitorId, resolutionType }); + return IncidentModel.countDocuments(matchStage); + }; + + findSummaryByTeamId = async (teamId: string, limit: number): Promise => { + const matchStage = { teamId: new mongoose.Types.ObjectId(teamId) }; + + const counts = await IncidentModel.aggregate([ + { $match: matchStage }, + { + $group: { + _id: "$status", + count: { $sum: 1 }, + manualResolutions: { + $sum: { $cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0] }, + }, + automaticResolutions: { + $sum: { $cond: [{ $eq: ["$resolutionType", "automatic"] }, 1, 0] }, + }, + }, + }, + ]); + + let total = 0; + let active = 0; + let manual = 0; + let automatic = 0; + + counts.forEach((item) => { + total += item.count; + if (item._id === true) { + active = item.count; + } + manual += item.manualResolutions; + automatic += item.automaticResolutions; + }); + + const resolutionTimeResult = await IncidentModel.aggregate([ + { $match: { ...matchStage, status: false, endTime: { $exists: true, $ne: null } } }, + { $project: { resolutionTime: { $subtract: ["$endTime", "$startTime"] } } }, + { $group: { _id: null, avgResolutionTime: { $avg: "$resolutionTime" } } }, + ]); + const avgResolutionTimeMs = resolutionTimeResult[0]?.avgResolutionTime || 0; + const avgResolutionTimeHours = Math.round((avgResolutionTimeMs / (1000 * 60 * 60) || 0) * 100) / 100; + + const monitorResult = await IncidentModel.aggregate([ + { $match: matchStage }, + { $group: { _id: "$monitorId", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 1 }, + { + $lookup: { + from: "monitors", + localField: "_id", + foreignField: "_id", + as: "monitor", + }, + }, + { $project: { monitorId: "$_id", count: 1, monitorName: { $arrayElemAt: ["$monitor.name", 0] } } }, + ]); + + const latestLimit = Math.max(1, Number.isFinite(Number(limit)) ? Number(limit) : 10); + const latestIncidents = await IncidentModel.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: -1 } }, + { $limit: latestLimit }, + { + $lookup: { + from: "monitors", + localField: "monitorId", + foreignField: "_id", + as: "monitor", + }, + }, + { + $project: { + _id: 1, + monitorId: 1, + monitorName: { $arrayElemAt: ["$monitor.name", 0] }, + status: 1, + startTime: 1, + endTime: 1, + resolutionType: 1, + message: 1, + statusCode: 1, + createdAt: 1, + }, + }, + ]); + + return { + total, + totalActive: active, + totalManualResolutions: manual, + totalAutomaticResolutions: automatic, + avgResolutionTimeHours, + topMonitor: monitorResult[0] + ? { + monitorId: this.toStringId(monitorResult[0].monitorId), + monitorName: monitorResult[0].monitorName ?? null, + incidentCount: monitorResult[0].count, + } + : null, + latestIncidents: latestIncidents.map((incident) => ({ + id: this.toStringId(incident._id), + monitorId: this.toStringId(incident.monitorId), + monitorName: incident.monitorName ?? null, + status: incident.status, + startTime: this.toDateString(incident.startTime), + endTime: incident.endTime ? this.toDateString(incident.endTime) : null, + resolutionType: incident.resolutionType ?? null, + message: incident.message ?? null, + statusCode: incident.statusCode ?? null, + createdAt: this.toDateString(incident.createdAt), + })), + }; + }; } export default MongoIncidentRepository; diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index b2ad444d3..bc69eeed0 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -1,6 +1,7 @@ const SERVICE_NAME = "incidentService"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; +import { ParseBoolean } from "@/utils/utils.js"; import type { IIncidentsRepository } from "@/repositories/index.js"; import type { Incident } from "@/types/index.js"; @@ -136,16 +137,13 @@ class IncidentService { const startDate = dateRangeLookup[dateRange]; - const endDate = new Date(); - const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20; - const parsedStatus = status === "true" ? "true" : status === "false" ? "false" : undefined; + const parsedStatus = typeof status === "undefined" ? undefined : ParseBoolean(status); - const res = await this.incidentsRepository.findByTeamId( + const incidents = await this.incidentsRepository.findByTeamId( teamId, startDate, - endDate, parsedPage, parsedRowsPerPage, sortOrder, @@ -154,18 +152,9 @@ class IncidentService { resolutionType ); - // const result = await this.db.incidentModule.getIncidentsByTeam({ - // teamId, - // sortOrder, - // dateRange, - // page, - // rowsPerPage, - // status, - // monitorId, - // resolutionType, - // }); + const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType); - return res; + return { incidents, count }; } catch (error: any) { this.logger.error({ service: SERVICE_NAME, @@ -178,18 +167,14 @@ class IncidentService { } }; - getIncidentSummary = async ({ teamId, query }: { teamId: string; query?: any }) => { + getIncidentSummary = async (teamId: string, limit?: string) => { try { if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw this.errorService.createBadRequestError(" team ID in request"); } - const { limit } = query || {}; - - const summary = await this.db.incidentModule.getIncidentSummary({ - teamId, - limit, - }); + const parsedLimit = limit && Number.isFinite(parseInt(limit, 10)) ? parseInt(limit, 10) : 10; + const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit); return summary; } catch (error: any) { @@ -256,7 +241,7 @@ class IncidentService { for (const item of resolveItems as any[]) { try { - await this.resolveIncident(item.monitor); + await this.resolveIncidentForMonitor(item.monitor); } catch (error: any) { this.logger.error({ service: SERVICE_NAME, @@ -378,6 +363,20 @@ class IncidentService { throw error; } }; + + private resolveIncidentForMonitor = async (monitor: Monitor) => { + if (!monitor?.id) { + return; + } + const incident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId); + if (!incident) { + return; + } + incident.status = false; + incident.endTime = new Date().toISOString(); + incident.resolutionType = "automatic"; + await this.incidentsRepository.updateById(incident.id, incident.teamId, incident); + }; } export default IncidentService; diff --git a/server/src/types/incident.ts b/server/src/types/incident.ts index 65186f08c..39dfb3d0d 100644 --- a/server/src/types/incident.ts +++ b/server/src/types/incident.ts @@ -18,3 +18,32 @@ export interface Incident { createdAt: string; updatedAt: string; } + +export interface IncidentSummaryTopMonitor { + monitorId: string; + monitorName: string | null; + incidentCount: number; +} + +export interface IncidentSummaryItem { + id: string; + monitorId: string; + monitorName: string | null; + status: boolean; + startTime: string; + endTime: string | null; + resolutionType: IncidentResolutionType; + message: string | null; + statusCode: number | null; + createdAt: string; +} + +export interface IncidentSummary { + total: number; + totalActive: number; + totalManualResolutions: number; + totalAutomaticResolutions: number; + avgResolutionTimeHours: number; + topMonitor: IncidentSummaryTopMonitor | null; + latestIncidents: IncidentSummaryItem[]; +} diff --git a/server/src/utils/utils.js b/server/src/utils/utils.ts similarity index 74% rename from server/src/utils/utils.js rename to server/src/utils/utils.ts index ee4ee1c6d..a82d8734f 100755 --- a/server/src/utils/utils.js +++ b/server/src/utils/utils.ts @@ -1,4 +1,4 @@ -const ParseBoolean = (value) => { +export const ParseBoolean = (value: boolean | string | null | undefined) => { if (value === true || value === "true") { return true; } else if (value === false || value === "false" || value === null || value === undefined) { @@ -6,7 +6,7 @@ const ParseBoolean = (value) => { } }; -const getTokenFromHeaders = (headers) => { +export const getTokenFromHeaders = (headers: Record) => { const authorizationHeader = headers.authorization; if (!authorizationHeader) throw new Error("No auth headers"); @@ -15,5 +15,3 @@ const getTokenFromHeaders = (headers) => { return parts[1]; }; - -export { ParseBoolean, getTokenFromHeaders }; From 7afdcb8ebcf058ba8232c7ba03251b4fe295b640 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:01:50 +0000 Subject: [PATCH 089/103] fix query param --- server/src/controllers/incidentController.ts | 2 +- server/src/service/business/incidentService.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index a94474c5a..93783b20c 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -19,7 +19,7 @@ class IncidentController { try { const result = await this.incidentService.getIncidentsByTeam({ teamId: req?.user?.teamId, - query: req?.query?.limit, + query: req?.query, }); return res.status(200).json({ diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index bc69eeed0..513f16b1e 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -134,7 +134,6 @@ class IncidentService { } const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = query || {}; - const startDate = dateRangeLookup[dateRange]; const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; From 69d050ff6115a007c074e8fec36953ef2260fef3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:04:30 +0000 Subject: [PATCH 090/103] remove incidents from buffer --- .../src/service/business/incidentService.ts | 156 ------------------ .../service/infrastructure/bufferService.ts | 61 +------ 2 files changed, 1 insertion(+), 216 deletions(-) diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 513f16b1e..82211bc4e 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -220,162 +220,6 @@ class IncidentService { throw error; } }; - - processIncidentsFromBuffer = async (incidentBufferItems: any[]) => { - try { - if (!incidentBufferItems || incidentBufferItems.length === 0) { - return; - } - - const createItems = []; - const resolveItems = []; - - for (const item of incidentBufferItems) { - if (item.action === "resolve") { - resolveItems.push(item); - } else { - createItems.push(item); - } - } - - for (const item of resolveItems as any[]) { - try { - await this.resolveIncidentForMonitor(item.monitor); - } catch (error: any) { - this.logger.error({ - service: SERVICE_NAME, - method: "processIncidentsFromBuffer", - message: `Failed to resolve incident from buffer: ${error.message}`, - monitorId: item.monitor?._id, - stack: error.stack, - }); - } - } - - if (createItems.length === 0) { - return; - } - - const groupedByMonitor: Record = {}; - for (const item of createItems) { - if (!item.monitor || !item.monitor.id || !item.check || !item.check.id) { - this.logger.warn({ - service: SERVICE_NAME, - method: "processIncidentsFromBuffer", - message: "Skipping item with missing monitor or check data", - item, - }); - continue; - } - - const monitorId = item.monitor.id.toString(); - if (!groupedByMonitor[monitorId]) { - groupedByMonitor[monitorId] = []; - } - groupedByMonitor[monitorId].push(item); - } - - const monitorIds = Object.keys(groupedByMonitor); - if (monitorIds.length === 0) { - return; - } - - const activeIncidents = await this.db.incidentModule.getActiveIncidentsByMonitors(monitorIds); - - const incidentsCreatedInFlush: Record = {}; - const checksToAddToIncidents = []; - const newIncidentsToCreate = []; - - for (const [monitorId, items] of Object.entries(groupedByMonitor)) { - const existingIncident = activeIncidents.get(monitorId) || incidentsCreatedInFlush[monitorId]; - - if (existingIncident) { - const incidentId = existingIncident._id ? existingIncident._id.toString() : existingIncident; - for (const item of items) { - checksToAddToIncidents.push({ - incidentId, - checkId: item.check.id.toString(), - }); - } - } else { - const firstItem = items[0]; - const incidentData = { - monitorId: firstItem.monitor.id, - teamId: firstItem.monitor.teamId, - type: firstItem.monitor.type, - startTime: new Date(), - status: true, - message: firstItem.check.message || null, - statusCode: firstItem.check.statusCode || null, - checks: [firstItem.check.id], - }; - - newIncidentsToCreate.push({ - incidentData, - monitorId, - remainingChecks: items.slice(1), // Checks restantes para agregar después - }); - } - } - - if (newIncidentsToCreate.length > 0) { - const incidentDataArray = newIncidentsToCreate.map((item) => item.incidentData); - await this.db.incidentModule.createIncidents(incidentDataArray); - - const createdIncidentsMap = await this.db.incidentModule.getActiveIncidentsByMonitors(newIncidentsToCreate.map((item) => item.monitorId)); - - for (const item of newIncidentsToCreate) { - const createdIncident = createdIncidentsMap.get(item.monitorId); - if (createdIncident && createdIncident._id) { - const incidentId = createdIncident._id.toString(); - incidentsCreatedInFlush[item.monitorId] = incidentId; - - for (const remainingItem of item.remainingChecks) { - checksToAddToIncidents.push({ - incidentId, - checkId: remainingItem.check.id, - }); - } - } - } - } - - if (checksToAddToIncidents.length > 0) { - await this.db.incidentModule.addChecksToIncidentsBatch(checksToAddToIncidents); - } - - this.logger.info({ - service: SERVICE_NAME, - method: "processIncidentsFromBuffer", - message: `Processed ${incidentBufferItems.length} incident buffer items`, - created: newIncidentsToCreate.length, - checksAdded: checksToAddToIncidents.length, - resolved: resolveItems.length, - }); - } catch (error: any) { - this.logger.error({ - service: SERVICE_NAME, - method: "processIncidentsFromBuffer", - message: error.message, - stack: error.stack, - }); - throw error; - } - }; - - private resolveIncidentForMonitor = async (monitor: Monitor) => { - if (!monitor?.id) { - return; - } - const incident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId); - if (!incident) { - return; - } - incident.status = false; - incident.endTime = new Date().toISOString(); - incident.resolutionType = "automatic"; - await this.incidentsRepository.updateById(incident.id, incident.teamId, incident); - }; } export default IncidentService; diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 0363aa362..89cd6a55f 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -7,21 +7,17 @@ class BufferService { static SERVICE_NAME = SERVICE_NAME; private BUFFER_TIMEOUT: number; private logger: any; - private incidentService: any; private SERVICE_NAME: string; private buffer: any[]; - private incidentBuffer: any[]; private bufferTimer: NodeJS.Timeout | null = null; private checksService: any; - constructor({ logger, incidentService, checkService }: { logger: any; incidentService: any; checkService: any }) { + constructor({ logger, checkService }: { logger: any; checkService: any }) { this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute this.logger = logger; - this.incidentService = incidentService; this.checksService = checkService; this.SERVICE_NAME = SERVICE_NAME; this.buffer = []; - this.incidentBuffer = []; this.scheduleNextFlush(); this.logger.info({ message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`, @@ -47,28 +43,6 @@ class BufferService { } } - addIncidentToBuffer({ monitor, check, action = "create" }: { monitor: any; check: Check; action?: string }) { - try { - if (!monitor || !check) { - this.logger.warn({ - message: "Skipping incident buffer item: missing monitor or check", - service: this.SERVICE_NAME, - method: "addIncidentToBuffer", - }); - return; - } - - this.incidentBuffer.push({ monitor, check, action }); - } catch (error: any) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "addIncidentToBuffer", - stack: error.stack, - }); - } - } - removeCheckFromBuffer(checkToRemove: Check) { try { if (!checkToRemove) { @@ -141,40 +115,7 @@ class BufferService { }); } - try { - if (this.incidentBuffer.length > 0 && this.incidentService) { - await this.flushIncidentBuffer(); - } - } catch (error: any) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "flushBuffer", - stack: error.stack, - }); - } - this.buffer = []; - this.incidentBuffer = []; - } - - async flushIncidentBuffer() { - if (!this.incidentService || this.incidentBuffer.length === 0) { - return; - } - - try { - const itemsToProcess = [...this.incidentBuffer]; - await this.incidentService.processIncidentsFromBuffer(itemsToProcess); - } catch (error: any) { - this.logger.error({ - message: `Error flushing incident buffer: ${error.message}`, - service: this.SERVICE_NAME, - method: "flushIncidentBuffer", - stack: error.stack, - }); - throw error; - } } } From 427e5a18eab629713ba93ea4b1187bc5a0976845 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:12:05 +0000 Subject: [PATCH 091/103] incidents -> checks --- .../Checks/Components/IncidentTable/index.jsx | 264 ++++++++++++++++ .../Components/OptionsHeader/index.jsx | 15 +- .../Components/OptionsHeader/skeleton.jsx | 0 .../Components/StatusBoxes/StatusBox.jsx | 0 .../Components/StatusBoxes/index.jsx | 0 .../Components/StatusBoxes/skeleton.jsx | 0 client/src/Pages/Checks/index.jsx | 120 +++++++ .../Components/ActiveIncidentsPanel/index.jsx | 0 .../Components/IncidentTable/index.jsx | 293 +++++++----------- .../IncidentsSummaryPanel/index.jsx | 0 .../IncidentsSummaryPanel/skeleton.jsx | 0 .../LatestIncidentsPanel/IncidentItem.jsx | 0 .../Components/LatestIncidentsPanel/index.jsx | 0 .../Components/OptionsHeader/index.jsx | 15 +- .../Components/StatisticsPanel/index.jsx | 0 .../Components/SummaryCard/index.jsx | 0 .../hooks/useFetchIncidents.js | 0 client/src/Pages/Incidents/index.jsx | 153 +++++---- .../Components/IncidentTable/index.jsx | 209 ------------- client/src/Pages/Incidents2/index.jsx | 151 --------- client/src/Routes/index.jsx | 8 +- 21 files changed, 615 insertions(+), 613 deletions(-) create mode 100644 client/src/Pages/Checks/Components/IncidentTable/index.jsx rename client/src/Pages/{Incidents2 => Checks}/Components/OptionsHeader/index.jsx (87%) rename client/src/Pages/{Incidents => Checks}/Components/OptionsHeader/skeleton.jsx (100%) rename client/src/Pages/{Incidents => Checks}/Components/StatusBoxes/StatusBox.jsx (100%) rename client/src/Pages/{Incidents => Checks}/Components/StatusBoxes/index.jsx (100%) rename client/src/Pages/{Incidents => Checks}/Components/StatusBoxes/skeleton.jsx (100%) create mode 100644 client/src/Pages/Checks/index.jsx rename client/src/Pages/{Incidents2 => Incidents}/Components/ActiveIncidentsPanel/index.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/IncidentsSummaryPanel/index.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/IncidentsSummaryPanel/skeleton.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/LatestIncidentsPanel/IncidentItem.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/LatestIncidentsPanel/index.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/StatisticsPanel/index.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/SummaryCard/index.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/hooks/useFetchIncidents.js (100%) delete mode 100644 client/src/Pages/Incidents2/Components/IncidentTable/index.jsx delete mode 100644 client/src/Pages/Incidents2/index.jsx diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx new file mode 100644 index 000000000..e066049e6 --- /dev/null +++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx @@ -0,0 +1,264 @@ +//Components +import Stack from "@mui/material/Stack"; +import DataTable from "@/Components/v1/Table/index.jsx"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableRow from "@mui/material/TableRow"; +import TableCell from "@mui/material/TableCell"; +import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; +import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; +import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; +import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; + +//Utils +import { formatDateWithTz } from "@/Utils/timeUtils.js"; +import { useSelector } from "react-redux"; +import { useState } from "react"; +import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; +import { + useFetchChecksTeam, + useFetchChecksByMonitor, + useResolveIncident, +} from "@/Hooks/checkHooks.js"; +import { Button, Typography, useTheme } from "@mui/material"; +import { lighten } from "@mui/material/styles"; + +const GetTooltip = (row) => { + const theme = useTheme(); + const phases = row?.timings?.phases; + + const phaseKeyFormattingMap = { + firstByte: "first byte", + }; + return ( + + {`Status code: ${row?.statusCode}`} + {`Response time: ${row?.responseTime} ms`} + {phases && ( + <> + {`Request timing: `} + + + {Object.keys(phases)?.map((phaseKey) => ( + + + + {`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`} + + + + {`${phases[phaseKey]} ms`} + + + ))} + +
+ + )} +
+ ); +}; + +const IncidentTable = ({ + isLoading, + monitors, + selectedMonitor, + filter, + dateRange, + updateTrigger, + setUpdateTrigger, +}) => { + //Redux state + const uiTimezone = useSelector((state) => state.ui.timezone); + + //Local state + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const selectedMonitorDetails = monitors?.[selectedMonitor]; + const selectedMonitorType = selectedMonitorDetails?.type; + + //Hooks + const [resolveIncident, resolveLoading] = useResolveIncident(); + + const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] = + useFetchChecksByMonitor({ + monitorId: selectedMonitor === "0" ? undefined : selectedMonitor, + type: selectedMonitorType, + status: false, + sortOrder: "desc", + limit: null, + dateRange, + filter: filter === "resolved" ? "all" : filter, + ack: filter === "resolved" ? true : false, + page: page, + rowsPerPage: rowsPerPage, + enabled: selectedMonitor !== "0", + updateTrigger, + }); + + const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] = + useFetchChecksTeam({ + status: false, + sortOrder: "desc", + limit: null, + dateRange, + filter: filter === "resolved" ? "all" : filter, + ack: filter === "resolved" ? true : false, + page: page, + rowsPerPage: rowsPerPage, + enabled: selectedMonitor === "0", + updateTrigger, + }); + + const checks = selectedMonitor === "0" ? checksTeam : checksMonitor; + const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor; + isLoading = isLoadingTeam || isLoadingMonitor; + const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor; + + const { t } = useTranslation(); + + //Handlers + const handleChangePage = (_, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(event.target.value); + }; + + const handleResolveIncident = (checkId) => { + resolveIncident(checkId, setUpdateTrigger); + }; + + const headers = [ + { + id: "monitorName", + content: t("incidentsTableMonitorName"), + render: (row) => monitors[row.monitorId]?.name ?? "N/A", + }, + { + id: "status", + content: t("incidentsTableStatus"), + render: (row) => { + const status = row.status === true ? "up" : "down"; + return ( + + ); + }, + }, + { + id: "dateTime", + content: t("incidentsTableDateTime"), + render: (row) => { + const formattedDate = formatDateWithTz( + row.createdAt, + "YYYY-MM-DD HH:mm:ss A", + uiTimezone + ); + return formattedDate; + }, + }, + { + id: "statusCode", + content: t("incidentsTableStatusCode"), + render: (row) => , + }, + { id: "message", content: t("incidentsTableMessage"), render: (row) => row.message }, + { + id: "action", + content: t("actions"), + render: (row) => { + return row.ack === false ? ( + + ) : ( + + {t("incidentsTableResolvedAt")}{" "} + {formatDateWithTz(row.ackAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone)} + + ); + }, + }, + ]; + + if (isLoading || resolveLoading) return ; + + if (networkError) { + return ( + + + + ); + } + + if (!isLoading && typeof checksCount === "undefined") { + return {t("incidentsTableNoIncidents")}; + } + + return ( + <> + + + + ); +}; + +IncidentTable.propTypes = { + isLoading: PropTypes.bool, + monitors: PropTypes.object, + selectedMonitor: PropTypes.string, + filter: PropTypes.string, + dateRange: PropTypes.string, + updateTrigger: PropTypes.bool, + setUpdateTrigger: PropTypes.func, +}; +export default IncidentTable; diff --git a/client/src/Pages/Incidents2/Components/OptionsHeader/index.jsx b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx similarity index 87% rename from client/src/Pages/Incidents2/Components/OptionsHeader/index.jsx rename to client/src/Pages/Checks/Components/OptionsHeader/index.jsx index 3a0ca1985..c16f54f18 100644 --- a/client/src/Pages/Incidents2/Components/OptionsHeader/index.jsx +++ b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx @@ -5,6 +5,7 @@ import PropTypes from "prop-types"; //Utils import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton.jsx"; import { useTranslation } from "react-i18next"; const OptionsHeader = ({ @@ -14,27 +15,27 @@ const OptionsHeader = ({ monitors, filter = "all", setFilter, - dateRange = "all", + dateRange = "hour", setDateRange, }) => { const theme = useTheme(); const { t } = useTranslation(); const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; - const filterOptions = [ - { _id: "all", name: t("incidentsPage.incidentsOptionsHeaderFilterAll") }, - { _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") }, - { _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") }, - { _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") }, + { _id: "all", name: t("incidentsOptionsHeaderFilterAll") }, + { _id: "down", name: t("incidentsOptionsHeaderFilterDown") }, + { _id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, + { _id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, ]; + // The stacks below which are three in number have the same style so const stackStyles = { direction: "row", alignItems: "center", gap: theme.spacing(6), }; - if (!shouldRender) return; + if (!shouldRender) return ; return ( { + // Redux state + const { t } = useTranslation(); + + const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }]; + + // Local state + const [selectedMonitor, setSelectedMonitor] = useState("0"); + const [filter, setFilter] = useState(undefined); + const [dateRange, setDateRange] = useState(undefined); + const [monitorLookup, setMonitorLookup] = useState(undefined); + const [updateTrigger, setUpdateTrigger] = useState(false); + + //Hooks + const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks(); + + //Utils + const theme = useTheme(); + const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({}); + const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({ + updateTrigger, + }); + const { monitorId } = useParams(); + + useEffect(() => { + if (monitorId) { + setSelectedMonitor(monitorId); + } + }, [monitorId]); + + useEffect(() => { + const monitorLookup = monitors?.reduce((acc, monitor) => { + acc[monitor._id] = { + _id: monitor._id, + name: monitor.name, + type: monitor.type, + }; + return acc; + }, {}); + setMonitorLookup(monitorLookup); + }, [monitors]); + + const handleAcknowledge = () => { + const monitorId = selectedMonitor === "0" ? null : selectedMonitor; + acknowledge(setUpdateTrigger, monitorId); + }; + + if (networkError || networkErrorSummary) { + return ( + + + + ); + } + + return ( + + + + + + + + + + ); +}; + +export default Checks; diff --git a/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx b/client/src/Pages/Incidents/Components/ActiveIncidentsPanel/index.jsx similarity index 100% rename from client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx rename to client/src/Pages/Incidents/Components/ActiveIncidentsPanel/index.jsx diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx index e066049e6..670da4a81 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx @@ -1,10 +1,5 @@ //Components -import Stack from "@mui/material/Stack"; import DataTable from "@/Components/v1/Table/index.jsx"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableRow from "@mui/material/TableRow"; -import TableCell from "@mui/material/TableCell"; import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; @@ -13,213 +8,154 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; //Utils -import { formatDateWithTz } from "@/Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; import { useSelector } from "react-redux"; -import { useState } from "react"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { - useFetchChecksTeam, - useFetchChecksByMonitor, - useResolveIncident, -} from "@/Hooks/checkHooks.js"; import { Button, Typography, useTheme } from "@mui/material"; -import { lighten } from "@mui/material/styles"; - -const GetTooltip = (row) => { - const theme = useTheme(); - const phases = row?.timings?.phases; - - const phaseKeyFormattingMap = { - firstByte: "first byte", - }; - return ( - - {`Status code: ${row?.statusCode}`} - {`Response time: ${row?.responseTime} ms`} - {phases && ( - <> - {`Request timing: `} - - - {Object.keys(phases)?.map((phaseKey) => ( - - - - {`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`} - - - - {`${phases[phaseKey]} ms`} - - - ))} - -
- - )} -
- ); -}; const IncidentTable = ({ - isLoading, - monitors, - selectedMonitor, - filter, - dateRange, - updateTrigger, - setUpdateTrigger, + monitors = [], + incidents = [], + incidentsCount = 0, + isLoading = false, + networkError = false, + page = 0, + rowsPerPage = 10, + handleChangePage, + handleChangeRowsPerPage, + resolveIncident, + handleUpdateTrigger, }) => { - //Redux state const uiTimezone = useSelector((state) => state.ui.timezone); - //Local state - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const selectedMonitorDetails = monitors?.[selectedMonitor]; - const selectedMonitorType = selectedMonitorDetails?.type; - - //Hooks - const [resolveIncident, resolveLoading] = useResolveIncident(); - - const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] = - useFetchChecksByMonitor({ - monitorId: selectedMonitor === "0" ? undefined : selectedMonitor, - type: selectedMonitorType, - status: false, - sortOrder: "desc", - limit: null, - dateRange, - filter: filter === "resolved" ? "all" : filter, - ack: filter === "resolved" ? true : false, - page: page, - rowsPerPage: rowsPerPage, - enabled: selectedMonitor !== "0", - updateTrigger, - }); - - const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] = - useFetchChecksTeam({ - status: false, - sortOrder: "desc", - limit: null, - dateRange, - filter: filter === "resolved" ? "all" : filter, - ack: filter === "resolved" ? true : false, - page: page, - rowsPerPage: rowsPerPage, - enabled: selectedMonitor === "0", - updateTrigger, - }); - - const checks = selectedMonitor === "0" ? checksTeam : checksMonitor; - const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor; - isLoading = isLoadingTeam || isLoadingMonitor; - const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor; - const { t } = useTranslation(); + const theme = useTheme(); - //Handlers - const handleChangePage = (_, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(event.target.value); - }; - - const handleResolveIncident = (checkId) => { - resolveIncident(checkId, setUpdateTrigger); + const handleResolveIncident = async (incidentId) => { + try { + await resolveIncident(incidentId); + handleUpdateTrigger(); + } catch (error) { + console.error(t("incidentsPage.errorResolvingIncident"), error); + } }; const headers = [ { id: "monitorName", content: t("incidentsTableMonitorName"), - render: (row) => monitors[row.monitorId]?.name ?? "N/A", + render: (row) => { + console.log(monitors, row); + const monitor = monitors.find((monitor) => monitor.id === row.monitorId); + return monitor ? monitor.name : "N/A"; + }, }, { id: "status", content: t("incidentsTableStatus"), render: (row) => { - const status = row.status === true ? "up" : "down"; + const status = row.status === true ? "down" : "up"; + const statusText = + row.status === true ? t("incidentsPage.active") : t("incidentsPage.resolved"); return ( ); }, }, { - id: "dateTime", - content: t("incidentsTableDateTime"), + id: "startTime", + content: t("incidentsPage.startTime"), render: (row) => { const formattedDate = formatDateWithTz( - row.createdAt, + row.startTime || row.createdAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone ); return formattedDate; }, }, + { + id: "endTime", + content: t("incidentsPage.endTime"), + render: (row) => { + if (row.endTime) { + return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone); + } + return "-"; + }, + }, + { + id: "resolutionType", + content: t("incidentsPage.resolutionType"), + render: (row) => { + if (row.resolutionType) { + return ( + + {row.resolutionType} + + ); + } + return "-"; + }, + }, { id: "statusCode", content: t("incidentsTableStatusCode"), render: (row) => , }, - { id: "message", content: t("incidentsTableMessage"), render: (row) => row.message }, + { + id: "message", + content: t("incidentsTableMessage"), + render: (row) => row.message || "-", + }, { id: "action", content: t("actions"), render: (row) => { - return row.ack === false ? ( - - ) : ( - - {t("incidentsTableResolvedAt")}{" "} - {formatDateWithTz(row.ackAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone)} - - ); + if (row.status === true) { + return ( + + ); + } else { + return ( + + {t("incidentsPage.incidentsTableResolved")} + + ); + } }, }, ]; - if (isLoading || resolveLoading) return ; + if (isLoading) return ; if (networkError) { return ( @@ -229,20 +165,25 @@ const IncidentTable = ({ ); } - if (!isLoading && typeof checksCount === "undefined") { - return {t("incidentsTableNoIncidents")}; + if (!isLoading && !networkError && incidents?.length === 0) { + return ( + + {t("incidentsTableNoIncidents", "No incidents found")} + + ); } + const incidentsData = Array.isArray(incidents) ? incidents : []; + return ( <> { const theme = useTheme(); const { t } = useTranslation(); const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; + const filterOptions = [ - { _id: "all", name: t("incidentsOptionsHeaderFilterAll") }, - { _id: "down", name: t("incidentsOptionsHeaderFilterDown") }, - { _id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, - { _id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, + { _id: "all", name: t("incidentsPage.incidentsOptionsHeaderFilterAll") }, + { _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") }, + { _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") }, + { _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") }, ]; - // The stacks below which are three in number have the same style so const stackStyles = { direction: "row", alignItems: "center", gap: theme.spacing(6), }; - if (!shouldRender) return ; + if (!shouldRender) return; return ( { - // Redux state +// Hooks +import useFetchIncidents from "./hooks/useFetchIncidents.js"; +import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks.js"; + +const Incidents2 = () => { const { t } = useTranslation(); - const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }]; + const BREADCRUMBS = [ + { name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }, + ]; - // Local state const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [filter, setFilter] = useState(undefined); - const [dateRange, setDateRange] = useState(undefined); + const [filter, setFilter] = useState("all"); + const [dateRange, setDateRange] = useState("all"); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); const [monitorLookup, setMonitorLookup] = useState(undefined); const [updateTrigger, setUpdateTrigger] = useState(false); + const handleUpdateTrigger = () => { + setUpdateTrigger((prev) => !prev); + }; - //Hooks - const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks(); + const [monitors, isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId( + {} + ); + + const { + incidents, + incidentsCount, + isLoading: isLoadingIncidents, + networkError: incidentsNetworkError, + fetchIncidents, + fetchActiveIncidents, + fetchResolvedIncidents, + resolveIncident, + } = useFetchIncidents(); + + const networkError = monitorsNetworkError || incidentsNetworkError; - //Utils const theme = useTheme(); - const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({}); - const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({ - updateTrigger, - }); - const { monitorId } = useParams(); useEffect(() => { - if (monitorId) { - setSelectedMonitor(monitorId); + setPage(0); + }, [selectedMonitor, filter, dateRange]); + + useEffect(() => { + const config = { + monitorId: selectedMonitor !== "0" ? selectedMonitor : undefined, + sortOrder: "desc", + dateRange, + page, + rowsPerPage, + }; + + if (filter === "active") { + fetchActiveIncidents(config); + } else if (filter === "resolved") { + fetchResolvedIncidents(config); + } else { + fetchIncidents(config); } - }, [monitorId]); + }, [ + selectedMonitor, + filter, + dateRange, + page, + rowsPerPage, + updateTrigger, + fetchActiveIncidents, + fetchResolvedIncidents, + fetchIncidents, + ]); useEffect(() => { - const monitorLookup = monitors?.reduce((acc, monitor) => { - acc[monitor._id] = { - _id: monitor._id, + const lookup = monitors?.reduce((acc, monitor) => { + acc[monitor.id] = { + id: monitor.id, name: monitor.name, type: monitor.type, }; return acc; }, {}); - setMonitorLookup(monitorLookup); + setMonitorLookup(lookup); }, [monitors]); - const handleAcknowledge = () => { - const monitorId = selectedMonitor === "0" ? null : selectedMonitor; - acknowledge(setUpdateTrigger, monitorId); - }; - - if (networkError || networkErrorSummary) { + if (networkError) { return ( @@ -75,27 +105,23 @@ const Checks = () => { ); } + const handleChangePage = (_, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + return ( - - - - + + + { dateRange={dateRange} setDateRange={setDateRange} /> + ); }; -export default Checks; +export default Incidents2; diff --git a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx deleted file mode 100644 index 670da4a81..000000000 --- a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx +++ /dev/null @@ -1,209 +0,0 @@ -//Components -import DataTable from "@/Components/v1/Table/index.jsx"; -import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; -import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; - -//Utils -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; -import { Button, Typography, useTheme } from "@mui/material"; - -const IncidentTable = ({ - monitors = [], - incidents = [], - incidentsCount = 0, - isLoading = false, - networkError = false, - page = 0, - rowsPerPage = 10, - handleChangePage, - handleChangeRowsPerPage, - resolveIncident, - handleUpdateTrigger, -}) => { - const uiTimezone = useSelector((state) => state.ui.timezone); - - const { t } = useTranslation(); - const theme = useTheme(); - - const handleResolveIncident = async (incidentId) => { - try { - await resolveIncident(incidentId); - handleUpdateTrigger(); - } catch (error) { - console.error(t("incidentsPage.errorResolvingIncident"), error); - } - }; - - const headers = [ - { - id: "monitorName", - content: t("incidentsTableMonitorName"), - render: (row) => { - console.log(monitors, row); - const monitor = monitors.find((monitor) => monitor.id === row.monitorId); - return monitor ? monitor.name : "N/A"; - }, - }, - { - id: "status", - content: t("incidentsTableStatus"), - render: (row) => { - const status = row.status === true ? "down" : "up"; - const statusText = - row.status === true ? t("incidentsPage.active") : t("incidentsPage.resolved"); - return ( - - ); - }, - }, - { - id: "startTime", - content: t("incidentsPage.startTime"), - render: (row) => { - const formattedDate = formatDateWithTz( - row.startTime || row.createdAt, - "YYYY-MM-DD HH:mm:ss A", - uiTimezone - ); - return formattedDate; - }, - }, - { - id: "endTime", - content: t("incidentsPage.endTime"), - render: (row) => { - if (row.endTime) { - return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone); - } - return "-"; - }, - }, - { - id: "resolutionType", - content: t("incidentsPage.resolutionType"), - render: (row) => { - if (row.resolutionType) { - return ( - - {row.resolutionType} - - ); - } - return "-"; - }, - }, - { - id: "statusCode", - content: t("incidentsTableStatusCode"), - render: (row) => , - }, - { - id: "message", - content: t("incidentsTableMessage"), - render: (row) => row.message || "-", - }, - { - id: "action", - content: t("actions"), - render: (row) => { - if (row.status === true) { - return ( - - ); - } else { - return ( - - {t("incidentsPage.incidentsTableResolved")} - - ); - } - }, - }, - ]; - - if (isLoading) return ; - - if (networkError) { - return ( - - - - ); - } - - if (!isLoading && !networkError && incidents?.length === 0) { - return ( - - {t("incidentsTableNoIncidents", "No incidents found")} - - ); - } - - const incidentsData = Array.isArray(incidents) ? incidents : []; - - return ( - <> - - - - ); -}; - -IncidentTable.propTypes = { - incidents: PropTypes.array.isRequired, // Array of incident objects - incidentsCount: PropTypes.number.isRequired, // Total count for pagination - isLoading: PropTypes.bool.isRequired, // Loading state - networkError: PropTypes.bool, // Network error object - page: PropTypes.number.isRequired, // Current page number - rowsPerPage: PropTypes.number.isRequired, // Number of rows per page - handleChangePage: PropTypes.func.isRequired, // Handler for page change - handleChangeRowsPerPage: PropTypes.func.isRequired, // Handler for rows per page change - resolveIncident: PropTypes.func.isRequired, - handleUpdateTrigger: PropTypes.func.isRequired, -}; - -export default IncidentTable; diff --git a/client/src/Pages/Incidents2/index.jsx b/client/src/Pages/Incidents2/index.jsx deleted file mode 100644 index 4e68279c5..000000000 --- a/client/src/Pages/Incidents2/index.jsx +++ /dev/null @@ -1,151 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import IncidentTable from "./Components/IncidentTable/index.jsx"; -import OptionsHeader from "./Components/OptionsHeader/index.jsx"; -import IncidentsSummaryPanel from "./Components/IncidentsSummaryPanel/index.jsx"; - -//Utils -import { useTheme } from "@emotion/react"; -import { useState, useEffect } from "react"; -import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; -import { useTranslation } from "react-i18next"; - -// Hooks -import useFetchIncidents from "./hooks/useFetchIncidents.js"; -import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks.js"; - -const Incidents2 = () => { - const { t } = useTranslation(); - - const BREADCRUMBS = [ - { name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }, - ]; - - const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [filter, setFilter] = useState("all"); - const [dateRange, setDateRange] = useState("all"); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const [monitorLookup, setMonitorLookup] = useState(undefined); - const [updateTrigger, setUpdateTrigger] = useState(false); - const handleUpdateTrigger = () => { - setUpdateTrigger((prev) => !prev); - }; - - const [monitors, isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId( - {} - ); - - const { - incidents, - incidentsCount, - isLoading: isLoadingIncidents, - networkError: incidentsNetworkError, - fetchIncidents, - fetchActiveIncidents, - fetchResolvedIncidents, - resolveIncident, - } = useFetchIncidents(); - - const networkError = monitorsNetworkError || incidentsNetworkError; - - const theme = useTheme(); - - useEffect(() => { - setPage(0); - }, [selectedMonitor, filter, dateRange]); - - useEffect(() => { - const config = { - monitorId: selectedMonitor !== "0" ? selectedMonitor : undefined, - sortOrder: "desc", - dateRange, - page, - rowsPerPage, - }; - - if (filter === "active") { - fetchActiveIncidents(config); - } else if (filter === "resolved") { - fetchResolvedIncidents(config); - } else { - fetchIncidents(config); - } - }, [ - selectedMonitor, - filter, - dateRange, - page, - rowsPerPage, - updateTrigger, - fetchActiveIncidents, - fetchResolvedIncidents, - fetchIncidents, - ]); - - useEffect(() => { - const lookup = monitors?.reduce((acc, monitor) => { - acc[monitor.id] = { - id: monitor.id, - name: monitor.name, - type: monitor.type, - }; - return acc; - }, {}); - setMonitorLookup(lookup); - }, [monitors]); - - if (networkError) { - return ( - - - - ); - } - - const handleChangePage = (_, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - - return ( - - - - - - - - - - ); -}; - -export default Incidents2; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index d9f6d58cb..878b17b09 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -28,9 +28,11 @@ import InfrastructureDetails from "../Pages/Infrastructure/Details/index.jsx"; // Server Status import ServerUnreachable from "../Pages/ServerUnreachable.jsx"; +// Checks +import Checks from "../Pages/Checks/index.jsx"; + // Incidents -import Checks from "../Pages/Incidents"; -import Incidents2 from "../Pages/Incidents2/index.jsx"; +import Incidents from "../Pages/Incidents/index.jsx"; // Status pages import CreateStatus from "../Pages/StatusPage/Create/index.jsx"; @@ -136,7 +138,7 @@ const Routes = () => { /> } + element={} /> Date: Tue, 20 Jan 2026 19:21:03 +0000 Subject: [PATCH 092/103] remove ack --- client/src/Hooks/checkHooks.js | 27 -------------- .../Checks/Components/IncidentTable/index.jsx | 37 +------------------ client/src/Pages/Checks/index.jsx | 24 +----------- 3 files changed, 4 insertions(+), 84 deletions(-) diff --git a/client/src/Hooks/checkHooks.js b/client/src/Hooks/checkHooks.js index b8af32073..49a9b6ba3 100644 --- a/client/src/Hooks/checkHooks.js +++ b/client/src/Hooks/checkHooks.js @@ -185,36 +185,9 @@ const useResolveIncident = () => { return [resolveIncident, isLoading]; }; -const useAcknowledgeChecks = () => { - const [isLoading, setIsLoading] = useState(false); - const { t } = useTranslation(); - - const acknowledge = async (setUpdateTrigger, monitorId = null) => { - setIsLoading(true); - try { - if (monitorId) { - await networkService.updateMonitorChecksStatus({ monitorId, ack: true }); - } else { - await networkService.updateAllChecksStatus({ ack: true }); - } - setUpdateTrigger((prev) => !prev); - } catch (error) { - const toastMessage = monitorId - ? t("checkHooks.failureResolveMonitor") - : t("checkHooks.failureResolveAll"); - createToast({ body: toastMessage }); - } finally { - setIsLoading(false); - } - }; - - return { acknowledge, isLoading }; -}; - export { useFetchChecksByMonitor, useFetchChecksTeam, useFetchChecksSummaryByTeamId, useResolveIncident, - useAcknowledgeChecks, }; diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx index e066049e6..cce9b10b7 100644 --- a/client/src/Pages/Checks/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx @@ -18,11 +18,7 @@ import { useSelector } from "react-redux"; import { useState } from "react"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { - useFetchChecksTeam, - useFetchChecksByMonitor, - useResolveIncident, -} from "@/Hooks/checkHooks.js"; +import { useFetchChecksTeam, useFetchChecksByMonitor } from "@/Hooks/checkHooks.js"; import { Button, Typography, useTheme } from "@mui/material"; import { lighten } from "@mui/material/styles"; @@ -93,7 +89,6 @@ const IncidentTable = ({ filter, dateRange, updateTrigger, - setUpdateTrigger, }) => { //Redux state const uiTimezone = useSelector((state) => state.ui.timezone); @@ -105,8 +100,6 @@ const IncidentTable = ({ const selectedMonitorType = selectedMonitorDetails?.type; //Hooks - const [resolveIncident, resolveLoading] = useResolveIncident(); - const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] = useFetchChecksByMonitor({ monitorId: selectedMonitor === "0" ? undefined : selectedMonitor, @@ -153,10 +146,6 @@ const IncidentTable = ({ setRowsPerPage(event.target.value); }; - const handleResolveIncident = (checkId) => { - resolveIncident(checkId, setUpdateTrigger); - }; - const headers = [ { id: "monitorName", @@ -195,31 +184,9 @@ const IncidentTable = ({ render: (row) => , }, { id: "message", content: t("incidentsTableMessage"), render: (row) => row.message }, - { - id: "action", - content: t("actions"), - render: (row) => { - return row.ack === false ? ( - - ) : ( - - {t("incidentsTableResolvedAt")}{" "} - {formatDateWithTz(row.ackAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone)} - - ); - }, - }, ]; - if (isLoading || resolveLoading) return ; + if (isLoading) return ; if (networkError) { return ( diff --git a/client/src/Pages/Checks/index.jsx b/client/src/Pages/Checks/index.jsx index 698fc4a2f..d0c2ea80d 100644 --- a/client/src/Pages/Checks/index.jsx +++ b/client/src/Pages/Checks/index.jsx @@ -10,10 +10,7 @@ import { Box, Button } from "@mui/material"; //Utils import { useTheme } from "@emotion/react"; import { useFetchMonitorsByTeamId } from "@/Hooks/monitorHooks.js"; -import { - useFetchChecksSummaryByTeamId, - useAcknowledgeChecks, -} from "@/Hooks/checkHooks.js"; +import { useFetchChecksSummaryByTeamId } from "@/Hooks/checkHooks.js"; import { useState, useEffect } from "react"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; import { useTranslation } from "react-i18next"; @@ -34,7 +31,6 @@ const Checks = () => { const [updateTrigger, setUpdateTrigger] = useState(false); //Hooks - const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks(); //Utils const theme = useTheme(); @@ -62,11 +58,6 @@ const Checks = () => { setMonitorLookup(monitorLookup); }, [monitors]); - const handleAcknowledge = () => { - const monitorId = selectedMonitor === "0" ? null : selectedMonitor; - acknowledge(setUpdateTrigger, monitorId); - }; - if (networkError || networkErrorSummary) { return ( @@ -78,18 +69,7 @@ const Checks = () => { return ( - - - + Date: Tue, 20 Jan 2026 19:22:04 +0000 Subject: [PATCH 093/103] remove dependency --- server/src/config/services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index cbd58f2e9..74a261d05 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -222,7 +222,7 @@ export const initializeServices = async ({ checksRepository, }); - const bufferService = new BufferService({ logger, incidentService, checkService }); + const bufferService = new BufferService({ logger, checkService }); const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); From 476ff39a17cd423bb7ebec59f6ffa80ac94661dd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:25:00 +0000 Subject: [PATCH 094/103] remove check ack routes and controller functions --- server/src/controllers/checkController.ts | 47 ----------------------- server/src/routes/v1/checkRoute.js | 5 --- 2 files changed, 52 deletions(-) diff --git a/server/src/controllers/checkController.ts b/server/src/controllers/checkController.ts index 8ae6fd785..8277df3e7 100755 --- a/server/src/controllers/checkController.ts +++ b/server/src/controllers/checkController.ts @@ -77,53 +77,6 @@ class CheckController { } }; - ackCheck = async (req: Request, res: Response, next: NextFunction) => { - try { - await ackCheckBodyValidation.validateAsync(req.body); - - const updatedCheck = await this.checkService.ackCheck({ - checkId: req?.params?.checkId, - teamId: req?.user?.teamId, - ack: req?.body?.ack, - }); - - return res.status(200).json({ - success: true, - msg: "Check acknowledged successfully", - data: updatedCheck, - }); - } catch (error) { - next(error); - } - }; - - ackAllChecks = async (req: Request, res: Response, next: NextFunction) => { - try { - await ackAllChecksParamValidation.validateAsync(req.params); - await ackAllChecksBodyValidation.validateAsync(req.body); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw new Error("No team ID in request"); - } - - const updatedChecks = await this.checkService.ackAllChecks({ - monitorId: req?.params?.monitorId, - path: req?.params?.path, - teamId: req?.user?.teamId, - ack: req?.body?.ack, - }); - - return res.status(200).json({ - success: true, - msg: "All checks acknowledged successfully", - data: updatedChecks, - }); - } catch (error) { - next(error); - } - }; - deleteChecks = async (req: Request, res: Response, next: NextFunction) => { try { await deleteChecksParamValidation.validateAsync(req.params); diff --git a/server/src/routes/v1/checkRoute.js b/server/src/routes/v1/checkRoute.js index 78f6b8a42..6565425d4 100755 --- a/server/src/routes/v1/checkRoute.js +++ b/server/src/routes/v1/checkRoute.js @@ -14,13 +14,8 @@ class CheckRoutes { this.router.get("/team", this.checkController.getChecksByTeam); this.router.put("/team/ttl", isAllowed(["admin", "superadmin"]), this.checkController.updateChecksTTL); this.router.delete("/team", isAllowed(["admin", "superadmin"]), this.checkController.deleteChecksByTeamId); - - this.router.put("/check/:checkId", this.checkController.ackCheck); - this.router.get("/:monitorId", this.checkController.getChecksByMonitor); this.router.delete("/:monitorId", this.checkController.deleteChecks); - - this.router.put("/:path/:monitorId?", this.checkController.ackAllChecks); } getRouter() { From c0e4893f964aaebbc5aae1c4b8be91ca286ebf69 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:26:46 +0000 Subject: [PATCH 095/103] remove ack service and db methods --- server/src/db/modules/checkModule.js | 35 --------------------- server/src/service/business/checkService.ts | 27 ---------------- 2 files changed, 62 deletions(-) diff --git a/server/src/db/modules/checkModule.js b/server/src/db/modules/checkModule.js index e420d5ce7..e6304e20c 100755 --- a/server/src/db/modules/checkModule.js +++ b/server/src/db/modules/checkModule.js @@ -178,41 +178,6 @@ class CheckModule { } }; - ackCheck = async (checkId, teamId, ack) => { - try { - const updatedCheck = await CheckModel.findOneAndUpdate( - { _id: checkId, "metadata.teamId": teamId }, - { $set: { ack, ackAt: new Date() } }, - { new: true } - ); - - if (!updatedCheck) { - throw new Error("Check not found"); - } - - return updatedCheck; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "ackCheck"; - throw error; - } - }; - - ackAllChecks = async (monitorId, teamId, ack, path) => { - try { - const filter = - path === "monitor" - ? { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) } - : { "metadata.teamId": new mongoose.Types.ObjectId(teamId) }; - const updatedChecks = await CheckModel.updateMany(filter, { $set: { ack, ackAt: new Date() } }); - return updatedChecks.modifiedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "ackAllChecks"; - throw error; - } - }; - getChecksSummaryByTeamId = async ({ teamId }) => { try { const matchStage = { diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 3e6c727b5..8dee9d215 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -174,33 +174,6 @@ class CheckService { return summary; }; - ackCheck = async ({ checkId, teamId, ack }: { checkId: string; teamId: string; ack: any }) => { - if (!checkId) { - throw this.errorService.createBadRequestError("No check ID in request"); - } - - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); - } - - const updatedCheck = await this.db.checkModule.ackCheck(checkId, teamId, ack); - return updatedCheck; - }; - - ackAllChecks = async ({ monitorId, path, teamId, ack }: { monitorId: string; path: string; teamId: string; ack: any }) => { - if (path === "monitor") { - if (!monitorId) { - throw this.errorService.createBadRequestError("No monitor ID in request"); - } - - // For verificaiton, throws an error if monitor doesn't belong to team - await this.monitorsRepository.findById(monitorId, teamId); - } - - const updatedChecks = await this.db.checkModule.ackAllChecks(monitorId, teamId, ack, path); - return updatedChecks; - }; - deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); From c1b2bda62392665afffa9c44334156348b3845d1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:41:57 +0000 Subject: [PATCH 096/103] _id -> id --- .../src/Pages/Checks/Components/IncidentTable/index.jsx | 4 +++- .../src/Pages/Checks/Components/OptionsHeader/index.jsx | 9 +++++---- client/src/Pages/Checks/index.jsx | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx index cce9b10b7..31475f8b1 100644 --- a/client/src/Pages/Checks/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx @@ -150,7 +150,9 @@ const IncidentTable = ({ { id: "monitorName", content: t("incidentsTableMonitorName"), - render: (row) => monitors[row.monitorId]?.name ?? "N/A", + render: (row) => { + return monitors?.[row.metadata?.monitorId]?.name || "N/A"; + }, }, { id: "status", diff --git a/client/src/Pages/Checks/Components/OptionsHeader/index.jsx b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx index c16f54f18..74bea4711 100644 --- a/client/src/Pages/Checks/Components/OptionsHeader/index.jsx +++ b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx @@ -20,12 +20,13 @@ const OptionsHeader = ({ }) => { const theme = useTheme(); const { t } = useTranslation(); + console.log(monitors); const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; const filterOptions = [ - { _id: "all", name: t("incidentsOptionsHeaderFilterAll") }, - { _id: "down", name: t("incidentsOptionsHeaderFilterDown") }, - { _id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, - { _id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, + { id: "all", name: t("incidentsOptionsHeaderFilterAll") }, + { id: "down", name: t("incidentsOptionsHeaderFilterDown") }, + { id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, + { id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, ]; // The stacks below which are three in number have the same style so diff --git a/client/src/Pages/Checks/index.jsx b/client/src/Pages/Checks/index.jsx index d0c2ea80d..aa8717c1a 100644 --- a/client/src/Pages/Checks/index.jsx +++ b/client/src/Pages/Checks/index.jsx @@ -48,8 +48,8 @@ const Checks = () => { useEffect(() => { const monitorLookup = monitors?.reduce((acc, monitor) => { - acc[monitor._id] = { - _id: monitor._id, + acc[monitor.id] = { + id: monitor.id, name: monitor.name, type: monitor.type, }; From a69091bb2875bfda3f057bbdd20c61b996d47c48 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:49:40 +0000 Subject: [PATCH 097/103] replaced monitor with updatedMonitor --- .../SuperSimpleQueue/SuperSimpleQueueHelper.ts | 16 ++++++++++++---- .../notificationProviders/utils.ts | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 26d45db22..9a2957ee9 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -86,20 +86,28 @@ class SuperSimpleQueueHelper { // Step 5 handle notifications (best effort, continue even in event of failure, don't wait) this.notificationsService - .handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged) + .handleNotifications(statusChangeResult.monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged) .catch((error: any) => { this.logger.error({ message: error.message, service: SERVICE_NAME, method: "getMonitorJob", - details: `Error sending notifications for job ${monitor.id}: ${error.message}`, + details: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error.message}`, stack: error.stack, }); }); - // Step 6. Handle incidents + // Step 6. Handle incidents (best effort, don't wait) if (statusChangeResult.statusChanged) { - await this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code); + this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code).catch((error: any) => { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "getMonitorJob", + details: `Error handling incident for job ${monitor.id}: ${error.message}`, + stack: error.stack, + }); + }); } } catch (error: any) { this.logger.warn({ diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index a7e954e55..f71933425 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -163,7 +163,7 @@ export const buildHardwareEmail = async (emailService: any, monitor: Monitor, al }; export const buildEmail = async (emailService: any, monitor: Monitor): Promise => { - const template = monitor.status === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; + const template = monitor.status === true ? "serverIsUpTemplate" : "serverIsDownTemplate"; const context = { monitor: monitor.name, url: monitor.url }; const html = await emailService.buildEmail(template, context); return html; From 9af25a19e52192df3c96d77507272a0679f2b294 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 20:14:23 +0000 Subject: [PATCH 098/103] findByMonitorId --- server/src/config/services.ts | 2 - server/src/db/modules/checkModule.js | 28 +++---- .../repositories/checks/IChecksRepository.ts | 9 +++ .../checks/MongoChecksRepistory.ts | 80 +++++++++++++++++++ server/src/service/business/checkService.ts | 39 +++++---- 5 files changed, 117 insertions(+), 41 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 74a261d05..0fb78da25 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -214,8 +214,6 @@ export const initializeServices = async ({ const checkService = new CheckService({ db, - settingsService, - stringService, errorService, monitorsRepository, logger, diff --git a/server/src/db/modules/checkModule.js b/server/src/db/modules/checkModule.js index e6304e20c..4fdf10678 100755 --- a/server/src/db/modules/checkModule.js +++ b/server/src/db/modules/checkModule.js @@ -20,29 +20,16 @@ class CheckModule { this.User = User; } - createChecks = async (checks) => { - try { - await CheckModel.insertMany(checks, { ordered: false, runValidators: true }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createCheck"; - throw error; - } - }; - - getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => { + getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, page, rowsPerPage, status }) => { try { status = status === "true" ? true : status === "false" ? false : undefined; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] }; - // Match const matchStage = { "metadata.monitorId": new ObjectId(monitorId), ...(typeof status !== "undefined" && { status }), - ...(typeof ack !== "undefined" && ackStage), ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], @@ -106,17 +93,22 @@ class CheckModule { } }; - getChecksByTeam = async ({ sortOrder, dateRange, filter, ack, page, rowsPerPage, teamId }) => { + getChecksByTeam = async ({ sortOrder, dateRange, filter, page, rowsPerPage, teamId }) => { try { + console.log({ + sortOrder, + dateRange, + filter, + page, + rowsPerPage, + teamId, + }); page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] }; - const matchStage = { "metadata.teamId": new ObjectId(teamId), status: false, - ...(typeof ack !== "undefined" && ackStage), ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 7b98ae7b6..72f7e632d 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -59,6 +59,15 @@ export interface IChecksRepository { // single fetch // collection fetch + findByMonitorId( + monitorId: string, + sortOrder: string, + dateRange: string, + filter: string, + page: number, + rowsPerPage: number, + status: boolean | undefined + ): Promise; findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findDateRangeChecksByMonitor( monitorId: string, diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 7707a7b17..f89e2318a 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -18,6 +18,15 @@ import mongoose from "mongoose"; const SERVICE_NAME = "StatusService"; +const dateRangeLookup: Record = { + recent: new Date(new Date().setDate(new Date().getDate() - 2)), + hour: new Date(new Date().setHours(new Date().getHours() - 1)), + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: undefined, +}; + export type LatestChecksMap = Record; type DateRange = { start: Date; end: Date }; type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number }; @@ -189,6 +198,77 @@ class MongoChecksRepository implements IChecksRepository { return await CheckModel.insertMany(checks); }; + findByMonitorId = async ( + monitorId: string, + sortOrder: string, + dateRange: string, + filter: string, + page: number, + rowsPerPage: number, + status: boolean | undefined + ) => { + // Match + const matchStage: Record = { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + ...(typeof status !== "undefined" && { status }), + ...(dateRangeLookup[dateRange] && { + createdAt: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; + + if (filter !== undefined) { + switch (filter) { + case "all": + break; + case "down": + break; + case "resolve": + matchStage.statusCode = 5000; + break; + default: + this.logger.warn({ + message: "invalid filter", + service: SERVICE_NAME, + method: "getChecks", + }); + break; + } + } + + //Sort + const convertedSortOrder = sortOrder === "asc" ? 1 : -1; + + // Pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + const checks = await CheckModel.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: convertedSortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { + $ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0], + }, + checks: { + $ifNull: ["$checks", []], + }, + }, + }, + ]); + return checks[0]; + }; + findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => { if (monitorIds.length === 0) { return {}; diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 8dee9d215..3a1a5fb18 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,6 +1,7 @@ import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; +import { ParseBoolean } from "@/utils/utils.js"; const SERVICE_NAME = "checkService"; @@ -8,32 +9,24 @@ class CheckService { static SERVICE_NAME = SERVICE_NAME; private db: any; - private settingsService: any; - private stringService: any; private errorService: any; private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; private logger: any; constructor({ db, - settingsService, - stringService, errorService, monitorsRepository, logger, checksRepository, }: { db: any; - settingsService: any; - stringService: any; errorService: any; monitorsRepository: IMonitorsRepository; logger: any; checksRepository: IChecksRepository; }) { this.db = db; - this.settingsService = settingsService; - this.stringService = stringService; this.errorService = errorService; this.monitorsRepository = monitorsRepository; this.logger = logger; @@ -132,22 +125,27 @@ class CheckService { // For verificaiton, throws an error if monitor doesn't belong to team await this.monitorsRepository.findById(monitorId, teamId); - let { sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; - const result = await this.db.checkModule.getChecksByMonitor({ - monitorId, - sortOrder, - dateRange, - filter, - ack, - page, - rowsPerPage, - status, - }); + let { sortOrder, dateRange, filter, page, rowsPerPage, status } = query; + const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status); + const parsedPage = page ? parseInt(page) : page; + const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; + + const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus); + + // const result = await this.db.checkModule.getChecksByMonitor({ + // monitorId, + // sortOrder, + // dateRange, + // filter, + // page, + // rowsPerPage, + // status, + // }); return result; }; getChecksByTeam = async ({ teamId, query }: { teamId: string; query: any }) => { - let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query; + let { sortOrder, dateRange, filter, page, rowsPerPage } = query; if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); @@ -157,7 +155,6 @@ class CheckService { sortOrder, dateRange, filter, - ack, page, rowsPerPage, teamId, From 27dea6a08e766cdde8b554f1c4857aeebf18edc0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 21:18:26 +0000 Subject: [PATCH 099/103] implement remaining check methods --- server/src/config/services.ts | 4 - server/src/db/MongoDB.js | 2 - server/src/db/modules/checkModule.js | 247 ------------------ server/src/db/modules/checkModuleQueries.js | 44 ---- .../repositories/checks/IChecksRepository.ts | 7 +- .../checks/MongoChecksRepistory.ts | 128 ++++++++- server/src/service/business/checkService.ts | 37 +-- server/src/service/business/monitorService.ts | 8 +- 8 files changed, 138 insertions(+), 339 deletions(-) delete mode 100755 server/src/db/modules/checkModule.js delete mode 100644 server/src/db/modules/checkModuleQueries.js diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 0fb78da25..e3587c662 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -64,7 +64,6 @@ import AppSettings from "../db/models/AppSettings.js"; import Incident from "../db/models/Incident.js"; import InviteModule from "../db/modules/inviteModule.js"; -import CheckModule from "../db/modules/checkModule.js"; import StatusPageModule from "../db/modules/statusPageModule.js"; import UserModule from "../db/modules/userModule.js"; import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js"; @@ -150,7 +149,6 @@ export const initializeServices = async ({ const stringService = new StringService(translationService); // Create DB - const checkModule = new CheckModule({ logger, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); @@ -163,7 +161,6 @@ export const initializeServices = async ({ const db = new MongoDB({ logger, envSettings, - checkModule, inviteModule, statusPageModule, userModule, @@ -213,7 +210,6 @@ export const initializeServices = async ({ }); const checkService = new CheckService({ - db, errorService, monitorsRepository, logger, diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index f7e58f34e..a0c580f1d 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -7,7 +7,6 @@ class MongoDB { constructor({ logger, envSettings, - checkModule, inviteModule, statusPageModule, userModule, @@ -22,7 +21,6 @@ class MongoDB { this.userModule = userModule; this.inviteModule = inviteModule; this.recoveryModule = recoveryModule; - this.checkModule = checkModule; this.maintenanceWindowModule = maintenanceWindowModule; this.notificationModule = notificationModule; this.settingsModule = settingsModule; diff --git a/server/src/db/modules/checkModule.js b/server/src/db/modules/checkModule.js deleted file mode 100755 index 4fdf10678..000000000 --- a/server/src/db/modules/checkModule.js +++ /dev/null @@ -1,247 +0,0 @@ -import { ObjectId } from "mongodb"; -import mongoose from "mongoose"; -import { CheckModel } from "@/db/models/index.js"; -import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js"; - -const SERVICE_NAME = "checkModule"; -const dateRangeLookup = { - recent: new Date(new Date().setDate(new Date().getDate() - 2)), - hour: new Date(new Date().setHours(new Date().getHours() - 1)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: undefined, -}; - -class CheckModule { - constructor({ logger, Monitor, User }) { - this.logger = logger; - this.Monitor = Monitor; - this.User = User; - } - - getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, page, rowsPerPage, status }) => { - try { - status = status === "true" ? true : status === "false" ? false : undefined; - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - - // Match - const matchStage = { - "metadata.monitorId": new ObjectId(monitorId), - ...(typeof status !== "undefined" && { status }), - ...(dateRangeLookup[dateRange] && { - createdAt: { - $gte: dateRangeLookup[dateRange], - }, - }), - }; - - if (filter !== undefined) { - switch (filter) { - case "all": - break; - case "down": - break; - case "resolve": - matchStage.statusCode = 5000; - break; - default: - this.logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getChecks", - }); - break; - } - } - - //Sort - sortOrder = sortOrder === "asc" ? 1 : -1; - - // Pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - const checks = await CheckModel.aggregate([ - { $match: matchStage }, - { $sort: { createdAt: sortOrder } }, - { - $facet: { - summary: [{ $count: "checksCount" }], - checks: [{ $skip: skip }, { $limit: rowsPerPage }], - }, - }, - { - $project: { - checksCount: { - $ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0], - }, - checks: { - $ifNull: ["$checks", []], - }, - }, - }, - ]); - return checks[0]; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getChecks"; - throw error; - } - }; - - getChecksByTeam = async ({ sortOrder, dateRange, filter, page, rowsPerPage, teamId }) => { - try { - console.log({ - sortOrder, - dateRange, - filter, - page, - rowsPerPage, - teamId, - }); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - - const matchStage = { - "metadata.teamId": new ObjectId(teamId), - status: false, - ...(dateRangeLookup[dateRange] && { - createdAt: { - $gte: dateRangeLookup[dateRange], - }, - }), - }; - // Add filter to match stage - if (filter !== undefined) { - switch (filter) { - case "all": - break; - case "down": - break; - case "resolve": - matchStage.statusCode = 5000; - break; - default: - this.logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getChecksByTeam", - }); - break; - } - } - - sortOrder = sortOrder === "asc" ? 1 : -1; - - // pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - const aggregatePipeline = [ - { $match: matchStage }, - - { $sort: { createdAt: sortOrder } }, - { - $facet: { - summary: [{ $count: "checksCount" }], - checks: [{ $skip: skip }, { $limit: rowsPerPage }], - }, - }, - { - $project: { - checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, - checks: "$checks", - }, - }, - ]; - - const checks = await CheckModel.aggregate(aggregatePipeline); - return checks[0]; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getChecksByTeam"; - throw error; - } - }; - - getChecksSummaryByTeamId = async ({ teamId }) => { - try { - const matchStage = { - "metadata.teamId": new ObjectId(teamId), - }; - const checks = await CheckModel.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage })); - return checks[0].summary; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getChecksSummaryByTeamId"; - throw error; - } - }; - deleteChecks = async (monitorId) => { - try { - const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteChecks"; - throw error; - } - }; - - deleteChecksByTeamId = async (teamId) => { - try { - // Find all monitor IDs for this team (only get _id field for efficiency) - const teamMonitors = await this.Monitor.find({ teamId }, { _id: 1 }); - const monitorIds = teamMonitors.map((monitor) => monitor._id); - - // Delete all checks for these monitors in one operation - const deleteResult = await CheckModel.deleteMany({ "metadata.monitorId": { $in: monitorIds } }); - - return deleteResult.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteChecksByTeamId"; - throw error; - } - }; - - updateChecksTTL = async (teamId, ttl) => { - try { - await CheckModel.collection.dropIndex("expiry_1"); - } catch (error) { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "updateChecksTTL", - stack: error.stack, - }); - } - - try { - await CheckModel.collection.createIndex( - { expiry: 1 }, - { expireAfterSeconds: ttl, partialFilterExpression: { "metadata.mode": { $exists: true } } } - ); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateChecksTTL"; - throw error; - } - // Update user - try { - await this.User.updateMany({ teamId: teamId }, { checkTTL: ttl }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateChecksTTL"; - throw error; - } - }; -} - -export default CheckModule; diff --git a/server/src/db/modules/checkModuleQueries.js b/server/src/db/modules/checkModuleQueries.js deleted file mode 100644 index f2ad2a922..000000000 --- a/server/src/db/modules/checkModuleQueries.js +++ /dev/null @@ -1,44 +0,0 @@ -const buildChecksSummaryByTeamIdPipeline = ({ matchStage }) => { - return [ - { $match: matchStage }, - { - $facet: { - summary: [ - { - $group: { - _id: null, - totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } }, - resolvedChecks: { - $sum: { - $cond: [{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] }, 1, 0], - }, - }, - downChecks: { - $sum: { - $cond: [{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, 1, 0], - }, - }, - cannotResolveChecks: { - $sum: { - $cond: [{ $eq: ["$statusCode", 5000] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ], - }, - }, - { - $project: { - summary: { $arrayElemAt: ["$summary", 0] }, - }, - }, - ]; -}; - -export { buildChecksSummaryByTeamIdPipeline }; diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 72f7e632d..1e0c478b9 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -68,15 +68,18 @@ export interface IChecksRepository { rowsPerPage: number, status: boolean | undefined ): Promise; - findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; - findDateRangeChecksByMonitor( + findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise; + findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; + findByDateRangeAndMonitorId( monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType } ): Promise; + findSummaryByTeamId(teamId: string): Promise; // update //delete deleteByMonitorId(monitorId: string): Promise; + deleteByTeamId(teamId: string): Promise; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index f89e2318a..b9cf2041f 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -18,7 +18,7 @@ import mongoose from "mongoose"; const SERVICE_NAME = "StatusService"; -const dateRangeLookup: Record = { +const dateRangeLookup: Record = { recent: new Date(new Date().setDate(new Date().getDate() - 2)), hour: new Date(new Date().setHours(new Date().getHours() - 1)), day: new Date(new Date().setDate(new Date().getDate() - 1)), @@ -269,7 +269,67 @@ class MongoChecksRepository implements IChecksRepository { return checks[0]; }; - findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => { + findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => { + const matchStage: Record = { + "metadata.teamId": new mongoose.Types.ObjectId(teamId), + status: false, + ...(dateRangeLookup[dateRange] && { + createdAt: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; + // Add filter to match stage + if (filter !== undefined) { + switch (filter) { + case "all": + break; + case "down": + break; + case "resolve": + matchStage.statusCode = 5000; + break; + default: + this.logger.warn({ + message: "invalid filter", + service: SERVICE_NAME, + method: "getChecksByTeam", + }); + break; + } + } + + const parsedSortOrder = sortOrder === "asc" ? 1 : -1; + + // pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + const aggregatePipeline: any = [ + { $match: matchStage }, + + { $sort: { createdAt: parsedSortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, + checks: "$checks", + }, + }, + ]; + + const checks = await CheckModel.aggregate(aggregatePipeline); + return checks[0]; + }; + + findLatestByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => { if (monitorIds.length === 0) { return {}; } @@ -303,7 +363,7 @@ class MongoChecksRepository implements IChecksRepository { }, {}); }; - findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => { + findByDateRangeAndMonitorId = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => { const monitorObjectId = new mongoose.Types.ObjectId(monitorId); if (options?.type === "hardware") { return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString); @@ -314,6 +374,63 @@ class MongoChecksRepository implements IChecksRepository { return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString); }; + findSummaryByTeamId = async (teamId: string) => { + const matchStage = { + "metadata.teamId": new mongoose.Types.ObjectId(teamId), + }; + const checks = await CheckModel.aggregate([ + { $match: matchStage }, + { + $facet: { + summary: [ + { + $group: { + _id: null, + totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } }, + resolvedChecks: { + $sum: { + $cond: [{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] }, 1, 0], + }, + }, + downChecks: { + $sum: { + $cond: [{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, 1, 0], + }, + }, + cannotResolveChecks: { + $sum: { + $cond: [{ $eq: ["$statusCode", 5000] }, 1, 0], + }, + }, + }, + }, + { + $project: { + _id: 0, + }, + }, + ], + }, + }, + { + $project: { + summary: { $arrayElemAt: ["$summary", 0] }, + }, + }, + ]); + return checks[0].summary; + }; + + deleteByMonitorId = async (monitorId: string): Promise => { + const result = await CheckModel.deleteMany({ "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) }); + return result.deletedCount; + }; + + deleteByTeamId = async (teamId: string) => { + const deleteResult = await CheckModel.deleteMany({ "metadata.teamId": teamId }); + return deleteResult.deletedCount; + }; + private findUptimeDateRangeChecks = async ( monitorType: Exclude, monitorObjectId: mongoose.Types.ObjectId, @@ -477,11 +594,6 @@ class MongoChecksRepository implements IChecksRepository { }; }; - deleteByMonitorId = async (monitorId: string): Promise => { - const result = await CheckModel.deleteMany({ "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) }); - return result.deletedCount; - }; - private getHardwareAggregateData = async (monitorId: string, dates: DateRange): Promise => { const result = await CheckModel.aggregate([ { diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 3a1a5fb18..3a932ed3f 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,6 +1,7 @@ import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; +import { AppError } from "@/utils/AppError.js"; import { ParseBoolean } from "@/utils/utils.js"; const SERVICE_NAME = "checkService"; @@ -8,25 +9,21 @@ const SERVICE_NAME = "checkService"; class CheckService { static SERVICE_NAME = SERVICE_NAME; - private db: any; private errorService: any; private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; private logger: any; constructor({ - db, errorService, monitorsRepository, logger, checksRepository, }: { - db: any; errorService: any; monitorsRepository: IMonitorsRepository; logger: any; checksRepository: IChecksRepository; }) { - this.db = db; this.errorService = errorService; this.monitorsRepository = monitorsRepository; this.logger = logger; @@ -132,15 +129,6 @@ class CheckService { const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus); - // const result = await this.db.checkModule.getChecksByMonitor({ - // monitorId, - // sortOrder, - // dateRange, - // filter, - // page, - // rowsPerPage, - // status, - // }); return result; }; @@ -151,14 +139,10 @@ class CheckService { throw this.errorService.createBadRequestError("No team ID in request"); } - const checkData = await this.db.checkModule.getChecksByTeam({ - sortOrder, - dateRange, - filter, - page, - rowsPerPage, - teamId, - }); + const parsedPage = page ? parseInt(page) : page; + const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; + + const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId); return checkData; }; @@ -166,8 +150,7 @@ class CheckService { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); } - - const summary = await this.db.checkModule.getChecksSummaryByTeamId({ teamId }); + const summary = await this.checksRepository.findSummaryByTeamId(teamId); return summary; }; @@ -183,7 +166,7 @@ class CheckService { // For verificaiton, throws an error if monitor doesn't belong to team await this.monitorsRepository.findById(monitorId, teamId); - const deletedCount = await this.db.checkModule.deleteChecks(monitorId); + const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId); return deletedCount; }; deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => { @@ -191,14 +174,12 @@ class CheckService { throw this.errorService.createBadRequestError("No team ID in request"); } - const deletedCount = await this.db.checkModule.deleteChecksByTeamId(teamId); + const deletedCount = await this.checksRepository.deleteByTeamId(teamId); return deletedCount; }; updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => { - const SECONDS_PER_DAY = 86400; - const newTTL = parseInt(ttl, 10) * SECONDS_PER_DAY; - await this.db.checkModule.updateChecksTTL(teamId, newTTL); + throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500 }); }; } diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 7c8ad199e..fee3a9ffc 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -253,7 +253,7 @@ export class MonitorService implements IMonitorService { } const rangeKey = (dateRange as DateRangeKey) ?? "recent"; const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), { + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { type: monitor.type, }); const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id); @@ -292,7 +292,7 @@ export class MonitorService implements IMonitorService { const rangeKey = (dateRange as DateRangeKey) ?? "recent"; const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), { + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { type: monitor.type, }); @@ -323,7 +323,7 @@ export class MonitorService implements IMonitorService { const rangeKey = (dateRange as DateRangeKey) ?? "recent"; const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), { + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { type: monitor.type, }); @@ -394,7 +394,7 @@ export class MonitorService implements IMonitorService { requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType)); const limitPerMonitor = snapshotOnlyRequest ? 1 : 25; - const checksMap = await this.checksRepository.findLatestChecksByMonitorIds( + const checksMap = await this.checksRepository.findLatestByMonitorIds( monitorsList.map((monitor) => monitor.id), { limitPerMonitor } ); From 118372630e6df6c16bffcbf10c877d47d053a971 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 21:31:53 +0000 Subject: [PATCH 100/103] return types --- .../repositories/checks/IChecksRepository.ts | 60 ++---------------- server/src/types/check.ts | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 1e0c478b9..7f4da1933 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -1,58 +1,6 @@ -import type { Check, MonitorType } from "@/types/index.js"; +import type { Check, ChecksQueryResult, ChecksSummary, MonitorType, PageSpeedChecksResult, HardwareChecksResult, UptimeChecksResult } from "@/types/index.js"; import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; -export interface PageSpeedChecksResult { - monitorType: "pagespeed"; - checks: Check[]; -} - -export interface HardwareChecksResult { - monitorType: "hardware"; - aggregateData: { - latestCheck: Check | null; - totalChecks: number; - }; - upChecks: { - totalChecks: number; - }; - checks: Array<{ - _id: string; - avgCpuUsage: number; - avgMemoryUsage: number; - avgTemperature: number[]; - disks: Array<{ - name: string; - readSpeed: number; - writeSpeed: number; - totalBytes: number; - freeBytes: number; - usagePercent: number; - }>; - net: Array<{ - name: string; - bytesSentPerSecond: number; - deltaBytesRecv: number; - deltaPacketsSent: number; - deltaPacketsRecv: number; - deltaErrIn: number; - deltaErrOut: number; - deltaDropIn: number; - deltaDropOut: number; - deltaFifoIn: number; - deltaFifoOut: number; - }>; - }>; -} - -export interface UptimeChecksResult { - monitorType: Exclude; - groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; - groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; - groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; - uptimePercentage: number; - avgResponseTime: number; -} - export interface IChecksRepository { // create createChecks(checks: Check[]): Promise; @@ -67,8 +15,8 @@ export interface IChecksRepository { page: number, rowsPerPage: number, status: boolean | undefined - ): Promise; - findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise; + ): Promise; + findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise; findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findByDateRangeAndMonitorId( monitorId: string, @@ -77,7 +25,7 @@ export interface IChecksRepository { dateString: string, options?: { type?: MonitorType } ): Promise; - findSummaryByTeamId(teamId: string): Promise; + findSummaryByTeamId(teamId: string): Promise; // update //delete deleteByMonitorId(monitorId: string): Promise; diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 96c6f6e7f..f8ee4f618 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -108,3 +108,66 @@ export interface Check { createdAt: string; updatedAt: string; } +export interface ChecksQueryResult { + checksCount: number; + checks: Check[]; +} + +export interface PageSpeedChecksResult { + monitorType: "pagespeed"; + checks: Check[]; +} + +export interface HardwareChecksResult { + monitorType: "hardware"; + aggregateData: { + latestCheck: Check | null; + totalChecks: number; + }; + upChecks: { + totalChecks: number; + }; + checks: Array<{ + _id: string; + avgCpuUsage: number; + avgMemoryUsage: number; + avgTemperature: number[]; + disks: Array<{ + name: string; + readSpeed: number; + writeSpeed: number; + totalBytes: number; + freeBytes: number; + usagePercent: number; + }>; + net: Array<{ + name: string; + bytesSentPerSecond: number; + deltaBytesRecv: number; + deltaPacketsSent: number; + deltaPacketsRecv: number; + deltaErrIn: number; + deltaErrOut: number; + deltaDropIn: number; + deltaDropOut: number; + deltaFifoIn: number; + deltaFifoOut: number; + }>; + }>; +} + +export interface UptimeChecksResult { + monitorType: Exclude; + groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; + groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + uptimePercentage: number; + avgResponseTime: number; +} + +export interface ChecksSummary { + totalChecks: number; + resolvedChecks: number; + downChecks: number; + cannotResolveChecks: number; +} From 102bd0a5f86231f3eb9595a3b91c37491786b9c5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 21:34:40 +0000 Subject: [PATCH 101/103] format --- .../repositories/checks/IChecksRepository.ts | 10 ++++- server/src/service/business/inviteService.js | 44 ------------------- 2 files changed, 9 insertions(+), 45 deletions(-) delete mode 100644 server/src/service/business/inviteService.js diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 7f4da1933..ab5dc6ce5 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -1,4 +1,12 @@ -import type { Check, ChecksQueryResult, ChecksSummary, MonitorType, PageSpeedChecksResult, HardwareChecksResult, UptimeChecksResult } from "@/types/index.js"; +import type { + Check, + ChecksQueryResult, + ChecksSummary, + MonitorType, + PageSpeedChecksResult, + HardwareChecksResult, + UptimeChecksResult, +} from "@/types/index.js"; import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; export interface IChecksRepository { diff --git a/server/src/service/business/inviteService.js b/server/src/service/business/inviteService.js deleted file mode 100644 index 369ed195a..000000000 --- a/server/src/service/business/inviteService.js +++ /dev/null @@ -1,44 +0,0 @@ -const SERVICE_NAME = "inviteService"; - -class InviteService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ db, settingsService, emailService, stringService, errorService }) { - this.db = db; - this.settingsService = settingsService; - this.emailService = emailService; - this.stringService = stringService; - this.errorService = errorService; - } - - get serviceName() { - return InviteService.SERVICE_NAME; - } - - getInviteToken = async ({ invite, teamId }) => { - invite.teamId = teamId; - const inviteToken = await this.db.inviteModule.requestInviteToken(invite); - return inviteToken; - }; - - sendInviteEmail = async ({ inviteRequest, firstName }) => { - const inviteToken = await this.db.inviteModule.requestInviteToken({ ...inviteRequest }); - const { clientHost } = this.settingsService.getSettings(); - - const html = await this.emailService.buildEmail("employeeActivationTemplate", { - name: firstName, - link: `${clientHost}/register/${inviteToken.token}`, - }); - const result = await this.emailService.sendEmail(inviteRequest.email, "Welcome to Uptime Monitor", html); - if (!result) { - throw this.errorService.createServerError("Failed to send invite e-mail... Please verify your settings."); - } - }; - - verifyInviteToken = async ({ inviteToken }) => { - const invite = await this.db.inviteModule.getInviteToken(inviteToken); - return invite; - }; -} - -export default InviteService; From de236f03eb762fa005e57511fafb5b998691fcbe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 21:53:19 +0000 Subject: [PATCH 102/103] js->ts --- .../{diagnosticService.js => diagnosticService.ts} | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) rename server/src/service/business/{diagnosticService.js => diagnosticService.ts} (88%) diff --git a/server/src/service/business/diagnosticService.js b/server/src/service/business/diagnosticService.ts similarity index 88% rename from server/src/service/business/diagnosticService.js rename to server/src/service/business/diagnosticService.ts index 353604932..6c49933a4 100644 --- a/server/src/service/business/diagnosticService.js +++ b/server/src/service/business/diagnosticService.ts @@ -3,6 +3,14 @@ import os from "os"; const SERVICE_NAME = "diagnosticService"; +interface MemoryUsageMetrics { + rss: number; + heapTotal: number; + heapUsed: number; + external: number; + arrayBuffers: number; +} + class DiagnosticService { static SERVICE_NAME = SERVICE_NAME; @@ -47,8 +55,8 @@ class DiagnosticService { }; const used = process.memoryUsage(); - const memoryUsage = {}; - for (let key in used) { + const memoryUsage: Record = {}; + for (const key of Object.keys(used) as Array) { memoryUsage[`${key}Mb`] = Math.round((used[key] / 1024 / 1024) * 100) / 100; // MB } @@ -70,7 +78,7 @@ class DiagnosticService { performance.mark("end"); performance.measure("eventLoopDelay", "start", "end"); const entries = performance.getEntriesByName("eventLoopDelay"); - if (entries.length > 0) { + if (entries.length > 0 && entries[0] !== undefined) { eventLoopDelay = entries[0].duration; } From fa520e5f88bcfe4f835bd39642710a1f6f38a567 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 22:06:55 +0000 Subject: [PATCH 103/103] js->ts --- .../{emailService.js => emailService.ts} | 61 ++++++++----------- server/src/types/email.ts | 17 ++++++ server/src/types/index.ts | 1 + 3 files changed, 42 insertions(+), 37 deletions(-) rename server/src/service/infrastructure/{emailService.js => emailService.ts} (69%) create mode 100644 server/src/types/email.ts diff --git a/server/src/service/infrastructure/emailService.js b/server/src/service/infrastructure/emailService.ts similarity index 69% rename from server/src/service/infrastructure/emailService.js rename to server/src/service/infrastructure/emailService.ts index bc040d51a..8598f112f 100755 --- a/server/src/service/infrastructure/emailService.js +++ b/server/src/service/infrastructure/emailService.ts @@ -1,28 +1,27 @@ import { fileURLToPath } from "url"; import path from "path"; +import { EmailTransportConfig } from "@/types/index.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const SERVICE_NAME = "EmailService"; -/** - * Represents an email service that can load templates, build, and send emails. - */ class EmailService { static SERVICE_NAME = SERVICE_NAME; - /** - * Constructs an instance of the EmailService, initializing template loaders and the email transporter. - * @param {Object} settingsService - The settings service to get email configuration. - * @param {Object} fs - The file system module. - * @param {Object} path - The path module. - * @param {Function} compile - The Handlebars compile function. - * @param {Function} mjml2html - The MJML to HTML conversion function. - * @param {Object} nodemailer - The nodemailer module. - * @param {Object} logger - The logger module. - */ - constructor(settingsService, fs, path, compile, mjml2html, nodemailer, logger) { + private settingsService: any; + private fs: any; + private path: any; + private compile: any; + private mjml2html: any; + private nodemailer: any; + private logger: any; + private transporter: any; + private templateLookup: Record; + private loadTemplate: (templateName: string) => Function; + + constructor(settingsService: any, fs: any, path: any, compile: any, mjml2html: any, nodemailer: any, logger: any) { this.settingsService = settingsService; this.fs = fs; this.path = path; @@ -31,6 +30,10 @@ class EmailService { this.nodemailer = nodemailer; this.logger = logger; this.init(); + this.templateLookup = {}; + this.loadTemplate = () => { + return () => {}; + }; } get serviceName() { @@ -38,18 +41,12 @@ class EmailService { } init = async () => { - /** - * Loads an email template from the filesystem. - * - * @param {string} templateName - The name of the template to load. - * @returns {Function} A compiled template function that can be used to generate HTML email content. - */ this.loadTemplate = (templateName) => { try { const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`); const templateContent = this.fs.readFileSync(templatePath, "utf8"); return this.compile(templateContent); - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: SERVICE_NAME, @@ -59,11 +56,6 @@ class EmailService { } }; - /** - * A lookup object to access preloaded email templates. - * @type {Object.} - * TODO Load less used templates in their respective functions - */ this.templateLookup = { welcomeEmailTemplate: this.loadTemplate("welcomeEmail"), employeeActivationTemplate: this.loadTemplate("employeeActivation"), @@ -74,19 +66,14 @@ class EmailService { hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"), testEmailTemplate: this.loadTemplate("testEmailTemplate"), }; - - /** - * The email transporter used to send emails. - * @type {Object} - */ }; - buildEmail = async (template, context) => { + buildEmail = async (template: string, context: Record) => { try { - const mjml = this.templateLookup[template](context); + const mjml = this.templateLookup[template]?.(context); const html = await this.mjml2html(mjml); return html.html; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: SERVICE_NAME, @@ -96,8 +83,8 @@ class EmailService { } }; - sendEmail = async (to, subject, html, transportConfig) => { - let config; + sendEmail = async (to: string, subject: string, html: string, transportConfig?: EmailTransportConfig) => { + let config: EmailTransportConfig; if (typeof transportConfig !== "undefined") { config = transportConfig; } else { @@ -157,7 +144,7 @@ class EmailService { html: html, }); return info?.messageId; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: SERVICE_NAME, diff --git a/server/src/types/email.ts b/server/src/types/email.ts new file mode 100644 index 000000000..2ffae187b --- /dev/null +++ b/server/src/types/email.ts @@ -0,0 +1,17 @@ +import { Settings } from "./settings.js"; + +export type EmailTransportConfig = Pick< + Settings, + | "systemEmailHost" + | "systemEmailPort" + | "systemEmailAddress" + | "systemEmailPassword" + | "systemEmailUser" + | "systemEmailConnectionHost" + | "systemEmailTLSServername" + | "systemEmailSecure" + | "systemEmailPool" + | "systemEmailIgnoreTLS" + | "systemEmailRequireTLS" + | "systemEmailRejectUnauthorized" +>; diff --git a/server/src/types/index.ts b/server/src/types/index.ts index e28d75e91..b0b6b07c5 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -10,3 +10,4 @@ export * from "@/types/settings.js"; export * from "@/types/notification.js"; export * from "@/types/alert.js"; export * from "@/types/incident.js"; +export * from "@/types/email.js";