Compare commits

...
Author SHA1 Message Date
Alexander Holliday 321ca59380 Merge pull request #3368 from bluewave-labs/develop
Develop -> master (3.5.0)
2026-03-04 10:41:02 -08:00
Alexander Holliday 189293b04d Merge pull request #3366 from bluewave-labs/fix/eslint-warnings
fix: eslint warnings
2026-03-04 10:29:35 -08:00
Alex Holliday 13e9185011 bump version 2026-03-04 18:24:06 +00:00
Alex Holliday a7cf78cecc emailService 2026-03-04 18:16:31 +00:00
Alex Holliday 0b30e23783 globalPing 2026-03-04 17:50:27 +00:00
Alexander Holliday 1155619835 Merge pull request #3365 from bluewave-labs/fix/eslint
fix: eslint
2026-03-04 09:47:51 -08:00
Alex Holliday e0608ad3ed notificationMessageBuilder 2026-03-04 17:43:25 +00:00
Alex Holliday d665e95f02 discord 2026-03-04 17:39:31 +00:00
Alex Holliday afc27c804d email 2026-03-04 17:32:50 +00:00
Alex Holliday 1deb22bf63 pagerduty 2026-03-04 17:17:52 +00:00
Alex Holliday af8b128ac2 notificaiton providers 2026-03-03 23:27:48 +00:00
Alex Holliday 00150ba6f4 checkService, bufferService 2026-03-03 23:19:54 +00:00
Alex Holliday 7f04dd5d9a checksService 2026-03-03 23:09:10 +00:00
Alex Holliday ab5389190d stautsService and settingsService 2026-03-03 23:01:35 +00:00
Alexander Holliday e3ecfe6ad8 Merge pull request #3323 from bluewave-labs/develop
develop -> master
2026-02-20 12:38:33 -08:00
Alexander Holliday f4664eec16 Merge pull request #3321 from bluewave-labs/develop
develop->master
2026-02-20 12:20:49 -08:00
Alexander Holliday 56f601e34b Merge pull request #3319 from bluewave-labs/develop
develop -> master
2026-02-20 12:07:29 -08:00
Alexander Holliday 1244ff6d93 Merge pull request #3212 from bluewave-labs/develop
develop -> master
2026-01-27 13:58:48 -08:00
Alexander Holliday c0ea804c98 Merge pull request #3209 from bluewave-labs/develop
develop -> master
2026-01-27 13:40:25 -08:00
Alexander Holliday c21cf29db6 Merge pull request #3207 from bluewave-labs/develop
develop -> master
2026-01-27 13:20:18 -08:00
Alexander Holliday 3d0da70ce6 Merge pull request #3204 from bluewave-labs/develop
develop -> master
2026-01-27 13:09:47 -08:00
Alexander Holliday feb061240b Merge pull request #3042 from bluewave-labs/develop
develp->master
2025-10-27 12:09:00 -07:00
Alexander Holliday a924227805 Merge pull request #2964 from bluewave-labs/develop
develop -> master
2025-09-18 13:40:45 -07:00
Alexander Holliday 2144d3f8c0 Merge pull request #2962 from bluewave-labs/develop
develop -> master
2025-09-18 12:04:51 -07:00
Alexander Holliday fcd6786149 Merge pull request #2936 from bluewave-labs/develop
develop -> master
2025-09-08 20:43:21 -07:00
Alexander Holliday ea968ebf3c Merge pull request #2933 from bluewave-labs/develop
develop -> master
2025-09-08 20:33:35 -07:00
Alexander Holliday 8a7b852974 Merge pull request #2929 from bluewave-labs/develop
develop -> master
2025-09-08 07:24:16 -07:00
Alexander Holliday c8d3e3b70c Merge pull request #2921 from bluewave-labs/develop
develop -> master
2025-09-04 15:46:30 -07:00
Alexander Holliday 7a90a6f9f4 Merge pull request #2915 from bluewave-labs/develop
develop->master
2025-09-01 10:33:09 -07:00
27 changed files with 255 additions and 244 deletions

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig(({}) => {
let version = "3.4.0";
let version = "3.5.0";
return {
base: "/",
+1 -3
View File
@@ -3,8 +3,6 @@ import { MonitorModel } from "../dist/db/models/Monitor.js";
import { CheckModel } from "../dist/db/models/Check.js";
const DEFAULT_MONITOR_ID = "000000000000000000000001";
const DEFAULT_TEAM_ID = "0000000000000000000000aa";
const DEFAULT_USER_ID = "0000000000000000000000bb";
const DEFAULT_MONITOR_TYPE = "http";
const DEFAULT_TOTAL = 1_000_000;
const DEFAULT_BATCH_SIZE = 5_000;
@@ -12,7 +10,7 @@ const DEFAULT_BATCH_SIZE = 5_000;
const parseObjectId = (value, fallback) => {
try {
return new mongoose.Types.ObjectId(value || fallback);
} catch (error) {
} catch {
console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`);
return new mongoose.Types.ObjectId(fallback);
}
-1
View File
@@ -29,7 +29,6 @@ import { NotificationMessageBuilder, INotificationMessageBuilder } from "../serv
import axios from "axios";
import got from "got";
import ping from "ping";
import http from "http";
import https from "https";
import Docker from "dockerode";
import net from "net";
@@ -11,7 +11,6 @@ import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
import { IStatusPageService } from "@/service/business/statusPageService.js";
import { IMonitorsRepository } from "@/repositories/index.js";
import { ISettingsService } from "@/service/system/settingsService.js";
import { ParseBoolean } from "@/utils/utils.js";
import { NormalizeData } from "@/utils/dataUtils.js";
const SERVICE_NAME = "statusPageController";
@@ -110,6 +109,7 @@ class StatusPageController {
const normalizedMonitors = sortedMonitors.map((monitor) => {
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
if (!showURL) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { url, port, secret, notifications, ...rest } = monitor;
return { ...rest, checks: normalizedChecks };
}
@@ -36,7 +36,7 @@ class MongoInvitesRepository implements IInvitesRepository {
create = async (invite: Partial<Invite>) => {
await InviteModel.deleteMany({ email: invite.email });
invite.token = crypto.randomBytes(32).toString("hex");
let inviteToken = await InviteModel.create(invite);
const inviteToken = await InviteModel.create(invite);
return this.toEntity(inviteToken);
};
@@ -92,7 +92,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
}
// Sorting
let sort: Record<string, any> = {};
const sort: Record<string, any> = {};
if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1;
}
@@ -52,7 +52,7 @@ class MongoSettingsRepository implements ISettingsRepository {
};
findSingleton = async () => {
let settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean();
const settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean();
if (!settings) {
return null;
}
+32 -16
View File
@@ -1,6 +1,6 @@
import { Types } from "mongoose";
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
import type { MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js";
import type { MonitorStatusResponse, CheckErrorInfo, Check, ILighthouseAudit, ChecksQueryResult, ChecksSummary } from "@/types/index.js";
import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js";
import { ParseBoolean } from "@/utils/utils.js";
@@ -8,12 +8,23 @@ import { ILogger } from "@/utils/logger.js";
const SERVICE_NAME = "checkService";
class CheckService {
export interface ICheckService {
createChecks(checks: Check[]): Promise<Check[]>;
buildCheck(statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>): Partial<Check> | undefined;
getChecksByMonitor(params: { monitorId: string; query: Record<string, string>; teamId: string }): Promise<ChecksQueryResult>;
getChecksByTeam(params: { teamId: string; query: Record<string, string> }): Promise<ChecksQueryResult>;
getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise<ChecksSummary>;
deleteChecks(params: { monitorId: string; teamId: string }): Promise<number>;
deleteChecksByTeamId(params: { teamId: string }): Promise<number>;
updateChecksTTL(params: { teamId: string; ttl: string }): Promise<void>;
}
class CheckService implements ICheckService {
static SERVICE_NAME = SERVICE_NAME;
private monitorsRepository: IMonitorsRepository;
private checksRepository: IChecksRepository;
private logger: any;
private logger: ILogger;
constructor(monitorsRepository: IMonitorsRepository, logger: ILogger, checksRepository: IChecksRepository) {
this.monitorsRepository = monitorsRepository;
this.logger = logger;
@@ -53,13 +64,12 @@ class CheckService {
message: "Failed to build check",
service: SERVICE_NAME,
method: "buildCheck",
details: "empty payload",
});
return undefined;
}
const categories = pageSpeedPayload.lighthouseResult?.categories ?? {};
const audits = pageSpeedPayload.lighthouseResult?.audits ?? {};
const mapAudit = (audit: any) => {
const mapAudit = (audit: ILighthouseAudit | undefined) => {
if (!audit || typeof audit !== "object") {
return undefined;
}
@@ -102,7 +112,7 @@ class CheckService {
return check;
};
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => {
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: Record<string, string>; teamId: string }) => {
if (!monitorId) {
throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
}
@@ -110,28 +120,34 @@ class CheckService {
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
}
// For verificaiton, throws an error if monitor doesn't belong to team
// For verification, throws an error if monitor doesn't belong to team
await this.monitorsRepository.findById(monitorId, teamId);
let { sortOrder, dateRange, filter, page, rowsPerPage, status } = query;
const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query;
if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) {
throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
}
const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status);
const parsedPage = page ? parseInt(page) : page;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
const parsedPage = page ? parseInt(page) : 0;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus);
return result;
};
getChecksByTeam = async ({ teamId, query }: { teamId: string; query: any }) => {
let { sortOrder, dateRange, filter, page, rowsPerPage } = query;
getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record<string, string> }) => {
const { sortOrder, dateRange, filter, page, rowsPerPage } = query;
if (!sortOrder || !dateRange || !filter || !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) : page;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
const parsedPage = page ? parseInt(page) : 0;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId);
return checkData;
@@ -154,7 +170,7 @@ class CheckService {
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 });
}
// For verificaiton, throws an error if monitor doesn't belong to team
// 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);
@@ -170,7 +186,7 @@ class CheckService {
};
updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => {
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500 });
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } });
};
}
@@ -170,7 +170,7 @@ class GeoChecksService implements IGeoChecksService {
});
}
let { sortOrder, dateRange, page, rowsPerPage, continent } = query;
const { sortOrder, dateRange, page, rowsPerPage, continent } = query;
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
this.logger.debug({
@@ -2,7 +2,7 @@ const SERVICE_NAME = "JobQueueHelper";
import type { Monitor } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
import type { StatusChangeResult, MonitorStatusResponse, HardwareStatusPayload, MonitorStatus } from "@/types/index.js";
import type { StatusChangeResult } from "@/types/index.js";
import IncidentService from "@/service/business/incidentService.js";
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
import {
@@ -3,6 +3,7 @@ import type { GeoCheck } from "@/types/index.js";
import type { IGeoChecksService } from "../business/geoChecksService.js";
import type { ILogger } from "@/utils/logger.js";
import type { ISettingsService } from "@/service/system/settingsService.js";
import { ICheckService } from "../business/checkService.js";
const SERVICE_NAME = "BufferService";
export interface IBufferService {
@@ -19,13 +20,13 @@ class BufferService implements IBufferService {
private BUFFER_TIMEOUT: number;
private logger: ILogger;
private SERVICE_NAME: string;
private buffer: any[];
private geoBuffer: any[];
private buffer: Check[];
private geoBuffer: GeoCheck[];
private bufferTimer: NodeJS.Timeout | null = null;
private checksService: any;
private checksService: ICheckService;
private geoChecksService: IGeoChecksService;
constructor(logger: ILogger, checkService: any, geoChecksService: IGeoChecksService, settingsService: ISettingsService) {
constructor(logger: ILogger, checkService: ICheckService, geoChecksService: IGeoChecksService, settingsService: ISettingsService) {
this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute
this.logger = logger;
this.checksService = checkService;
@@ -48,12 +49,12 @@ class BufferService implements IBufferService {
addToBuffer(check: Check) {
try {
this.buffer.push(check);
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME,
method: "addToBuffer",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
}
}
@@ -61,12 +62,12 @@ class BufferService implements IBufferService {
addGeoCheckToBuffer(geoCheck: GeoCheck) {
try {
this.geoBuffer.push(geoCheck);
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME,
method: "addGeoCheckToBuffer",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
}
}
@@ -98,12 +99,12 @@ class BufferService implements IBufferService {
}
return false;
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME,
method: "removeCheckFromBuffer",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
return false;
}
@@ -117,12 +118,12 @@ class BufferService implements IBufferService {
try {
await this.flushBuffer();
await this.flushGeoBuffer();
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: `Error in flush cycle: ${error.message}`,
message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME,
method: "scheduleNextFlush",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
} finally {
// Schedule the next flush only after the current one completes
@@ -141,12 +142,12 @@ class BufferService implements IBufferService {
await this.checksService.createChecks(this.buffer);
this.buffer = [];
}
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: `Error flushing checks buffer: ${error.message}`,
message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME,
method: "flushBuffer",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
// Clear buffer even on error to prevent infinite retry loops
this.buffer = [];
@@ -164,12 +165,12 @@ class BufferService implements IBufferService {
await this.geoChecksService.createGeoChecks(this.geoBuffer);
this.geoBuffer = [];
}
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: `Error flushing geo checks buffer: ${error.message}`,
message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME,
method: "flushGeoBuffer",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
// Clear buffer even on error to prevent infinite retry loops
this.geoBuffer = [];
@@ -1,27 +1,51 @@
import { fileURLToPath } from "url";
import path from "path";
import { EmailTransportConfig } from "@/types/index.js";
import { ISettingsService } from "@/service/system/settingsService.js";
import { ILogger } from "@/utils/logger.js";
import fs from "node:fs";
import path from "node:path";
import nodemailer from "nodemailer";
import mjml2html from "mjml";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const SERVICE_NAME = "EmailService";
type MjmlFn = typeof mjml2html;
type FileSystem = typeof fs;
type PathModule = typeof path;
type Mailer = typeof nodemailer;
type TemplateCompiler = (template: string) => (context: Record<string, unknown>) => string;
class EmailService {
export interface IEmailService {
init(): void;
buildEmail(template: string, context: Record<string, unknown>): Promise<string | undefined>;
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string | false | undefined>;
}
class EmailService implements IEmailService {
static SERVICE_NAME = SERVICE_NAME;
private settingsService: any;
private fs: any;
private path: any;
private compile: any;
private mjml2html: any;
private nodemailer: any;
private logger: any;
private transporter: any;
private templateLookup: Record<string, Function>;
private loadTemplate: (templateName: string) => Function;
private settingsService: ISettingsService;
private fs: FileSystem;
private path: PathModule;
private compile: TemplateCompiler;
private mjml2html: MjmlFn;
private nodemailer: Mailer;
private logger: ILogger;
private transporter: ReturnType<typeof import("nodemailer").createTransport> | null = null;
private templateLookup: Record<string, ((context: Record<string, unknown>) => string) | undefined>;
private loadTemplate: (templateName: string) => ((context: Record<string, unknown>) => string) | undefined;
constructor(settingsService: any, fs: any, path: any, compile: any, mjml2html: any, nodemailer: any, logger: any) {
constructor(
settingsService: ISettingsService,
fs: FileSystem,
path: PathModule,
compile: TemplateCompiler,
mjml2html: MjmlFn,
nodemailer: Mailer,
logger: ILogger
) {
this.settingsService = settingsService;
this.fs = fs;
this.path = path;
@@ -30,9 +54,7 @@ class EmailService {
this.nodemailer = nodemailer;
this.logger = logger;
this.templateLookup = {};
this.loadTemplate = () => {
return () => {};
};
this.loadTemplate = () => undefined;
this.init();
}
@@ -46,12 +68,12 @@ class EmailService {
const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`);
const templateContent = this.fs.readFileSync(templatePath, "utf8");
return this.compile(templateContent);
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME,
method: "loadTemplate",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
}
};
@@ -66,17 +88,20 @@ class EmailService {
};
};
buildEmail = async (template: string, context: Record<string, any>) => {
buildEmail = async (template: string, context: Record<string, unknown>) => {
try {
const mjml = this.templateLookup[template]?.(context);
const html = await this.mjml2html(mjml);
if (!mjml) {
throw new Error(`Template ${template} not found`);
}
const html = this.mjml2html(mjml);
return html.html;
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME,
method: "buildEmail",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
}
};
@@ -125,11 +150,12 @@ class EmailService {
try {
await this.transporter.verify();
} catch (error) {
} catch (error: unknown) {
this.logger.warn({
message: "Email transporter verification failed",
service: SERVICE_NAME,
method: "verifyTransporter",
stack: error instanceof Error ? error.stack : undefined,
});
return false;
}
@@ -142,12 +168,12 @@ class EmailService {
html: html,
});
return info?.messageId;
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME,
method: "sendEmail",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
}
};
@@ -95,12 +95,12 @@ class GlobalPingService implements IGlobalPingService {
});
return measurementId;
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: "GlobalPing API unavailable, skipping geo check",
service: SERVICE_NAME,
method: "createMeasurement",
details: error,
stack: error instanceof Error ? error.stack : undefined,
});
return null;
}
@@ -140,12 +140,12 @@ class GlobalPingService implements IGlobalPingService {
// Still in-progress, wait and poll again
await this.sleep(POLL_INTERVAL_MS);
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: "Error polling GlobalPing API",
service: SERVICE_NAME,
method: "pollForResults",
details: error.message,
stack: error instanceof Error ? error.stack : undefined,
});
return [];
}
@@ -1,4 +1,4 @@
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse } from "@/types/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type {
NotificationMessage,
@@ -30,8 +30,8 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
clientHost: string
): NotificationMessage {
const type = this.determineNotificationType(decision, monitor);
const severity = this.determineSeverity(type, monitor.status);
const content = this.buildContent(type, monitor, monitorStatusResponse, decision, clientHost);
const severity = this.determineSeverity(type);
const content = this.buildContent(type, monitor, monitorStatusResponse);
return {
type,
@@ -77,7 +77,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
return "monitor_up";
}
private determineSeverity(type: NotificationType, monitorStatus: string): NotificationSeverity {
private determineSeverity(type: NotificationType): NotificationSeverity {
switch (type) {
case "monitor_down":
return "critical";
@@ -93,28 +93,22 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}
}
private buildContent(
type: NotificationType,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
): NotificationContent {
private buildContent(type: NotificationType, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent {
switch (type) {
case "monitor_down":
return this.buildMonitorDownContent(monitor, monitorStatusResponse, clientHost);
return this.buildMonitorDownContent(monitor, monitorStatusResponse);
case "monitor_up":
return this.buildMonitorUpContent(monitor, clientHost);
return this.buildMonitorUpContent(monitor);
case "threshold_breach":
return this.buildThresholdBreachContent(monitor, monitorStatusResponse, decision, clientHost);
return this.buildThresholdBreachContent(monitor, monitorStatusResponse as MonitorStatusResponse<HardwareStatusPayload>);
case "threshold_resolved":
return this.buildThresholdResolvedContent(monitor, clientHost);
return this.buildThresholdResolvedContent(monitor);
default:
return this.buildDefaultContent(monitor, clientHost);
return this.buildDefaultContent(monitor);
}
}
private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, clientHost: string): NotificationContent {
private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent {
const title = `Monitor Down: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" is currently down and unreachable.`;
const details = [`URL: ${monitor.url}`, `Status: Down`, `Type: ${monitor.type}`];
@@ -137,7 +131,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
};
}
private buildMonitorUpContent(monitor: Monitor, clientHost: string): NotificationContent {
private buildMonitorUpContent(monitor: Monitor): NotificationContent {
const title = `Monitor Recovered: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" is back up and operational.`;
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
@@ -150,12 +144,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
};
}
private buildThresholdBreachContent(
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
): NotificationContent {
private buildThresholdBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): NotificationContent {
const title = `Threshold Breach: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`;
const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`];
@@ -171,7 +160,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
};
}
private buildThresholdResolvedContent(monitor: Monitor, clientHost: string): NotificationContent {
private buildThresholdResolvedContent(monitor: Monitor): NotificationContent {
const title = `Thresholds Resolved: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" thresholds have returned to normal.`;
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
@@ -184,7 +173,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
};
}
private buildDefaultContent(monitor: Monitor, clientHost: string): NotificationContent {
private buildDefaultContent(monitor: Monitor): NotificationContent {
return {
title: `Monitor: ${monitor.name}`,
summary: `Status update for monitor "${monitor.name}".`,
@@ -193,7 +182,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
};
}
public extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): ThresholdBreach[] {
public extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): ThresholdBreach[] {
const breaches: ThresholdBreach[] = [];
// Check if this is a hardware monitor with threshold data
@@ -202,7 +191,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}
// Cast to HardwareStatusPayload type
const payload = monitorStatusResponse.payload as { data?: any };
const payload = monitorStatusResponse.payload;
const hardware = payload.data;
if (!hardware) {
@@ -1,14 +1,15 @@
const SERVICE_NAME = "DiscordProvider";
import type { Notification } from "@/types/index.js";
import type { AlertDiscordPayload, DiscordEmbedField, Notification } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import type { NotificationMessage, NotificationSeverity } from "@/types/notificationMessage.js";
import got from "got";
import { ILogger } from "@/utils/logger.js";
export class DiscordProvider implements INotificationProvider {
private logger: any;
private logger: ILogger;
constructor(logger: any) {
constructor(logger: ILogger) {
this.logger = logger;
}
@@ -36,9 +37,6 @@ export class DiscordProvider implements INotificationProvider {
}
};
/**
* New unified message format implementation
*/
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
if (!notification.address) {
this.logger.warn({
@@ -71,18 +69,17 @@ export class DiscordProvider implements INotificationProvider {
}
}
private buildDiscordEmbed(message: NotificationMessage): any {
// Map severity to Discord embed colors
const colorMap: Record<string, number> = {
private buildDiscordEmbed(message: NotificationMessage): AlertDiscordPayload {
const colorMap: Record<NotificationSeverity, number> = {
critical: 0xdc2626, // red-600
warning: 0xf59e0b, // amber-500
info: 0x3b82f6, // blue-500
success: 0x10b981, // green-500
};
const color = colorMap[message.severity] || colorMap.info;
const color = colorMap[message.severity] ?? colorMap.info;
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
const fields: Array<DiscordEmbedField> = [];
// Add monitor details
fields.push({
@@ -1,15 +1,15 @@
const SERVICE_NAME = "EmailProvider";
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
import type { Notification } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import { buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import type { ILogger } from "@/utils/logger.js";
import { IEmailService } from "@/service/infrastructure/emailService.js";
export class EmailProvider implements INotificationProvider {
private emailService: any;
private logger: any;
private emailService: IEmailService;
private logger: ILogger;
constructor(emailService: any, logger: any) {
constructor(emailService: IEmailService, logger: ILogger) {
this.emailService = emailService;
this.logger = logger;
}
@@ -18,6 +18,24 @@ export class EmailProvider implements INotificationProvider {
const subject = "Test notification";
const html = await buildTestEmail(this.emailService);
if (!notification.address) {
this.logger.warn({
message: "Missing address",
service: SERVICE_NAME,
method: "sendTestAlert",
});
return false;
}
if (!html) {
this.logger.warn({
message: "Failed to build test email content",
service: SERVICE_NAME,
method: "sendTestAlert",
});
return false;
}
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
if (!messageId) {
this.logger.warn({
@@ -38,6 +56,15 @@ export class EmailProvider implements INotificationProvider {
const subject = this.buildSubject(message);
const html = await this.buildEmailFromMessage(message);
if (!html) {
this.logger.warn({
message: "Failed to build email content",
service: SERVICE_NAME,
method: "sendMessage",
});
return false;
}
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
if (!messageId) {
this.logger.warn({
@@ -65,7 +92,7 @@ export class EmailProvider implements INotificationProvider {
}
}
private async buildEmailFromMessage(message: NotificationMessage): Promise<string> {
private async buildEmailFromMessage(message: NotificationMessage): Promise<string | undefined> {
const context = {
title: message.content.title,
summary: message.content.summary,
@@ -88,16 +115,6 @@ export class EmailProvider implements INotificationProvider {
const html = await this.emailService.buildEmail("unifiedNotificationTemplate", context);
this.logger.info({
message: "[DEBUG] Email HTML generated",
service: SERVICE_NAME,
method: "buildEmailFromMessage",
details: {
htmlLength: html?.length || 0,
htmlPreview: html?.substring(0, 200),
},
});
return html;
}
@@ -1,14 +1,15 @@
const SERVICE_NAME = "MatrixProvider";
import got from "got";
import type { INotificationProvider } from "@/service/index.js";
import type { Notification } from "@/types/index.js";
import type { AlertMatrixPayload, Notification } from "@/types/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import { ILogger } from "@/utils/logger.js";
export class MatrixProvider implements INotificationProvider {
private logger: any;
private logger: ILogger;
constructor(logger: any) {
constructor(logger: ILogger) {
this.logger = logger;
}
@@ -42,9 +43,6 @@ export class MatrixProvider implements INotificationProvider {
}
};
/**
* New unified message format - builds Matrix HTML message from NotificationMessage
*/
sendMessage = async (notification: Notification, message: NotificationMessage): Promise<boolean> => {
const { homeserverUrl, accessToken, roomId } = notification;
@@ -87,7 +85,7 @@ export class MatrixProvider implements INotificationProvider {
* Returns both plain text and HTML formatted versions
* Matrix supports HTML subset for rich formatting
*/
private buildMatrixMessage(message: NotificationMessage): { plainText: string; htmlText: string } {
private buildMatrixMessage(message: NotificationMessage): AlertMatrixPayload {
const plainLines: string[] = [];
const htmlLines: string[] = [];
@@ -1,15 +1,16 @@
const SERVICE_NAME = "PagerDutyProvider";
import got from "got";
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { Notification } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import { ILogger } from "@/utils/logger.js";
import { AlertPagerDutyPayload } from "@/types/index.js";
export class PagerDutyProvider implements INotificationProvider {
private logger: any;
private logger: ILogger;
constructor(logger: any) {
constructor(logger: ILogger) {
this.logger = logger;
}
@@ -77,7 +78,7 @@ export class PagerDutyProvider implements INotificationProvider {
}
}
private buildPagerDutyPayload(notification: Notification, message: NotificationMessage): any {
private buildPagerDutyPayload(notification: Notification, message: NotificationMessage): AlertPagerDutyPayload {
// Map our notification type to PagerDuty event_action
const eventAction = message.type === "monitor_up" || message.type === "threshold_resolved" ? "resolve" : "trigger";
@@ -106,7 +107,7 @@ export class PagerDutyProvider implements INotificationProvider {
}
// Build custom details
const customDetails: any = {
const customDetails: Record<string, unknown> = {
monitor_name: message.monitor.name,
monitor_url: message.monitor.url,
monitor_type: message.monitor.type,
@@ -1,15 +1,15 @@
const SERVICE_NAME = "SlackProvider";
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
import type { Notification } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import got, { HTTPError } from "got";
import { ILogger } from "@/utils/logger.js";
export class SlackProvider implements INotificationProvider {
private logger: any;
private logger: ILogger;
constructor(logger: any) {
constructor(logger: ILogger) {
this.logger = logger;
}
@@ -78,7 +78,7 @@ export class SlackProvider implements INotificationProvider {
* Uses Slack's rich formatting with sections, fields, and context blocks
*/
private buildSlackPayload(message: NotificationMessage): object {
const blocks: any[] = [];
const blocks: unknown[] = [];
// Determine color based on severity
const colorMap = {
@@ -1,6 +1,5 @@
import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js";
// Test notification helpers - used by all providers for test alerts
export const buildTestEmail = async (emailService: any) => {
import { IEmailService } from "@/service/infrastructure/emailService.js";
export const buildTestEmail = async (emailService: IEmailService) => {
const context = { testName: "Monitoring System" };
const html = await emailService.buildEmail("testEmailTemplate", context);
return html;
@@ -1,15 +1,15 @@
const SERVICE_NAME = "WebhookProvider";
import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js";
import type { Notification } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import got from "got";
import { ILogger } from "@/utils/logger.js";
export class WebhookProvider implements INotificationProvider {
private logger: any;
private logger: ILogger;
constructor(logger: any) {
constructor(logger: ILogger) {
this.logger = logger;
}
@@ -46,10 +46,6 @@ export class WebhookProvider implements INotificationProvider {
}
};
/**
* Build webhook payload from NotificationMessage
* Format: { text: string, severity: string, monitor: object, details: object }
*/
private buildWebhookPayload(message: NotificationMessage): object {
const lines: string[] = [];
@@ -1,4 +1,4 @@
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification, MonitorStatus } from "@/types/index.js";
import type { Monitor, MonitorStatusResponse, Notification } from "@/types/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js";
import { INotificationProvider } from "./notificationProviders/INotificationProvider.js";
@@ -15,13 +15,15 @@ import type {
GrpcStatusPayload,
CheckSnapshot,
MonitorStats,
CheckDiskInfo,
} from "@/types/index.js";
import { AppError } from "@/utils/AppError.js";
import { ILogger } from "@/utils/logger.js";
import { IBufferService } from "./bufferService.js";
const SERVICE_NAME = "StatusService";
export interface IStatusService {
updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise<boolean>;
handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise<void>;
updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse): Promise<boolean>;
updateMonitorStatus(
statusResponse: MonitorStatusResponse<
| PingStatusPayload
@@ -40,15 +42,15 @@ export interface IStatusService {
export class StatusService implements IStatusService {
static SERVICE_NAME = SERVICE_NAME;
private logger: any;
private buffer: any;
private logger: ILogger;
private buffer: IBufferService;
private monitorsRepository: IMonitorsRepository;
private monitorStatsRepository: IMonitorStatsRepository;
private checksRepository: IChecksRepository;
constructor(
logger: ILogger,
buffer: any,
buffer: IBufferService,
monitorsRepository: IMonitorsRepository,
monitorStatsRepository: IMonitorStatsRepository,
checksRepository: IChecksRepository
@@ -64,7 +66,7 @@ export class StatusService implements IStatusService {
return StatusService.SERVICE_NAME;
}
async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: MonitorStatusResponse }) {
async updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse) {
try {
const monitorId = monitor.id;
const { responseTime, status } = networkResponse;
@@ -164,62 +166,17 @@ export class StatusService implements IStatusService {
}
return true;
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
service: SERVICE_NAME,
message: error.message,
message: error instanceof Error ? error.message : "Unknown error",
method: "updateRunningStats",
stack: error.stack,
stack: error instanceof Error ? error.stack : undefined,
});
return false;
}
}
handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => {
try {
let savedCheck: Check | null = null;
if (!check.id) {
try {
savedCheck = await this.checksRepository.create(check);
this.buffer.removeCheckFromBuffer(check);
} catch (checkError: any) {
this.logger.error({
service: SERVICE_NAME,
method: "handleIncidentForCheck",
message: `Failed to save check immediately for ${errorContext}: ${checkError.message}`,
monitorId: monitor.id,
stack: checkError.stack,
});
savedCheck = null;
}
}
if (savedCheck && savedCheck.id) {
try {
this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action });
} catch (incidentError: any) {
this.logger.error({
service: SERVICE_NAME,
method: "handleIncidentForCheck",
message: `Failed to add incident to buffer for ${errorContext}: ${incidentError.message}`,
monitorId: monitor.id,
action,
stack: incidentError.stack,
});
}
}
} catch (error: any) {
this.logger.error({
service: SERVICE_NAME,
method: "handleIncidentForCheck",
message: `Error in ${errorContext}: ${error.message}`,
monitorId: monitor?.id,
stack: error.stack,
});
}
};
updateMonitorStatus = async (
statusResponse: MonitorStatusResponse<
| PingStatusPayload
@@ -239,7 +196,7 @@ export class StatusService implements IStatusService {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
// Update running stats
this.updateRunningStats({ monitor, networkResponse: statusResponse });
this.updateRunningStats(monitor, statusResponse);
monitor.statusWindow = monitor.statusWindow || [];
monitor.statusWindow.push(status);
@@ -322,7 +279,8 @@ export class StatusService implements IStatusService {
const memoryBreach = memoryUsage !== -1 && memoryUsage > monitor.memoryAlertThreshold / 100;
const diskBreach =
metrics.disk?.some((d: any) => typeof d?.usage_percent === "number" && d.usage_percent > monitor.diskAlertThreshold / 100) ?? false;
metrics.disk?.some((d: CheckDiskInfo) => typeof d?.usage_percent === "number" && d.usage_percent > monitor.diskAlertThreshold / 100) ??
false;
const temps = metrics.cpu?.temperature ?? [];
const tempBreach = temps.some((temp: number) => temp > monitor.tempAlertThreshold);
@@ -400,10 +358,13 @@ export class StatusService implements IStatusService {
timestamp: new Date().getTime(),
thresholdBreaches,
};
} catch (error: any) {
error.service = SERVICE_NAME;
error.method = "updateStatus";
throw error;
} catch (error: unknown) {
throw new AppError({
message: `Failed to update monitor status: ${error instanceof Error ? error.message : "Unknown error"}`,
details: `Error updating status for monitor with id ${check.metadata.monitorId}`,
service: SERVICE_NAME,
method: "updateMonitorStatus",
});
}
};
@@ -419,13 +380,13 @@ export class StatusService implements IStatusService {
}
this.buffer.addToBuffer(check);
return true;
} catch (error: any) {
} catch (error: unknown) {
this.logger.error({
message: error.message,
service: error.service || SERVICE_NAME,
method: error.method || "insertCheck",
details: error.details || `Error inserting check for monitor: ${check?.metadata.monitorId}`,
stack: error.stack,
message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME,
method: "insertCheck",
details: { msg: `Error inserting check for monitor: ${check?.metadata.monitorId}` },
stack: error instanceof Error ? error.stack : undefined,
});
}
};
+1 -1
View File
@@ -22,7 +22,7 @@ export interface ISettingsService {
}
class SettingsService implements ISettingsService {
static SERVICE_NAME = "SettingsService";
static SERVICE_NAME = SERVICE_NAME;
private settings: EnvConfig;
private settingsRepository: ISettingsRepository;
+20 -7
View File
@@ -4,16 +4,29 @@ export interface AlertWebhookPayload {
}
export interface AlertPagerDutyPayload {
routingKey: string;
monitorUrl?: string;
routing_key?: string;
dedup_key?: string;
event_action?: "trigger" | "resolve";
payload: Record<string, unknown>;
}
export interface AlertMatrixPayload {
friendlyName?: string;
homeserverUrl: string;
accessToken: string;
roomId: string;
monitorName: string;
plainText: string;
htmlText: string;
}
export interface DiscordEmbedField {
name: string;
value: string;
inline?: boolean;
}
export interface AlertDiscordPayload {
title: string;
description: string;
color: number;
fields: DiscordEmbedField[];
timestamp: string;
}
export interface Alert {
+1 -1
View File
@@ -5,7 +5,7 @@ declare global {
interface Request {
file?: Multer.File;
user?: User | undefined;
resource?: any;
resource?: unknown;
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ export type DurationUnit = "seconds" | "minutes" | "hours" | "days";
export interface MaintenanceWindow {
id: string;
monitorId: String;
monitorId: string;
teamId: string;
active: boolean;
name: string;