From 5a7b51bfec063f7c0ed94bcf71cffde4b53d5192 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 21:49:48 +0000 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 0f764ca0841ab2910851d31549b49a30d17d0b62 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 23:45:40 +0000 Subject: [PATCH 04/13] 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 8814596d6e566e8bd10b28efe3b0734c7e0036b3 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 15 Jan 2026 21:27:37 -0500 Subject: [PATCH 05/13] Fix pagespeed monitor deletion failing with undefined ID The delete handler was using monitor._id from state, which was undefined because the fetched monitor object didn't have _id populated. Changed to use monitorId directly from URL params, which is already available and used by other handlers in the same component (e.g., handlePause). Fixes "Failed to delete monitor" error when removing pagespeed monitors. --- client/src/Pages/PageSpeed/Create/index.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx index 2cce26159..69218d8cb 100644 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ b/client/src/Pages/PageSpeed/Create/index.jsx @@ -178,8 +178,7 @@ const PageSpeedSetup = () => { const handleRemove = async (event) => { event.preventDefault(); - const TEMP_MONITOR = { id: monitor._id }; - await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/pagespeed" }); + await deleteMonitor({ monitor: { id: monitorId }, redirect: "/pagespeed" }); }; const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; From ba0c89fc6c7382b4ee1e4f522087227a9a7c8266 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 15 Jan 2026 23:37:50 -0500 Subject: [PATCH 06/13] feat: migrate icons from custom SVGs and MUI to Lucide React Replace custom SVG icons and @mui/icons-material with lucide-react for consistent, lightweight icon management across the application. ## Changes - Add lucide-react package and remove @mui/icons-material - Create theme-aware Icon wrapper component with color resolution - Create iconMap.js for legacy icon name mappings - Migrate 64 components to use new Icon component - Remove 74 unused SVG files from assets/icons/ ## Benefits - Consistent icon API across the application - Smaller bundle size with tree-shakeable imports - Theme-aware color support via MUI theme paths - Simpler maintenance with single icon library --- client/package-lock.json | 62 +++------ client/package.json | 2 +- .../src/Components/v1/ActionsMenu/index.jsx | 4 +- client/src/Components/v1/Alert/index.jsx | 17 +-- client/src/Components/v1/ArrowLeft/index.jsx | 19 +-- client/src/Components/v1/ArrowRight/index.jsx | 13 +- client/src/Components/v1/Check/Check.jsx | 7 +- client/src/Components/v1/Common/AppBar.jsx | 7 +- .../src/Components/v1/FilterHeader/index.jsx | 7 +- client/src/Components/v1/Icon/iconMap.js | 120 ++++++++++++++++++ client/src/Components/v1/Icon/index.jsx | 63 +++++++++ .../Components/v1/Inputs/Checkbox/index.jsx | 7 +- .../v1/Inputs/ImageUpload/index.jsx | 7 +- .../src/Components/v1/Inputs/Radio/index.jsx | 4 +- .../src/Components/v1/Inputs/Search/index.jsx | 23 ++-- .../src/Components/v1/Inputs/Select/index.jsx | 4 +- .../v1/Inputs/TextInput/Adornments/index.jsx | 11 +- .../Components/v1/MonitorActions/index.jsx | 4 +- .../v1/MonitorDetailsControlHeader/index.jsx | 11 +- .../v1/NotificationConfig/index.jsx | 8 +- .../src/Components/v1/ProgressBars/index.jsx | 11 +- .../Components/v1/ProgressStepper/index.jsx | 7 +- .../Components/v1/ShareComponent/index.jsx | 4 +- .../v1/Sidebar/components/authFooter.jsx | 31 +++-- .../v1/Sidebar/components/navItem.jsx | 3 +- client/src/Components/v1/Sidebar/index.jsx | 54 +++----- client/src/Components/v1/StarPrompt/index.jsx | 4 +- .../v1/Table/TablePagination/index.jsx | 4 +- client/src/Components/v1/Toast/index.jsx | 15 +-- .../components/AddMemberMenu/index.jsx | 4 +- .../Account/components/RoleTable/index.jsx | 8 +- client/src/Pages/Auth/CheckEmail.jsx | 4 +- client/src/Pages/Auth/ForgotPassword.jsx | 4 +- .../src/Pages/Auth/NewPasswordConfirmed.jsx | 4 +- client/src/Pages/Auth/SetNewPassword.jsx | 4 +- .../Components/StatusBoxes/StatusBox.jsx | 13 +- .../Components/ActiveIncidentsPanel/index.jsx | 7 +- .../LatestIncidentsPanel/IncidentItem.jsx | 7 +- .../Components/StatisticsPanel/index.jsx | 13 +- .../Components/MonitorActionButtons.jsx | 7 +- .../Monitors/Components/Filters/index.jsx | 4 +- .../Components/MonitorsTable/index.jsx | 7 +- .../Components/MonitorsTableMenu/index.jsx | 4 +- .../Components/MonitorList/index.jsx | 4 +- .../Maintenance/CreateMaintenance/index.jsx | 4 +- .../MaintenanceTable/ActionsMenu/index.jsx | 4 +- .../Maintenance/MaintenanceTable/index.jsx | 11 +- .../Notifications/components/ActionMenu.jsx | 4 +- client/src/Pages/PageSpeed/Create/index.jsx | 7 +- .../Components/Charts/AreaChartLegend.jsx | 4 +- .../Components/Charts/PieChartLegend.jsx | 4 +- .../Components/PageSpeedAreaChart/index.jsx | 4 +- .../Components/PerformanceReport/index.jsx | 4 +- .../Monitors/Components/Card/index.jsx | 5 +- .../Create/Components/MonitorList/index.jsx | 7 +- .../Create/Components/Progress/index.jsx | 4 +- .../Components/ControlsHeader/index.jsx | 7 +- .../Status/Components/StatusBar/index.jsx | 11 +- .../Components/StatusPagesTable/index.jsx | 4 +- client/src/Pages/Uptime/Create/index.jsx | 7 +- .../Details/Components/ChartBoxes/index.jsx | 10 +- .../Components/Charts/ResponseTimeChart.jsx | 4 +- .../Components/ResponseTable/index.jsx | 4 +- .../Monitors/Components/Filter/index.jsx | 4 +- .../Components/StatusBoxes/statusBox.jsx | 9 +- .../Components/UptimeDataTable/index.jsx | 11 +- client/src/assets/icons/alert-icon.svg | 3 - .../assets/icons/average-response-icon.svg | 3 - client/src/assets/icons/bwu-icon.svg | 4 - client/src/assets/icons/calendar-check.svg | 3 - client/src/assets/icons/calendar.svg | 3 - client/src/assets/icons/certificate.svg | 3 - client/src/assets/icons/changeLog.svg | 3 - client/src/assets/icons/check-icon.svg | 3 - client/src/assets/icons/check-outlined.svg | 3 - client/src/assets/icons/check.svg | 4 - client/src/assets/icons/checkbox-filled.svg | 4 - client/src/assets/icons/checkbox-green.svg | 5 - client/src/assets/icons/checkbox-outline.svg | 3 - client/src/assets/icons/checkbox-red.svg | 5 - client/src/assets/icons/clock-snooze.svg | 3 - client/src/assets/icons/close-icon.svg | 3 - client/src/assets/icons/cpu-chip.svg | 3 - client/src/assets/icons/dashboard.svg | 3 - client/src/assets/icons/discussions.svg | 7 - .../src/assets/icons/distributed-uptime.svg | 3 - client/src/assets/icons/docs.svg | 3 - client/src/assets/icons/dots-vertical.svg | 5 - client/src/assets/icons/down-arrow.svg | 3 - client/src/assets/icons/edit.svg | 3 - client/src/assets/icons/email.svg | 3 - client/src/assets/icons/folder.svg | 4 - client/src/assets/icons/groups.svg | 1 - client/src/assets/icons/history-icon.svg | 3 - client/src/assets/icons/incidents.svg | 3 - client/src/assets/icons/integrations.svg | 3 - client/src/assets/icons/interval-check.svg | 3 - client/src/assets/icons/jupiter_ag_logo.svg | 52 -------- .../assets/icons/jupiter_logo_banner_dark.svg | 35 ----- .../icons/jupiter_logo_banner_light.svg | 35 ----- client/src/assets/icons/key.svg | 3 - client/src/assets/icons/left-arrow-double.svg | 4 - client/src/assets/icons/left-arrow-long.svg | 4 - client/src/assets/icons/left-arrow.svg | 4 - client/src/assets/icons/lock.svg | 3 - client/src/assets/icons/logout.svg | 3 - client/src/assets/icons/logs.svg | 5 - client/src/assets/icons/mail.svg | 4 - client/src/assets/icons/maintenance.svg | 3 - .../src/assets/icons/monitor-graph-line.svg | 3 - client/src/assets/icons/monitors.svg | 3 - client/src/assets/icons/notifications.svg | 5 - client/src/assets/icons/open-in-new-page.svg | 3 - client/src/assets/icons/page-speed.svg | 3 - client/src/assets/icons/pause-icon.svg | 3 - .../src/assets/icons/performance-report.svg | 3 - client/src/assets/icons/radio-checked.svg | 4 - .../src/assets/icons/response-time-icon.svg | 3 - client/src/assets/icons/resume-icon.svg | 4 - .../src/assets/icons/right-arrow-double.svg | 4 - client/src/assets/icons/right-arrow.svg | 4 - client/src/assets/icons/ruler-icon.svg | 3 - client/src/assets/icons/search.svg | 3 - client/src/assets/icons/selector-vertical.svg | 3 - client/src/assets/icons/settings-bold.svg | 4 - client/src/assets/icons/settings.svg | 4 - client/src/assets/icons/solana_logo.svg | 28 ---- .../src/assets/icons/solana_logo_banner.svg | 24 ---- client/src/assets/icons/spedometer-icon.svg | 3 - client/src/assets/icons/speedometer-icon.svg | 3 - client/src/assets/icons/status-pages.svg | 3 - client/src/assets/icons/support.svg | 3 - client/src/assets/icons/top-right-arrow.svg | 3 - client/src/assets/icons/trash-bin.svg | 3 - client/src/assets/icons/up-arrow.svg | 3 - client/src/assets/icons/uptime-icon.svg | 3 - client/src/assets/icons/user-edit.svg | 3 - client/src/assets/icons/user-two.svg | 3 - client/src/assets/icons/user.svg | 3 - client/src/assets/icons/warning-icon.svg | 3 - 140 files changed, 421 insertions(+), 746 deletions(-) create mode 100644 client/src/Components/v1/Icon/iconMap.js create mode 100644 client/src/Components/v1/Icon/index.jsx delete mode 100644 client/src/assets/icons/alert-icon.svg delete mode 100644 client/src/assets/icons/average-response-icon.svg delete mode 100644 client/src/assets/icons/bwu-icon.svg delete mode 100644 client/src/assets/icons/calendar-check.svg delete mode 100644 client/src/assets/icons/calendar.svg delete mode 100644 client/src/assets/icons/certificate.svg delete mode 100644 client/src/assets/icons/changeLog.svg delete mode 100644 client/src/assets/icons/check-icon.svg delete mode 100644 client/src/assets/icons/check-outlined.svg delete mode 100644 client/src/assets/icons/check.svg delete mode 100644 client/src/assets/icons/checkbox-filled.svg delete mode 100644 client/src/assets/icons/checkbox-green.svg delete mode 100644 client/src/assets/icons/checkbox-outline.svg delete mode 100644 client/src/assets/icons/checkbox-red.svg delete mode 100644 client/src/assets/icons/clock-snooze.svg delete mode 100644 client/src/assets/icons/close-icon.svg delete mode 100644 client/src/assets/icons/cpu-chip.svg delete mode 100644 client/src/assets/icons/dashboard.svg delete mode 100644 client/src/assets/icons/discussions.svg delete mode 100644 client/src/assets/icons/distributed-uptime.svg delete mode 100644 client/src/assets/icons/docs.svg delete mode 100644 client/src/assets/icons/dots-vertical.svg delete mode 100644 client/src/assets/icons/down-arrow.svg delete mode 100644 client/src/assets/icons/edit.svg delete mode 100644 client/src/assets/icons/email.svg delete mode 100644 client/src/assets/icons/folder.svg delete mode 100644 client/src/assets/icons/groups.svg delete mode 100644 client/src/assets/icons/history-icon.svg delete mode 100644 client/src/assets/icons/incidents.svg delete mode 100644 client/src/assets/icons/integrations.svg delete mode 100644 client/src/assets/icons/interval-check.svg delete mode 100644 client/src/assets/icons/jupiter_ag_logo.svg delete mode 100644 client/src/assets/icons/jupiter_logo_banner_dark.svg delete mode 100644 client/src/assets/icons/jupiter_logo_banner_light.svg delete mode 100644 client/src/assets/icons/key.svg delete mode 100644 client/src/assets/icons/left-arrow-double.svg delete mode 100644 client/src/assets/icons/left-arrow-long.svg delete mode 100644 client/src/assets/icons/left-arrow.svg delete mode 100644 client/src/assets/icons/lock.svg delete mode 100644 client/src/assets/icons/logout.svg delete mode 100644 client/src/assets/icons/logs.svg delete mode 100644 client/src/assets/icons/mail.svg delete mode 100644 client/src/assets/icons/maintenance.svg delete mode 100644 client/src/assets/icons/monitor-graph-line.svg delete mode 100644 client/src/assets/icons/monitors.svg delete mode 100644 client/src/assets/icons/notifications.svg delete mode 100644 client/src/assets/icons/open-in-new-page.svg delete mode 100644 client/src/assets/icons/page-speed.svg delete mode 100644 client/src/assets/icons/pause-icon.svg delete mode 100644 client/src/assets/icons/performance-report.svg delete mode 100644 client/src/assets/icons/radio-checked.svg delete mode 100644 client/src/assets/icons/response-time-icon.svg delete mode 100644 client/src/assets/icons/resume-icon.svg delete mode 100644 client/src/assets/icons/right-arrow-double.svg delete mode 100644 client/src/assets/icons/right-arrow.svg delete mode 100644 client/src/assets/icons/ruler-icon.svg delete mode 100644 client/src/assets/icons/search.svg delete mode 100644 client/src/assets/icons/selector-vertical.svg delete mode 100644 client/src/assets/icons/settings-bold.svg delete mode 100644 client/src/assets/icons/settings.svg delete mode 100644 client/src/assets/icons/solana_logo.svg delete mode 100644 client/src/assets/icons/solana_logo_banner.svg delete mode 100644 client/src/assets/icons/spedometer-icon.svg delete mode 100644 client/src/assets/icons/speedometer-icon.svg delete mode 100644 client/src/assets/icons/status-pages.svg delete mode 100644 client/src/assets/icons/support.svg delete mode 100644 client/src/assets/icons/top-right-arrow.svg delete mode 100644 client/src/assets/icons/trash-bin.svg delete mode 100644 client/src/assets/icons/up-arrow.svg delete mode 100644 client/src/assets/icons/uptime-icon.svg delete mode 100644 client/src/assets/icons/user-edit.svg delete mode 100644 client/src/assets/icons/user-two.svg delete mode 100644 client/src/assets/icons/user.svg delete mode 100644 client/src/assets/icons/warning-icon.svg diff --git a/client/package-lock.json b/client/package-lock.json index e917619ae..176b2c5b4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,7 +12,6 @@ "@emotion/styled": "^11.13.0", "@hello-pangea/dnd": "^18.0.0", "@hookform/resolvers": "5.2.2", - "@mui/icons-material": "6.4.11", "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", "@mui/material": "6.4.11", "@mui/x-charts": "^7.5.1", @@ -25,6 +24,7 @@ "human-interval": "2.0.1", "i18next": "25.4.2", "joi": "17.13.3", + "lucide-react": "^0.562.0", "mui-color-input": "^6.0.0", "pretty-ms": "9.3.0", "react": "18.3.1", @@ -97,7 +97,6 @@ "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", @@ -409,7 +408,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", @@ -453,7 +451,6 @@ "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", @@ -1189,32 +1186,6 @@ "url": "https://opencollective.com/mui-org" } }, - "node_modules/@mui/icons-material": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.11.tgz", - "integrity": "sha512-+jjJGIrB1awNbMv4ZVPPdN/p7O1UKFZ+xqRvNIQ8B1KnlID5hPMPBLM6UUbRF4bu3UDCbu79rn9Nye5LGNzmeA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^6.4.11", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/lab": { "version": "6.0.0-dev.240424162023-9968b4889d", "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-dev.240424162023-9968b4889d.tgz", @@ -1376,7 +1347,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", @@ -1541,6 +1511,7 @@ "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", @@ -1568,6 +1539,7 @@ "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", @@ -1598,6 +1570,7 @@ "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", @@ -1673,6 +1646,7 @@ "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", @@ -2738,7 +2712,6 @@ "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2760,7 +2733,6 @@ "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" @@ -2824,7 +2796,6 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3131,7 +3102,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -3544,8 +3514,7 @@ "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", @@ -3952,7 +3921,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", @@ -4760,7 +4728,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5464,6 +5431,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6028,7 +6004,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" @@ -6042,7 +6017,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" }, @@ -6246,8 +6220,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", @@ -6385,7 +6358,6 @@ "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" }, @@ -7021,7 +6993,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7141,7 +7112,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", diff --git a/client/package.json b/client/package.json index 376671942..50fafb35c 100644 --- a/client/package.json +++ b/client/package.json @@ -17,7 +17,6 @@ "@emotion/styled": "^11.13.0", "@hello-pangea/dnd": "^18.0.0", "@hookform/resolvers": "5.2.2", - "@mui/icons-material": "6.4.11", "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", "@mui/material": "6.4.11", "@mui/x-charts": "^7.5.1", @@ -30,6 +29,7 @@ "human-interval": "2.0.1", "i18next": "25.4.2", "joi": "17.13.3", + "lucide-react": "^0.562.0", "mui-color-input": "^6.0.0", "pretty-ms": "9.3.0", "react": "18.3.1", diff --git a/client/src/Components/v1/ActionsMenu/index.jsx b/client/src/Components/v1/ActionsMenu/index.jsx index 5cd0dfa5b..8fcc77f5d 100644 --- a/client/src/Components/v1/ActionsMenu/index.jsx +++ b/client/src/Components/v1/ActionsMenu/index.jsx @@ -2,7 +2,7 @@ import IconButton from "@mui/material/IconButton"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -import Settings from "../../../assets/icons/settings-bold.svg?react"; +import Icon from "../Icon"; import Dialog from "../Dialog/index.jsx"; // Utils @@ -84,7 +84,7 @@ const ActionsMenu = ({ }, }} > - + , - error: , - warning: , + info: , + error: , + warning: , }; /** @@ -97,11 +94,7 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { }, }} > - + )} diff --git a/client/src/Components/v1/ArrowLeft/index.jsx b/client/src/Components/v1/ArrowLeft/index.jsx index a327d6bb7..e18a7f2d5 100644 --- a/client/src/Components/v1/ArrowLeft/index.jsx +++ b/client/src/Components/v1/ArrowLeft/index.jsx @@ -1,27 +1,28 @@ -import LeftArrow from "../../../assets/icons/left-arrow.svg?react"; -import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react"; -import LeftArrowLong from "../../../assets/icons/left-arrow-long.svg?react"; +import Icon from "../Icon"; import PropTypes from "prop-types"; const ArrowLeft = ({ type, color = "#667085", ...props }) => { if (type === "double") { return ( - ); } else if (type === "long") { return ( - ); } else { return ( - ); diff --git a/client/src/Components/v1/ArrowRight/index.jsx b/client/src/Components/v1/ArrowRight/index.jsx index 08a2639b0..3ae64a580 100644 --- a/client/src/Components/v1/ArrowRight/index.jsx +++ b/client/src/Components/v1/ArrowRight/index.jsx @@ -1,19 +1,20 @@ -import RightArrow from "../../../assets/icons/right-arrow.svg?react"; -import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react"; +import Icon from "../Icon"; import PropTypes from "prop-types"; const ArrowRight = ({ type, color = "#667085", ...props }) => { if (type === "double") { return ( - ); } else { return ( - ); diff --git a/client/src/Components/v1/Check/Check.jsx b/client/src/Components/v1/Check/Check.jsx index 53971316b..5e25569be 100644 --- a/client/src/Components/v1/Check/Check.jsx +++ b/client/src/Components/v1/Check/Check.jsx @@ -1,7 +1,6 @@ import "./check.css"; import PropTypes from "prop-types"; -import CheckGrey from "../../../assets/icons/check.svg?react"; -import CheckOutlined from "../../../assets/icons/check-outlined.svg?react"; +import Icon from "../Icon"; import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; @@ -35,7 +34,7 @@ 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/FilterHeader/index.jsx b/client/src/Components/v1/FilterHeader/index.jsx index 36dd04081..de2c7f0e9 100644 --- a/client/src/Components/v1/FilterHeader/index.jsx +++ b/client/src/Components/v1/FilterHeader/index.jsx @@ -1,7 +1,7 @@ import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material"; import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; +import Icon from "../Icon"; /** * A reusable filter header component that displays a dropdown menu with selectable options. @@ -29,9 +29,10 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) =>