From d9cb81e71de1b614ce3858100514c88925748bbb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Mar 2026 11:14:40 -0700 Subject: [PATCH 1/9] backend implementation --- server/src/config/services.ts | 1 + server/src/db/models/Check.ts | 12 +- .../repositories/checks/IChecksRepository.ts | 1 + .../checks/MongoChecksRepistory.ts | 15 +-- server/src/service/business/checkService.ts | 31 ++++- .../SuperSimpleQueue/SuperSimpleQueue.ts | 6 +- .../SuperSimpleQueueHelper.ts | 109 ++++++++++++------ .../service/infrastructure/networkService.ts | 10 +- server/src/types/check.ts | 5 +- server/src/types/network.ts | 2 +- 10 files changed, 117 insertions(+), 75 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 9434b3ae5..b31c8a5e9 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -252,6 +252,7 @@ export const initializeServices = async ({ statusService, notificationsService, checkService, + settingsService, bufferService, incidentService, maintenanceWindowsRepository, diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index 7bd1cec6f..b7f88d012 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -21,13 +21,11 @@ type CheckMetadataDocument = Omit & { type: MonitorType; }; -type CheckDocumentBase = Omit & { +type CheckDocumentBase = Omit & { metadata: CheckMetadataDocument; expiry: Date; - ackAt?: Date | null; createdAt: Date; updatedAt: Date; - __v: number; }; interface CheckDocument extends CheckDocumentBase { @@ -229,14 +227,6 @@ const CheckSchema = new Schema( type: Date, default: Date.now, }, - ack: { - type: Boolean, - default: false, - }, - ackAt: { - type: Date, - default: undefined, - }, cpu: { type: cpuSchema, default: () => ({}), diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index c89e5d5fc..b96d14933 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -47,4 +47,5 @@ export interface IChecksRepository { deleteByMonitorId(monitorId: string): Promise; deleteByTeamId(teamId: string): Promise; deleteByMonitorIdsNotIn(monitorIds: string[]): Promise; + deleteOlderThan(date: Date): Promise; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index b019a0430..3a4485f64 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -46,13 +46,6 @@ class MongoChecksRepository implements IChecksRepository { return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); }; - const toOptionalDateString = (value?: Date | string | null): string | undefined => { - if (!value) { - return undefined; - } - return toDateString(value); - }; - const mapTimings = (timings?: GotTimings): GotTimings => { const phases = timings?.phases ?? { wait: 0, @@ -171,8 +164,6 @@ class MongoChecksRepository implements IChecksRepository { timings: mapTimings(doc.timings), statusCode: doc.statusCode ?? 0, message: doc.message ?? "", - ack: doc.ack ?? false, - ackAt: toOptionalDateString(doc.ackAt), expiry: toDateString(doc.expiry), cpu: mapCpu(doc.cpu), memory: mapMemory(doc.memory), @@ -186,7 +177,6 @@ class MongoChecksRepository implements IChecksRepository { seo: doc.seo, performance: doc.performance, audits: mapAudits(doc.audits), - __v: doc.__v ?? 0, createdAt: toDateString(doc.createdAt), updatedAt: toDateString(doc.updatedAt), }; @@ -412,6 +402,11 @@ class MongoChecksRepository implements IChecksRepository { return result.deletedCount ?? 0; }; + deleteOlderThan = async (date: Date): Promise => { + const result = await CheckModel.deleteMany({ createdAt: { $lt: date } }); + return result.deletedCount ?? 0; + }; + private findUptimeDateRangeChecks = async ( monitorType: Exclude, monitorObjectId: mongoose.Types.ObjectId, diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index c83a3d411..a95939947 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,6 +1,14 @@ import { Types } from "mongoose"; import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; -import type { MonitorStatusResponse, CheckErrorInfo, Check, ILighthouseAudit, ChecksQueryResult, ChecksSummary } from "@/types/index.js"; +import type { + MonitorStatusResponse, + CheckErrorInfo, + Check, + ILighthouseAudit, + ChecksQueryResult, + ChecksSummary, + MonitorPayloadMap, +} from "@/types/index.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; import { ParseBoolean } from "@/utils/utils.js"; @@ -10,12 +18,13 @@ const SERVICE_NAME = "checkService"; export interface ICheckService { createChecks(checks: Check[]): Promise; - buildCheck(statusResponse: MonitorStatusResponse): Partial | undefined; + buildCheck(statusResponse: MonitorStatusResponse): Check | undefined; getChecksByMonitor(params: { monitorId: string; query: Record; teamId: string }): Promise; getChecksByTeam(params: { teamId: string; query: Record }): Promise; getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise; deleteChecks(params: { monitorId: string; teamId: string }): Promise; deleteChecksByTeamId(params: { teamId: string }): Promise; + deleteOlderThan(date: Date): Promise; updateChecksTTL(params: { teamId: string; ttl: string }): Promise; } @@ -39,12 +48,14 @@ export class CheckService implements ICheckService { return this.checksRepository.createChecks(checks); }; - buildCheck = (statusResponse: MonitorStatusResponse) => { + buildCheck = (statusResponse: MonitorStatusResponse): Check | undefined => { const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse; - const check: Partial = { + const now = new Date().toISOString(); + const check: Check = { id: new Types.ObjectId().toString(), - createdAt: new Date().toISOString(), + createdAt: now, + updatedAt: now, metadata: { monitorId, teamId, @@ -55,6 +66,7 @@ export class CheckService implements ICheckService { responseTime: responseTime || 0, timings: timings, message, + expiry: now, }; if (type === "pagespeed") { @@ -190,6 +202,15 @@ export class CheckService implements ICheckService { return deletedCount; }; + deleteOlderThan = async (date: Date) => { + if (!date) { + throw new AppError({ message: "No date provided", service: SERVICE_NAME, method: "deleteChecksOlderThan", status: 400 }); + } + + const deletedCount = await this.checksRepository.deleteOlderThan(date); + return deletedCount; + }; + updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => { throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } }); }; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index a96e7855b..d5a0fa12f 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -89,9 +89,10 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { try { this.scheduler.start(); - this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); - this.scheduler.addTemplate("geo-check-job", this.helper.getGeoCheckJob()); + this.scheduler.addTemplate("monitor-job", this.helper.getHeartbeatJob()); + this.scheduler.addTemplate("geo-check-job", this.helper.getHeartbeatGeoJob()); this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob()); + this.scheduler.addTemplate("cleanup-retention-job", this.helper.getCleanupRetentionJob()); const monitors = await this.monitorsRepository.findAll(); if (!monitors) { return true; @@ -104,6 +105,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { } this.scheduler.addJob({ id: "cleanup-orphaned", template: "cleanup-orphaned", active: true }); + this.scheduler.addJob({ id: "cleanup-retention", template: "cleanup-retention-job", active: true, repeat: 24 * 60 * 60 * 1000 }); return true; } catch (error: any) { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index eef0a75be..eca380537 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -2,8 +2,16 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { supportsGeoCheck } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js"; -import type { StatusChangeResult } from "@/types/index.js"; +import { + ICheckService, + INetworkService, + INotificationsService, + ISettingsService, + IStatusService, + IncidentService, + type IGeoChecksService, +} from "@/service/index.js"; +import type { MaintenanceWindow, StatusChangeResult } from "@/types/index.js"; import { IMaintenanceWindowsRepository, IMonitorsRepository, @@ -18,9 +26,10 @@ import { IBufferService } from "@/service/index.js"; export interface ISuperSimpleQueueHelper { readonly serviceName: string; - getMonitorJob(): (monitor: Monitor) => Promise; + getHeartbeatJob(): (monitor: Monitor) => Promise; + getHeartbeatGeoJob(): (monitor: Monitor) => Promise; getCleanupOrphanedJob(): () => Promise; - getGeoCheckJob(): (monitor: Monitor) => Promise; + getCleanupRetentionJob(): () => Promise; isInMaintenanceWindow(monitorId: string, teamId: string): Promise; } @@ -41,11 +50,12 @@ export interface MonitorActionDecision { export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; - private logger: any; + private logger: ILogger; private networkService: INetworkService; private statusService: IStatusService; private notificationsService: INotificationsService; - private checkService: any; + private checkService: ICheckService; + private settingsService: ISettingsService; private buffer: IBufferService; private incidentService: IncidentService; private maintenanceWindowsRepository: IMaintenanceWindowsRepository; @@ -62,7 +72,8 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { networkService: INetworkService, statusService: IStatusService, notificationsService: INotificationsService, - checkService: any, + checkService: ICheckService, + settingsService: ISettingsService, buffer: IBufferService, incidentService: IncidentService, maintenanceWindowsRepository: IMaintenanceWindowsRepository, @@ -78,6 +89,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.networkService = networkService; this.statusService = statusService; this.checkService = checkService; + this.settingsService = settingsService; this.buffer = buffer; this.notificationsService = notificationsService; this.incidentService = incidentService; @@ -95,7 +107,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { return SuperSimpleQueueHelper.SERVICE_NAME; } - getMonitorJob = () => { + getHeartbeatJob = () => { return async (monitor: Monitor) => { try { const monitorId = monitor.id; @@ -126,7 +138,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { } // Step 3. Build check - const check = await this.checkService.buildCheck(status); + const check = this.checkService.buildCheck(status); if (!check) { this.logger.warn({ message: `No check could be built for monitor ${monitorId}`, @@ -146,33 +158,31 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { // Step 6. Handle notifications (best effort, continue even in event of failure, don't wait) if (decision.shouldSendNotification) { - this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: any) => { + this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: unknown) => { this.logger.error({ - message: error.message, + message: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`, service: SERVICE_NAME, method: "getMonitorJob", - details: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error.message}`, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); }); } // Step 7. Handle incidents (best effort, don't wait) - this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: any) => { + this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: unknown) => { this.logger.warn({ - message: error.message, + message: `Error handling incident for job ${monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`, service: SERVICE_NAME, method: "getMonitorJob", - details: `Error handling incident for job ${monitor.id}: ${error.message}`, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); }); - } catch (error: any) { + } catch (error: unknown) { this.logger.warn({ - message: error.message, - service: error.service || SERVICE_NAME, - method: error.method || "getMonitorJob", - stack: error.stack, + message: error instanceof Error ? error.message : "Unknown error", + service: SERVICE_NAME, + method: "getMonitorJob", + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -258,19 +268,19 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { service: SERVICE_NAME, method: "getCleanupOrphanedJob", }); - } catch (error: any) { + } catch (error: unknown) { this.logger.warn({ - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, method: "getCleanupOrphanedJob", - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } }; }; - getGeoCheckJob = () => { + getHeartbeatGeoJob = () => { return async (monitor: Monitor) => { try { const monitorId = monitor.id; @@ -278,7 +288,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { // Step 1: Validate monitor eligibility if (!monitorId) { - throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" }); + throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getHeartbeatGeoJob" }); } if (!monitor.geoCheckEnabled) { @@ -288,7 +298,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.debug({ message: `Monitor ${monitorId} type does not support geo checks, skipping`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -297,7 +307,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.warn({ message: `No geo check locations configured for monitor ${monitorId}`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -308,7 +318,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.debug({ message: `Monitor ${monitorId} is in maintenance window, skipping geo check`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -319,7 +329,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.warn({ message: `No geo check could be built for monitor ${monitorId}`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -330,14 +340,14 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.debug({ message: `Geo check job executed for monitor ${monitorId}`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, - method: "getGeoCheckJob", - stack: error.stack, + method: "getHeartbeatGeoJob", + stack: error instanceof Error ? error.stack : undefined, }); // Don't throw - geo check failures shouldn't crash the job scheduler } @@ -347,7 +357,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { async isInMaintenanceWindow(monitorId: string, teamId: string) { const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId); // Check for active maintenance window: - const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => { + const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: boolean, window: MaintenanceWindow) => { if (window.active) { const start = new Date(window.start); const end = new Date(window.end); @@ -374,6 +384,31 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { return maintenanceWindowIsActive; } + getCleanupRetentionJob = () => { + return async () => { + try { + const settings = await this.settingsService.getDBSettings(); + + const checkTTL = settings.checkTTL; // Check TTL is in DAYS, not MS + const checkTTLInMs = checkTTL * 24 * 60 * 60 * 1000; + const cutoffDate = new Date(Date.now() - checkTTLInMs); + const deleteCount = await this.checkService.deleteOlderThan(cutoffDate); + this.logger.info({ + message: `Deleted ${deleteCount} checks older than ${cutoffDate.toISOString()}`, + service: SERVICE_NAME, + method: "getCleanupRetentionJob", + }); + } catch (error: unknown) { + this.logger.error({ + message: error instanceof Error ? error.message : "Unknown error", + service: SERVICE_NAME, + method: "getCleanupRetentionJob", + stack: error instanceof Error ? error.stack : undefined, + }); + } + }; + }; + private evaluateMonitorAction(statusChangeResult: StatusChangeResult): MonitorActionDecision { const { monitor, statusChanged, prevStatus } = statusChangeResult; diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index e475bc24b..9111ef7b2 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,4 +1,4 @@ -import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; +import type { Monitor, MonitorStatusResponse, MonitorPayloadMap, MonitorType } from "@/types/index.js"; import type { AxiosStatic } from "axios"; import { AppError } from "@/utils/AppError.js"; import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js"; @@ -8,7 +8,7 @@ const SERVICE_NAME = "NetworkService"; export interface INetworkService { readonly serviceName: string; - requestStatus(monitor: Monitor): Promise>; + requestStatus(monitor: Monitor & { type: T }): Promise>; requestWebhook( type: string, url: string, @@ -43,12 +43,12 @@ export class NetworkService implements INetworkService { } // Main entry point - async requestStatus(monitor: Monitor) { + async requestStatus(monitor: Monitor & { type: T }): Promise> { const provider = this.providers.find((p) => p.supports(monitor.type)); if (!provider) { - return this.handleUnsupportedType(monitor.type); + return this.handleUnsupportedType(monitor.type) as Promise>; } - return provider.handle(monitor); + return provider.handle(monitor) as Promise>; } private async handleUnsupportedType(type: string): Promise { diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 6c4c26445..810454fdd 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -98,8 +98,6 @@ export interface Check { timings?: GotTimings; statusCode: number; message: string; - ack: boolean; - ackAt?: string | null; expiry: string; cpu?: CheckCpuInfo; memory?: CheckMemoryInfo; @@ -113,7 +111,6 @@ export interface Check { seo?: number; performance?: number; audits?: CheckAudits; - __v: number; createdAt: string; updatedAt: string; } @@ -205,4 +202,4 @@ export type NormalizedUptimeCheck = T & { originalAvgResponseTime: number; }; -export type CheckSnapshot = Omit; +export type CheckSnapshot = Omit; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 07e7c3736..6c7172724 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -126,7 +126,7 @@ export interface MonitorPayloadMap { game: GameStatusPayload; grpc: GrpcStatusPayload; websocket: WebSocketStatusPayload; - default: unknown; + unknown: unknown; } export type StatusChangeResult = { From 5ab230f77b93d3975a114e725b01f5487211b604 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Mar 2026 11:15:50 -0700 Subject: [PATCH 2/9] remove any types --- .../SuperSimpleQueue/SuperSimpleQueue.ts | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index d5a0fa12f..f4fd5b1e6 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -2,7 +2,7 @@ import { IMonitorsRepository } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; import Scheduler from "super-simple-scheduler"; import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; -import { supportsGeoCheck } from "@/types/monitor.js"; +import { Monitor, MonitorType, supportsGeoCheck } from "@/types/monitor.js"; const SERVICE_NAME = "JobQueue"; type QueueJobFailure = { @@ -42,11 +42,11 @@ type QueueJobSummary = { 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; + addJob(monitorId: string, monitor: Monitor): Promise; + deleteJob(monitor: Monitor): Promise; + pauseJob(monitor: Monitor): Promise; + resumeJob(monitor: Monitor): Promise; + updateJob(monitor: Monitor): Promise; shutdown(): Promise; getMetrics(): Promise; getJobs(): Promise; @@ -108,18 +108,17 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { this.scheduler.addJob({ id: "cleanup-retention", template: "cleanup-retention-job", active: true, repeat: 24 * 60 * 60 * 1000 }); return true; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: "Failed to initialize SuperSimpleQueue", + message: `Failed to initialize SuperSimpleQueue: ${error instanceof Error ? error.message : String(error)}`, service: SERVICE_NAME, method: "init", - details: error, }); return false; } }; - addJob = async (monitorId: string, monitor: any) => { + addJob = async (monitorId: string, monitor: Monitor) => { this.scheduler.addJob({ id: monitorId, template: "monitor-job", @@ -145,12 +144,12 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { } }; - deleteJob = async (monitor: any) => { + deleteJob = async (monitor: Monitor) => { this.scheduler.removeJob(monitor.id); this.scheduler.removeJob(`${monitor.id}-geo`); }; - pauseJob = async (monitor: any) => { + pauseJob = async (monitor: Monitor) => { const result = await this.scheduler.pauseJob(monitor.id); if (result === false) { throw new Error("Failed to pause monitor"); @@ -163,7 +162,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { }); }; - resumeJob = async (monitor: any) => { + resumeJob = async (monitor: Monitor) => { const result = await this.scheduler.resumeJob(monitor.id); if (result === false) { throw new Error("Failed to resume monitor"); @@ -178,7 +177,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { }); }; - updateJob = async (monitor: any) => { + updateJob = async (monitor: Monitor) => { this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor }); // Handle geo check job lifecycle @@ -246,8 +245,8 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { failingJobs: 0, jobsWithFailures: [] as Array<{ monitorId: string | number; - monitorUrl: any; - monitorType: any; + monitorUrl: string; + monitorType: MonitorType; failedAt: number | null; failCount: number; failReason: string | null; From b1147b8bf94d908f040b34983d6b37732994e032 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Mar 2026 11:28:46 -0700 Subject: [PATCH 3/9] FE implementation --- .../SuperSimpleQueue/SuperSimpleQueueHelper.ts | 9 +++++++++ server/src/validation/settingsValidation.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index eca380537..382318945 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -390,6 +390,15 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { const settings = await this.settingsService.getDBSettings(); const checkTTL = settings.checkTTL; // Check TTL is in DAYS, not MS + + if (checkTTL === 366) { + this.logger.info({ + message: `Check TTL is set to unlimited, skipping cleanup`, + service: SERVICE_NAME, + method: "getCleanupRetentionJob", + }); + return; + } const checkTTLInMs = checkTTL * 24 * 60 * 60 * 1000; const cutoffDate = new Date(Date.now() - checkTTLInMs); const deleteCount = await this.checkService.deleteOlderThan(cutoffDate); diff --git a/server/src/validation/settingsValidation.ts b/server/src/validation/settingsValidation.ts index ee2a2ee87..0aeec943f 100644 --- a/server/src/validation/settingsValidation.ts +++ b/server/src/validation/settingsValidation.ts @@ -6,7 +6,7 @@ import { z } from "zod"; export const updateAppSettingsBodyValidation = z .object({ - checkTTL: z.union([z.number(), z.literal("")]).optional(), + checkTTL: z.union([z.number().int().min(1).max(366), z.literal("")]).optional(), systemEmailPort: z.union([z.number(), z.literal("")]).optional(), pagespeedApiKey: z.union([z.string(), z.literal("")]).optional(), From 77af9d2e934aeb8b693666fd1b4b0edf6ae7af04 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Mar 2026 11:32:01 -0700 Subject: [PATCH 4/9] FE UI, userService type fix --- client/src/Components/inputs/Slider.tsx | 22 +++++++++++-- client/src/Pages/Settings/index.tsx | 37 ++++++++++++++++++++++ client/src/Validation/settings.ts | 2 +- client/src/locales/en.json | 12 ++++++- server/src/service/business/userService.ts | 2 +- 5 files changed, 69 insertions(+), 6 deletions(-) diff --git a/client/src/Components/inputs/Slider.tsx b/client/src/Components/inputs/Slider.tsx index 83037ab54..a17fc8a10 100644 --- a/client/src/Components/inputs/Slider.tsx +++ b/client/src/Components/inputs/Slider.tsx @@ -11,20 +11,26 @@ import type { ResponsiveStyleValue } from "@mui/system"; interface SliderInputProps extends SliderProps { sx?: SliderProps["sx"]; showValue?: boolean; + formatDisplayValue?: (value: number) => string; } export const SliderInput = forwardRef( - ({ sx, showValue = false, ...props }, ref) => { + ({ sx, showValue = false, formatDisplayValue, ...props }, ref) => { const theme = useTheme(); const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; + const displayValue = + showValue && formatDisplayValue && typeof props.value === "number" + ? formatDisplayValue(props.value) + : props.value; + return ( - {showValue && {props.value}} + {showValue && {displayValue}} string; sliderMaxWidth?: ResponsiveStyleValue; } export const SliderWithLabel = forwardRef( ( - { fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props }, + { + fieldLabel, + required, + showValue = true, + formatDisplayValue, + value, + sliderMaxWidth = "100%", + ...props + }, ref ) => { const theme = useTheme(); @@ -82,6 +97,7 @@ export const SliderWithLabel = forwardRef diff --git a/client/src/Pages/Settings/index.tsx b/client/src/Pages/Settings/index.tsx index 44fb4a42f..e96fc4837 100644 --- a/client/src/Pages/Settings/index.tsx +++ b/client/src/Pages/Settings/index.tsx @@ -462,6 +462,43 @@ export const SettingsPage = () => { /> )} + {/* Check Retention */} + {isAdmin && ( + ( + field.onChange(value)} + valueLabelDisplay="auto" + valueLabelFormat={(value: number) => + value >= 366 + ? t("pages.settings.form.retention.option.days.unlimited") + : `${value}` + } + formatDisplayValue={(value: number) => + value >= 366 + ? t("pages.settings.form.retention.option.days.unlimited") + : `${value}` + } + /> + )} + /> + } + /> + )} + {/* Global Thresholds */} {isAdmin && ( (val.trim() === "" ? undefined : val.trim())) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 5f582cac5..4c4965aec 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1041,7 +1041,7 @@ }, "stats": { "title": "Monitor history", - "description": "Define how long you want to retain historical data. You can also clear all existing data.", + "description": "Clear all monitoring history and statistics for your team. This action is irreversible.", "option": { "clear": { "label": "Clear all stats. This is irreversible." @@ -1052,6 +1052,16 @@ "description": "This cannot be undone" } }, + "retention": { + "title": "Check retention", + "description": "Set how long check data is retained before being automatically cleaned up.", + "option": { + "days": { + "label": "Retention period (days)", + "unlimited": "Unlimited" + } + } + }, "thresholds": { "title": "Global Thresholds", "description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.", diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index f224e9ba4..3337e491d 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -349,7 +349,7 @@ export class UserService implements IUserService { if (res && res.length > 0) { await Promise.all( res.map(async (monitor) => { - await this.jobQueue.deleteJob(monitor.id); + await this.jobQueue.deleteJob(monitor); }) ); } From 6ad0a6a29d42dee8fb18e245517551e3807d6ad1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Mar 2026 10:20:23 -0700 Subject: [PATCH 5/9] validation fix, batch deleteOlderThan --- client/src/Pages/Settings/index.tsx | 7 +- client/src/Types/Check.ts | 9 +- client/src/Validation/settings.ts | 7 +- server/src/controllers/checkController.ts | 74 ++++++------- .../checks/MongoChecksRepistory.ts | 41 +++++-- server/src/routes/checkRoute.ts | 1 - server/src/service/business/checkService.ts | 103 ++++++++++-------- .../SuperSimpleQueueHelper.ts | 4 +- server/src/types/check.ts | 3 + server/src/validation/checkValidation.ts | 20 ++-- server/src/validation/settingsValidation.ts | 4 +- 11 files changed, 155 insertions(+), 118 deletions(-) diff --git a/client/src/Pages/Settings/index.tsx b/client/src/Pages/Settings/index.tsx index e96fc4837..679d66126 100644 --- a/client/src/Pages/Settings/index.tsx +++ b/client/src/Pages/Settings/index.tsx @@ -37,6 +37,7 @@ import { } from "@/Features/UI/uiSlice.js"; import timezones from "@/Utils/timezones.json"; import type { RootState } from "@/Types/state"; +import { CHECK_TTL_SENTINEL } from "@/Types/Check"; interface Timezone { id: string; @@ -477,18 +478,18 @@ export const SettingsPage = () => { {...field} fieldLabel={t("pages.settings.form.retention.option.days.label")} min={1} - max={366} + max={CHECK_TTL_SENTINEL} sliderMaxWidth={{ xs: "100%", md: "50%" }} value={field.value || 30} onChange={(_, value) => field.onChange(value)} valueLabelDisplay="auto" valueLabelFormat={(value: number) => - value >= 366 + value >= CHECK_TTL_SENTINEL ? t("pages.settings.form.retention.option.days.unlimited") : `${value}` } formatDisplayValue={(value: number) => - value >= 366 + value >= CHECK_TTL_SENTINEL ? t("pages.settings.form.retention.option.days.unlimited") : `${value}` } diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index c69fe7972..27db3d411 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -1,3 +1,5 @@ +export const CHECK_TTL_SENTINEL = 366; + export interface CheckMetadata { monitorId: string; teamId: string; @@ -125,8 +127,6 @@ export interface Check { timings?: CheckTimings; statusCode: number; message: string; - ack: boolean; - ackAt?: string | null; expiry: string; cpu?: CheckCpuInfo; memory?: CheckMemoryInfo; @@ -243,10 +243,7 @@ export interface ChecksSummary { downChecks: number; } -export type CheckSnapshot = Omit< - Check, - "metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt" -> & { +export type CheckSnapshot = Omit & { originalResponseTime: number; }; diff --git a/client/src/Validation/settings.ts b/client/src/Validation/settings.ts index effb819a8..975927d1c 100644 --- a/client/src/Validation/settings.ts +++ b/client/src/Validation/settings.ts @@ -1,3 +1,4 @@ +import { CHECK_TTL_SENTINEL } from "@/Types/Check"; import { z } from "zod"; export const settingsSchema = z.object({ @@ -11,7 +12,11 @@ export const settingsSchema = z.object({ systemEmailSecure: z.boolean().optional(), systemEmailPool: z.boolean().optional(), showURL: z.boolean().optional(), - checkTTL: z.number().int().min(1, "Please enter a value").max(366, "Maximum 366"), + checkTTL: z + .number() + .int() + .min(1, "Please enter a value") + .max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`), pagespeedApiKey: z .string() .transform((val) => (val.trim() === "" ? undefined : val.trim())) diff --git a/server/src/controllers/checkController.ts b/server/src/controllers/checkController.ts index ec6722bd3..879ceaffe 100755 --- a/server/src/controllers/checkController.ts +++ b/server/src/controllers/checkController.ts @@ -4,18 +4,19 @@ import { getChecksParamValidation, getChecksQueryValidation, getTeamChecksQueryValidation, + getChecksSummaryByTeamIdQueryValidation, deleteChecksParamValidation, - deleteChecksByTeamIdParamValidation, - updateChecksTTLBodyValidation, } from "@/validation/checkValidation.js"; +import { ICheckService } from "@/service/index.js"; +import { requireTeamId } from "@/controllers/controllerUtils.js"; const SERVICE_NAME = "checkController"; class CheckController { static SERVICE_NAME = SERVICE_NAME; - private checkService: any; - constructor(checkService: any) { + private checkService: ICheckService; + constructor(checkService: ICheckService) { this.checkService = checkService; } @@ -25,13 +26,20 @@ class CheckController { getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - getChecksParamValidation.parse(req.params); - getChecksQueryValidation.parse(req.query); + const validatedParams = getChecksParamValidation.parse(req.params); + const validatedQuery = getChecksQueryValidation.parse(req.query); + + const teamId = requireTeamId(req.user?.teamId); const result = await this.checkService.getChecksByMonitor({ - monitorId: req?.params?.monitorId, - query: req?.query, - teamId: req?.user?.teamId, + monitorId: validatedParams.monitorId, + teamId, + sortOrder: validatedQuery.sortOrder, + dateRange: validatedQuery.dateRange, + filter: validatedQuery.filter, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + status: validatedQuery.status, }); return res.status(200).json({ @@ -46,10 +54,16 @@ class CheckController { getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => { try { - getTeamChecksQueryValidation.parse(req.query); + const validatedQuery = getTeamChecksQueryValidation.parse(req.query); + const teamId = requireTeamId(req.user?.teamId); + const checkData = await this.checkService.getChecksByTeam({ - teamId: req?.user?.teamId, - query: req?.query, + teamId, + sortOrder: validatedQuery.sortOrder, + dateRange: validatedQuery.dateRange, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + filter: validatedQuery.filter, }); return res.status(200).json({ success: true, @@ -63,8 +77,11 @@ class CheckController { getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - const dateRange = req.query?.dateRange || "hour"; - const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId, dateRange }); + const validatedQuery = getChecksSummaryByTeamIdQueryValidation.parse(req.query); + const teamId = requireTeamId(req.user?.teamId); + const dateRange = validatedQuery.dateRange ?? "hour"; + + const summary = await this.checkService.getChecksSummaryByTeamId({ teamId, dateRange }); return res.status(200).json({ success: true, msg: "Checks summary retrieved successfully", @@ -77,11 +94,12 @@ class CheckController { deleteChecks = async (req: Request, res: Response, next: NextFunction) => { try { - deleteChecksParamValidation.parse(req.params); + const validatedParams = deleteChecksParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); const deletedCount = await this.checkService.deleteChecks({ - monitorId: req.params.monitorId as string, - teamId: req?.user?.teamId, + monitorId: validatedParams.monitorId, + teamId, }); return res.status(200).json({ @@ -96,9 +114,9 @@ class CheckController { deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - deleteChecksByTeamIdParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); - const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId }); + const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId }); return res.status(200).json({ success: true, @@ -109,24 +127,6 @@ class CheckController { next(error); } }; - - updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => { - try { - updateChecksTTLBodyValidation.parse(req.body); - - await this.checkService.updateChecksTTL({ - teamId: req?.user?.teamId, - ttl: req?.body?.ttl, - }); - - return res.status(200).json({ - success: true, - msg: "Checks TTL updated successfully", - }); - } catch (error) { - next(error); - } - }; } export default CheckController; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 3a4485f64..6c7d469ce 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -13,9 +13,10 @@ import type { GotTimings, MonitorType, } from "@/types/index.js"; -import { CheckModel, MonitorModel, type CheckDocument } from "@/db/models/index.js"; +import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; import { getDateForRange } from "@/utils/dataUtils.js"; +import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "StatusService"; @@ -26,8 +27,8 @@ type HardwareUpChecks = { totalChecks: number }; class MongoChecksRepository implements IChecksRepository { static SERVICE_NAME = SERVICE_NAME; - private logger: any; - constructor(logger: any) { + private logger: ILogger; + constructor(logger: ILogger) { this.logger = logger; } @@ -227,7 +228,7 @@ class MongoChecksRepository implements IChecksRepository { status: boolean | undefined ) => { // Match - const matchStage: Record = { + const matchStage: Record = { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), ...(typeof status !== "undefined" && { status }), ...(getDateForRange(dateRange) && { @@ -279,7 +280,7 @@ class MongoChecksRepository implements IChecksRepository { }; findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => { - const matchStage: Record = { + const matchStage: Record = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), ...(getDateForRange(dateRange) && { createdAt: { @@ -348,7 +349,7 @@ class MongoChecksRepository implements IChecksRepository { ); const mapped = results.reduce((acc, { monitorId, docs }) => { - acc[monitorId] = docs.map((doc: any) => this.toEntity(doc as CheckDocument)); + acc[monitorId] = docs.map((doc: CheckDocument) => this.toEntity(doc)); return acc; }, {}); return mapped; @@ -402,11 +403,31 @@ class MongoChecksRepository implements IChecksRepository { return result.deletedCount ?? 0; }; - deleteOlderThan = async (date: Date): Promise => { - const result = await CheckModel.deleteMany({ createdAt: { $lt: date } }); - return result.deletedCount ?? 0; - }; + deleteOlderThan = async (cutoffDate: Date, batchDays: number = 30): Promise => { + // Find the oldest check that is older than the cutoff + const oldest = await CheckModel.findOne({ createdAt: { $lt: cutoffDate } }) + .sort({ createdAt: 1 }) + .select({ createdAt: 1 }); + if (!oldest?.createdAt) return 0; + let totalDeleted = 0; + let batchStart = new Date(oldest.createdAt); + const batchMs = batchDays * 24 * 60 * 60 * 1000; + + // Delete in 30 day chunks until we reach the cutoff + while (batchStart < cutoffDate) { + // Advance startTime by batchMs to get to the end of last batch + const nextStart = batchStart.getTime() + batchMs; + const batchEnd = new Date(Math.min(nextStart, cutoffDate.getTime())); + const result = await CheckModel.deleteMany({ + createdAt: { $gte: batchStart, $lt: batchEnd }, + }); + totalDeleted += result.deletedCount ?? 0; + batchStart = batchEnd; + } + + return totalDeleted; + }; private findUptimeDateRangeChecks = async ( monitorType: Exclude, monitorObjectId: mongoose.Types.ObjectId, diff --git a/server/src/routes/checkRoute.ts b/server/src/routes/checkRoute.ts index 4df286846..71a0cd5f3 100755 --- a/server/src/routes/checkRoute.ts +++ b/server/src/routes/checkRoute.ts @@ -15,7 +15,6 @@ class CheckRoutes { initRoutes() { this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId); this.router.get("/team", this.checkController.getChecksByTeam); - this.router.put("/team/ttl", isAllowed(["admin", "superadmin"]), this.checkController.updateChecksTTL); this.router.delete("/team", isAllowed(["admin", "superadmin"]), this.checkController.deleteChecksByTeamId); this.router.get("/:monitorId", this.checkController.getChecksByMonitor); this.router.delete("/:monitorId", this.checkController.deleteChecks); diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index a95939947..719124b10 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -19,13 +19,28 @@ const SERVICE_NAME = "checkService"; export interface ICheckService { createChecks(checks: Check[]): Promise; buildCheck(statusResponse: MonitorStatusResponse): Check | undefined; - getChecksByMonitor(params: { monitorId: string; query: Record; teamId: string }): Promise; - getChecksByTeam(params: { teamId: string; query: Record }): Promise; + getChecksByMonitor(params: { + monitorId: string; + teamId: string; + sortOrder: string; + dateRange: string; + page: number; + rowsPerPage: number; + filter?: string; + status?: boolean; + }): Promise; + getChecksByTeam(params: { + teamId: string; + sortOrder: string; + dateRange: string; + page: number; + rowsPerPage: number; + filter?: string; + }): Promise; getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise; deleteChecks(params: { monitorId: string; teamId: string }): Promise; deleteChecksByTeamId(params: { teamId: string }): Promise; deleteOlderThan(date: Date): Promise; - updateChecksTTL(params: { teamId: string; ttl: string }): Promise; } export class CheckService implements ICheckService { @@ -124,7 +139,25 @@ export class CheckService implements ICheckService { return check; }; - getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: Record; teamId: string }) => { + getChecksByMonitor = async ({ + monitorId, + teamId, + sortOrder, + dateRange, + filter, + page, + rowsPerPage, + status, + }: { + monitorId: string; + teamId: string; + sortOrder: string; + dateRange: string; + page: number; + rowsPerPage: number; + status?: boolean; + filter?: string; + }) => { if (!monitorId) { throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); } @@ -135,69 +168,49 @@ export class CheckService implements ICheckService { // For verification, throws an error if monitor doesn't belong to team await this.monitorsRepository.findById(monitorId, teamId); - const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query; - if (!sortOrder || !dateRange || !page || !rowsPerPage) { - throw new AppError({ - message: `Missing required query parameters sortOrder: ${sortOrder}, dateRange: ${dateRange}, filter: ${filter}, page: ${page}, rowsPerPage: ${rowsPerPage}`, - service: SERVICE_NAME, - method: "getChecksByMonitor", - status: 400, - }); - } const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status); - const parsedPage = page ? parseInt(page) : 0; - const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5; + const parsedPage = page ? page : 0; + const parsedRowsPerPage = rowsPerPage ? rowsPerPage : 5; const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus); return result; }; - getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record }) => { - const { sortOrder, dateRange, filter, page, rowsPerPage } = query; - if (!sortOrder || !dateRange || !page || !rowsPerPage) { - throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 }); - } - - if (!teamId) { - throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 }); - } - - const parsedPage = page ? parseInt(page) : 0; - const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5; + getChecksByTeam = async ({ + teamId, + sortOrder, + dateRange, + filter, + page, + rowsPerPage, + }: { + teamId: string; + sortOrder: string; + dateRange: string; + page: number; + rowsPerPage: number; + filter?: string; + }) => { + const parsedPage = page ? page : 0; + const parsedRowsPerPage = rowsPerPage ? rowsPerPage : 5; const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId); return checkData; }; getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: string }) => { - if (!teamId) { - throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksSummaryByTeamId", status: 400 }); - } const summary = await this.checksRepository.findSummaryByTeamId(teamId, dateRange); return summary; }; deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { - if (!monitorId) { - throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 }); - } - - if (!teamId) { - throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 }); - } - - // For verification, throws an error if monitor doesn't belong to team await this.monitorsRepository.findById(monitorId, teamId); const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId); return deletedCount; }; deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => { - if (!teamId) { - throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecksByTeamId", status: 400 }); - } - const deletedCount = await this.checksRepository.deleteByTeamId(teamId); return deletedCount; }; @@ -210,8 +223,4 @@ export class CheckService implements ICheckService { const deletedCount = await this.checksRepository.deleteOlderThan(date); return deletedCount; }; - - updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => { - throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } }); - }; } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 382318945..b6908127b 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -11,7 +11,7 @@ import { IncidentService, type IGeoChecksService, } from "@/service/index.js"; -import type { MaintenanceWindow, StatusChangeResult } from "@/types/index.js"; +import { CHECK_TTL_SENTINEL, type MaintenanceWindow, type StatusChangeResult } from "@/types/index.js"; import { IMaintenanceWindowsRepository, IMonitorsRepository, @@ -391,7 +391,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { const checkTTL = settings.checkTTL; // Check TTL is in DAYS, not MS - if (checkTTL === 366) { + if (checkTTL === CHECK_TTL_SENTINEL) { this.logger.info({ message: `Check TTL is set to unlimited, skipping cleanup`, service: SERVICE_NAME, diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 810454fdd..066b898ab 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -1,5 +1,8 @@ import type { MonitorType } from "@/types/index.js"; import type { Response } from "got"; + +export const CHECK_TTL_SENTINEL = 366; + export type GotTimings = Response["timings"]; export interface CheckMetadata { diff --git a/server/src/validation/checkValidation.ts b/server/src/validation/checkValidation.ts index d4f75f3ac..2a768f068 100644 --- a/server/src/validation/checkValidation.ts +++ b/server/src/validation/checkValidation.ts @@ -12,32 +12,34 @@ export const getChecksParamValidation = z.object({ export const getChecksQueryValidation = z.object({ type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"]).optional(), - sortOrder: z.enum(["asc", "desc"]).optional(), + sortOrder: z.enum(["asc", "desc"]), limit: z.coerce.number().optional(), - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), filter: z.enum(["all", "up", "down", "resolve"]).optional(), ack: booleanCoercion.optional(), - page: z.coerce.number().optional(), - rowsPerPage: z.coerce.number().optional(), + page: z.coerce.number(), + rowsPerPage: z.coerce.number(), status: booleanCoercion.optional(), continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(), }); export const getTeamChecksQueryValidation = z.object({ - sortOrder: z.enum(["asc", "desc"]).optional(), + sortOrder: z.enum(["asc", "desc"]), limit: z.coerce.number().optional(), - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), filter: z.enum(["all", "up", "down", "resolve"]).optional(), ack: booleanCoercion.optional(), - page: z.coerce.number().optional(), - rowsPerPage: z.coerce.number().optional(), + page: z.coerce.number(), + rowsPerPage: z.coerce.number(), }); export const deleteChecksParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), }); -export const deleteChecksByTeamIdParamValidation = z.object({}); +export const getChecksSummaryByTeamIdQueryValidation = z.object({ + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), +}); export const updateChecksTTLBodyValidation = z.object({ ttl: z.number().min(1, "TTL is required"), diff --git a/server/src/validation/settingsValidation.ts b/server/src/validation/settingsValidation.ts index 0aeec943f..e7f0dc9b4 100644 --- a/server/src/validation/settingsValidation.ts +++ b/server/src/validation/settingsValidation.ts @@ -1,3 +1,4 @@ +import { CHECK_TTL_SENTINEL } from "@/types/check.js"; import { z } from "zod"; //**************************************** @@ -6,9 +7,8 @@ import { z } from "zod"; export const updateAppSettingsBodyValidation = z .object({ - checkTTL: z.union([z.number().int().min(1).max(366), z.literal("")]).optional(), + checkTTL: z.union([z.number().int().min(1).max(CHECK_TTL_SENTINEL), z.literal("")]).optional(), systemEmailPort: z.union([z.number(), z.literal("")]).optional(), - pagespeedApiKey: z.union([z.string(), z.literal("")]).optional(), language: z.union([z.string(), z.literal("")]).optional(), timezone: z.union([z.string(), z.literal("")]).optional(), From cadf68740aea80b6a376ee3fe5fb3de00478eb30 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Mar 2026 10:33:18 -0700 Subject: [PATCH 6/9] remove old expiry field, remove vestigal checks --- client/src/Types/Check.ts | 3 +-- server/src/db/models/Check.ts | 8 ++------ server/src/service/business/checkService.ts | 12 +++++------- server/src/types/check.ts | 3 +-- server/src/validation/checkValidation.ts | 4 ---- 5 files changed, 9 insertions(+), 21 deletions(-) diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index 27db3d411..35b931c2c 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -127,7 +127,6 @@ export interface Check { timings?: CheckTimings; statusCode: number; message: string; - expiry: string; cpu?: CheckCpuInfo; memory?: CheckMemoryInfo; disk?: CheckDiskInfo[]; @@ -243,7 +242,7 @@ export interface ChecksSummary { downChecks: number; } -export type CheckSnapshot = Omit & { +export type CheckSnapshot = Omit & { originalResponseTime: number; }; diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index b7f88d012..541abb4d9 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -21,9 +21,8 @@ type CheckMetadataDocument = Omit & { type: MonitorType; }; -type CheckDocumentBase = Omit & { +type CheckDocumentBase = Omit & { metadata: CheckMetadataDocument; - expiry: Date; createdAt: Date; updatedAt: Date; }; @@ -223,10 +222,7 @@ const CheckSchema = new Schema( message: { type: String, }, - expiry: { - type: Date, - default: Date.now, - }, + cpu: { type: cpuSchema, default: () => ({}), diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 719124b10..6522d627c 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -11,7 +11,6 @@ import type { } from "@/types/index.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; -import { ParseBoolean } from "@/utils/utils.js"; import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "checkService"; @@ -168,11 +167,10 @@ export class CheckService implements ICheckService { // For verification, throws an error if monitor doesn't belong to team await this.monitorsRepository.findById(monitorId, teamId); - const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status); - const parsedPage = page ? page : 0; - const parsedRowsPerPage = rowsPerPage ? rowsPerPage : 5; + const parsedPage = page ?? 0; + const parsedRowsPerPage = rowsPerPage ?? 5; - const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus); + const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, status); return result; }; @@ -192,8 +190,8 @@ export class CheckService implements ICheckService { rowsPerPage: number; filter?: string; }) => { - const parsedPage = page ? page : 0; - const parsedRowsPerPage = rowsPerPage ? rowsPerPage : 5; + const parsedPage = page ?? 0; + const parsedRowsPerPage = rowsPerPage ?? 5; const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId); return checkData; diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 066b898ab..f4c64f926 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -101,7 +101,6 @@ export interface Check { timings?: GotTimings; statusCode: number; message: string; - expiry: string; cpu?: CheckCpuInfo; memory?: CheckMemoryInfo; disk?: CheckDiskInfo[]; @@ -205,4 +204,4 @@ export type NormalizedUptimeCheck = T & { originalAvgResponseTime: number; }; -export type CheckSnapshot = Omit; +export type CheckSnapshot = Omit; diff --git a/server/src/validation/checkValidation.ts b/server/src/validation/checkValidation.ts index 2a768f068..e511118e6 100644 --- a/server/src/validation/checkValidation.ts +++ b/server/src/validation/checkValidation.ts @@ -40,7 +40,3 @@ export const deleteChecksParamValidation = z.object({ export const getChecksSummaryByTeamIdQueryValidation = z.object({ dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), }); - -export const updateChecksTTLBodyValidation = z.object({ - ttl: z.number().min(1, "TTL is required"), -}); From bbdadb0bf55121c45e76045e3952035ec1385f09 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Mar 2026 10:34:09 -0700 Subject: [PATCH 7/9] remove expiry from repository and service --- server/src/repositories/checks/MongoChecksRepistory.ts | 1 - server/src/service/business/checkService.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 6c7d469ce..4055ddfc3 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -165,7 +165,6 @@ class MongoChecksRepository implements IChecksRepository { timings: mapTimings(doc.timings), statusCode: doc.statusCode ?? 0, message: doc.message ?? "", - expiry: toDateString(doc.expiry), cpu: mapCpu(doc.cpu), memory: mapMemory(doc.memory), disk: mapDisks(doc.disk), diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 6522d627c..f85eaa878 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -80,7 +80,6 @@ export class CheckService implements ICheckService { responseTime: responseTime || 0, timings: timings, message, - expiry: now, }; if (type === "pagespeed") { From 839f1beb416371c68448730fb1e8f45c00af7ba3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Mar 2026 10:37:44 -0700 Subject: [PATCH 8/9] remove __v from client type --- client/src/Types/Check.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index 35b931c2c..9f7771222 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -139,7 +139,6 @@ export interface Check { seo?: number; performance?: number; audits?: CheckAudits; - __v: number; createdAt: string; updatedAt: string; } From dc7d442ace8942b82ba17c0b3e5afa1c434f3633 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Mar 2026 10:44:02 -0700 Subject: [PATCH 9/9] remove unnecessary check --- server/src/service/business/checkService.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index f85eaa878..374090709 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -213,10 +213,6 @@ export class CheckService implements ICheckService { }; deleteOlderThan = async (date: Date) => { - if (!date) { - throw new AppError({ message: "No date provided", service: SERVICE_NAME, method: "deleteChecksOlderThan", status: 400 }); - } - const deletedCount = await this.checksRepository.deleteOlderThan(date); return deletedCount; };