Merge branch 'develop' into incidents-feat-modal

This commit is contained in:
karenvicent committed 2026-01-21 15:35:22 -05:00
commit 0ea4a5024a
48 files changed
+541 -1708

No files matched your search

@@ -124,7 +124,7 @@ const MonitorDetailsControlHeader = ({
});
}}
>
{monitor?.isActive ? "Pause" : "Resume"}
{monitor?.isActive ? t("pause") : t("resume")}
</Button>
)}
{isAdmin && (
+8 -7
View File
@@ -80,13 +80,13 @@ const UptimeCreate = ({ isClone = false }) => {
const [isFetchingMonitor] = useFetchMonitorById({
monitorId,
setMonitor,
updateTrigger: true,
updateTrigger,
});
// Fetch games
const [isFetchingGames] = useFetchMonitorGames({
setGames,
triggerUpdate: true,
updateTrigger,
});
// Combine the loading states
@@ -283,10 +283,6 @@ const UptimeCreate = ({ isClone = false }) => {
}));
};
const handlePause = async () => {
await pauseMonitor({ monitorId, triggerUpdate });
};
const handleRemove = async (event) => {
event.preventDefault();
const TEMP_MONITOR = { id: monitor.id };
@@ -438,7 +434,12 @@ const UptimeCreate = ({ isClone = false }) => {
/>
)
}
onClick={handlePause}
onClick={() => {
pauseMonitor({
monitorId: monitor?.id,
triggerUpdate,
});
}}
>
{monitor?.isActive ? t("pause") : t("resume")}
</Button>
+47 -7
View File
@@ -36,7 +36,6 @@
"mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.14",
"papaparse": "^5.5.2",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
@@ -46,6 +45,9 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/compression": "1.8.1",
"@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19",
"@types/dockerode": "^4.0.0",
"@types/express": "5.0.3",
"@types/gamedig": "^5.0.3",
@@ -58,6 +60,7 @@
"@types/nodemailer": "7.0.1",
"@types/papaparse": "^5.5.2",
"@types/ping": "0.4.4",
"@types/swagger-ui-express": "4.1.8",
"c8": "10.1.3",
"eslint": "^9.17.0",
"eslint-plugin-mocha": "^10.5.0",
@@ -4324,6 +4327,17 @@
"@types/node": "*"
}
},
"node_modules/@types/compression": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
@@ -4334,6 +4348,26 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie-parser": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
"integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/express": "*"
}
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/docker-modem": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
@@ -4370,6 +4404,7 @@
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
@@ -4650,6 +4685,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/swagger-ui-express": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
"integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@@ -11567,12 +11613,6 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/papaparse": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
"license": "MIT"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+4 -1
View File
@@ -48,7 +48,6 @@
"mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.14",
"papaparse": "^5.5.2",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
@@ -58,6 +57,9 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/compression": "1.8.1",
"@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19",
"@types/dockerode": "^4.0.0",
"@types/express": "5.0.3",
"@types/gamedig": "^5.0.3",
@@ -70,6 +72,7 @@
"@types/nodemailer": "7.0.1",
"@types/papaparse": "^5.5.2",
"@types/ping": "0.4.4",
"@types/swagger-ui-express": "4.1.8",
"c8": "10.1.3",
"eslint": "^9.17.0",
"eslint-plugin-mocha": "^10.5.0",
+20 -5
View File
@@ -5,12 +5,27 @@ import helmet from "helmet";
import compression from "compression";
import cookieParser from "cookie-parser";
import swaggerUi from "swagger-ui-express";
import { handleErrors } from "./middleware/handleErrors.js";
import { setupRoutes } from "./config/routes.js";
import { generalApiLimiter } from "./middleware/rateLimiter.js";
import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js";
import { handleErrors } from "@/middleware/handleErrors.js";
import { generalApiLimiter } from "@/middleware/rateLimiter.js";
import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js";
import { setupRoutes } from "@/config/routes.js";
import { InitializedServices } from "@/config/services.js";
import { InitializedControllers } from "@/config/controllers.js";
import { EnvConfig } from "@/service/system/settingsService.js";
export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => {
export const createApp = ({
services,
controllers,
envSettings,
frontendPath,
openApiSpec,
}: {
services: InitializedServices;
controllers: InitializedControllers;
envSettings: EnvConfig;
frontendPath: string;
openApiSpec: any;
}) => {
const allowedOrigin = envSettings.clientHost;
const app = express();
app.use(generalApiLimiter);
+30 -21
View File
@@ -10,26 +10,35 @@ import StatusPageController from "../controllers/statusPageController.js";
import NotificationController from "../controllers/notificationController.js";
import DiagnosticController from "../controllers/diagnosticController.js";
import IncidentController from "../controllers/incidentController.js";
import type { InitializedSerivces } from "@/config/services.js";
export const initializeControllers = (services: InitializedSerivces) => {
const controllers: Record<string, any> = {};
import type { InitializedServices } from "@/config/services.js";
controllers.authController = new AuthController(services.userService);
controllers.monitorController = new MonitorController(services.monitorService);
controllers.settingsController = new SettingsController(services.settingsService, services.emailService, services.db);
controllers.checkController = new CheckController(services.checkService);
controllers.inviteController = new InviteController(services.inviteService);
controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService);
controllers.queueController = new QueueController(services.jobQueue);
controllers.logController = new LogController(services.logger);
controllers.statusPageController = new StatusPageController(services.db);
controllers.notificationController = new NotificationController(services.notificationsService, services.db, services.monitorsRepository);
controllers.diagnosticController = new DiagnosticController(services.diagnosticService);
controllers.incidentController = new IncidentController(services.incidentService);
return controllers;
export interface InitializedControllers {
authController: AuthController;
monitorController: MonitorController;
settingsController: SettingsController;
checkController: CheckController;
inviteController: InviteController;
maintenanceWindowController: MaintenanceWindowController;
queueController: QueueController;
logController: LogController;
statusPageController: StatusPageController;
notificationController: NotificationController;
diagnosticController: DiagnosticController;
incidentController: IncidentController;
}
export const initializeControllers = (services: InitializedServices): InitializedControllers => {
return {
authController: new AuthController(services.userService),
monitorController: new MonitorController(services.monitorService),
settingsController: new SettingsController(services.settingsService, services.emailService, services.db),
checkController: new CheckController(services.checkService),
inviteController: new InviteController(services.inviteService),
maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService),
queueController: new QueueController(services.jobQueue),
logController: new LogController(services.logger),
statusPageController: new StatusPageController(services.db),
notificationController: new NotificationController(services.notificationsService, services.db, services.monitorsRepository),
diagnosticController: new DiagnosticController(services.diagnosticService),
incidentController: new IncidentController(services.incidentService),
};
};
View File
Whitespace-only changes.
-25
View File
@@ -1,25 +0,0 @@
import dotenv from "dotenv";
export interface IEevConfig {
NODE_ENV: string;
JWT_SECRET: string;
PORT: number;
PAGESPEED_API_KEY: string;
SMTP_HOST: string;
SMTP_PORT: number;
SMTP_USER: string;
SMTP_PASS: string;
}
dotenv.config();
export const config: IEevConfig = {
NODE_ENV: process.env.NODE_ENV || "development",
JWT_SECRET: process.env.JWT_SECRET || "your_jwt_secret",
PORT: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
PAGESPEED_API_KEY: process.env.PAGESPEED_API_KEY || "",
SMTP_HOST: process.env.SMTP_HOST || "smtp.example.com",
SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,
SMTP_USER: process.env.SMTP_USER || "user@example.com",
SMTP_PASS: process.env.SMTP_PASS || "your_smtp_password",
};
+12 -13
View File
@@ -1,23 +1,22 @@
import { createVerifyJWT } from "../middleware/verifyJWT.js";
import { authApiLimiter } from "../middleware/rateLimiter.js";
import AuthRoutes from "../routes/v1/authRoute.js";
import InviteRoutes from "../routes/v1/inviteRoute.js";
import MonitorRoutes from "../routes/v1/monitorRoute.js";
import CheckRoutes from "../routes/v1/checkRoute.js";
import SettingsRoutes from "../routes/v1/settingsRoute.js";
import MaintenanceWindowRoutes from "../routes/v1/maintenanceWindowRoute.js";
import StatusPageRoutes from "../routes/v1/statusPageRoute.js";
import QueueRoutes from "../routes/v1/queueRoute.js";
import LogRoutes from "../routes/v1/logRoutes.js";
import DiagnosticRoutes from "../routes/v1/diagnosticRoute.js";
import NotificationRoutes from "../routes/v1/notificationRoute.js";
import AuthRoutes from "../routes/authRoute.js";
import InviteRoutes from "../routes/inviteRoute.js";
import MonitorRoutes from "../routes/monitorRoute.js";
import CheckRoutes from "../routes/checkRoute.js";
import SettingsRoutes from "../routes/settingsRoute.js";
import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js";
import StatusPageRoutes from "../routes/statusPageRoute.js";
import QueueRoutes from "../routes/queueRoute.js";
import LogRoutes from "../routes/logRoutes.js";
import DiagnosticRoutes from "../routes/diagnosticRoute.js";
import NotificationRoutes from "../routes/notificationRoute.js";
import IncidentRoutes from "../routes/v1/incidentRoute.js";
import IncidentRoutes from "../routes/incidentRoute.js";
export const setupRoutes = (app: any, controllers: Record<string, any>, services: Record<string, any>) => {
const verifyJWT = createVerifyJWT(services.settingsService);
// V1
const authRoutes = new AuthRoutes(controllers.authController, verifyJWT);
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
const settingsRoutes = new SettingsRoutes(controllers.settingsController);
+14 -14
View File
@@ -1,4 +1,3 @@
import TranslationService from "../service/system/translationService.js";
import MongoDB from "../db/MongoDB.js";
import NetworkService from "../service/infrastructure/networkService.js";
import EmailService from "../service/infrastructure/emailService.js";
@@ -24,7 +23,6 @@ import InviteService from "../service/business/inviteService.js";
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
import { MonitorService } from "@/service/index.js";
import IncidentService from "../service/business/incidentService.js";
import papaparse from "papaparse";
import axios from "axios";
import got from "got";
import ping from "ping";
@@ -82,6 +80,7 @@ import {
MongoSettingsRepository,
MongoNotificationsRepository,
MongoIncidentRepository,
MongoTeamsRepository,
IMonitorsRepository,
IChecksRepository,
IMonitorStatsRepository,
@@ -92,12 +91,13 @@ import {
ISettingsRepository,
INotificationsRepository,
IIncidentsRepository,
ITeamsRepository,
} from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js";
export type InitializedSerivces = {
//v1
export type InitializedServices = {
settingsService: any;
translationService: any;
db: any;
networkService: any;
emailService: any;
@@ -126,6 +126,7 @@ export type InitializedSerivces = {
settingsRepository: ISettingsRepository;
notificationsRepository: INotificationsRepository;
incidentsRepository: IIncidentsRepository;
teamsRepository: ITeamsRepository;
};
export const initializeServices = async ({
@@ -133,13 +134,10 @@ export const initializeServices = async ({
envSettings,
settingsService,
}: {
logger: any;
envSettings: any;
logger: ILogger;
envSettings: EnvConfig;
settingsService: any;
}): Promise<InitializedSerivces> => {
const translationService = new TranslationService(logger);
await translationService.initialize();
}): Promise<InitializedServices> => {
// Create DB
const inviteModule = new InviteModule({ InviteToken, crypto });
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings });
@@ -176,6 +174,8 @@ export const initializeServices = async ({
const settingsRepository = new MongoSettingsRepository();
const notificationsRepository = new MongoNotificationsRepository();
const incidentsRepository = new MongoIncidentRepository();
const teamsRepository = new MongoTeamsRepository();
const networkService = new NetworkService({
axios,
got,
@@ -206,7 +206,7 @@ export const initializeServices = async ({
checksRepository,
});
const bufferService = new BufferService({ logger, checkService });
const bufferService = new BufferService({ logger, checkService, settingsService });
const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository });
@@ -259,6 +259,7 @@ export const initializeServices = async ({
invitesRepository,
recoveryTokensRepository,
settingsRepository,
teamsRepository,
});
const diagnosticService = new DiagnosticService();
@@ -276,7 +277,6 @@ export const initializeServices = async ({
const monitorService = new MonitorService({
jobQueue: superSimpleQueue,
emailService,
papaparse,
logger,
errorService,
games,
@@ -289,7 +289,6 @@ export const initializeServices = async ({
const services = {
//v1
settingsService,
translationService,
db,
networkService,
emailService,
@@ -318,6 +317,7 @@ export const initializeServices = async ({
settingsRepository,
notificationsRepository,
incidentsRepository,
teamsRepository,
};
return services;
+14 -4
View File
@@ -1,5 +1,6 @@
import { AppError } from "@/utils/AppError.js";
import { Request, Response, NextFunction } from "express";
import { requireTeamId } from "./controllerUtils.js";
const SERVICE_NAME = "incidentController";
@@ -17,10 +18,19 @@ class IncidentController {
getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await this.incidentService.getIncidentsByTeam({
teamId: req?.user?.teamId,
query: req?.query,
});
const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {};
const teamId = requireTeamId(req.user?.teamId);
const result = await this.incidentService.getIncidentsByTeam(
teamId,
sortOrder,
dateRange,
page,
rowsPerPage,
status,
monitorId,
resolutionType
);
return res.status(200).json({
success: true,
+5 -17
View File
@@ -174,27 +174,15 @@ class MonitorController {
createBulkMonitors = async (req: Request, res: Response, next: NextFunction) => {
try {
if (!req.file) {
throw new AppError({ message: "No file uploaded", status: 400 });
const userId = req.user?.id;
if (!userId) {
throw new AppError({ message: "User ID is required", status: 400 });
}
if (!req.file.mimetype.includes("csv")) {
throw new AppError({ message: "File is not a CSV", status: 400 });
}
if (req.file.size === 0) {
throw new AppError({ message: "File is empty", status: 400 });
}
const userId = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req?.user?.teamId);
const fileData = req?.file?.buffer?.toString("utf-8");
if (!fileData) {
throw new AppError({ message: "Cannot get file from buffer", status: 400 });
}
const monitors = await this.monitorService.createBulkMonitors(fileData, userId, teamId);
const data = req.body;
const monitors = await this.monitorService.createBulkMonitors(data, userId, teamId);
return res.status(200).json({
success: true,
-14
View File
@@ -1,14 +0,0 @@
import mongoose from "mongoose";
const TeamSchema = mongoose.Schema(
{
email: {
type: String,
required: true,
unique: true,
},
},
{
timestamps: true,
}
);
export default mongoose.model("Team", TeamSchema);
+28
View File
@@ -0,0 +1,28 @@
import { Schema, model, type Types } from "mongoose";
import type { Team } from "@/types/index.js";
type TeamDocumentBase = Omit<Team, "id" | "createdAt" | "updatedAt"> & {};
interface TeamDocument extends TeamDocumentBase {
_id: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const TeamSchema = new Schema<TeamDocument>(
{
email: {
type: String,
required: true,
unique: true,
},
},
{
timestamps: true,
}
);
const TeamModel = model<TeamDocument>("Team", TeamSchema);
export type { TeamDocument };
export { TeamModel };
export default TeamModel;
+3
View File
@@ -27,3 +27,6 @@ export { default as NotificationModel } from "@/db/models/Notification.js";
export * from "@/db/models/Incident.js";
export { default as IncidentModel } from "@/db/models/Incident.js";
export * from "@/db/models/Team.js";
export { default as TeamModel } from "@/db/models/Team.js";
+3 -3
View File
@@ -7,12 +7,12 @@ import path from "path";
import fs from "fs";
import { runMigrations } from "./db/migration/index.js";
import Logger from "./utils/logger.js";
import SettingsService from "./service/system/settingsService.js";
import Logger, { ILogger } from "@/utils/logger.js";
import SettingsService from "@/service/system/settingsService.js";
import AppSettings from "./db/models/AppSettings.js";
const SERVICE_NAME = "Server";
let logger;
let logger: ILogger;
const startApp = async () => {
// FE path
+3
View File
@@ -27,3 +27,6 @@ export { default as MongoNotificationsRepository } from "@/repositories/notifica
export * from "@/repositories/incidents/IIncidentsRepository.js";
export { default as MongoIncidentRepository } from "@/repositories/incidents/MongoIncidentRepository.js";
export * from "@/repositories/teams/ITeamsRepository.js";
export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeamsRepository.js";
@@ -1,4 +1,4 @@
import { type MonitorType, type Monitor } from "@/types/index.js";
import { type MonitorType, type Monitor, type MonitorsSummary } from "@/types/index.js";
export interface TeamQueryConfig {
limit?: number;
@@ -37,6 +37,6 @@ export interface IMonitorsRepository {
findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise<number>;
// other
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<any>;
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>;
findGroupsByTeamId(teamId: string): Promise<string[]>;
}
@@ -1,6 +1,6 @@
import { MonitorModel } from "@/db/models/index.js";
import type { MonitorDocument } from "@/db/models/index.js";
import type { Monitor } from "@/types/index.js";
import type { Monitor, MonitorsSummary } from "@/types/index.js";
import mongoose, { type FilterQuery } from "mongoose";
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
import { AppError } from "@/utils/AppError.js";
@@ -146,10 +146,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return { monitors: this.mapDocuments(monitors), deletedCount };
};
findMonitorsSummaryByTeamId = async (
teamId: string,
config?: SummaryConfig
): Promise<{ totalMonitors: number; upMonitors: number; downMonitors: number; pausedMonitors: number }> => {
findMonitorsSummaryByTeamId = async (teamId: string, config?: SummaryConfig): Promise<MonitorsSummary> => {
const match: FilterQuery<MonitorDocument> = { teamId: new mongoose.Types.ObjectId(teamId) };
if (config?.type !== undefined) {
match.type = Array.isArray(config.type) ? { $in: config.type } : config.type;
@@ -0,0 +1,9 @@
import type { Team } from "@/types/index.js";
export interface ITeamsRepository {
// create
create(email: string): Promise<Team>;
// fetch
// update
// delete
// other
}
@@ -0,0 +1,36 @@
import { Team } from "@/types/index.js";
import { TeamDocument, TeamModel } from "@/db/models/index.js";
import { ITeamsRepository } from "@/repositories/index.js";
import mongoose from "mongoose";
class MongoTeamsRepository implements ITeamsRepository {
private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => {
if (!value) {
return "";
}
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
};
private toDateString = (value?: Date | string | null): string => {
if (!value) {
return new Date(0).toISOString();
}
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
};
private toEntity = (doc: TeamDocument): Team => {
return {
id: this.toStringId(doc._id),
email: doc.email,
createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt),
};
};
create = async (email: string) => {
const team = await TeamModel.create({ email });
return this.toEntity(team);
};
}
export default MongoTeamsRepository;
@@ -1,17 +1,20 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { Router, RequestHandler } from "express";
import { isAllowed } from "../middleware/isAllowed.js";
import multer from "multer";
const upload = multer();
class AuthRoutes {
constructor(authController, verifyJWT) {
private router: Router;
private authController: any;
constructor(authController: any, verifyJWT: RequestHandler) {
this.router = Router();
this.authController = authController;
this.initRoutes(verifyJWT);
}
initRoutes(verifyJWT) {
initRoutes(verifyJWT: RequestHandler) {
this.router.post("/register", upload.single("profileImage"), this.authController.registerUser);
this.router.post("/login", this.authController.loginUser);
@@ -1,9 +1,12 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "../middleware/isAllowed.js";
class CheckRoutes {
constructor(checkController) {
private router: Router;
private checkController: any;
constructor(checkController: any) {
this.router = Router();
this.checkController = checkController;
this.initRoutes();
@@ -1,14 +1,17 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { RequestHandler, Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js";
class DiagnosticRoutes {
constructor(diagnosticController, verifyJWT) {
private router: Router;
private diagnosticController: any;
constructor(diagnosticController: any, verifyJWT: RequestHandler) {
this.router = Router();
this.diagnosticController = diagnosticController;
this.initRoutes(verifyJWT);
}
initRoutes(verifyJWT) {
initRoutes(verifyJWT: RequestHandler) {
this.router.get("/system", verifyJWT, isAllowed(["admin", "superadmin"]), this.diagnosticController.getSystemStats);
}
@@ -1,8 +1,11 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "../middleware/isAllowed.js";
class IncidentRoutes {
constructor(incidentController) {
private router: Router;
private incidentController: any;
constructor(incidentController: any) {
this.router = Router();
this.incidentController = incidentController;
this.initRoutes();
@@ -1,14 +1,17 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { RequestHandler, Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js";
class InviteRoutes {
constructor(inviteController, verifyJWT) {
private router: Router;
private inviteController: any;
constructor(inviteController: any, verifyJWT: RequestHandler) {
this.router = Router();
this.inviteController = inviteController;
this.initRoutes(verifyJWT);
}
initRoutes(verifyJWT) {
initRoutes(verifyJWT: RequestHandler) {
this.router.post("/send", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.sendInviteEmail);
this.router.post("/verify", this.inviteController.verifyInviteToken);
this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.getInviteToken);
@@ -1,7 +1,10 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "../middleware/isAllowed.js";
class LogRoutes {
constructor(logController) {
private router: Router;
private logController: any;
constructor(logController: any) {
this.router = Router();
this.logController = logController;
this.initRoutes();
@@ -1,7 +1,10 @@
import { Router } from "express";
class MaintenanceWindowRoutes {
constructor(maintenanceWindowController) {
private router: Router;
private mwController: any;
constructor(maintenanceWindowController: any) {
this.router = Router();
this.mwController = maintenanceWindowController;
this.initRoutes();
@@ -1,7 +1,7 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "@/middleware/isAllowed.js";
import multer from "multer";
import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js";
import { fetchMonitorCertificate } from "@/controllers/controllerUtils.js";
const upload = multer({
storage: multer.memoryStorage(), // Store file in memory as Buffer
});
@@ -1,6 +1,9 @@
import { Router } from "express";
class NotificationRoutes {
constructor(notificationController) {
private router: Router;
private notificationController: any;
constructor(notificationController: any) {
this.router = Router();
this.notificationController = notificationController;
this.initializeRoutes();
@@ -1,7 +1,10 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "../middleware/isAllowed.js";
class QueueRoutes {
constructor(queueController) {
private router: Router;
private queueController: any;
constructor(queueController: any) {
this.router = Router();
this.queueController = queueController;
this.initRoutes();
@@ -1,8 +1,11 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "../middleware/isAllowed.js";
class SettingsRoutes {
constructor(settingsController) {
private router: Router;
private settingsController: any;
constructor(settingsController: any) {
this.router = Router();
this.settingsController = settingsController;
this.initRoutes();
@@ -1,15 +1,18 @@
import { Router } from "express";
import { RequestHandler, Router } from "express";
import multer from "multer";
const upload = multer();
class StatusPageRoutes {
constructor(statusPageController, verifyJWT) {
private router: Router;
private statusPageController: any;
constructor(statusPageController: any, verifyJWT: RequestHandler) {
this.router = Router();
this.statusPageController = statusPageController;
this.initRoutes(verifyJWT);
}
initRoutes(verifyJWT) {
initRoutes(verifyJWT: RequestHandler) {
this.router.get("/", this.statusPageController.getStatusPage);
this.router.get("/team", verifyJWT, this.statusPageController.getStatusPagesByTeamId);
+10 -2
View File
@@ -124,13 +124,21 @@ class IncidentService {
}
};
getIncidentsByTeam = async ({ teamId, query }: { teamId: string; query?: any }) => {
getIncidentsByTeam = async (
teamId: string,
sortOrder: string,
dateRange: string,
page: string,
rowsPerPage: string,
status: string,
monitorId: string,
resolutionType: string
) => {
try {
if (!teamId) {
throw this.errorService.createBadRequestError("No team ID in request");
}
const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = query || {};
const startDate = dateRangeLookup[dateRange];
const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
+55 -77
View File
@@ -1,13 +1,20 @@
import { createMonitorsBodyValidation } from "@/validation/joi.js";
import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js";
import { type Monitor } from "@/types/index.js";
import type { MonitorType } from "@/types/monitor.js";
import type {
MonitorType,
MonitorsWithChecksByTeamIdResult,
UptimeDetailsResult,
HardwareDetailsResult,
PageSpeedDetailsResult,
} from "@/types/monitor.js";
import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js";
import fs from "fs";
import { fileURLToPath } from "url";
import path from "path";
import { AppError } from "../infrastructure/errorService.js";
import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
const SERVICE_NAME = "MonitorService";
type DateRangeKey = "recent" | "day" | "week" | "month" | "all";
@@ -17,13 +24,13 @@ export interface IMonitorService {
// create
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
createBulkMonitors(fileData: string, userId: string, teamId: string): Promise<any>;
addDemoMonitors(args: { userId: string; teamId: string }): Promise<any[]>;
createBulkMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
// read
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<any>;
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<UptimeDetailsResult>;
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<HardwareDetailsResult>;
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<PageSpeedDetailsResult>;
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
getMonitorsByTeamId(args: {
teamId: string;
@@ -34,7 +41,7 @@ export interface IMonitorService {
filter?: string;
field?: string;
order?: "asc" | "desc";
}): Promise<any>;
}): Promise<Monitor[] | null>;
getMonitorsWithChecksByTeamId(args: {
teamId: string;
limit?: number;
@@ -45,7 +52,7 @@ export interface IMonitorService {
field?: string;
order?: "asc" | "desc";
explain?: boolean;
}): Promise<{ summary: any; count: number; monitors: any[] }>;
}): Promise<MonitorsWithChecksByTeamIdResult>;
getAllGames(): any;
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
@@ -59,15 +66,14 @@ export interface IMonitorService {
// other
sendTestEmail(args: { to: string }): Promise<string>;
exportMonitorsToJSON(args: { teamId: string }): Promise<any[]>;
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
}
export class MonitorService implements IMonitorService {
static SERVICE_NAME = SERVICE_NAME;
private jobQueue: any;
private jobQueue: ISuperSimpleQueue;
private emailService: any;
private papaparse: any;
private logger: any;
private errorService: any;
private games: any;
@@ -79,7 +85,6 @@ export class MonitorService implements IMonitorService {
constructor({
jobQueue,
emailService,
papaparse,
logger,
errorService,
games,
@@ -88,9 +93,8 @@ export class MonitorService implements IMonitorService {
monitorStatsRepository,
statusPagesRepository,
}: {
jobQueue: any;
jobQueue: ISuperSimpleQueue;
emailService: any;
papaparse: any;
logger: any;
errorService: any;
games: any;
@@ -101,7 +105,6 @@ export class MonitorService implements IMonitorService {
}) {
this.jobQueue = jobQueue;
this.emailService = emailService;
this.papaparse = papaparse;
this.logger = logger;
this.errorService = errorService;
this.games = games;
@@ -149,63 +152,14 @@ export class MonitorService implements IMonitorService {
this.jobQueue.addJob(monitor.id, monitor);
};
createBulkMonitors = async (fileData: string, userId: string, teamId: string): Promise<any> => {
const { parse } = this.papaparse;
createBulkMonitors = async (monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null> => {
const createdMonitors = await this.monitorsRepository.createBulkMonitors(monitors);
if (!monitors || monitors.length === 0) {
throw new AppError("Failed to create monitors", 500);
}
return new Promise<any>((resolve, reject) => {
parse(fileData, {
header: true,
skipEmptyLines: true,
transform: (value: string, header: string): string | number | undefined => {
if (value === "") return undefined; // Empty fields become undefined
// Handle 'port' and 'interval' fields, check if they're valid numbers
if (["port", "interval"].includes(header)) {
const num = parseInt(value, 10);
if (isNaN(num)) {
throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`);
}
return num;
}
return value;
},
complete: async ({ data, errors }: { data: any[]; errors: Error[] }): Promise<void> => {
try {
if (errors.length > 0) {
throw this.errorService.createServerError("Error parsing CSV");
}
if (!data || data.length === 0) {
throw this.errorService.createServerError("CSV file contains no data rows");
}
const enrichedData = data.map((monitor: Monitor) => ({
...monitor,
userId,
teamId,
description: monitor.description || monitor.name || monitor.url,
name: monitor.name || monitor.url,
type: monitor.type || "http",
}));
await createMonitorsBodyValidation.validateAsync(enrichedData);
const monitors = await this.monitorsRepository.createBulkMonitors(enrichedData);
await Promise.all(
monitors.map(async (monitor: Monitor) => {
this.jobQueue.addJob(monitor.id, monitor);
})
);
resolve(monitors);
} catch (error) {
reject(error);
}
},
});
});
await Promise.all(createdMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
return createdMonitors;
};
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
@@ -241,7 +195,7 @@ export class MonitorService implements IMonitorService {
monitorId: string;
dateRange: string;
normalize?: boolean;
}): Promise<any> => {
}): Promise<UptimeDetailsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
@@ -276,7 +230,15 @@ export class MonitorService implements IMonitorService {
};
};
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
getHardwareDetailsById = async ({
teamId,
monitorId,
dateRange,
}: {
teamId: string;
monitorId: string;
dateRange: string;
}): Promise<HardwareDetailsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
@@ -307,7 +269,15 @@ export class MonitorService implements IMonitorService {
};
};
getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
getPageSpeedDetailsById = async ({
teamId,
monitorId,
dateRange,
}: {
teamId: string;
monitorId: string;
dateRange: string;
}): Promise<PageSpeedDetailsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
@@ -336,12 +306,20 @@ export class MonitorService implements IMonitorService {
monitorStats,
};
};
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
return monitor;
};
getMonitorsByTeamId = async ({ teamId, type, filter }: { teamId: string; type?: MonitorType | MonitorType[]; filter?: string }): Promise<any> => {
getMonitorsByTeamId = async ({
teamId,
type,
filter,
}: {
teamId: string;
type?: MonitorType | MonitorType[];
filter?: string;
}): Promise<Monitor[] | null> => {
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
type,
filter,
@@ -369,7 +347,7 @@ export class MonitorService implements IMonitorService {
field?: string;
order?: "asc" | "desc";
explain?: boolean;
}): Promise<{ summary: any; count: number; monitors: any[] }> => {
}): Promise<MonitorsWithChecksByTeamIdResult> => {
const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId);
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
+18 -7
View File
@@ -1,9 +1,15 @@
import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository, ISettingsRepository } from "@/repositories/index.js";
import Team from "@/db/models/Team.js";
import {
IInvitesRepository,
IMonitorsRepository,
IRecoveryTokensRepository,
IUsersRepository,
ISettingsRepository,
ITeamsRepository,
} from "@/repositories/index.js";
import type { User } from "@/types/index.js";
import bcrypt from "bcryptjs";
import { AppError } from "@/utils/AppError.js";
import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
const SERVICE_NAME = "userService";
@@ -21,6 +27,7 @@ class UserService {
private invitesRepository: IInvitesRepository;
private recoveryTokensRepository: IRecoveryTokensRepository;
private settingsRepository: ISettingsRepository;
private teamsRepository: ITeamsRepository;
constructor({
crypto,
@@ -34,6 +41,7 @@ class UserService {
invitesRepository,
recoveryTokensRepository,
settingsRepository,
teamsRepository,
}: {
crypto: any;
emailService: any;
@@ -47,6 +55,7 @@ class UserService {
invitesRepository: IInvitesRepository;
recoveryTokensRepository: IRecoveryTokensRepository;
settingsRepository: ISettingsRepository;
teamsRepository: ITeamsRepository;
}) {
this.emailService = emailService;
this.settingsService = settingsService;
@@ -59,6 +68,7 @@ class UserService {
this.invitesRepository = invitesRepository;
this.recoveryTokensRepository = recoveryTokensRepository;
this.settingsRepository = settingsRepository;
this.teamsRepository = teamsRepository;
}
get serviceName() {
@@ -85,10 +95,11 @@ class UserService {
const jwtSecret = this.crypto.randomBytes(64).toString("hex");
await this.settingsRepository.update({ jwtSecret });
// Create a new team
const team = new Team({
email: user.email,
});
user.teamId = team._id;
if (!user.email) {
throw new AppError({ message: "Email is required for first user", service: SERVICE_NAME, method: "registerUser", status: 400 });
}
const team = await this.teamsRepository.create(user.email);
user.teamId = team.id;
}
const newUser = await this.usersRepository.create({ ...user }, file);
@@ -1,4 +1,3 @@
import { config } from "@/config/index.js";
import type { Check } from "@/types/index.js";
const SERVICE_NAME = "BufferService";
@@ -12,8 +11,9 @@ class BufferService {
private bufferTimer: NodeJS.Timeout | null = null;
private checksService: any;
constructor({ logger, checkService }: { logger: any; checkService: any }) {
this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute
constructor({ logger, checkService, settingsService }: { logger: any; checkService: any; settingsService: any }) {
this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute
console.log(this.BUFFER_TIMEOUT);
this.logger = logger;
this.checksService = checkService;
this.SERVICE_NAME = SERVICE_NAME;
@@ -1,6 +1,17 @@
const SERVICE_NAME = "SettingsService";
const envConfig = {
export type EnvConfig = {
jwtSecret: string | undefined;
jwtTTL: string | undefined;
systemEmailHost: string | undefined;
nodeEnv: string | undefined;
logLevel: string | undefined;
clientHost: string | undefined;
dbConnectionString: string | undefined;
port: string | undefined;
};
const envConfig: EnvConfig = {
jwtSecret: process.env.JWT_SECRET,
jwtTTL: process.env.TOKEN_TTL,
systemEmailHost: process.env.SYSTEM_EMAIL_HOST,
@@ -10,19 +21,20 @@ const envConfig = {
dbConnectionString: process.env.DB_CONNECTION_STRING,
port: process.env.PORT,
};
/**
* SettingsService
*
* This service is responsible for loading and managing the application settings.
*/
class SettingsService {
static SERVICE_NAME = "SettingsService";
/**
* Constructs a new SettingsService
* @constructor
* @throws {Error}
*/ constructor(AppSettings) {
export interface ISettingsService {
readonly serviceName: string;
loadSettings(): EnvConfig;
getSettings(): EnvConfig;
getDBSettings(): Promise<Record<string, any>>;
}
class SettingsService implements ISettingsService {
static SERVICE_NAME = "SettingsService";
private AppSettings: any;
private settings: EnvConfig;
constructor(AppSettings: any) {
this.AppSettings = AppSettings;
this.settings = { ...envConfig };
}
@@ -31,19 +43,10 @@ class SettingsService {
return SettingsService.SERVICE_NAME;
}
/**
* Load settings from env settings
* @returns {Object>} The settings.
*/
loadSettings() {
return this.settings;
}
/**
* Get the current settings.
* @returns {Object} The current settings.
* @throws Will throw an error if settings have not been loaded.
*/
getSettings() {
if (!this.settings) {
throw new Error("Settings have not been loaded");
@@ -1,92 +0,0 @@
import fs from "fs";
import path from "path";
class TranslationService {
static SERVICE_NAME = "TranslationService";
constructor(logger) {
this.logger = logger;
this.translations = {};
this._language = "en";
this.localesDir = path.join(process.cwd(), "src/locales");
}
get serviceName() {
return TranslationService.SERVICE_NAME;
}
setLanguage(language) {
this._language = language;
}
get language() {
return this._language;
}
async initialize() {
try {
await this.loadFromFiles();
} catch (error) {
this.logger.error({
message: error.message,
service: "TranslationService",
method: "initialize",
stack: error.stack,
});
}
}
async loadFromFiles() {
try {
if (!fs.existsSync(this.localesDir)) {
return false;
}
const files = fs.readdirSync(this.localesDir).filter((file) => file.endsWith(".json"));
if (files.length === 0) {
return false;
}
for (const file of files) {
const language = file.replace(".json", "");
const filePath = path.join(this.localesDir, file);
const content = fs.readFileSync(filePath, "utf8");
this.translations[language] = JSON.parse(content);
}
this.logger.info({
message: "Translations loaded from files successfully",
service: "TranslationService",
method: "loadFromFiles",
});
return true;
} catch (error) {
this.logger.error({
message: error.message,
service: "TranslationService",
method: "loadFromFiles",
stack: error.stack,
});
return false;
}
}
getTranslation(key) {
let language = this._language;
try {
return this.translations[language]?.[key] || this.translations["en"]?.[key] || key;
} catch (error) {
this.logger.error({
message: error.message,
service: "TranslationService",
method: "getTranslation",
stack: error.stack,
});
return key;
}
}
}
export default TranslationService;
+17 -3
View File
@@ -156,11 +156,17 @@ export interface HardwareChecksResult {
}>;
}
export interface GroupedCheck {
_id: string;
avgResponseTime: number;
totalChecks: number;
}
export interface UptimeChecksResult {
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
groupedChecks: GroupedCheck[];
groupedUpChecks: GroupedCheck[];
groupedDownChecks: GroupedCheck[];
uptimePercentage: number;
avgResponseTime: number;
}
@@ -171,3 +177,11 @@ export interface ChecksSummary {
downChecks: number;
cannotResolveChecks: number;
}
export type NormalizedCheck<T extends Check = Check> = T & {
originalResponseTime: number;
};
export type NormalizedUptimeCheck<T extends GroupedCheck = GroupedCheck> = T & {
originalAvgResponseTime: number;
};
+1
View File
@@ -11,3 +11,4 @@ export * from "@/types/notification.js";
export * from "@/types/alert.js";
export * from "@/types/incident.js";
export * from "@/types/email.js";
export * from "@/types/team.js";
+75
View File
@@ -44,3 +44,78 @@ export interface Monitor {
createdAt: string;
updatedAt: string;
}
export interface MonitorsSummary {
totalMonitors: number;
upMonitors: number;
downMonitors: number;
pausedMonitors: number;
}
export interface MonitorWithChecks extends Monitor {
checks: import("./check.js").Check[];
}
export interface MonitorsWithChecksByTeamIdResult {
summary: MonitorsSummary | null;
count: number;
monitors: MonitorWithChecks[];
}
export interface UptimeDetailsResult {
monitorData: {
monitor: Monitor;
groupedChecks: import("./check.js").GroupedCheck[];
groupedUpChecks: import("./check.js").GroupedCheck[];
groupedDownChecks: import("./check.js").GroupedCheck[];
groupedAvgResponseTime: number;
groupedUptimePercentage: number;
};
monitorStats: import("./monitorStats.js").MonitorStats | null;
}
export interface HardwareDetailsResult extends Monitor {
stats: {
aggregateData: {
latestCheck: import("./check.js").Check | null;
totalChecks: number;
};
upChecks: {
totalChecks: number;
};
checks: Array<{
_id: string;
avgCpuUsage: number;
avgMemoryUsage: number;
avgTemperature: number[];
disks: Array<{
name: string;
readSpeed: number;
writeSpeed: number;
totalBytes: number;
freeBytes: number;
usagePercent: number;
}>;
net: Array<{
name: string;
bytesSentPerSecond: number;
deltaBytesRecv: number;
deltaPacketsSent: number;
deltaPacketsRecv: number;
deltaErrIn: number;
deltaErrOut: number;
deltaDropIn: number;
deltaDropOut: number;
deltaFifoIn: number;
deltaFifoOut: number;
}>;
}>;
};
}
export interface PageSpeedDetailsResult {
monitor: Monitor & {
checks: import("./check.js").Check[];
};
monitorStats: import("./monitorStats.js").MonitorStats | null;
}
+6
View File
@@ -0,0 +1,6 @@
export interface Team {
id: string;
email: string;
createdAt: string;
updatedAt: string;
}
@@ -1,35 +1,33 @@
const calculatePercentile = (arr, percentile) => {
import type { Check, GroupedCheck, NormalizedCheck, NormalizedUptimeCheck } from "@/types/index.js";
const calculatePercentile = (arr: Check[], percentile: number): number => {
const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime);
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 sorted[lower]!.responseTime;
return sorted[lower]!.responseTime * (1 - weight) + sorted[upper]!.responseTime * weight;
};
const calculatePercentileUptimeDetails = (arr, percentile) => {
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;
if (upper >= sorted.length) return sorted[lower]!.avgResponseTime;
return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight;
};
const NormalizeData = (checks, rangeMin, rangeMax) => {
export const NormalizeData = <T extends Check>(checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck<T>[] => {
if (checks.length > 1) {
// Get the 5th and 95th percentile
const min = calculatePercentile(checks, 0);
const max = calculatePercentile(checks, 95);
const normalizedChecks = checks.map((check) => {
const originalResponseTime = check.responseTime;
// Normalize the response time between 1 and 100
let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min);
// Put a floor on the response times so we don't have extreme outliers
// Better visuals
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
return {
...check,
@@ -45,19 +43,16 @@ const NormalizeData = (checks, rangeMin, rangeMax) => {
});
}
};
const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => {
export const NormalizeDataUptimeDetails = <T extends GroupedCheck>(checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck<T>[] => {
if (checks.length > 1) {
// Get the 5th and 95th percentile
const min = calculatePercentileUptimeDetails(checks, 0);
const max = calculatePercentileUptimeDetails(checks, 95);
const normalizedChecks = checks.map((check) => {
const originalResponseTime = check.avgResponseTime;
// Normalize the response time between 1 and 100
let normalizedResponseTime = rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min);
// Put a floor on the response times so we don't have extreme outliers
// Better visuals
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
return {
...check,
@@ -69,32 +64,7 @@ const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => {
return normalizedChecks;
} else {
return checks.map((check) => {
return { ...check, originalResponseTime: check.responseTime };
return { ...check, originalAvgResponseTime: check.avgResponseTime };
});
}
};
const safelyParseFloat = (value, defaultValue = 0) => {
if (value === null || typeof value === "undefined") {
return defaultValue;
}
const stringValue = String(value).trim();
if (typeof value === "number" && !isNaN(value)) {
return value;
}
if (stringValue === "") {
return defaultValue;
}
const parsedValue = parseFloat(stringValue);
if (isNaN(parsedValue) || !isFinite(parsedValue)) {
return defaultValue;
}
return parsedValue;
};
export { safelyParseFloat, calculatePercentile, NormalizeData, calculatePercentileUptimeDetails, NormalizeDataUptimeDetails };
File diff suppressed because it is too large. Load diff
+13 -5
View File
@@ -1,6 +1,7 @@
import { createLogger, format, transports, Logger as WinstonLogger } from "winston";
import type { Logform } from "winston";
import dotenv from "dotenv";
import { EnvConfig } from "@/service/system/settingsService.js";
dotenv.config();
const SERVICE_NAME = "Logger";
@@ -18,19 +19,26 @@ interface LogEntry extends LogConfig {
timestamp: string;
}
interface EnvSettings {
logLevel?: string;
export interface ILogger {
readonly serviceName: string;
info(config: LogConfig): void;
warn(config: LogConfig): void;
error(config: LogConfig): void;
debug(config: LogConfig): void;
cacheLog(entry: LogEntry): void;
getLogs(): LogEntry[];
buildLogEntry(level: string, config: LogConfig): LogEntry;
}
class Logger {
class Logger implements ILogger {
static SERVICE_NAME = SERVICE_NAME;
private logger: WinstonLogger;
private envSettings: EnvSettings;
private envSettings: Partial<EnvConfig>;
private logCache: LogEntry[];
private maxCacheSize: number;
constructor({ envSettings }: { envSettings: EnvSettings }) {
constructor({ envSettings }: { envSettings: Partial<EnvConfig> }) {
this.envSettings = envSettings;
this.logCache = [];
this.maxCacheSize = 1000;
-13
View File
@@ -1,13 +0,0 @@
export const ROLES = {
SUPERADMIN: "superadmin",
ADMIN: "admin",
USER: "user",
DEMO: "demo",
};
export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO];
export const EDITABLE_ROLES = [
{ role: ROLES.ADMIN, _id: ROLES.ADMIN },
{ role: ROLES.USER, _id: ROLES.USER },
];
+3 -3
View File
@@ -1,5 +1,5 @@
import joi from "joi";
import { ROLES, VALID_ROLES } from "../utils/roleUtils.js";
import { UserRoles } from "@/types/user.js";
//****************************************
// Custom Validators
@@ -689,7 +689,7 @@ const editUserByIdBodyValidation = joi.object({
email: joi.string().email().required(),
role: joi
.array()
.items(joi.string().valid(...VALID_ROLES))
.items(joi.string().valid(...UserRoles))
.min(1)
.required(),
});
@@ -700,7 +700,7 @@ const editSuperadminUserByIdBodyValidation = joi.object({
email: joi.string().email().required(),
role: joi
.array()
.items(joi.string().valid(...VALID_ROLES, ROLES.SUPERADMIN))
.items(joi.string().valid(...UserRoles))
.min(1)
.required(),
});