Merge branch 'develop' into feat/choose-disks-monitor

This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-12-18 10:53:01 -08:00
commit 03d1dcaefb
146 files changed
+1990 -8890

No files matched your search

+205 -26
View File
@@ -166,6 +166,38 @@ class IncidentModule {
}
};
/**
* Get incidents by team with filtering
*
* STATUS PARAMETER CONTRACT (IMPORTANT - Frontend dependency):
* This method has an implicit contract with frontend's useFetchIncidents hook:
*
* - status === undefined: Frontend uses this to represent "all incidents" (filter === "all")
* Backend applies $or logic: { status: true } OR { status: false, endTime: >= dateRange }
* This shows all active incidents + resolved incidents within the date range
*
* - status === true: Frontend uses for "active incidents" (filter === "active")
* Backend shows all active incidents (no date filter applied)
* Active incidents are currently happening, so date range doesn't apply
*
* - status === false: Frontend uses for "resolved incidents" (filter === "resolved")
* Backend filters by endTime >= dateRange
* Only shows incidents that were resolved within the specified date range
*
* WARNING: Changing this logic will break frontend filtering behavior.
* The frontend depends on undefined status triggering the $or logic for "all incidents".
* This contract must be maintained when modifying either frontend or backend code.
*
* @param {Object} params - Query parameters
* @param {string} params.teamId - Team ID
* @param {string} [params.sortOrder] - Sort order (asc/desc)
* @param {string} [params.dateRange] - Date range filter
* @param {number} [params.page] - Page number
* @param {number} [params.rowsPerPage] - Rows per page
* @param {boolean|undefined} [params.status] - Status filter. undefined = all, true = active, false = resolved
* @param {string} [params.monitorId] - Monitor ID filter
* @param {string} [params.resolutionType] - Resolution type filter
*/
getIncidentsByTeam = async ({ teamId, sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType }) => {
try {
page = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
@@ -185,13 +217,30 @@ class IncidentModule {
...(statusBoolean !== undefined && { status: statusBoolean }),
...(monitorId && { monitorId: new ObjectId(monitorId) }),
...(resolutionType && { resolutionType }),
...(dateRangeLookup[dateRange] && {
startTime: {
$gte: dateRangeLookup[dateRange],
},
}),
};
// Date range filter logic (see contract documentation above):
// - Active incidents (statusBoolean === true): always show (no date filter) - they're currently happening
// - Resolved incidents (statusBoolean === false): filter by endTime (when they were resolved)
// - All incidents (statusBoolean === undefined): show all active + resolved in the range using $or
if (dateRangeLookup[dateRange]) {
const dateThreshold = dateRangeLookup[dateRange];
if (statusBoolean === true) {
// Active incidents: show all active incidents regardless of when they started
// No date filter applied
} else if (statusBoolean === false) {
// Resolved incidents: only show if resolved in the range
matchStage.endTime = { $gte: dateThreshold };
} else {
// All incidents: show all active + resolved incidents in the range
// This is the critical contract: undefined status triggers $or logic
matchStage.$or = [
{ status: true }, // All active incidents
{ status: false, endTime: { $gte: dateThreshold } }, // Resolved in range
];
}
}
sortOrder = sortOrder === "asc" ? 1 : -1;
const skip = page * rowsPerPage;
@@ -199,6 +248,14 @@ class IncidentModule {
const incidents = await this.Incident.aggregate([
{ $match: matchStage },
{ $sort: { startTime: sortOrder } },
{
$lookup: {
from: "monitors",
localField: "monitorId",
foreignField: "_id",
as: "monitor",
},
},
{
$facet: {
summary: [{ $count: "incidentsCount" }],
@@ -211,7 +268,27 @@ class IncidentModule {
$ifNull: [{ $arrayElemAt: ["$summary.incidentsCount", 0] }, 0],
},
incidents: {
$ifNull: ["$incidents", []],
$map: {
input: { $ifNull: ["$incidents", []] },
as: "incident",
in: {
$mergeObjects: [
"$$incident",
{
monitorName: {
$let: {
vars: {
monitor: { $arrayElemAt: ["$$incident.monitor", 0] },
},
in: {
$ifNull: ["$$monitor.name", null],
},
},
},
},
],
},
},
},
},
},
@@ -225,18 +302,14 @@ class IncidentModule {
}
};
getIncidentSummary = async ({ teamId, dateRange }) => {
getIncidentSummary = async ({ teamId, limit = 10 }) => {
try {
const matchStage = {
teamId: new ObjectId(teamId),
...(dateRangeLookup[dateRange] && {
startTime: {
$gte: dateRangeLookup[dateRange],
},
}),
};
const summary = await this.Incident.aggregate([
// Get basic counts and resolution types
const countsPipeline = [
{ $match: matchStage },
{
$group: {
@@ -247,29 +320,135 @@ class IncidentModule {
$cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0],
},
},
automaticResolutions: {
$sum: {
$cond: [{ $eq: ["$resolutionType", "automatic"] }, 1, 0],
},
},
},
},
]);
];
const result = {
total: 0,
active: 0,
resolved: 0,
manual: 0,
};
const counts = await this.Incident.aggregate(countsPipeline);
summary.forEach((item) => {
result.total += item.count;
// Calculate totals
let total = 0;
let active = 0;
let resolved = 0;
let manual = 0;
let automatic = 0;
counts.forEach((item) => {
total += item.count;
if (item._id === true) {
result.active = item.count;
active = item.count;
}
if (item._id === false) {
result.resolved = item.count;
resolved = item.count;
}
result.manual += item.manualResolutions;
manual += item.manualResolutions;
automatic += item.automaticResolutions;
});
return result;
// Calculate average resolution time (in milliseconds)
const resolutionTimePipeline = [
{ $match: { ...matchStage, status: false, endTime: { $exists: true, $ne: null } } },
{
$project: {
resolutionTime: {
$subtract: ["$endTime", "$startTime"],
},
},
},
{
$group: {
_id: null,
avgResolutionTime: { $avg: "$resolutionTime" },
},
},
];
const resolutionTimeResult = await this.Incident.aggregate(resolutionTimePipeline);
const avgResolutionTimeMs = resolutionTimeResult[0]?.avgResolutionTime || 0;
const avgResolutionTimeHours = avgResolutionTimeMs / (1000 * 60 * 60); // Convert to hours
// Get monitor with most incidents
const monitorPipeline = [
{ $match: matchStage },
{
$group: {
_id: "$monitorId",
count: { $sum: 1 },
},
},
{ $sort: { count: -1 } },
{ $limit: 1 },
{
$lookup: {
from: "monitors",
localField: "_id",
foreignField: "_id",
as: "monitor",
},
},
{
$project: {
monitorId: "$_id",
count: 1,
monitorName: { $arrayElemAt: ["$monitor.name", 0] },
},
},
];
const monitorResult = await this.Incident.aggregate(monitorPipeline);
const topMonitor = monitorResult[0] || null;
// Get latest incidents
const latestIncidentsPipeline = [
{ $match: matchStage },
{ $sort: { createdAt: -1 } },
{ $limit: Math.max(1, parseInt(limit) || 10) },
{
$lookup: {
from: "monitors",
localField: "monitorId",
foreignField: "_id",
as: "monitor",
},
},
{
$project: {
_id: 1,
monitorId: 1,
monitorName: { $arrayElemAt: ["$monitor.name", 0] },
status: 1,
startTime: 1,
endTime: 1,
resolutionType: 1,
message: 1,
statusCode: 1,
createdAt: 1,
},
},
];
const latestIncidents = await this.Incident.aggregate(latestIncidentsPipeline);
return {
totalActive: active,
avgResolutionTimeHours: Math.round(avgResolutionTimeHours * 100) / 100, // Round to 2 decimal places
topMonitor: topMonitor
? {
monitorId: topMonitor.monitorId,
monitorName: topMonitor.monitorName,
incidentCount: topMonitor.count,
}
: null,
total: total,
totalManualResolutions: manual,
totalAutomaticResolutions: automatic,
latestIncidents: latestIncidents,
};
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getIncidentSummary";
-25
View File
@@ -1,25 +0,0 @@
import mongoose from "mongoose";
const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/checkmate";
export const connectDatabase = async (): Promise<boolean> => {
try {
await mongoose.connect(MONGODB_URI);
console.log("Connected to MongoDB");
return true;
} catch (error) {
console.error("MongoDB connection error:", error);
process.exit(1);
}
};
export const disconnectDatabase = async (): Promise<boolean> => {
try {
await mongoose.disconnect();
console.log("Disconnected from MongoDB");
return true;
} catch (error) {
console.error("MongoDB disconnection error:", error);
return false;
}
};
-50
View File
@@ -1,50 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IRole extends Document {
_id: Types.ObjectId;
name: string;
description?: string;
permissions: string[];
isSystem: boolean;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
const roleSchema = new Schema<IRole>(
{
name: {
type: String,
required: true,
trim: true,
maxlength: 50,
},
description: {
type: String,
trim: true,
maxlength: 200,
},
permissions: [
{
type: String,
required: true,
},
],
isSystem: {
type: Boolean,
default: false,
},
isActive: {
type: Boolean,
default: true,
},
},
{
timestamps: true,
}
);
roleSchema.index({ name: 1 }, { unique: true });
export const Role = mongoose.model<IRole>("Role_v2", roleSchema);
-102
View File
@@ -1,102 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface ITokenizedUser {
sub: string;
roles: string[];
}
export interface IUser extends Document {
_id: Types.ObjectId;
email: string;
firstName: string;
lastName: string;
passwordHash: string;
roles: Types.ObjectId[];
profile: {
avatar?: string;
bio?: string;
timezone: string;
locale: string;
};
preferences: {
theme: "light" | "dark" | "system";
};
lastLoginAt?: Date;
isActive: boolean;
isVerified: boolean;
createdAt: Date;
updatedAt: Date;
}
const userSchema = new Schema<IUser>(
{
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
},
firstName: {
type: String,
required: true,
trim: true,
maxlength: 50,
},
lastName: {
type: String,
required: true,
trim: true,
maxlength: 50,
},
passwordHash: {
type: String,
required: true,
},
roles: [
{
type: Schema.Types.ObjectId,
ref: "Role_v2",
},
],
profile: {
avatar: {
type: String,
},
bio: {
type: String,
maxlength: 200,
},
timezone: {
type: String,
default: "UTC",
},
locale: {
type: String,
default: "en",
},
},
preferences: {
theme: {
type: String,
enum: ["light", "dark", "system"],
default: "system",
},
},
lastLoginAt: {
type: Date,
},
isActive: {
type: Boolean,
default: true,
},
isVerified: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
}
);
export const User = mongoose.model<IUser>("User_v2", userSchema);
-297
View File
@@ -1,297 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
import { MonitorType, MonitorTypes, MonitorStatus, MonitorStatuses } from "../monitors/Monitor.js";
import type { Response } from "got";
export type GotTimings = Response["timings"];
export interface ITimingPhases {
wait: number;
dns: number;
tcp: number;
tls: number;
request: number;
firstByte: number;
download: number;
total: number;
}
export interface ICpuInfo {
physical_core: number;
logical_core: number;
frequency: number;
current_frequency: number;
temperature: number[]; // per-core temps
free_percent: number;
usage_percent: number;
}
export interface IMemoryInfo {
total_bytes: number;
available_bytes: number;
used_bytes: number;
usage_percent: number;
}
export interface IHostInfo {
os?: string;
platform?: string;
kernel_version?: string;
pretty_name?: string;
}
export interface IDiskInfo {
device: string;
total_bytes: number;
free_bytes: number;
used_bytes: number;
usage_percent: number;
total_inodes?: number;
free_inodes?: number;
used_inodes?: number;
inodes_usage_percent?: number;
read_bytes?: number;
write_bytes?: number;
read_time?: number;
write_time?: number;
}
export interface INetInfo {
name: string;
bytes_sent: number;
bytes_recv: number;
packets_sent: number;
packets_recv: number;
err_in: number;
err_out: number;
drop_in: number;
drop_out: number;
fifo_in: number;
fifo_out: number;
}
export interface ICaptureInfo {
version?: string;
mode?: string;
}
export interface ISystemInfo {
cpu: ICpuInfo;
memory: IMemoryInfo;
disk: IDiskInfo[];
host: IHostInfo;
net: INetInfo[];
}
export interface ILighthouseAudit {
id?: string;
title?: string;
score?: number | null;
displayValue?: string;
numericValue?: number;
numericUnit?: string;
}
export interface ILighthouseCategories {
accessibility?: { score?: number | null };
"best-practices"?: { score?: number | null };
seo?: { score?: number | null };
performance?: { score?: number | null };
}
export interface ILighthouseResult {
categories?: ILighthouseCategories;
audits?: Record<string, ILighthouseAudit>;
}
export interface ICheckLighthouseFields {
accessibility: number;
bestPractices: number;
seo: number;
performance: number;
audits: {
cls: ILighthouseAudit;
si: ILighthouseAudit;
fcp: ILighthouseAudit;
lcp: ILighthouseAudit;
tbt: ILighthouseAudit;
};
}
export interface ICheck extends Document {
_id: Types.ObjectId;
monitorId: Types.ObjectId;
type: MonitorType;
status: MonitorStatus;
message: string;
responseTime?: number; // in ms
timings?: GotTimings;
httpStatusCode?: number;
errorMessage?: string;
ack: boolean;
ackAt?: Date;
ackBy?: Types.ObjectId;
expiry: Date;
createdAt: Date;
updatedAt: Date;
system?: ISystemInfo;
capture?: ICaptureInfo;
lighthouse?: ICheckLighthouseFields;
}
const CheckSchema = new Schema<ICheck>(
{
monitorId: { type: Schema.Types.ObjectId, ref: "Monitor_v2", required: true },
type: {
type: String,
required: true,
enum: MonitorTypes,
},
status: {
type: String,
required: true,
enum: MonitorStatuses,
},
message: { type: String, trim: true },
responseTime: { type: Number },
timings: {
start: { type: Date },
socket: { type: Date },
lookup: { type: Date },
connect: { type: Date },
secureConnect: { type: Date },
response: { type: Date },
end: { type: Date },
phases: {
wait: { type: Number },
dns: { type: Number },
tcp: { type: Number },
tls: { type: Number },
request: { type: Number },
firstByte: { type: Number },
download: { type: Number },
total: { type: Number },
},
},
system: {
type: {
cpu: {
physical_core: { type: Number },
logical_core: { type: Number },
frequency: { type: Number },
current_frequency: { type: Number },
temperature: [{ type: Number }],
free_percent: { type: Number },
usage_percent: { type: Number },
},
memory: {
total_bytes: { type: Number },
available_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
},
disk: [
{
device: { type: String },
total_bytes: { type: Number },
free_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
total_inodes: { type: Number },
free_inodes: { type: Number },
used_inodes: { type: Number },
inodes_usage_percent: { type: Number },
read_bytes: { type: Number },
write_bytes: { type: Number },
read_time: { type: Number },
write_time: { type: Number },
},
],
host: {
os: { type: String },
platform: { type: String },
kernel_version: { type: String },
pretty_name: { type: String },
},
net: [
{
name: { type: String },
bytes_sent: { type: Number },
bytes_recv: { type: Number },
packets_sent: { type: Number },
packets_recv: { type: Number },
err_in: { type: Number },
err_out: { type: Number },
drop_in: { type: Number },
drop_out: { type: Number },
fifo_in: { type: Number },
fifo_out: { type: Number },
},
],
},
required: false,
},
capture: {
type: {
version: { type: String },
mode: { type: String },
},
required: false,
},
lighthouse: {
accessibility: { type: Number, required: false },
bestPractices: { type: Number, required: false },
seo: { type: Number, required: false },
performance: { type: Number, required: false },
audits: {
cls: {
type: Object,
},
si: {
type: Object,
},
fcp: {
type: Object,
},
lcp: {
type: Object,
},
tbt: {
type: Object,
},
},
type: {
accessibility: { type: Number },
bestPractices: { type: Number },
seo: { type: Number },
performance: { type: Number },
audits: {
cls: { type: Object },
si: { type: Object },
fcp: { type: Object },
lcp: { type: Object },
tbt: { type: Object },
},
},
required: false,
},
httpStatusCode: { type: Number },
errorMessage: { type: String, trim: true },
ack: { type: Boolean, required: true, default: false },
ackAt: { type: Date },
ackBy: { type: Schema.Types.ObjectId, ref: "User_v2" },
expiry: {
type: Date,
default: Date.now,
expires: 60 * 60 * 24 * 30,
},
},
{ timestamps: true }
);
CheckSchema.index({ monitorId: 1, createdAt: -1 });
CheckSchema.index({ status: 1 });
CheckSchema.index({ status: 1, ack: 1 });
CheckSchema.index({ ack: 1, ackAt: 1 });
CheckSchema.index({ createdAt: -1 });
export const Check = mongoose.model<ICheck>("Check_v2", CheckSchema);
-33
View File
@@ -1,33 +0,0 @@
export { User } from "./auth/User.js";
export type { IUser } from "./auth/User.js";
export type { ITokenizedUser } from "./auth/User.js";
export { Role } from "./auth/Role.js";
export type { IRole } from "./auth/Role.js";
export { connectDatabase, disconnectDatabase } from "../index.js";
export { Monitor } from "./monitors/Monitor.js";
export { MonitorStatuses } from "./monitors/Monitor.js";
export type { IMonitor } from "./monitors/Monitor.js";
export { Check } from "./checks/Check.js";
export type {
ICheck,
ISystemInfo,
ICaptureInfo,
INetInfo,
IDiskInfo,
IHostInfo,
IMemoryInfo,
ICpuInfo,
ILighthouseAudit,
ITimingPhases,
ILighthouseCategories,
ILighthouseResult,
ICheckLighthouseFields,
} from "./checks/Check.js";
export type { IMonitorStats } from "./monitors/MonitorStats.js";
export { MonitorStats } from "./monitors/MonitorStats.js";
export type { INotificationChannel } from "./notification-channel/NotificationChannel.js";
export { NotificationChannel } from "./notification-channel/NotificationChannel.js";
export type { IMaintenance } from "./maintenance/Maintenance.js";
export { Maintenance } from "./maintenance/Maintenance.js";
export type { IInvite } from "./invite/Invite.js";
export { Invite } from "./invite/Invite.js";
-43
View File
@@ -1,43 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IInvite extends Document {
_id: Types.ObjectId;
email: string;
tokenHash: string;
roles: Types.ObjectId[];
createdBy: Types.ObjectId;
updatedBy: Types.ObjectId;
expiry: Date;
createdAt: Date;
updatedAt: Date;
}
const InviteSchema = new Schema<IInvite>(
{
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
},
roles: [
{
type: Schema.Types.ObjectId,
ref: "Role_v2",
required: true,
},
],
tokenHash: { type: String, required: true, unique: true },
expiry: {
type: Date,
default: Date.now,
expires: 60 * 60 * 24,
},
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
},
{ timestamps: true }
);
export const Invite = mongoose.model<IInvite>("Invite_v2", InviteSchema);
@@ -1,39 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IMaintenance extends Document {
_id: Types.ObjectId;
name: string;
isActive: boolean;
monitors: Types.ObjectId[];
startTime: Date;
endTime: Date;
createdBy: Types.ObjectId;
updatedBy: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const MaintenanceSchema = new Schema<IMaintenance>(
{
name: { type: String, required: true, trim: true },
isActive: { type: Boolean, required: true, default: true },
monitors: [
{
type: Schema.Types.ObjectId,
ref: "Monitor_v2",
required: true,
},
],
startTime: { type: Date, required: true },
endTime: { type: Date, required: true },
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
},
{ timestamps: true }
);
MaintenanceSchema.index({ isActive: 1 });
MaintenanceSchema.index({ startTime: 1 });
MaintenanceSchema.index({ endTime: 1 });
export const Maintenance = mongoose.model<IMaintenance>("Maintenance_v2", MaintenanceSchema);
@@ -1,96 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
import { Check, MonitorStats } from "../index.js";
export const MonitorTypes = ["http", "https", "ping", "infrastructure", "pagespeed"] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export const MonitorStatuses = ["up", "down", "paused", "initializing"] as const;
export type MonitorStatus = (typeof MonitorStatuses)[number];
export interface IMonitor extends Document {
_id: Types.ObjectId;
name: string;
url: string;
secret?: string;
type: MonitorType;
interval: number; // in ms
isActive: boolean;
status: MonitorStatus;
n: number; // Number of consecutive successes required to change status
lastCheckedAt?: Date;
latestChecks: {
status: MonitorStatus;
responseTime: number;
checkedAt: Date;
}[];
notificationChannels?: Types.ObjectId[];
createdBy: Types.ObjectId;
updatedBy: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const MonitorSchema = new Schema<IMonitor>(
{
name: { type: String, required: true, trim: true, maxlength: 100 },
url: { type: String, required: true, trim: true },
secret: { type: String, required: false },
type: {
type: String,
required: true,
enum: MonitorTypes,
},
interval: { type: Number, required: true, default: 60000 },
isActive: { type: Boolean, required: true, default: true },
status: {
type: String,
required: true,
default: "initializing",
enum: MonitorStatuses,
},
n: { type: Number, required: true, default: 1 },
lastCheckedAt: { type: Date },
latestChecks: {
type: [
{
status: {
type: String,
required: true,
enum: MonitorStatuses,
},
responseTime: { type: Number, required: true },
checkedAt: { type: Date, required: true },
},
],
default: [],
},
notificationChannels: {
type: [{ type: Schema.Types.ObjectId, ref: "NotificationChannel_v2" }],
default: [],
},
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
},
{ timestamps: true }
);
MonitorSchema.pre("deleteOne", { document: true, query: false }, async function (next) {
try {
const monitorId = this._id;
await Check.deleteMany({ monitorId });
await MonitorStats.deleteMany({ monitorId });
next();
} catch (error: any) {
next(error);
}
});
MonitorSchema.index({ isActive: 1 });
MonitorSchema.index({ status: 1 });
MonitorSchema.index({ type: 1 });
MonitorSchema.index({ lastCheckedAt: 1 });
MonitorSchema.index({ isActive: 1, status: 1 });
MonitorSchema.index({ createdBy: 1 });
MonitorSchema.index({ updatedBy: 1 });
export const Monitor = mongoose.model<IMonitor>("Monitor_v2", MonitorSchema);
@@ -1,77 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
import { MonitorStatus, MonitorStatuses } from "./Monitor.js";
export interface IMonitorStats extends mongoose.Document {
monitorId: mongoose.Types.ObjectId;
avgResponseTime: number;
maxResponseTime: number;
totalChecks: number;
totalUpChecks: number;
totalDownChecks: number;
uptimePercentage: number;
lastCheckTimestamp: number;
lastResponseTime: number;
timeOfLastFailure: number;
currentStreak: number;
currentStreakStatus: MonitorStatus;
currentStreakStartedAt: number;
createdAt: Date;
updatedAt: Date;
}
const MonitorStatsSchema = new Schema<IMonitorStats>(
{
monitorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor_v2",
immutable: true,
index: true,
},
avgResponseTime: {
type: Number,
default: 0,
},
maxResponseTime: {
type: Number,
default: 0,
},
lastResponseTime: {
type: Number,
default: 0,
},
totalChecks: {
type: Number,
default: 0,
},
totalUpChecks: {
type: Number,
default: 0,
},
totalDownChecks: {
type: Number,
default: 0,
},
uptimePercentage: {
type: Number,
default: 0,
},
lastCheckTimestamp: {
type: Number,
default: 0,
},
timeOfLastFailure: {
type: Number,
default: 0,
},
currentStreak: { type: Number, required: false, default: 0 },
currentStreakStatus: {
type: String,
required: false,
enum: MonitorStatuses,
},
currentStreakStartedAt: { type: Number, required: false },
},
{ timestamps: true }
);
export const MonitorStats = mongoose.model<IMonitorStats>("MonitorStats_v2", MonitorStatsSchema);
@@ -1,50 +0,0 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const;
export type ChannelType = (typeof ChannelTypes)[number];
export interface INotificationChannelConfig {
url?: string; // For webhook, slack, discord
emailAddress?: string; // For email
}
export interface INotificationChannel {
_id: Types.ObjectId;
name: string;
type: ChannelType;
config: INotificationChannelConfig;
isActive: boolean;
createdBy: Types.ObjectId;
updatedBy: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const NotificationChannelConfigSchema = new Schema<INotificationChannelConfig>(
{
url: { type: String, required: false },
emailAddress: { type: String, required: false },
},
{ _id: false, strict: "throw" }
);
const NotificationChannelSchema = new Schema<INotificationChannel>(
{
name: { type: String, required: true, trim: true },
type: {
type: String,
required: true,
enum: ChannelTypes,
},
config: { type: NotificationChannelConfigSchema, required: true },
isActive: { type: Boolean, required: true, default: true },
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
},
{ timestamps: true }
);
NotificationChannelSchema.index({ isActive: 1 });
NotificationChannelSchema.index({ type: 1 });
NotificationChannelSchema.index({ type: 1, isActive: 1 });
export const NotificationChannel = mongoose.model<INotificationChannel>("NotificationChannel_v2", NotificationChannelSchema);