diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx
index ebfdad569..70f4325c6 100644
--- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx
+++ b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes.tsx
@@ -11,7 +11,8 @@ export const StatusBoxes = ({ monitor }: { monitor: Monitor }) => {
const { t } = useTranslation();
const theme = useTheme();
- const latestCheck = monitor?.recentChecks?.[0];
+ const recentChecks = monitor?.recentChecks ?? [];
+ const latestCheck = recentChecks[recentChecks.length - 1];
// Get data from latest check
const physicalCores = getCores(latestCheck?.cpu?.physical_core);
const logicalCores = getCores(latestCheck?.cpu?.logical_core);
@@ -21,13 +22,7 @@ export const StatusBoxes = ({ monitor }: { monitor: Monitor }) => {
const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0;
const diskTotalBytes =
latestCheck?.disk?.reduce((acc, disk) => acc + (disk.total_bytes || 0), 0) || 0;
- const os = getOsAndPlatform(latestCheck?.host);
-
- const platform = latestCheck?.host?.platform ?? undefined;
- const osPlatform =
- typeof os === "undefined" && typeof platform === "undefined"
- ? undefined
- : `${os} ${platform}`;
+ const osPlatform = getOsAndPlatform(latestCheck?.host);
return (
-
+
{
- const check = row.recentChecks?.[0];
+ const recentChecks = row.recentChecks ?? [];
+ const check = recentChecks[recentChecks.length - 1];
const cpuUsage = (check?.cpu?.usage_percent || 0) * 100;
return ;
},
@@ -257,7 +258,8 @@ export const InfraMonitorsTable = ({
id: "memory",
content: t("pages.infrastructure.table.headers.memory"),
render: (row) => {
- const check = row.recentChecks?.[0];
+ const recentChecks = row.recentChecks ?? [];
+ const check = recentChecks[recentChecks.length - 1];
const memoryUsage = (check?.memory?.usage_percent || 0) * 100;
return ;
},
@@ -266,7 +268,8 @@ export const InfraMonitorsTable = ({
id: "disk",
content: t("pages.infrastructure.table.headers.disk"),
render: (row) => {
- const check = row.recentChecks?.[0];
+ const recentChecks = row.recentChecks ?? [];
+ const check = recentChecks[recentChecks.length - 1];
const totalDiskUsage = check?.disk?.reduce(
(acc, disk) => acc + (disk?.usage_percent || 0),
diff --git a/client/src/Pages/PageSpeed/Details/index.tsx b/client/src/Pages/PageSpeed/Details/index.tsx
index 48a9ba99c..ea9da4a7f 100644
--- a/client/src/Pages/PageSpeed/Details/index.tsx
+++ b/client/src/Pages/PageSpeed/Details/index.tsx
@@ -45,6 +45,8 @@ const PageSpeedDetails = () => {
const monitor = monitorData?.monitorData?.monitor;
const groupedChecks = monitorData?.monitorData?.groupedChecks || [];
const monitorStats = monitorData?.monitorStats || null;
+ const recentChecks = monitor?.recentChecks ?? [];
+ const latestCheck = recentChecks[recentChecks.length - 1];
return (
{
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(10)}
>
-
-
+
+
);
diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedInfrastructure.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedInfrastructure.tsx
index ab7d0dc35..3c92e6b0a 100644
--- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedInfrastructure.tsx
+++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedInfrastructure.tsx
@@ -36,7 +36,8 @@ const heatLevel = (value: number): GaugeFillLevel =>
export const ThemedInfrastructure = ({ monitor, sxApi }: Props) => {
const { t } = useTranslation();
- const latest = monitor.recentChecks?.[0];
+ const recentChecks = monitor.recentChecks ?? [];
+ const latest = recentChecks[recentChecks.length - 1];
const renderEmpty = () => (
{t("pages.statusPages.monitorsList.noData")}
diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts
index c930eacad..e32762d95 100644
--- a/client/src/Types/Check.ts
+++ b/client/src/Types/Check.ts
@@ -250,7 +250,44 @@ export interface ChecksSummary {
downChecks: number;
}
-export type CheckSnapshot = Omit;
+export type SnapshotCpuInfo = Pick<
+ CheckCpuInfo,
+ | "physical_core"
+ | "logical_core"
+ | "frequency"
+ | "current_frequency"
+ | "temperature"
+ | "usage_percent"
+>;
+export type SnapshotMemoryInfo = Pick<
+ CheckMemoryInfo,
+ "total_bytes" | "used_bytes" | "usage_percent"
+>;
+export type SnapshotDiskInfo = Pick<
+ CheckDiskInfo,
+ "device" | "total_bytes" | "used_bytes" | "usage_percent"
+>;
+export type SnapshotHostInfo = Pick;
+
+export type CheckSnapshot = Pick<
+ Check,
+ | "id"
+ | "status"
+ | "responseTime"
+ | "statusCode"
+ | "message"
+ | "createdAt"
+ | "accessibility"
+ | "bestPractices"
+ | "seo"
+ | "performance"
+ | "audits"
+> & {
+ cpu?: SnapshotCpuInfo;
+ memory?: SnapshotMemoryInfo;
+ disk?: SnapshotDiskInfo[];
+ host?: SnapshotHostInfo;
+};
export interface HasResponseTime {
responseTime: number;
}
diff --git a/server/src/db/migration/0010_slimRecentChecks.ts b/server/src/db/migration/0010_slimRecentChecks.ts
new file mode 100644
index 000000000..2b8f8e661
--- /dev/null
+++ b/server/src/db/migration/0010_slimRecentChecks.ts
@@ -0,0 +1,128 @@
+import { MonitorModel } from "../../domain/monitors/monitor.model.js";
+import type { ILogger } from "@/utils/logger.js";
+
+const KEEP_KEYS = [
+ "id",
+ "status",
+ "responseTime",
+ "statusCode",
+ "message",
+ "createdAt",
+ "cpu",
+ "memory",
+ "disk",
+ "host",
+ "accessibility",
+ "bestPractices",
+ "seo",
+ "performance",
+ "audits",
+];
+
+/**
+ * Slim stored recentChecks snapshots down to the fields the slim CheckSnapshot type keeps.
+ * The schema change only affects new writes; paused monitors never rotate their snapshots out.
+ * Idempotent: re-running on already-slim snapshots is a no-op re-pick.
+ */
+export async function slimRecentChecks(logger: ILogger): Promise {
+ const SERVICE_NAME = "Migration:SlimRecentChecks";
+ try {
+ logger.info({ service: SERVICE_NAME, message: "Slimming recentChecks snapshots" });
+
+ const result = await MonitorModel.updateMany({ "recentChecks.0": { $exists: true } }, [
+ {
+ $set: {
+ recentChecks: {
+ $map: {
+ input: "$recentChecks",
+ as: "snap",
+ in: {
+ $mergeObjects: [
+ // 1) keep whitelisted top-level keys only (drops timings/errors/capture/net)
+ {
+ $arrayToObject: {
+ $filter: {
+ input: { $objectToArray: "$$snap" },
+ as: "kv",
+ cond: { $in: ["$$kv.k", KEEP_KEYS] },
+ },
+ },
+ },
+ // 2) where a nested group exists, overwrite it with its slim pick
+ {
+ $cond: [
+ { $eq: [{ $type: "$$snap.cpu" }, "object"] },
+ {
+ cpu: {
+ physical_core: "$$snap.cpu.physical_core",
+ logical_core: "$$snap.cpu.logical_core",
+ frequency: "$$snap.cpu.frequency",
+ current_frequency: "$$snap.cpu.current_frequency",
+ temperature: "$$snap.cpu.temperature",
+ usage_percent: "$$snap.cpu.usage_percent",
+ },
+ },
+ {},
+ ],
+ },
+ {
+ $cond: [
+ { $eq: [{ $type: "$$snap.memory" }, "object"] },
+ {
+ memory: {
+ total_bytes: "$$snap.memory.total_bytes",
+ used_bytes: "$$snap.memory.used_bytes",
+ usage_percent: "$$snap.memory.usage_percent",
+ },
+ },
+ {},
+ ],
+ },
+ {
+ $cond: [
+ { $eq: [{ $type: "$$snap.disk" }, "array"] },
+ {
+ disk: {
+ $map: {
+ input: "$$snap.disk",
+ as: "d",
+ in: {
+ device: "$$d.device",
+ total_bytes: "$$d.total_bytes",
+ used_bytes: "$$d.used_bytes",
+ usage_percent: "$$d.usage_percent",
+ },
+ },
+ },
+ },
+ {},
+ ],
+ },
+ {
+ $cond: [
+ { $eq: [{ $type: "$$snap.host" }, "object"] },
+ {
+ host: {
+ os: "$$snap.host.os",
+ platform: "$$snap.host.platform",
+ pretty_name: "$$snap.host.pretty_name",
+ },
+ },
+ {},
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ logger.info({ service: SERVICE_NAME, message: `Slimmed recentChecks on ${result.modifiedCount} monitors` });
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ logger.error({ service: SERVICE_NAME, message: `Error slimming recentChecks: ${errorMessage}` });
+ throw error;
+ }
+}
diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts
index f59af32b7..24c330ef5 100644
--- a/server/src/db/migration/index.ts
+++ b/server/src/db/migration/index.ts
@@ -8,6 +8,7 @@ import { cleanupDuplicateMonitorStatsForUniqueIndex } from "./0006_cleanupDuplic
import { migrateMaintenanceWindowMonitorIdToArray } from "./0007_migrateMaintenanceWindowMonitorIdToArray.js";
import { backfillMonitorLastEvaluatedAt } from "./0008_backfillMonitorLastEvaluatedAt.js";
import { recomputeResponseTimeFromTimings } from "./0009_recomputeResponseTimeFromTimings.js";
+import { slimRecentChecks } from "./0010_slimRecentChecks.js";
import type { ILogger } from "@/utils/logger.js";
type MigrationEntry = {
@@ -25,6 +26,7 @@ const migrations: MigrationEntry[] = [
{ name: "0007_migrateMaintenanceWindowMonitorIdToArray", execute: migrateMaintenanceWindowMonitorIdToArray },
{ name: "0008_backfillMonitorLastEvaluatedAt", execute: backfillMonitorLastEvaluatedAt },
{ name: "0009_recomputeResponseTimeFromTimings", execute: recomputeResponseTimeFromTimings },
+ { name: "0010_slimRecentChecks", execute: slimRecentChecks },
];
const runMigrations = async (logger: ILogger) => {
diff --git a/server/src/domain/checks/check.repository.interface.ts b/server/src/domain/checks/check.repository.interface.ts
index e6b7c6368..6889d8d15 100644
--- a/server/src/domain/checks/check.repository.interface.ts
+++ b/server/src/domain/checks/check.repository.interface.ts
@@ -7,7 +7,6 @@ import type {
UptimeChecksResult,
} from "@/domain/checks/check.type.js";
import type { MonitorType } from "@/domain/monitors/monitor.type.js";
-import type { LatestChecksMap } from "@/domain/checks/check.repository.mongo.js";
import { CheckFilter, DateRange } from "@/types/query.js";
export interface IChecksRepository {
@@ -34,7 +33,6 @@ export interface IChecksRepository {
teamId: string,
filter?: CheckFilter
): Promise;
- findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise;
findByDateRangeAndMonitorId(
monitorId: string,
dateRange: DateRange,
diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts
index fcc967828..cb1c5bfba 100644
--- a/server/src/domain/checks/check.repository.mongo.ts
+++ b/server/src/domain/checks/check.repository.mongo.ts
@@ -27,8 +27,6 @@ import { NETWORK_ERROR } from "@/types/network.js";
const SERVICE_NAME = "StatusService";
-export type LatestChecksMap = Record;
-
class MongoChecksRepository implements IChecksRepository {
static SERVICE_NAME = SERVICE_NAME;
@@ -83,6 +81,7 @@ class MongoChecksRepository implements IChecksRepository {
os: host?.os ?? "",
platform: host?.platform ?? "",
kernel_version: host?.kernel_version ?? "",
+ pretty_name: host?.pretty_name ?? "",
});
const mapCapture = (capture?: CheckCaptureInfo): CheckCaptureInfo => ({
@@ -288,32 +287,6 @@ class MongoChecksRepository implements IChecksRepository {
return { checksCount, checks: this.mapDocuments(checks) };
};
- findLatestByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => {
- if (monitorIds.length === 0) {
- return {};
- }
- const limitPerMonitor = options?.limitPerMonitor ?? 25;
- const dateFilter = new Date(Date.now() - 24 * 60 * 60 * 1000);
- const results = await Promise.all(
- monitorIds.map(async (monitorId) => {
- const docs = await CheckModel.find({
- "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
- createdAt: { $gte: dateFilter },
- })
- .sort({ createdAt: -1 })
- .limit(limitPerMonitor)
- .lean();
- return { monitorId, docs };
- })
- );
-
- const mapped = results.reduce((acc, { monitorId, docs }) => {
- acc[monitorId] = docs.map((doc: CheckDocument) => this.toEntity(doc));
- return acc;
- }, {});
- return mapped;
- };
-
findByDateRangeAndMonitorId = async (monitorId: string, dateRange: DateRange, options?: { type?: MonitorType }) => {
const monitorObjectId = new mongoose.Types.ObjectId(monitorId);
const start = getDateForRange(dateRange);
diff --git a/server/src/domain/checks/check.snapshot.ts b/server/src/domain/checks/check.snapshot.ts
new file mode 100644
index 000000000..3ee208d04
--- /dev/null
+++ b/server/src/domain/checks/check.snapshot.ts
@@ -0,0 +1,85 @@
+import type {
+ Check,
+ CheckAudits,
+ CheckCpuInfo,
+ CheckDiskInfo,
+ CheckHostInfo,
+ CheckMemoryInfo,
+ CheckSnapshot,
+ SnapshotCpuInfo,
+ SnapshotDiskInfo,
+ SnapshotHostInfo,
+ SnapshotMemoryInfo,
+} from "@/domain/checks/check.type.js";
+import { toDateString } from "@/utils/mongoMappers.js";
+
+export type CheckSnapshotSource = Pick<
+ Check,
+ "id" | "status" | "responseTime" | "statusCode" | "message" | "accessibility" | "bestPractices" | "seo" | "performance"
+> & {
+ createdAt: string | Date;
+ cpu?: CheckCpuInfo;
+ memory?: CheckMemoryInfo;
+ disk?: CheckDiskInfo[];
+ host?: CheckHostInfo;
+ audits?: CheckAudits;
+};
+
+const mapCpu = (cpu?: CheckCpuInfo): SnapshotCpuInfo | undefined =>
+ cpu && {
+ physical_core: cpu.physical_core,
+ logical_core: cpu.logical_core,
+ frequency: cpu.frequency,
+ current_frequency: cpu.current_frequency,
+ temperature: cpu.temperature,
+ usage_percent: cpu.usage_percent,
+ };
+
+const mapMemory = (memory?: CheckMemoryInfo): SnapshotMemoryInfo | undefined =>
+ memory && {
+ total_bytes: memory.total_bytes,
+ used_bytes: memory.used_bytes,
+ usage_percent: memory.usage_percent,
+ };
+
+const mapDisk = (disk?: CheckDiskInfo[]): SnapshotDiskInfo[] | undefined =>
+ disk?.map((entry) => ({
+ device: entry.device,
+ total_bytes: entry.total_bytes,
+ used_bytes: entry.used_bytes,
+ usage_percent: entry.usage_percent,
+ }));
+
+const mapHost = (host?: CheckHostInfo): SnapshotHostInfo | undefined =>
+ host && {
+ os: host.os,
+ platform: host.platform,
+ pretty_name: host.pretty_name,
+ };
+
+const mapAudits = (audits?: CheckAudits): CheckAudits | undefined =>
+ audits && {
+ cls: audits.cls,
+ si: audits.si,
+ fcp: audits.fcp,
+ lcp: audits.lcp,
+ tbt: audits.tbt,
+ };
+
+export const toCheckSnapshot = (source: CheckSnapshotSource): CheckSnapshot => ({
+ id: source.id,
+ status: source.status,
+ responseTime: source.responseTime,
+ statusCode: source.statusCode,
+ message: source.message,
+ createdAt: toDateString(source.createdAt),
+ cpu: mapCpu(source.cpu),
+ memory: mapMemory(source.memory),
+ disk: mapDisk(source.disk),
+ host: mapHost(source.host),
+ accessibility: source.accessibility,
+ bestPractices: source.bestPractices,
+ seo: source.seo,
+ performance: source.performance,
+ audits: mapAudits(source.audits),
+});
diff --git a/server/src/domain/checks/check.type.ts b/server/src/domain/checks/check.type.ts
index d6e60c3e3..4e69a5241 100644
--- a/server/src/domain/checks/check.type.ts
+++ b/server/src/domain/checks/check.type.ts
@@ -168,8 +168,36 @@ export interface HasResponseTime {
responseTime: number;
}
-export type CheckSnapshot = Omit;
+export type SnapshotCpuInfo = Pick<
+ CheckCpuInfo,
+ "physical_core" | "logical_core" | "frequency" | "current_frequency" | "temperature" | "usage_percent"
+>;
+export type SnapshotMemoryInfo = Pick;
+export type SnapshotDiskInfo = Pick;
+export type SnapshotHostInfo = Pick;
+export type CheckSnapshot = Pick<
+ Check,
+ // uptime charts and down check tooltips
+ | "id"
+ | "status"
+ | "responseTime"
+ | "statusCode"
+ | "message"
+ | "createdAt"
+ // pagespeed
+ | "accessibility"
+ | "bestPractices"
+ | "seo"
+ | "performance"
+ | "audits"
+> & {
+ // hardware monitors only
+ cpu?: SnapshotCpuInfo;
+ memory?: SnapshotMemoryInfo;
+ disk?: SnapshotDiskInfo[];
+ host?: SnapshotHostInfo;
+};
export interface HardwareDiskStats {
name: string;
readSpeed: number;
diff --git a/server/src/domain/maintenance-windows/maintenance-window.service.ts b/server/src/domain/maintenance-windows/maintenance-window.service.ts
index 350e755aa..dd27ba0b3 100644
--- a/server/src/domain/maintenance-windows/maintenance-window.service.ts
+++ b/server/src/domain/maintenance-windows/maintenance-window.service.ts
@@ -106,7 +106,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
start: string;
end: string;
}) => {
- const monitors = await this.monitorsRepository.findByIds(monitorIDs);
+ const monitors = await this.monitorsRepository.findByIds(monitorIDs, { includeRecentChecks: false });
const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId);
@@ -193,7 +193,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
const update: Partial = rest;
if (monitors !== undefined) {
- const monitorDocs = await this.monitorsRepository.findByIds(monitors);
+ const monitorDocs = await this.monitorsRepository.findByIds(monitors, { includeRecentChecks: false });
const unauthorizedMonitors = monitorDocs.filter((monitor) => monitor.teamId !== teamId);
if (unauthorizedMonitors.length > 0) {
throw new AppError({
diff --git a/server/src/domain/monitors/monitor.model.ts b/server/src/domain/monitors/monitor.model.ts
index 446631f45..ffd4238d0 100644
--- a/server/src/domain/monitors/monitor.model.ts
+++ b/server/src/domain/monitors/monitor.model.ts
@@ -3,15 +3,11 @@ import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/domain/monito
import { DnsRecordTypes, MonitorTypes, MonitorStatuses, PageSpeedStrategies, HttpMethods } from "@/domain/monitors/monitor.type.js";
import type {
CheckAudits,
- CheckCaptureInfo,
- CheckCpuInfo,
- CheckDiskInfo,
- CheckErrorInfo,
- CheckHostInfo,
- CheckMemoryInfo,
- CheckNetworkInterfaceInfo,
- GotTimings,
ILighthouseAudit,
+ SnapshotCpuInfo,
+ SnapshotDiskInfo,
+ SnapshotHostInfo,
+ SnapshotMemoryInfo,
} from "@/domain/checks/check.type.js";
type CheckSnapshotDocument = Omit & { createdAt: Date };
@@ -37,121 +33,46 @@ interface MonitorDocument extends MonitorDocumentBase {
lastEvaluatedAt: number; // epoch ms
}
-const snapshotTimingPhasesSchema = new Schema(
- {
- wait: { type: Number },
- dns: { type: Number },
- tcp: { type: Number },
- tls: { type: Number },
- request: { type: Number },
- firstByte: { type: Number },
- download: { type: Number },
- total: { type: Number },
- },
- { _id: false }
-);
-
-const snapshotTimingsSchema = new Schema(
- {
- start: { type: Number },
- socket: { type: Number },
- lookup: { type: Number },
- connect: { type: Number },
- secureConnect: { type: Number },
- upload: { type: Number },
- response: { type: Number },
- end: { type: Number },
- phases: { type: snapshotTimingPhasesSchema },
- },
- { _id: false }
-);
-
-const snapshotCpuSchema = new Schema(
+const snapshotCpuSchema = new Schema(
{
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 },
},
{ _id: false }
);
-const snapshotMemorySchema = new Schema(
+const snapshotMemorySchema = new Schema(
{
total_bytes: { type: Number },
- available_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
},
{ _id: false }
);
-const snapshotDiskSchema = new Schema(
+const snapshotDiskSchema = new Schema(
{
device: { type: String },
- mountpoint: { 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 },
},
{ _id: false }
);
-const snapshotHostSchema = new Schema(
+const snapshotHostSchema = new Schema(
{
os: { type: String },
platform: { type: String },
- kernel_version: { type: String },
pretty_name: { type: String },
},
{ _id: false }
);
-const snapshotErrorSchema = new Schema(
- {
- metric: { type: [String] },
- err: { type: String },
- },
- { _id: false }
-);
-
-const snapshotCaptureSchema = new Schema(
- {
- version: { type: String },
- mode: { type: String },
- },
- { _id: false }
-);
-
-const snapshotNetworkInterfaceSchema = new Schema(
- {
- 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 },
- },
- { _id: false }
-);
-
const snapshotLighthouseAuditSchema = new Schema(
{
id: { type: String },
@@ -180,16 +101,12 @@ const checkSnapshotSchema = new Schema(
id: { type: String, required: true },
status: { type: Boolean, required: true },
responseTime: { type: Number },
- timings: { type: snapshotTimingsSchema },
statusCode: { type: Number },
message: { type: String },
cpu: { type: snapshotCpuSchema },
memory: { type: snapshotMemorySchema },
disk: { type: [snapshotDiskSchema] },
host: { type: snapshotHostSchema },
- errors: { type: [snapshotErrorSchema] },
- capture: { type: snapshotCaptureSchema },
- net: { type: [snapshotNetworkInterfaceSchema] },
accessibility: { type: Number },
bestPractices: { type: Number },
seo: { type: Number },
diff --git a/server/src/domain/monitors/monitor.repository.interface.ts b/server/src/domain/monitors/monitor.repository.interface.ts
index d3dc0597e..53c178a1d 100644
--- a/server/src/domain/monitors/monitor.repository.interface.ts
+++ b/server/src/domain/monitors/monitor.repository.interface.ts
@@ -1,4 +1,4 @@
-import type { MonitorType, Monitor, MonitorStatus, MonitorsSummary, CheckSnapshot } from "@/domain/monitors/monitor.type.js";
+import type { MonitorType, Monitor, MonitorStatus, MonitorsSummary, CheckSnapshot, MonitorScheduleFields } from "@/domain/monitors/monitor.type.js";
export interface TeamQueryConfig {
limit?: number;
@@ -26,11 +26,10 @@ export interface IMonitorsRepository {
findByIdLean(monitorId: string): Promise;
// collection fetch
- findAll(): Promise;
- findByTeamId(teamId: string, config: TeamQueryConfig): Promise;
+ findAllForScheduling(): Promise;
+ findByTeamId(teamId: string, config: TeamQueryConfig, options?: { includeRecentChecks?: boolean }): Promise;
findByTeamIdWithStats(teamId: string, config: TeamQueryConfig): Promise;
- findByIds(monitorIds: string[]): Promise;
- findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise;
+ findByIds(monitorIds: string[], options?: { includeRecentChecks?: boolean }): Promise;
// update
updateById(monitorId: string, teamId: string, updates: Partial): Promise;
diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts
index b96ec3907..2411f3840 100644
--- a/server/src/domain/monitors/monitor.repository.mongo.ts
+++ b/server/src/domain/monitors/monitor.repository.mongo.ts
@@ -1,12 +1,13 @@
import { MonitorModel } from "@/domain/monitors/monitor.model.js";
import type { MonitorDocument, CheckSnapshotDocument } from "@/domain/monitors/monitor.model.js";
import type { CheckSnapshot } from "@/domain/checks/check.type.js";
-import type { Monitor, MonitorStatus, MonitorsSummary } from "@/domain/monitors/monitor.type.js";
+import type { Monitor, MonitorScheduleFields, MonitorStatus, MonitorsSummary } from "@/domain/monitors/monitor.type.js";
import mongoose, { type FilterQuery, type PipelineStage } from "mongoose";
import { MongoBulkWriteError } from "mongodb";
import { AppError } from "@/utils/AppError.js";
import { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "@/domain/monitors/monitor.repository.interface.js";
import { toStringId, toDateString } from "@/utils/mongoMappers.js";
+import { toCheckSnapshot } from "@/domain/checks/check.snapshot.js";
class MongoMonitorsRepository implements IMonitorsRepository {
create = async (monitor: Monitor, teamId: string, userId: string) => {
const monitorModel = new MonitorModel({ ...monitor, teamId, userId });
@@ -40,13 +41,20 @@ class MongoMonitorsRepository implements IMonitorsRepository {
};
findByIdLean = async (monitorId: string): Promise => {
- const monitor = await MonitorModel.findOne({ _id: monitorId });
+ const monitor = await MonitorModel.findOne({ _id: monitorId }).select({ recentChecks: 0 });
return monitor ? this.toEntity(monitor) : null;
};
- findAll = async (): Promise => {
- const monitors = await MonitorModel.find();
- return this.mapDocuments(monitors);
+ findAllForScheduling = async (): Promise => {
+ const docs = await MonitorModel.find({}, { type: 1, isActive: 1, interval: 1, geoCheckEnabled: 1, geoCheckInterval: 1 }).lean();
+ return docs.map((doc) => ({
+ id: doc._id.toString(),
+ type: doc.type,
+ isActive: doc.isActive,
+ interval: doc.interval,
+ geoCheckEnabled: doc.geoCheckEnabled ?? false,
+ geoCheckInterval: doc.geoCheckInterval ?? 300000,
+ }));
};
private queryBuilder = (config: TeamQueryConfig, teamId: string): FilterQuery => {
@@ -96,10 +104,15 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return { sort: { [field]: order === "asc" ? 1 : -1 } as const, skip: Math.max(page, 0) * rowsPerPage, limit: rowsPerPage };
};
- findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => {
+ findByTeamId = async (teamId: string, config: TeamQueryConfig, options?: { includeRecentChecks?: boolean }): Promise => {
const query = this.queryBuilder(config, teamId);
const { sort, skip, limit } = this.pageOptions(config);
- const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limit);
+
+ const cursor = MonitorModel.find(query).sort(sort).skip(skip).limit(limit);
+ if (options?.includeRecentChecks === false) {
+ cursor.select({ recentChecks: 0 });
+ }
+ const documents = await cursor;
return this.mapDocuments(documents);
};
@@ -118,20 +131,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return documents.map((doc) => this.toEntity(doc));
};
- findByIds = async (monitorIds: string[]): Promise => {
- if (!monitorIds.length) {
- return [];
- }
-
- const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
-
- const pipeline: PipelineStage[] = [{ $match: { _id: { $in: objectIds } } }, ...this.uptimeStatsLookupStages];
-
- const documents = await MonitorModel.aggregate(pipeline);
- return documents.map((doc) => this.toEntity(doc));
- };
-
- findByIdsWithChecks = async (monitorIds: string[], checksCount: number = 25): Promise => {
+ findByIds = async (monitorIds: string[], options?: { includeRecentChecks?: boolean }): Promise => {
if (!monitorIds.length) {
return [];
}
@@ -140,57 +140,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
const pipeline: PipelineStage[] = [
{ $match: { _id: { $in: objectIds } } },
- {
- $lookup: {
- from: "checks",
- let: { monitorId: "$_id" },
- pipeline: [{ $match: { $expr: { $eq: ["$metadata.monitorId", "$$monitorId"] } } }, { $sort: { createdAt: -1 } }, { $limit: checksCount }],
- as: "checks",
- },
- },
- {
- $lookup: {
- from: "maintenancewindows",
- let: { monitorId: "$_id" },
- pipeline: [{ $match: { $expr: { $in: ["$$monitorId", { $ifNull: ["$monitorIds", []] }] } } }],
- as: "maintenanceWindows",
- },
- },
- {
- $lookup: {
- from: "monitorstats",
- localField: "_id",
- foreignField: "monitorId",
- as: "stats",
- },
- },
- {
- $addFields: {
- isMaintenance: {
- $reduce: {
- input: "$maintenanceWindows",
- initialValue: false,
- in: {
- $or: [
- "$$value",
- {
- $and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }],
- },
- ],
- },
- },
- },
- uptimePercentage: { $arrayElemAt: ["$stats.uptimePercentage", 0] },
- },
- },
- {
- $project: {
- maintenanceWindows: 0,
- stats: 0,
- },
- },
+ ...(options?.includeRecentChecks === false ? [{ $project: { recentChecks: 0 } } as PipelineStage] : []),
+ ...this.uptimeStatsLookupStages,
];
-
const documents = await MonitorModel.aggregate(pipeline);
return documents.map((doc) => this.toEntity(doc));
};
@@ -245,7 +197,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
},
...(statusPatch && { $set: statusPatch }),
},
- { returnDocument: "after" }
+ { returnDocument: "after", projection: { recentChecks: 0 } }
);
if (!updatedMonitor) {
@@ -473,7 +425,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
grpcServiceName: doc.grpcServiceName ?? undefined,
strategy: doc.strategy ?? undefined,
group: doc.group ?? null,
- recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
+ recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 300000,
@@ -485,30 +437,6 @@ class MongoMonitorsRepository implements IMonitorsRepository {
};
};
- private toCheckSnapshot = (doc: CheckSnapshotDocument): CheckSnapshot => {
- return {
- id: doc.id,
- status: doc.status,
- responseTime: doc.responseTime,
- timings: doc.timings,
- statusCode: doc.statusCode,
- message: doc.message,
- cpu: doc.cpu,
- memory: doc.memory,
- disk: doc.disk,
- host: doc.host,
- errors: doc.errors,
- capture: doc.capture,
- net: doc.net,
- accessibility: doc.accessibility,
- bestPractices: doc.bestPractices,
- seo: doc.seo,
- performance: doc.performance,
- audits: doc.audits,
- createdAt: toDateString(doc.createdAt),
- };
- };
-
deleteByTeamIdsNotIn = async (teamIds: string[]): Promise => {
const objectIds = teamIds.map((id) => new mongoose.Types.ObjectId(id));
const result = await MonitorModel.deleteMany({ teamId: { $nin: objectIds } });
diff --git a/server/src/domain/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts
index 7a8478c5a..31488c378 100644
--- a/server/src/domain/monitors/monitor.service.ts
+++ b/server/src/domain/monitors/monitor.service.ts
@@ -363,7 +363,7 @@ export class MonitorService implements IMonitorService {
const monitorsWithChecks = monitors.map((monitor: Monitor) => {
const rawChecks = monitor.recentChecks ?? [];
const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type);
- const checks = isSnapshotType ? rawChecks.slice(0, 1) : rawChecks;
+ const checks = isSnapshotType ? rawChecks.slice(-1) : rawChecks;
monitor.recentChecks = checks;
return monitor;
});
@@ -519,7 +519,7 @@ export class MonitorService implements IMonitorService {
};
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise => {
- const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
+ const monitors = await this.monitorsRepository.findByTeamId(teamId, {}, { includeRecentChecks: false });
if (monitors.length === 0) {
throw new AppError({ message: "No monitors found to export.", service: SERVICE_NAME, method: "exportMonitorsToJSON", status: 400 });
diff --git a/server/src/domain/monitors/monitor.type.ts b/server/src/domain/monitors/monitor.type.ts
index 865bafd76..0fb1ed397 100644
--- a/server/src/domain/monitors/monitor.type.ts
+++ b/server/src/domain/monitors/monitor.type.ts
@@ -178,3 +178,5 @@ export interface Game {
}
export type GamesMap = Record;
+
+export type MonitorScheduleFields = Pick;
diff --git a/server/src/domain/users/user.service.ts b/server/src/domain/users/user.service.ts
index f3db2f3b5..f042863aa 100644
--- a/server/src/domain/users/user.service.ts
+++ b/server/src/domain/users/user.service.ts
@@ -324,7 +324,7 @@ export class UserService implements IUserService {
}
if (roles.includes("superadmin")) {
- const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
+ const monitors = await this.monitorsRepository.findByTeamId(teamId, {}, { includeRecentChecks: false });
if (monitors) {
await Promise.all(monitors.map((monitor) => this.scheduler.deleteJob(monitor)));
}
diff --git a/server/src/service/statusService.ts b/server/src/service/statusService.ts
index 9be31e5f1..07ddee2a0 100755
--- a/server/src/service/statusService.ts
+++ b/server/src/service/statusService.ts
@@ -1,6 +1,6 @@
import { IMonitorStatsRepository } from "@/domain/monitor-stats/monitor-stats.repository.interface.js";
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
-import type { Check, CheckDiskInfo, CheckSnapshot } from "@/domain/checks/check.type.js";
+import type { Check, CheckDiskInfo } from "@/domain/checks/check.type.js";
import { MonitorStatuses, type Monitor, type MonitorStatus } from "@/domain/monitors/monitor.type.js";
import type {
DockerStatusPayload,
@@ -18,6 +18,7 @@ import { AppError } from "@/utils/AppError.js";
import { ILogger } from "@/utils/logger.js";
import type { HardwareStatusMetrics } from "@/types/network.js";
import { MAX_RECENT_CHECKS } from "@/domain/monitors/monitor.type.js";
+import { toCheckSnapshot } from "@/domain/checks/check.snapshot.js";
const SERVICE_NAME = "StatusService";
const HARDWARE_ALERT_COUNTER_START = 5;
@@ -87,30 +88,6 @@ export class StatusService implements IStatusService {
}
};
- private toCheckSnapshot = (check: Check): CheckSnapshot => {
- return {
- id: check.id,
- status: check.status,
- responseTime: check.responseTime,
- timings: check.timings,
- statusCode: check.statusCode,
- message: check.message,
- cpu: check.cpu,
- memory: check.memory,
- disk: check.disk,
- host: check.host,
- errors: check.errors,
- capture: check.capture,
- net: check.net,
- accessibility: check.accessibility,
- bestPractices: check.bestPractices,
- seo: check.seo,
- performance: check.performance,
- audits: check.audits,
- createdAt: check.createdAt,
- };
- };
-
private computeReachability = (
currentStatus: MonitorStatus,
window: Array,
@@ -207,7 +184,7 @@ export class StatusService implements IStatusService {
await this.tryUpdateRunningStats(monitor, statusResponse);
const prevStatus = monitor.status;
- const checkSnapshot = this.toCheckSnapshot(check);
+ const checkSnapshot = toCheckSnapshot(check);
// Project the window as it will look after updating DB
// This is done because we need the updated status window to compute new status, but we don't
diff --git a/server/src/worker/worker.job-scheduler.ts b/server/src/worker/worker.job-scheduler.ts
index 8d103e87f..aae8a489b 100644
--- a/server/src/worker/worker.job-scheduler.ts
+++ b/server/src/worker/worker.job-scheduler.ts
@@ -1,7 +1,7 @@
import { IJobScheduler } from "@/worker/worker.interface.js";
import { IJobsRepository } from "@/domain/jobs/job.repository.interface.js";
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
-import { Monitor, supportsGeoCheck } from "@/domain/monitors/monitor.type.js";
+import { Monitor, MonitorScheduleFields, supportsGeoCheck } from "@/domain/monitors/monitor.type.js";
import { type Job, type JobSeed, type JobType, jobId } from "@/domain/jobs/job.type.js";
import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js";
import { WorkerJobsPagination, WorkerJobSummary, WorkerMetrics } from "@/worker/worker.interface.js";
@@ -43,7 +43,7 @@ export class JobScheduler implements IJobScheduler {
"cleanup-retention": POLL_MS,
};
// Helpers, builds job seeds. immediate=true runs on the next tick; otherwise jitter spreads the herd.
- protected toCheckJob = (monitor: Monitor, now: number, immediate = false): JobSeed => ({
+ protected toCheckJob = (monitor: MonitorScheduleFields, now: number, immediate = false): JobSeed => ({
id: jobId("check", monitor.id),
type: "check",
refId: monitor.id,
@@ -52,7 +52,7 @@ export class JobScheduler implements IJobScheduler {
intervalMs: monitor.interval,
});
- protected toGeoCheckJob = (monitor: Monitor, now: number, immediate = false): JobSeed => ({
+ protected toGeoCheckJob = (monitor: MonitorScheduleFields, now: number, immediate = false): JobSeed => ({
...this.toCheckJob(monitor, now, immediate),
id: jobId("geo-check", monitor.id),
type: "geo-check",
@@ -187,7 +187,7 @@ export class JobScheduler implements IJobScheduler {
// Seed queue
protected reconcile = async () => {
const now = Date.now();
- const monitors = await this.monitorsRepository.findAll();
+ const monitors = await this.monitorsRepository.findAllForScheduling();
for (const monitor of monitors) {
await this.jobsRepository.upsertJob(this.toCheckJob(monitor, now));
if (supportsGeoCheck(monitor.type) && monitor.geoCheckEnabled) {
diff --git a/server/test/helpers/InMemoryMonitorsRepository.ts b/server/test/helpers/InMemoryMonitorsRepository.ts
index 1ff0b0383..2ec36e8e2 100644
--- a/server/test/helpers/InMemoryMonitorsRepository.ts
+++ b/server/test/helpers/InMemoryMonitorsRepository.ts
@@ -1,6 +1,6 @@
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "../../src/domain/monitors/monitor.repository.interface.ts";
import type { CheckSnapshot } from "../../src/domain/checks/check.type.ts";
-import type { Monitor, MonitorsSummary } from "../../src/domain/monitors/monitor.type.ts";
+import type { Monitor, MonitorScheduleFields, MonitorsSummary } from "../../src/domain/monitors/monitor.type.ts";
export class InMemoryMonitorsRepository implements IMonitorsRepository {
private monitors: Monitor[] = [];
@@ -24,8 +24,15 @@ export class InMemoryMonitorsRepository implements IMonitorsRepository {
return { ...monitor };
}
- async findAll(): Promise {
- return this.monitors.map((m) => ({ ...m }));
+ async findAllForScheduling(): Promise {
+ return this.monitors.map((m) => ({
+ id: m.id,
+ type: m.type,
+ isActive: m.isActive,
+ interval: m.interval,
+ geoCheckEnabled: m.geoCheckEnabled ?? false,
+ geoCheckInterval: m.geoCheckInterval ?? 300000,
+ }));
}
async findByTeamId(_teamId: string, _config: TeamQueryConfig): Promise {
@@ -45,10 +52,6 @@ export class InMemoryMonitorsRepository implements IMonitorsRepository {
return monitor ? { ...monitor } : null;
}
- async findByIdsWithChecks(monitorIds: string[], _checksCount?: number): Promise {
- return this.findByIds(monitorIds);
- }
-
async updateById(monitorId: string, teamId: string, updates: Partial): Promise {
const index = this.monitors.findIndex((m) => m.id === monitorId && m.teamId === teamId);
if (index === -1) {
diff --git a/server/test/unit/services/dbQueueWorker.test.ts b/server/test/unit/services/dbQueueWorker.test.ts
index d903ae887..8b13287d4 100644
--- a/server/test/unit/services/dbQueueWorker.test.ts
+++ b/server/test/unit/services/dbQueueWorker.test.ts
@@ -84,7 +84,7 @@ const createWorker = (overrides?: { queueMode?: QueueMode; queuePrimaryProcesses
const monitorsRepository = {
findByIds: jest.fn().mockResolvedValue([makeMonitor()]),
findByIdLean: jest.fn().mockResolvedValue(makeMonitor()),
- findAll: jest.fn().mockResolvedValue([]),
+ findAllForScheduling: jest.fn().mockResolvedValue([]),
updateById: jest.fn().mockResolvedValue({}),
};
const checksRepository = {
@@ -183,7 +183,11 @@ describe("DBQueueWorker", () => {
queueMode: "primary",
queuePrimaryProcesses: false, // isolate reconcile from the polling loops
mocks: {
- monitorsRepository: { findByIds: jest.fn(), findAll: jest.fn().mockResolvedValue([makeMonitor()]), updateById: jest.fn() },
+ monitorsRepository: {
+ findByIds: jest.fn(),
+ findAllForScheduling: jest.fn().mockResolvedValue([makeMonitor()]),
+ updateById: jest.fn(),
+ },
},
});
@@ -200,7 +204,11 @@ describe("DBQueueWorker", () => {
queueMode: "primary",
queuePrimaryProcesses: false,
mocks: {
- monitorsRepository: { findByIds: jest.fn(), findAll: jest.fn().mockResolvedValue([geoMonitor]), updateById: jest.fn() },
+ monitorsRepository: {
+ findByIds: jest.fn(),
+ findAllForScheduling: jest.fn().mockResolvedValue([geoMonitor]),
+ updateById: jest.fn(),
+ },
},
});
diff --git a/server/test/unit/services/jobScheduler.test.ts b/server/test/unit/services/jobScheduler.test.ts
index 098cef245..90c8c2850 100644
--- a/server/test/unit/services/jobScheduler.test.ts
+++ b/server/test/unit/services/jobScheduler.test.ts
@@ -38,7 +38,7 @@ const createScheduler = (overrides?: { queueMode?: QueueMode; mocks?: Record().mockResolvedValue(undefined),
};
const monitorsRepository = {
- findAll: jest.fn().mockResolvedValue([]),
+ findAllForScheduling: jest.fn().mockResolvedValue([]),
};
const logger = createMockLogger();
const mocks = { jobsRepository, queueWorkersRepository, monitorsRepository, logger, ...overrides?.mocks };
@@ -73,7 +73,7 @@ describe("JobScheduler", () => {
it("primary mode reconciles: seeds a check row per monitor plus the two cleanup rows", async () => {
const { scheduler, mocks } = createScheduler({
queueMode: "primary",
- mocks: { monitorsRepository: { findAll: jest.fn().mockResolvedValue([makeMonitor()]) } },
+ mocks: { monitorsRepository: { findAllForScheduling: jest.fn().mockResolvedValue([makeMonitor()]) } },
});
await scheduler.init();
@@ -87,7 +87,7 @@ describe("JobScheduler", () => {
it("primary mode also seeds a geo-check row for geo-enabled monitors", async () => {
const { scheduler, mocks } = createScheduler({
queueMode: "primary",
- mocks: { monitorsRepository: { findAll: jest.fn().mockResolvedValue([makeMonitor({ geoCheckEnabled: true })]) } },
+ mocks: { monitorsRepository: { findAllForScheduling: jest.fn().mockResolvedValue([makeMonitor({ geoCheckEnabled: true })]) } },
});
await scheduler.init();
@@ -98,12 +98,12 @@ describe("JobScheduler", () => {
it("worker mode heartbeats but does not reconcile (no monitor reads, no seeds)", async () => {
const { scheduler, mocks } = createScheduler({
queueMode: "worker",
- mocks: { monitorsRepository: { findAll: jest.fn().mockResolvedValue([makeMonitor()]) } },
+ mocks: { monitorsRepository: { findAllForScheduling: jest.fn().mockResolvedValue([makeMonitor()]) } },
});
await scheduler.init();
expect(mocks.queueWorkersRepository.upsert).toHaveBeenCalledWith("worker-1", "worker", false);
- expect(mocks.monitorsRepository.findAll).not.toHaveBeenCalled();
+ expect(mocks.monitorsRepository.findAllForScheduling).not.toHaveBeenCalled();
expect(mocks.jobsRepository.upsertJob).not.toHaveBeenCalled();
expect(mocks.jobsRepository.upsertCleanupJob).not.toHaveBeenCalled();
});
diff --git a/server/test/unit/services/maintenanceWindowService.test.ts b/server/test/unit/services/maintenanceWindowService.test.ts
index e15f20d46..481dc5983 100644
--- a/server/test/unit/services/maintenanceWindowService.test.ts
+++ b/server/test/unit/services/maintenanceWindowService.test.ts
@@ -123,7 +123,7 @@ describe("MaintenanceWindowService", () => {
await service.createMaintenanceWindow(defaultCreateParams);
- expect(monitorsRepository.findByIds).toHaveBeenCalledWith(["mon-1", "mon-2"]);
+ expect(monitorsRepository.findByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], { includeRecentChecks: false });
});
it("throws 403 when a monitor belongs to a different team", async () => {
@@ -404,7 +404,7 @@ describe("MaintenanceWindowService", () => {
body: { monitors: ["mon-1", "mon-2"] },
});
- expect(monitorsRepository.findByIds).toHaveBeenCalledWith(["mon-1", "mon-2"]);
+ expect(monitorsRepository.findByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], { includeRecentChecks: false });
});
it("throws 403 when a new monitor belongs to a different team", async () => {
diff --git a/server/test/unit/services/monitorService.test.ts b/server/test/unit/services/monitorService.test.ts
index 69edbba9c..0f56e5d2b 100644
--- a/server/test/unit/services/monitorService.test.ts
+++ b/server/test/unit/services/monitorService.test.ts
@@ -583,7 +583,7 @@ describe("MonitorService", () => {
expect(result.monitors).toEqual([]);
});
- it("uses snapshot (first check only) for hardware type monitors", async () => {
+ it("uses snapshot (latest check only) for hardware type monitors", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
(monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 1 });
(monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1);
@@ -602,6 +602,7 @@ describe("MonitorService", () => {
const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID, type: "hardware" });
expect(result.monitors[0].recentChecks).toHaveLength(1);
+ expect(result.monitors[0].recentChecks[0].responseTime).toBe(30);
});
it("uses snapshot for hardware type when type is array with hardware", async () => {
@@ -622,6 +623,7 @@ describe("MonitorService", () => {
const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID, type: ["hardware"] });
expect(result.monitors[0].recentChecks).toHaveLength(1);
+ expect(result.monitors[0].recentChecks[0].responseTime).toBe(20);
});
it("normalizes checks for non-snapshot types when type is an array", async () => {
diff --git a/server/test/unit/services/userService.test.ts b/server/test/unit/services/userService.test.ts
index bce8769cf..5187a3c60 100644
--- a/server/test/unit/services/userService.test.ts
+++ b/server/test/unit/services/userService.test.ts
@@ -472,7 +472,7 @@ describe("UserService", () => {
await service.deleteUser({ userId: "user-1", teamId: "team-1", roles: ["superadmin"] });
- expect(monitorsRepository.findByTeamId).toHaveBeenCalledWith("team-1", {});
+ expect(monitorsRepository.findByTeamId).toHaveBeenCalledWith("team-1", {}, { includeRecentChecks: false });
expect(scheduler.deleteJob).toHaveBeenCalledTimes(2);
expect(usersRepository.deleteById).toHaveBeenCalledWith("user-1");
});