diff --git a/server/src/api/controllers/authController.ts b/server/src/api/controllers/authController.ts index 1eb992529..ab8d1fc61 100755 --- a/server/src/api/controllers/authController.ts +++ b/server/src/api/controllers/authController.ts @@ -22,8 +22,6 @@ import { } from "@/api/validation/userValidation.js"; import { IUserService } from "@/domain/users/user.service.js"; -const SERVICE_NAME = "authController"; - export interface IAuthController { registerUser: RequestHandler; createUser: RequestHandler; @@ -42,18 +40,12 @@ export interface IAuthController { } class AuthController implements IAuthController { - static SERVICE_NAME = SERVICE_NAME; - private userService: IUserService; constructor(userService: IUserService) { this.userService = userService; } - get serviceName() { - return AuthController.SERVICE_NAME; - } - registerUser = catchAsync(async (req: Request, res: Response) => { const newUser = req.body.user; const newUserToken = req.body.token; diff --git a/server/src/api/controllers/checkController.ts b/server/src/api/controllers/checkController.ts index ccc91921d..7b69c7047 100755 --- a/server/src/api/controllers/checkController.ts +++ b/server/src/api/controllers/checkController.ts @@ -11,10 +11,7 @@ import { import { ICheckService } from "@/domain/checks/check.service.js"; import { requireTeamId } from "@/api/controllers/controllerUtils.js"; -const SERVICE_NAME = "checkController"; - export interface ICheckController { - serviceName: string; getChecksByMonitor: RequestHandler; getChecksByTeam: RequestHandler; getChecksSummaryByTeamId: RequestHandler; @@ -23,17 +20,11 @@ export interface ICheckController { } class CheckController implements ICheckController { - static SERVICE_NAME = SERVICE_NAME; - private checkService: ICheckService; constructor(checkService: ICheckService) { this.checkService = checkService; } - get serviceName() { - return CheckController.SERVICE_NAME; - } - getChecksByMonitor = catchAsync(async (req: Request, res: Response) => { const validatedParams = getChecksParamValidation.parse(req.params); const validatedQuery = getChecksQueryValidation.parse(req.query); diff --git a/server/src/api/controllers/controllerUtils.ts b/server/src/api/controllers/controllerUtils.ts index 45bf3fa9f..be3f46533 100755 --- a/server/src/api/controllers/controllerUtils.ts +++ b/server/src/api/controllers/controllerUtils.ts @@ -54,24 +54,6 @@ export const optionalNumber = (value: unknown, fieldName: string): number | unde throw new AppError({ message: `${fieldName} must be a number`, status: 400 }); }; -export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { - if (value === undefined) { - return undefined; - } - if (typeof value === "boolean") { - return value; - } - if (typeof value === "string") { - if (value === "true") { - return true; - } - if (value === "false") { - return false; - } - } - throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 }); -}; - export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { const parseSingle = (input: unknown): MonitorType => { if (typeof input !== "string") { diff --git a/server/src/api/controllers/diagnosticController.ts b/server/src/api/controllers/diagnosticController.ts index 0864f7237..9d70304d4 100755 --- a/server/src/api/controllers/diagnosticController.ts +++ b/server/src/api/controllers/diagnosticController.ts @@ -2,25 +2,17 @@ import { IDiagnosticService } from "@/domain/diagnostics/diagnostic.service.js"; import { Request, Response, RequestHandler } from "express"; import { catchAsync } from "@/utils/catchAsync.js"; -const SERVICE_NAME = "diagnosticController"; - export interface IDiagnosticController { getSystemStats: RequestHandler; } class DiagnosticController implements IDiagnosticController { - static SERVICE_NAME = SERVICE_NAME; - private diagnosticService: IDiagnosticService; constructor(diagnosticService: IDiagnosticService) { this.diagnosticService = diagnosticService; } - get serviceName() { - return DiagnosticController.SERVICE_NAME; - } - getSystemStats = catchAsync(async (req: Request, res: Response) => { const diagnostics = await this.diagnosticService.getSystemStats(); return res.status(200).json({ diff --git a/server/src/api/controllers/geoCheckController.ts b/server/src/api/controllers/geoCheckController.ts index 81082b6ed..24caef382 100644 --- a/server/src/api/controllers/geoCheckController.ts +++ b/server/src/api/controllers/geoCheckController.ts @@ -4,23 +4,15 @@ import { getChecksParamValidation, getChecksQueryValidation } from "@/api/valida import type { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js"; import { requireTeamId } from "./controllerUtils.js"; -const SERVICE_NAME = "geoCheckController"; - export interface IGeoCheckController { getGeoChecksByMonitor: RequestHandler; } class GeoCheckController implements IGeoCheckController { - static SERVICE_NAME = SERVICE_NAME; - private geoChecksService: IGeoChecksService; constructor(geoChecksService: IGeoChecksService) { this.geoChecksService = geoChecksService; } - get serviceName() { - return GeoCheckController.SERVICE_NAME; - } - getGeoChecksByMonitor = catchAsync(async (req: Request, res: Response) => { const validatedParams = getChecksParamValidation.parse(req.params); const validatedQuery = getChecksQueryValidation.parse(req.query); diff --git a/server/src/api/controllers/inviteController.ts b/server/src/api/controllers/inviteController.ts index 0efc1e0a7..511145b7b 100755 --- a/server/src/api/controllers/inviteController.ts +++ b/server/src/api/controllers/inviteController.ts @@ -3,7 +3,6 @@ import { catchAsync } from "@/utils/catchAsync.js"; import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/api/validation/authValidation.js"; import { requireFirstName, requireTeamId, requireUserRoles } from "@/api/controllers/controllerUtils.js"; import { IInviteService } from "@/domain/invites/invite.service.js"; -const SERVICE_NAME = "inviteController"; export interface IInviteController { getInviteToken: RequestHandler; @@ -12,16 +11,11 @@ export interface IInviteController { } class InviteController implements IInviteController { - static SERVICE_NAME = SERVICE_NAME; private inviteService: IInviteService; constructor(inviteService: IInviteService) { this.inviteService = inviteService; } - get serviceName() { - return InviteController.SERVICE_NAME; - } - getInviteToken = catchAsync(async (req: Request, res: Response) => { const teamId = requireTeamId(req.user?.teamId); const userRoles = requireUserRoles(req.user?.role); diff --git a/server/src/api/controllers/logController.ts b/server/src/api/controllers/logController.ts index f0449bbf2..316d03788 100644 --- a/server/src/api/controllers/logController.ts +++ b/server/src/api/controllers/logController.ts @@ -2,21 +2,15 @@ import { ILogger } from "@/utils/logger.js"; import { Request, Response, RequestHandler } from "express"; import { catchAsync } from "@/utils/catchAsync.js"; -const SERVICE_NAME = "LogController"; - export interface ILogController { getLogs: RequestHandler; } class LogController { - static SERVICE_NAME = SERVICE_NAME; private logger: ILogger; constructor(logger: ILogger) { this.logger = logger; } - get serviceName() { - return LogController.SERVICE_NAME; - } getLogs = catchAsync(async (req: Request, res: Response) => { const logs = this.logger.getLogs(); diff --git a/server/src/api/controllers/monitorController.ts b/server/src/api/controllers/monitorController.ts index 2ffb18802..664a78f45 100644 --- a/server/src/api/controllers/monitorController.ts +++ b/server/src/api/controllers/monitorController.ts @@ -23,8 +23,6 @@ import { AppError } from "@/utils/AppError.js"; import { IMonitorService } from "@/domain/monitors/monitor.service.js"; import { INotificationsService } from "@/domain/notifications/notification.service.js"; -const SERVICE_NAME = "monitorController"; - export interface IMonitorController { getMonitorCertificate: RequestHandler; getUptimeDetailsById: RequestHandler; @@ -47,8 +45,6 @@ export interface IMonitorController { updateNotifications: RequestHandler; } class MonitorController implements IMonitorController { - static SERVICE_NAME = SERVICE_NAME; - private monitorService: IMonitorService; private notificationsService: INotificationsService; @@ -57,10 +53,6 @@ class MonitorController implements IMonitorController { this.notificationsService = notificationsService; } - get serviceName() { - return MonitorController.SERVICE_NAME; - } - getMonitorCertificate = catchAsync(async (req: Request, res: Response) => { const validatedParams = getCertificateParamValidation.parse(req.params); const teamId = requireTeamId(req.user?.teamId); diff --git a/server/src/api/controllers/queueController.ts b/server/src/api/controllers/queueController.ts index 72c1463f9..03204167c 100755 --- a/server/src/api/controllers/queueController.ts +++ b/server/src/api/controllers/queueController.ts @@ -3,8 +3,6 @@ import { getQueueJobsQueryValidation } from "@/api/validation/queueValidation.js import { Request, Response, RequestHandler } from "express"; import { catchAsync } from "@/utils/catchAsync.js"; -const SERVICE_NAME = "JobQueueController"; - export interface IJobQueueController { getMetrics: RequestHandler; getJobs: RequestHandler; @@ -13,13 +11,8 @@ export interface IJobQueueController { } class JobQueueController implements IJobQueueController { - static SERVICE_NAME = SERVICE_NAME; constructor(private scheduler: IJobScheduler) {} - get serviceName() { - return JobQueueController.SERVICE_NAME; - } - getMetrics = catchAsync(async (req: Request, res: Response) => { const metrics = await this.scheduler.getMetrics(); res.status(200).json({ diff --git a/server/src/api/controllers/settingsController.ts b/server/src/api/controllers/settingsController.ts index 06085c3f8..3cb032e93 100644 --- a/server/src/api/controllers/settingsController.ts +++ b/server/src/api/controllers/settingsController.ts @@ -7,17 +7,13 @@ import { ISettingsService } from "@/domain/app-settings/app-settings.service.js" import { IEmailService } from "@/service/emailService.js"; import { Settings } from "@/domain/app-settings/app-settings.type.js"; -const SERVICE_NAME = "SettingsController"; - export interface ISettingsController { - serviceName: string; getAppSettings: RequestHandler; updateAppSettings: RequestHandler; sendTestEmail: RequestHandler; } class SettingsController implements ISettingsController { - static SERVICE_NAME = SERVICE_NAME; private settingsService: ISettingsService; private emailService: IEmailService; constructor(settingsService: ISettingsService, emailService: IEmailService) { @@ -25,10 +21,6 @@ class SettingsController implements ISettingsController { this.emailService = emailService; } - get serviceName() { - return SettingsController.SERVICE_NAME; - } - buildAppSettings = (dbSettings: Settings) => { const sanitizedSettings: Record = { ...dbSettings }; delete sanitizedSettings.version; diff --git a/server/src/api/controllers/statusPageController.ts b/server/src/api/controllers/statusPageController.ts index f75c5e7fb..038ddb423 100644 --- a/server/src/api/controllers/statusPageController.ts +++ b/server/src/api/controllers/statusPageController.ts @@ -13,10 +13,7 @@ import { requireTeamId, requireUserId } from "@/api/controllers/controllerUtils. import { IStatusPageService } from "@/domain/status-pages/status-page.service.js"; import { resolveStatusPageDomainFromRequest } from "@/utils/statusPageDomain.js"; -const SERVICE_NAME = "statusPageController"; - export interface IStatusPageController { - readonly serviceName: string; createStatusPage: RequestHandler; updateStatusPage: RequestHandler; getStatusPageByUrl: RequestHandler; @@ -26,16 +23,11 @@ export interface IStatusPageController { } class StatusPageController implements IStatusPageController { - static SERVICE_NAME = SERVICE_NAME; private statusPageService: IStatusPageService; constructor(statusPageService: IStatusPageService) { this.statusPageService = statusPageService; } - get serviceName() { - return StatusPageController.SERVICE_NAME; - } - createStatusPage = catchAsync(async (req: Request, res: Response) => { createStatusPageBodyValidation.parse(req.body); if (req.file) { diff --git a/server/src/api/validation/userValidation.ts b/server/src/api/validation/userValidation.ts index 21fb55317..882c2d898 100644 --- a/server/src/api/validation/userValidation.ts +++ b/server/src/api/validation/userValidation.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { UserRoles } from "@/domain/users/user.type.js"; -import { nameValidation, lowercaseEmailValidation, passwordValidation } from "./shared.js"; +import { nameValidation, lowercaseEmailValidation, passwordValidation, booleanCoercion } from "@/api/validation/shared.js"; //**************************************** // User Validations @@ -46,6 +46,7 @@ export const editUserBodyValidation = z profilePicture: z.string().optional(), password: passwordValidation.optional(), newPassword: passwordValidation.optional(), + deleteProfileImage: booleanCoercion.optional(), }) // Changing a password requires both the current password and the new one. // The service only runs the change-password flow when both are present, so diff --git a/server/src/db/db.mongo.ts b/server/src/db/db.mongo.ts index 5aca5550a..e0b39c188 100755 --- a/server/src/db/db.mongo.ts +++ b/server/src/db/db.mongo.ts @@ -21,10 +21,6 @@ class MongoDB implements IDb { this.db = null; } - get serviceName() { - return MongoDB.SERVICE_NAME; - } - connect = async () => { try { const connectionString = this.envSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; diff --git a/server/src/domain/app-settings/app-settings.service.ts b/server/src/domain/app-settings/app-settings.service.ts index 92a54702e..2df2b7512 100755 --- a/server/src/domain/app-settings/app-settings.service.ts +++ b/server/src/domain/app-settings/app-settings.service.ts @@ -20,8 +20,6 @@ export type EnvConfig = { }; export interface ISettingsService { - readonly serviceName: string; - loadSettings(): EnvConfig; getSettings(): EnvConfig; areStatusPageThemesEnabled(): boolean; getDBSettings(): Promise; @@ -57,14 +55,6 @@ export class SettingsService implements ISettingsService { this.settingsRepository = settingsRepository; } - get serviceName() { - return SettingsService.SERVICE_NAME; - } - - loadSettings() { - return this.settings; - } - getSettings() { return this.settings; } diff --git a/server/src/domain/checks/check.service.ts b/server/src/domain/checks/check.service.ts index ca7fca457..f3c18ec91 100644 --- a/server/src/domain/checks/check.service.ts +++ b/server/src/domain/checks/check.service.ts @@ -50,10 +50,6 @@ export class CheckService implements ICheckService { this.checksRepository = checksRepository; } - get serviceName() { - return CheckService.SERVICE_NAME; - } - createChecks = async (checks: Check[]) => { return this.checksRepository.createChecks(checks); }; diff --git a/server/src/domain/diagnostics/diagnostic.service.ts b/server/src/domain/diagnostics/diagnostic.service.ts index f26fa02e3..1f7a0314a 100644 --- a/server/src/domain/diagnostics/diagnostic.service.ts +++ b/server/src/domain/diagnostics/diagnostic.service.ts @@ -79,10 +79,6 @@ export class DiagnosticService implements IDiagnosticService { obs.observe({ entryTypes: ["measure"] }); } - get serviceName() { - return DiagnosticService.SERVICE_NAME; - } - private getCPUUsagePercentage = async (): Promise => { const startUsage = process.cpuUsage(); const timingPeriod = 1000; // measured in ms diff --git a/server/src/domain/geo-checks/geo-check.service.ts b/server/src/domain/geo-checks/geo-check.service.ts index 0ace565fc..040383811 100644 --- a/server/src/domain/geo-checks/geo-check.service.ts +++ b/server/src/domain/geo-checks/geo-check.service.ts @@ -11,7 +11,6 @@ import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "GeoChecksService"; export interface IGeoChecksService { - readonly serviceName: string; buildGeoCheck(monitor: Monitor): Promise; createGeoChecks(geoChecks: GeoCheck[]): Promise; getGeoChecksByMonitor(args: { @@ -50,10 +49,6 @@ export class GeoChecksService implements IGeoChecksService { this.monitorsRepository = monitorsRepository; } - get serviceName() { - return GeoChecksService.SERVICE_NAME; - } - async buildGeoCheck(monitor: Monitor): Promise { try { if (!monitor.url) { diff --git a/server/src/domain/incidents/incident.service.ts b/server/src/domain/incidents/incident.service.ts index 311fad567..98f092088 100644 --- a/server/src/domain/incidents/incident.service.ts +++ b/server/src/domain/incidents/incident.service.ts @@ -57,10 +57,6 @@ export class IncidentService implements IIncidentService { this.notificationMessageBuilder = notificationMessageBuilder; } - get serviceName() { - return IncidentService.SERVICE_NAME; - } - handleIncident = async ( monitor: Monitor, code: number, diff --git a/server/src/domain/invites/invite.service.ts b/server/src/domain/invites/invite.service.ts index 76c5c6289..3dcffd9e0 100644 --- a/server/src/domain/invites/invite.service.ts +++ b/server/src/domain/invites/invite.service.ts @@ -35,10 +35,6 @@ export class InviteService implements IInviteService { this.emailService = emailService; } - get serviceName() { - return InviteService.SERVICE_NAME; - } - getInviteToken = async ({ invite, teamId, userRoles }: { invite: Partial; teamId: string; userRoles: UserRole[] }) => { invite.teamId = teamId; diff --git a/server/src/domain/maintenance-windows/maintenance-window.service.ts b/server/src/domain/maintenance-windows/maintenance-window.service.ts index 5551aef62..350e755aa 100644 --- a/server/src/domain/maintenance-windows/maintenance-window.service.ts +++ b/server/src/domain/maintenance-windows/maintenance-window.service.ts @@ -62,10 +62,6 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { this.scheduler = scheduler; } - get serviceName() { - return MaintenanceWindowService.SERVICE_NAME; - } - private flipMonitorsLeavingMaintenance = async (monitorIds: string[], teamId: string, excludeWindowId: string, now: Date): Promise => { if (monitorIds.length === 0) return; diff --git a/server/src/domain/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index 66684c2ac..e7a79a161 100644 --- a/server/src/domain/monitors/monitor.service.ts +++ b/server/src/domain/monitors/monitor.service.ts @@ -31,8 +31,6 @@ const isUptimeChecksResult = (result: UptimeChecksResult | HardwareChecksResult supportsUptimeDetails(result.monitorType); export interface IMonitorService { - readonly serviceName: string; - // create createMonitor(teamId: string, userId: string, body: Partial): Promise; createMonitors(monitors: Array): Promise; @@ -135,10 +133,6 @@ export class MonitorService implements IMonitorService { this.incidentsRepository = incidentsRepository; } - get serviceName(): string { - return MonitorService.SERVICE_NAME; - } - private getDateRange = (dateRange: DateRangeKey) => { const startDates = { recent: new Date(new Date().setHours(new Date().getHours() - 2)), diff --git a/server/src/domain/queue-workers/queue-worker.model.ts b/server/src/domain/queue-workers/queue-worker.model.ts index 0dd90b5c5..c89ecca5d 100644 --- a/server/src/domain/queue-workers/queue-worker.model.ts +++ b/server/src/domain/queue-workers/queue-worker.model.ts @@ -4,7 +4,8 @@ import { QueueWorkerDocument } from "@/domain/queue-workers/queue-worker.type.js // Single source of truth for worker liveness: the TTL that GCs stale records and // the window readers use to count a worker as alive. Heartbeat cadence must stay well below it. -export const WORKER_TTL_SECONDS = 180; +const WORKER_TTL_SECONDS = 180; +export const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000; const QueueWorkerSchema = new Schema( { diff --git a/server/src/domain/users/user.repository.mongo.ts b/server/src/domain/users/user.repository.mongo.ts index b137c5f4c..af4b78b26 100644 --- a/server/src/domain/users/user.repository.mongo.ts +++ b/server/src/domain/users/user.repository.mongo.ts @@ -2,7 +2,6 @@ import { IUsersRepository } from "@/domain/users/user.repository.interface.js"; import { UserModel, type UserDocument } from "@/domain/users/user.model.js"; import type { User, UserProfileImage } from "@/domain/users/user.type.js"; import { GenerateAvatarImage } from "@/utils/imageProcessing.js"; -import { ParseBoolean } from "@/utils/utils.js"; import { AppError } from "@/utils/AppError.js"; import { toStringId, toDateString } from "@/utils/mongoMappers.js"; const SERVICE_NAME = "MongoUsersRepository"; @@ -87,9 +86,8 @@ class MongoUsersRepository implements IUsersRepository { const candidateUser = { ...patch }; let unsetFields: Record | undefined; - if (ParseBoolean(candidateUser.deleteProfileImage) === true) { + if (candidateUser.deleteProfileImage === true) { unsetFields = { profileImage: 1, avatarImage: 1 }; - delete candidateUser.deleteProfileImage; } else if (file) { // 1. Save the full size image candidateUser.profileImage = { diff --git a/server/src/domain/users/user.service.ts b/server/src/domain/users/user.service.ts index 7bdb9677f..f3db2f3b5 100644 --- a/server/src/domain/users/user.service.ts +++ b/server/src/domain/users/user.service.ts @@ -24,7 +24,7 @@ export interface IUserService { createUser(userData: Partial, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise; loginUser(email: string, password: string): Promise<{ user: User; token: string }>; editUser( - updates: Partial, + updates: Partial, file: Express.Multer.File | null, currentUserId: string, currentUserEmail: string @@ -103,10 +103,6 @@ export class UserService implements IUserService { this.teamsRepository = teamsRepository; } - get serviceName() { - return UserService.SERVICE_NAME; - } - issueToken = (payload: Partial, appSettings: EnvConfig) => { return this.jwt.sign(payload, appSettings.jwtSecret, { expiresIn: appSettings.jwtTTL }); }; @@ -242,7 +238,7 @@ export class UserService implements IUserService { }; editUser = async ( - updates: Partial, + updates: Partial, file: Express.Multer.File | null, currentUserId: string, currentUserEmail: string diff --git a/server/src/index.ts b/server/src/index.ts index b5e870ded..68ef00a1f 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -27,7 +27,7 @@ const startApp = async () => { const env = validateEnv(logger); logger.setLogLevel(env.LOG_LEVEL); const settingsService = new SettingsService(env); - const envSettings = settingsService.loadSettings(); + const envSettings = settingsService.getSettings(); const { queueMode, queuePrimaryProcesses } = envSettings; logger.info({ diff --git a/server/src/service/bufferService.ts b/server/src/service/bufferService.ts index d47a6472e..2d10aebbb 100755 --- a/server/src/service/bufferService.ts +++ b/server/src/service/bufferService.ts @@ -51,10 +51,6 @@ export class BufferService implements IBufferService { }); } - get serviceName() { - return BufferService.SERVICE_NAME; - } - addToBuffer(check: Check) { try { this.buffer.push(check); diff --git a/server/src/service/emailService.ts b/server/src/service/emailService.ts index c156cc18b..26b89f84b 100755 --- a/server/src/service/emailService.ts +++ b/server/src/service/emailService.ts @@ -56,10 +56,6 @@ export class EmailService implements IEmailService { this.init(); } - get serviceName() { - return EmailService.SERVICE_NAME; - } - init = () => { const loadTemplate = (templateName: string) => { try { @@ -79,7 +75,6 @@ export class EmailService implements IEmailService { this.templateLookup = { welcomeEmailTemplate: loadTemplate("welcomeEmail"), employeeActivationTemplate: loadTemplate("employeeActivation"), - noIncidentsThisWeekTemplate: loadTemplate("noIncidentsThisWeek"), passwordResetTemplate: loadTemplate("passwordReset"), testEmailTemplate: loadTemplate("testEmailTemplate"), unifiedNotificationTemplate: loadTemplate("unifiedNotification"), diff --git a/server/src/service/globalPingService.ts b/server/src/service/globalPingService.ts index cebd95408..4b0257df4 100644 --- a/server/src/service/globalPingService.ts +++ b/server/src/service/globalPingService.ts @@ -61,7 +61,6 @@ interface GlobalPingProbeResult { } export interface IGlobalPingService { - readonly serviceName: string; createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise; pollForResults(measurementId: string, timeoutMs?: number, customUpCodes?: HttpStatusCode[]): Promise; } @@ -75,10 +74,6 @@ export class GlobalPingService implements IGlobalPingService { this.logger = logger; } - get serviceName() { - return GlobalPingService.SERVICE_NAME; - } - async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise { try { if (!supportsGeoCheck(monitorType)) { diff --git a/server/src/service/networkService.ts b/server/src/service/networkService.ts index 3470c6086..632a03141 100644 --- a/server/src/service/networkService.ts +++ b/server/src/service/networkService.ts @@ -8,7 +8,6 @@ import { IStatusProvider } from "./network/IStatusProvider.js"; const SERVICE_NAME = "NetworkService"; export interface INetworkService { - readonly serviceName: string; requestStatus(monitor: Monitor & { type: T }): Promise>; requestWebhook( type: string, @@ -39,10 +38,6 @@ export class NetworkService implements INetworkService { this.logger = logger; } - get serviceName(): string { - return NetworkService.SERVICE_NAME; - } - // Main entry point async requestStatus(monitor: Monitor & { type: T }): Promise> { const provider = this.providers.find((p) => p.supports(monitor.type)); diff --git a/server/src/service/statusService.ts b/server/src/service/statusService.ts index ccda0c5d7..6435958cb 100755 --- a/server/src/service/statusService.ts +++ b/server/src/service/statusService.ts @@ -57,10 +57,6 @@ export class StatusService implements IStatusService { this.monitorStatsRepository = monitorStatsRepository; } - get serviceName() { - return StatusService.SERVICE_NAME; - } - async updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse) { try { await this.monitorStatsRepository.updateByMonitorId(monitor.id, { diff --git a/server/src/templates/addReview.mjml b/server/src/templates/addReview.mjml deleted file mode 100755 index e193ac1ba..000000000 --- a/server/src/templates/addReview.mjml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - Message from Checkmate Service - - - - - - -

Hello {{ name }}!

-

- We hope you’re finding Checkmate helpful in monitoring your infrastructure. Your support means a lot to us, and we - truly appreciate having you as part of our community. -

-

- If you’re happy with Checkmate, we’d love to hear about your experience! Leaving a review on G2 helps others discover Checkmate and - supports our ongoing improvements. -

- G2 Link: TBD -

Thank you for taking the time to share your thoughts - we greatly appreciate it!

- Checkmate Team -
-
- - - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/templates/noIncidentsThisWeek.mjml b/server/src/templates/noIncidentsThisWeek.mjml deleted file mode 100755 index d160e44de..000000000 --- a/server/src/templates/noIncidentsThisWeek.mjml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - Message from Checkmate Service - - - No incidents this week! - - - - - - -

Hello {{ name }}!

-

There were no incidents this week. Good job!

-

Current monitors:

-

Google: 100% availability

-

Canada.ca:100% availability

-
-
- - - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/utils/dataUtils.ts b/server/src/utils/dataUtils.ts index 7b46619e8..b264f85b1 100755 --- a/server/src/utils/dataUtils.ts +++ b/server/src/utils/dataUtils.ts @@ -20,72 +20,45 @@ export const getDateForRange = (dateRange: string): Date | undefined => { } }; -const calculatePercentile = (arr: T[], percentile: number): number => { - const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime); +const percentileBy = (arr: T[], percentile: number, value: (item: T) => number): number => { + const sorted = arr.slice().sort((a, b) => value(a) - value(b)); const index = (percentile / 100) * (sorted.length - 1); const lower = Math.floor(index); const upper = lower + 1; const weight = index % 1; - if (upper >= sorted.length) return sorted[lower]!.responseTime; - return sorted[lower]!.responseTime * (1 - weight) + sorted[upper]!.responseTime * weight; + if (upper >= sorted.length) return value(sorted[lower]!); + return value(sorted[lower]!) * (1 - weight) + value(sorted[upper]!) * weight; }; -const calculatePercentileUptimeDetails = (arr: GroupedCheck[], percentile: number): number => { - const sorted = arr.slice().sort((a, b) => a.avgResponseTime - b.avgResponseTime); - const index = (percentile / 100) * (sorted.length - 1); - const lower = Math.floor(index); - const upper = lower + 1; - const weight = index % 1; - if (upper >= sorted.length) return sorted[lower]!.avgResponseTime; - return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight; +const rescale = (raw: number, min: number, max: number, rangeMin: number, rangeMax: number): number => { + const scaled = rangeMin + ((raw - min) * (rangeMax - rangeMin)) / (max - min); + return Math.max(rangeMin, Math.min(rangeMax, scaled)); }; export const NormalizeData = (checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck[] => { - if (checks.length > 1) { - const min = calculatePercentile(checks, 0); - const max = calculatePercentile(checks, 95); - const normalizedChecks = checks.map((check) => { - const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0; - let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min); - - normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); - return { - ...check, - responseTime: normalizedResponseTime, - originalResponseTime: originalResponseTime, - }; - }); - - return normalizedChecks; - } else { - return checks.map((check) => { - const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0; - return { ...check, originalResponseTime: originalResponseTime }; - }); + const original = (check: T) => (typeof check.responseTime === "number" ? check.responseTime : 0); + if (checks.length <= 1) { + return checks.map((check) => ({ ...check, originalResponseTime: original(check) })); } + const min = percentileBy(checks, 0, (c) => c.responseTime); + const max = percentileBy(checks, 95, (c) => c.responseTime); + return checks.map((check) => ({ + ...check, + responseTime: rescale(check.responseTime, min, max, rangeMin, rangeMax), + originalResponseTime: original(check), + })); }; export const NormalizeDataUptimeDetails = (checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck[] => { - if (checks.length > 1) { - const min = calculatePercentileUptimeDetails(checks, 0); - const max = calculatePercentileUptimeDetails(checks, 95); - - const normalizedChecks = checks.map((check) => { - const originalResponseTime = check.avgResponseTime; - let normalizedResponseTime = rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min); - - normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); - return { - ...check, - avgResponseTime: normalizedResponseTime, - originalAvgResponseTime: originalResponseTime, - }; - }); - - return normalizedChecks; - } else { - return checks.map((check) => { - return { ...check, originalAvgResponseTime: check.avgResponseTime }; - }); + const original = (check: T) => (typeof check.avgResponseTime === "number" ? check.avgResponseTime : 0); + if (checks.length <= 1) { + return checks.map((check) => ({ ...check, originalAvgResponseTime: original(check) })); } + const min = percentileBy(checks, 0, (c) => c.avgResponseTime); + const max = percentileBy(checks, 95, (c) => c.avgResponseTime); + return checks.map((check) => ({ + ...check, + avgResponseTime: rescale(check.avgResponseTime, min, max, rangeMin, rangeMax), + originalAvgResponseTime: original(check), + })); }; diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts index 41142fd92..3cd8570ca 100755 --- a/server/src/utils/logger.ts +++ b/server/src/utils/logger.ts @@ -19,7 +19,6 @@ interface LogEntry extends LogConfig { } export interface ILogger { - readonly serviceName: string; info(config: LogConfig): void; warn(config: LogConfig): void; error(config: LogConfig): void; @@ -97,10 +96,6 @@ class Logger implements ILogger { }); } - get serviceName() { - return Logger.SERVICE_NAME; - } - info(config: LogConfig) { const logEntry = this.buildLogEntry("info", config); this.cacheLog(logEntry); diff --git a/server/src/utils/utils.ts b/server/src/utils/utils.ts deleted file mode 100755 index a82d8734f..000000000 --- a/server/src/utils/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ParseBoolean = (value: boolean | string | null | undefined) => { - if (value === true || value === "true") { - return true; - } else if (value === false || value === "false" || value === null || value === undefined) { - return false; - } -}; - -export const getTokenFromHeaders = (headers: Record) => { - const authorizationHeader = headers.authorization; - if (!authorizationHeader) throw new Error("No auth headers"); - - const parts = authorizationHeader.split(" "); - if (parts.length !== 2 || parts[0] !== "Bearer") throw new Error("Invalid auth headers"); - - return parts[1]; -}; diff --git a/server/src/worker/worker.db-queue.ts b/server/src/worker/worker.db-queue.ts index 50e41a4f1..6f4f31353 100644 --- a/server/src/worker/worker.db-queue.ts +++ b/server/src/worker/worker.db-queue.ts @@ -11,14 +11,13 @@ import { ICheckPipeline } from "@/worker/worker.check-pipeline.js"; import { IReactorDispatcher } from "@/worker/reactors/reactor.dispatcher.js"; import { IWorkerHelper } from "@/worker/worker.helper.js"; import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js"; -import { WORKER_TTL_SECONDS } from "@/domain/queue-workers/queue-worker.model.js"; +import { WORKER_STALE_MS } from "@/domain/queue-workers/queue-worker.model.js"; import { QueueMode } from "@/domain/app-settings/app-settings.type.js"; import { JobScheduler } from "@/worker/worker.job-scheduler.js"; import { IBufferService } from "@/service/bufferService.js"; +import { POLL_MS } from "@/worker/worker.job-scheduler.js"; const SERVICE_NAME = "JobQueue"; -const POLL_MS = 250; // base poll interval while a loop is actively claiming work const POLL_MAX_MS = 5000; // back off poll interval to 5s if no jobs -const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000; // a worker counts as alive if seen within this window const LOCK_RENEW_MS = LOCK_MS / 3; // renew an in-flight job's lock at 1/3 the lease, so it survives two missed renewals const DRAIN_TIMEOUT_MS = 25_000; const DRAIN_POLL_MS = 200; @@ -95,10 +94,6 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker { this.isDbConnected = dependencies.isDbConnected; } - get serviceName() { - return DBQueueWorker.SERVICE_NAME; - } - static async create(dependencies: DBQueueWorkerDependencies): Promise { const instance = new DBQueueWorker(dependencies); await instance.init(); diff --git a/server/src/worker/worker.helper.ts b/server/src/worker/worker.helper.ts index 3d2d2e183..9f044763d 100644 --- a/server/src/worker/worker.helper.ts +++ b/server/src/worker/worker.helper.ts @@ -12,7 +12,6 @@ import { ITeamsRepository } from "@/domain/teams/team.repository.interface.js"; import { ILogger } from "@/utils/logger.js"; export interface IWorkerHelper { - readonly serviceName: string; getCleanupOrphanedJob(): () => Promise; getCleanupRetentionJob(): () => Promise; } @@ -69,10 +68,6 @@ export class WorkerHelper implements IWorkerHelper { this.geoChecksRepository = geoChecksRepository; } - get serviceName() { - return WorkerHelper.SERVICE_NAME; - } - getCleanupOrphanedJob = () => { return async () => { try { diff --git a/server/src/worker/worker.interface.ts b/server/src/worker/worker.interface.ts index 98120fa47..e70af5f35 100644 --- a/server/src/worker/worker.interface.ts +++ b/server/src/worker/worker.interface.ts @@ -67,7 +67,6 @@ export type WorkerHealth = { inFlight: number; // total in-flight jobs across types }; export interface IJobScheduler { - readonly serviceName: string; addJob(monitorId: string, monitor: Monitor): Promise; deleteJob(monitor: Monitor): Promise; pauseJob(monitor: Monitor): Promise; @@ -83,7 +82,6 @@ export interface IJobScheduler { } export interface IQueueWorker extends IJobScheduler { - readonly serviceName: string; getHealth(): WorkerHealth; countDueBacklog(): Promise; countAliveWorkers(): Promise; diff --git a/server/src/worker/worker.job-scheduler.ts b/server/src/worker/worker.job-scheduler.ts index 458db1d08..433e69955 100644 --- a/server/src/worker/worker.job-scheduler.ts +++ b/server/src/worker/worker.job-scheduler.ts @@ -5,13 +5,12 @@ import { Monitor, supportsGeoCheck } from "@/domain/monitors/monitor.types.js"; import { type Job, type JobSeed, type JobType, jobId } from "@/domain/jobs/job.type.js"; import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js"; import { WorkerJobsPagination, WorkerJobSummary, WorkerMetrics } from "@/worker/worker.interface.js"; -import { WORKER_TTL_SECONDS } from "@/domain/queue-workers/queue-worker.model.js"; +import { WORKER_STALE_MS } from "@/domain/queue-workers/queue-worker.model.js"; import { QueueMode } from "@/domain/app-settings/app-settings.type.js"; import { ILogger } from "@/utils/logger.js"; -const POLL_MS = 250; // base poll interval while a loop is actively claiming work +export const POLL_MS = 250; // base poll interval while a loop is actively claiming work const SERVICE_NAME = "JobScheduler"; -const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000; // a worker counts as alive if seen within this window const HEARTBEAT_MS = WORKER_STALE_MS / 3; // Worker can miss two beats without being considered stale const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000; @@ -26,9 +25,6 @@ export class JobScheduler implements IJobScheduler { protected readonly workerId: string // same id MongoJobsRepository stamps into lockedBy ) {} - get serviceName() { - return JobScheduler.SERVICE_NAME; - } private heartbeatInFlight: Promise = Promise.resolve(); protected processesJobs = false; // scheduler-only by default; DBQueueWorker flips this on when it runs job loops diff --git a/server/test/unit/services/bufferService.test.ts b/server/test/unit/services/bufferService.test.ts index d994e5f24..90b5489bf 100644 --- a/server/test/unit/services/bufferService.test.ts +++ b/server/test/unit/services/bufferService.test.ts @@ -71,17 +71,6 @@ describe("BufferService", () => { // ── Static / instance properties ───────────────────────────────────── - describe("serviceName", () => { - it("returns BufferService from static property", () => { - expect(BufferService.SERVICE_NAME).toBe("BufferService"); - }); - - it("returns BufferService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("BufferService"); - }); - }); - // ── constructor ────────────────────────────────────────────────────── describe("constructor", () => { diff --git a/server/test/unit/services/checkService.test.ts b/server/test/unit/services/checkService.test.ts index 104ccbaa2..587731951 100644 --- a/server/test/unit/services/checkService.test.ts +++ b/server/test/unit/services/checkService.test.ts @@ -48,13 +48,6 @@ const makeStatusResponse = (overrides?: Partial): Monitor // ── Tests ──────────────────────────────────────────────────────────────────── describe("CheckService", () => { - describe("serviceName", () => { - it("returns checkService", () => { - const { service } = createService(); - expect(service.serviceName).toBe("checkService"); - }); - }); - // ── toCheck ────────────────────────────────────────────────────────────── describe("toCheck", () => { diff --git a/server/test/unit/services/diagnosticService.test.ts b/server/test/unit/services/diagnosticService.test.ts index 07962a68e..d2512717f 100644 --- a/server/test/unit/services/diagnosticService.test.ts +++ b/server/test/unit/services/diagnosticService.test.ts @@ -56,17 +56,6 @@ describe("DiagnosticService", () => { jest.useRealTimers(); }); - describe("serviceName", () => { - it("returns diagnosticService from static property", () => { - expect(DiagnosticService.SERVICE_NAME).toBe("diagnosticService"); - }); - - it("returns diagnosticService from instance getter", () => { - const service = new DiagnosticService(makeDb()); - expect(service.serviceName).toBe("diagnosticService"); - }); - }); - // ── constructor ───────────────────────────────────────────────────────── describe("constructor", () => { diff --git a/server/test/unit/services/emailService.test.ts b/server/test/unit/services/emailService.test.ts index fbfdf15fd..971c712da 100644 --- a/server/test/unit/services/emailService.test.ts +++ b/server/test/unit/services/emailService.test.ts @@ -82,11 +82,6 @@ describe("EmailService", () => { it("returns EmailService from static property", () => { expect(EmailService.SERVICE_NAME).toBe("EmailService"); }); - - it("returns EmailService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("EmailService"); - }); }); // ── init / loadTemplate ────────────────────────────────────────────── @@ -95,14 +90,14 @@ describe("EmailService", () => { it("loads all expected templates on construction", () => { const { mockFs } = createService(); - // 6 templates loaded - expect(mockFs.readFileSync).toHaveBeenCalledTimes(6); + // 5 templates loaded + expect(mockFs.readFileSync).toHaveBeenCalledTimes(5); }); it("compiles each loaded template", () => { const { compile } = createService(); - expect(compile).toHaveBeenCalledTimes(6); + expect(compile).toHaveBeenCalledTimes(5); }); it("logs error when a template file is not found", () => { @@ -146,7 +141,7 @@ describe("EmailService", () => { service.init(); - expect(mockFs.readFileSync).toHaveBeenCalledTimes(initialCalls + 6); + expect(mockFs.readFileSync).toHaveBeenCalledTimes(initialCalls + 5); }); }); diff --git a/server/test/unit/services/geoChecksService.test.ts b/server/test/unit/services/geoChecksService.test.ts index 8e50fa3e8..69bfff730 100644 --- a/server/test/unit/services/geoChecksService.test.ts +++ b/server/test/unit/services/geoChecksService.test.ts @@ -48,17 +48,6 @@ const createService = (overrides?: Record) => { // ── Tests ──────────────────────────────────────────────────────────────────── describe("GeoChecksService", () => { - describe("serviceName", () => { - it("returns GeoChecksService from static property", () => { - expect(GeoChecksService.SERVICE_NAME).toBe("GeoChecksService"); - }); - - it("returns GeoChecksService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("GeoChecksService"); - }); - }); - // ── buildGeoCheck ─────────────────────────────────────────────────────── describe("buildGeoCheck", () => { diff --git a/server/test/unit/services/globalPingService.test.ts b/server/test/unit/services/globalPingService.test.ts index 376653110..5169f205b 100644 --- a/server/test/unit/services/globalPingService.test.ts +++ b/server/test/unit/services/globalPingService.test.ts @@ -66,17 +66,6 @@ describe("GlobalPingService", () => { // ── Static / instance properties ───────────────────────────────────── - describe("serviceName", () => { - it("returns GlobalPingService from static property", () => { - expect(GlobalPingService.SERVICE_NAME).toBe("GlobalPingService"); - }); - - it("returns GlobalPingService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("GlobalPingService"); - }); - }); - // ── createMeasurement ──────────────────────────────────────────────── describe("createMeasurement", () => { diff --git a/server/test/unit/services/incidentService.test.ts b/server/test/unit/services/incidentService.test.ts index aa4fffce8..27b2736df 100644 --- a/server/test/unit/services/incidentService.test.ts +++ b/server/test/unit/services/incidentService.test.ts @@ -91,13 +91,6 @@ const makeDecision = (overrides?: Partial): MonitorAction // ── Tests ──────────────────────────────────────────────────────────────────── describe("IncidentService", () => { - describe("serviceName", () => { - it("returns incidentService", () => { - const { service } = createService(); - expect(service.serviceName).toBe("incidentService"); - }); - }); - // ── handleIncident ─────────────────────────────────────────────────────── describe("handleIncident", () => { diff --git a/server/test/unit/services/inviteService.test.ts b/server/test/unit/services/inviteService.test.ts index c050d91e6..59d472539 100644 --- a/server/test/unit/services/inviteService.test.ts +++ b/server/test/unit/services/inviteService.test.ts @@ -39,17 +39,6 @@ const createService = (overrides?: Record) => { // ── Tests ──────────────────────────────────────────────────────────────────── describe("InviteService", () => { - describe("serviceName", () => { - it("returns inviteService from static property", () => { - expect(InviteService.SERVICE_NAME).toBe("inviteService"); - }); - - it("returns inviteService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("inviteService"); - }); - }); - // ── getInviteToken ────────────────────────────────────────────────────── describe("getInviteToken", () => { diff --git a/server/test/unit/services/maintenanceWindowService.test.ts b/server/test/unit/services/maintenanceWindowService.test.ts index 9547eaac8..e15f20d46 100644 --- a/server/test/unit/services/maintenanceWindowService.test.ts +++ b/server/test/unit/services/maintenanceWindowService.test.ts @@ -94,17 +94,6 @@ const defaultCreateParams = { // ── Tests ──────────────────────────────────────────────────────────────────── describe("MaintenanceWindowService", () => { - describe("serviceName", () => { - it("returns maintenanceWindowService from static property", () => { - expect(MaintenanceWindowService.SERVICE_NAME).toBe("maintenanceWindowService"); - }); - - it("returns maintenanceWindowService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("maintenanceWindowService"); - }); - }); - // ── createMaintenanceWindow ────────────────────────────────────────────── describe("createMaintenanceWindow", () => { diff --git a/server/test/unit/services/monitorService.test.ts b/server/test/unit/services/monitorService.test.ts index 6a1cf8343..2b6f72d1d 100644 --- a/server/test/unit/services/monitorService.test.ts +++ b/server/test/unit/services/monitorService.test.ts @@ -111,13 +111,6 @@ const createService = ( }; describe("MonitorService", () => { - describe("serviceName", () => { - it("returns 'MonitorService'", () => { - const { service } = createService(); - expect(service.serviceName).toBe("MonitorService"); - }); - }); - describe("createMonitor", () => { it("creates a monitor and adds a job", async () => { const monitorsRepository = createMonitorsRepositoryMock(); diff --git a/server/test/unit/services/networkService.test.ts b/server/test/unit/services/networkService.test.ts index 268b52e3a..0943b1f22 100644 --- a/server/test/unit/services/networkService.test.ts +++ b/server/test/unit/services/networkService.test.ts @@ -43,17 +43,6 @@ const createService = (providers: IStatusProvider[] = []) => { describe("NetworkService", () => { // ── Static / instance properties ───────────────────────────────────────── - describe("serviceName", () => { - it("returns NetworkService from static property", () => { - expect(NetworkService.SERVICE_NAME).toBe("NetworkService"); - }); - - it("returns NetworkService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("NetworkService"); - }); - }); - // ── requestStatus ──────────────────────────────────────────────────────── describe("requestStatus", () => { diff --git a/server/test/unit/services/settingsService.test.ts b/server/test/unit/services/settingsService.test.ts index 43ea7fe18..063a64f6d 100644 --- a/server/test/unit/services/settingsService.test.ts +++ b/server/test/unit/services/settingsService.test.ts @@ -56,23 +56,12 @@ const createService = (envOverrides?: Partial) => { // ── Tests ──────────────────────────────────────────────────────────────────── describe("SettingsService", () => { - describe("serviceName", () => { - it("returns SettingsService from static property", () => { - expect(SettingsService.SERVICE_NAME).toBe("SettingsService"); - }); + // ── getSettings ───────────────────────────────────────────────────────── - it("returns SettingsService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("SettingsService"); - }); - }); - - // ── loadSettings ──────────────────────────────────────────────────────── - - describe("loadSettings", () => { + describe("getSettings", () => { it("returns env config mapped from constructor", () => { const { service } = createService(); - const settings = service.loadSettings(); + const settings = service.getSettings(); expect(settings).toEqual({ jwtSecret: "test-secret", @@ -93,7 +82,7 @@ describe("SettingsService", () => { CLIENT_CONFIG_LOG_LEVEL: "warn", }); - expect(service.loadSettings().clientConfig).toEqual({ + expect(service.getSettings().clientConfig).toEqual({ apiBaseUrl: "https://api.example.com/api/v1", logLevel: "warn", }); @@ -101,23 +90,13 @@ describe("SettingsService", () => { it("reflects custom env values", () => { const { service } = createService({ NODE_ENV: "production", LOG_LEVEL: "error" }); - const settings = service.loadSettings(); + const settings = service.getSettings(); expect(settings.nodeEnv).toBe("production"); expect(settings.logLevel).toBe("error"); }); }); - // ── getSettings ───────────────────────────────────────────────────────── - - describe("getSettings", () => { - it("returns the same config as loadSettings", () => { - const { service } = createService(); - - expect(service.getSettings()).toEqual(service.loadSettings()); - }); - }); - // ── areStatusPageThemesEnabled ────────────────────────────────────────── describe("areStatusPageThemesEnabled", () => { diff --git a/server/test/unit/services/statusService.test.ts b/server/test/unit/services/statusService.test.ts index 5712de301..4f4c0264a 100644 --- a/server/test/unit/services/statusService.test.ts +++ b/server/test/unit/services/statusService.test.ts @@ -143,13 +143,6 @@ const makeExistingStats = (overrides?: Record) => ({ // ── Tests ──────────────────────────────────────────────────────────────────── describe("StatusService", () => { - describe("serviceName", () => { - it("returns StatusService", () => { - const { service } = createService(); - expect(service.serviceName).toBe("StatusService"); - }); - }); - // ── updateRunningStats ─────────────────────────────────────────────────── describe("updateRunningStats", () => { diff --git a/server/test/unit/services/userService.test.ts b/server/test/unit/services/userService.test.ts index 061003e7f..bce8769cf 100644 --- a/server/test/unit/services/userService.test.ts +++ b/server/test/unit/services/userService.test.ts @@ -98,13 +98,6 @@ const createService = (overrides?: Record) => { // ── Tests ──────────────────────────────────────────────────────────────────── describe("UserService", () => { - describe("serviceName", () => { - it("returns userService", () => { - const { service } = createService(); - expect(service.serviceName).toBe("userService"); - }); - }); - // ── issueToken ────────────────────────────────────────────────────────── describe("issueToken", () => { diff --git a/server/test/unit/services/workerHelper.test.ts b/server/test/unit/services/workerHelper.test.ts index 96b45d3b3..fe7dca42d 100644 --- a/server/test/unit/services/workerHelper.test.ts +++ b/server/test/unit/services/workerHelper.test.ts @@ -71,13 +71,6 @@ const createHelper = (overrides?: Record) => { // ── Tests ──────────────────────────────────────────────────────────────────── describe("WorkerHelper", () => { - describe("serviceName", () => { - it("returns JobQueueHelper", () => { - const { helper } = createHelper(); - expect(helper.serviceName).toBe("JobQueueHelper"); - }); - }); - // ── getCleanupOrphanedJob ──────────────────────────────────────────────── describe("getCleanupOrphanedJob", () => { diff --git a/server/test/unit/utils/dataUtils.test.ts b/server/test/unit/utils/dataUtils.test.ts new file mode 100644 index 000000000..f4893fe03 --- /dev/null +++ b/server/test/unit/utils/dataUtils.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it } from "@jest/globals"; +import { getDateForRange, NormalizeData, NormalizeDataUptimeDetails } from "../../../src/utils/dataUtils.ts"; +import type { GroupedCheck } from "../../../src/domain/checks/check.type.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeCheck = (responseTime: number, overrides?: Record) => ({ + id: `check-${responseTime}`, + status: true, + responseTime, + ...overrides, +}); + +const makeGrouped = (avgResponseTime: number, overrides?: Partial): GroupedCheck => ({ + bucketDate: `bucket-${avgResponseTime}`, + avgResponseTime, + totalChecks: 5, + ...overrides, +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("getDateForRange", () => { + it.each([ + ["recent", 2 * 60 * 60 * 1000], + ["hour", 60 * 60 * 1000], + ["day", 24 * 60 * 60 * 1000], + ["week", 7 * 24 * 60 * 60 * 1000], + ["month", 30 * 24 * 60 * 60 * 1000], + ])("returns now minus the %s window", (range, offsetMs) => { + const before = Date.now(); + const result = getDateForRange(range as string); + const after = Date.now(); + + expect(result).toBeInstanceOf(Date); + expect(result!.getTime()).toBeGreaterThanOrEqual(before - offsetMs); + expect(result!.getTime()).toBeLessThanOrEqual(after - offsetMs); + }); + + it("returns undefined for 'all'", () => { + expect(getDateForRange("all")).toBeUndefined(); + }); + + it("returns undefined for unknown ranges", () => { + expect(getDateForRange("fortnight")).toBeUndefined(); + }); +}); + +describe("NormalizeData", () => { + // Fixture math: p0 of [100..500] is 100; p95 interpolates between the 4th and 5th + // sorted values at weight 0.8 → 400*0.2 + 500*0.8 = 480. Values rescale into + // [10, 100] via 10 + (rt - 100) * 90 / 380, clamped. + it("rescales responseTime into [rangeMin, rangeMax] anchored to the p0–p95 window", () => { + const checks = [100, 200, 300, 400, 500].map((rt) => makeCheck(rt)); + + const result = NormalizeData(checks, 10, 100); + + expect(result[0]!.responseTime).toBe(10); + expect(result[1]!.responseTime).toBeCloseTo(33.6842, 4); + expect(result[2]!.responseTime).toBeCloseTo(57.3684, 4); + expect(result[3]!.responseTime).toBeCloseTo(81.0526, 4); + }); + + it("clamps values above the p95 anchor to rangeMax", () => { + const checks = [100, 200, 300, 400, 500].map((rt) => makeCheck(rt)); + + const result = NormalizeData(checks, 10, 100); + + expect(result[4]!.responseTime).toBe(100); + }); + + it("preserves the raw value in originalResponseTime and spreads other fields", () => { + const checks = [makeCheck(100, { message: "OK" }), makeCheck(500)]; + + const result = NormalizeData(checks, 10, 100); + + expect(result[0]!.originalResponseTime).toBe(100); + expect(result[1]!.originalResponseTime).toBe(500); + expect(result[0]).toMatchObject({ id: "check-100", status: true, message: "OK" }); + }); + + it("passes a single check through, only adding originalResponseTime", () => { + const result = NormalizeData([makeCheck(250)], 10, 100); + + expect(result).toEqual([{ id: "check-250", status: true, responseTime: 250, originalResponseTime: 250 }]); + }); + + it("returns an empty array for empty input", () => { + expect(NormalizeData([], 10, 100)).toEqual([]); + }); + + it("records originalResponseTime as 0 for a non-numeric responseTime (failed checks)", () => { + const checks = [makeCheck(undefined as unknown as number), makeCheck(100), makeCheck(200)]; + + const result = NormalizeData(checks, 10, 100); + + expect(result[0]!.originalResponseTime).toBe(0); + }); + + // Characterization, not endorsement: when every value is equal the p0–p95 window + // has zero width and the rescale divides by zero. Fixing this is a planned + // follow-up (docs/planning/dataUtils-generic-normalization-refactor.md); this + // test pins the current behavior so the fix is a deliberate, visible change. + it("produces NaN when all values are equal (division by zero — known edge case)", () => { + const result = NormalizeData([makeCheck(50), makeCheck(50), makeCheck(50)], 10, 100); + + result.forEach((check) => { + expect(check.responseTime).toBeNaN(); + expect(check.originalResponseTime).toBe(50); + }); + }); +}); + +describe("NormalizeDataUptimeDetails", () => { + it("rescales avgResponseTime into [rangeMin, rangeMax] anchored to the p0–p95 window", () => { + const checks = [100, 200, 300, 400, 500].map((avg) => makeGrouped(avg)); + + const result = NormalizeDataUptimeDetails(checks, 10, 100); + + expect(result[0]!.avgResponseTime).toBe(10); + expect(result[1]!.avgResponseTime).toBeCloseTo(33.6842, 4); + expect(result[4]!.avgResponseTime).toBe(100); + }); + + it("preserves the raw value in originalAvgResponseTime and spreads other fields", () => { + const checks = [makeGrouped(100), makeGrouped(500)]; + + const result = NormalizeDataUptimeDetails(checks, 10, 100); + + expect(result[0]!.originalAvgResponseTime).toBe(100); + expect(result[1]!.originalAvgResponseTime).toBe(500); + expect(result[0]).toMatchObject({ bucketDate: "bucket-100", totalChecks: 5 }); + }); + + it("passes a single bucket through, only adding originalAvgResponseTime", () => { + const result = NormalizeDataUptimeDetails([makeGrouped(250)], 10, 100); + + expect(result).toEqual([{ bucketDate: "bucket-250", avgResponseTime: 250, totalChecks: 5, originalAvgResponseTime: 250 }]); + }); + + it("returns an empty array for empty input", () => { + expect(NormalizeDataUptimeDetails([], 10, 100)).toEqual([]); + }); + + it("records originalAvgResponseTime as 0 for a null avgResponseTime ($avg over an empty bucket)", () => { + const result = NormalizeDataUptimeDetails([makeGrouped(null as unknown as number)], 10, 100); + + expect(result[0]!.originalAvgResponseTime).toBe(0); + }); + + it("produces NaN when all values are equal (division by zero — known edge case)", () => { + const result = NormalizeDataUptimeDetails([makeGrouped(50), makeGrouped(50)], 10, 100); + + result.forEach((check) => { + expect(check.avgResponseTime).toBeNaN(); + expect(check.originalAvgResponseTime).toBe(50); + }); + }); +}); diff --git a/server/test/unit/validation/userValidation.test.ts b/server/test/unit/validation/userValidation.test.ts index c50a70caa..1c2ffb83a 100644 --- a/server/test/unit/validation/userValidation.test.ts +++ b/server/test/unit/validation/userValidation.test.ts @@ -43,4 +43,25 @@ describe("editUserBodyValidation", () => { }); expect(result.success).toBe(false); }); + + // Regression: `deleteProfileImage` was missing from this schema, so z.object() + // stripped it before it reached the repository and the remove-profile-image + // flow was silently a no-op. The flag arrives as the multipart form-data + // string "true", so it must both survive parsing and coerce to a boolean. + it("retains deleteProfileImage and coerces the multipart string 'true' to boolean true", () => { + const result = editUserBodyValidation.safeParse({ deleteProfileImage: "true" }); + expect(result.success).toBe(true); + expect(result.data).toEqual({ deleteProfileImage: true }); + }); + + it("coerces the multipart string 'false' to boolean false", () => { + const result = editUserBodyValidation.safeParse({ deleteProfileImage: "false" }); + expect(result.success).toBe(true); + expect(result.data).toEqual({ deleteProfileImage: false }); + }); + + it("rejects a non-boolean deleteProfileImage value", () => { + const result = editUserBodyValidation.safeParse({ deleteProfileImage: "banana" }); + expect(result.success).toBe(false); + }); });