From 50c644231ef22b9a4d054240d0bc27c312b42849 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 18:26:31 +0000 Subject: [PATCH 01/12] initial commit --- server/src/config/services.ts | 2 +- server/src/db/models/Monitor.ts | 20 +++++++++-- .../monitors/MongoMonitorsRepository.ts | 2 ++ server/src/service/business/checkService.ts | 3 ++ server/src/service/business/monitorService.ts | 12 +++---- .../service/infrastructure/statusService.ts | 36 ++++++++++++++++--- server/src/types/check.ts | 6 +++- server/src/types/monitor.ts | 6 +++- server/src/utils/dataUtils.ts | 6 ++-- 9 files changed, 72 insertions(+), 21 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 06701491d..4437e9176 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -176,7 +176,7 @@ export const initializeServices = async ({ const bufferService = new BufferService({ logger, checkService, settingsService }); - const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); + const statusService = new StatusService({ logger, buffer: bufferService, monitorsRepository }); const webhookProvider = new WebhookProvider(logger); const slackProvider = new SlackProvider(logger); diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index 17565a2e4..0eeca0ed7 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -1,15 +1,18 @@ import { Schema, model, Types, type UpdateQuery } from "mongoose"; -import type { Monitor, MonitorMatchMethod, MonitorThresholds } from "@/types/monitor.js"; +import type { Monitor, MonitorMatchMethod, MonitorThresholds, CheckSnapshot } from "@/types/monitor.js"; import { MonitorTypes } from "@/types/monitor.js"; import Check from "./Check.js"; import MonitorStats from "./MonitorStats.js"; import StatusPage from "./StatusPage.js"; +type CheckSnapshotDocument = Omit & { createdAt: Date }; + type MonitorDocumentBase = Omit< Monitor, - "id" | "userId" | "teamId" | "notifications" | "selectedDisks" | "statusWindow" | "createdAt" | "updatedAt" + "id" | "userId" | "teamId" | "notifications" | "selectedDisks" | "statusWindow" | "recentChecks" | "createdAt" | "updatedAt" > & { statusWindow: boolean[]; + recentChecks: CheckSnapshotDocument[]; notifications: Types.ObjectId[]; selectedDisks: string[]; matchMethod?: MonitorMatchMethod; @@ -34,6 +37,15 @@ const thresholdsSchema = new Schema( { _id: false } ); +const checkSnapshotSchema = new Schema( + { + id: { type: String, required: true }, + status: { type: Boolean, required: true }, + createdAt: { type: Date, required: true }, + }, + { _id: false, strict: false } +); + const MonitorSchema = new Schema( { userId: { @@ -165,6 +177,10 @@ const MonitorSchema = new Schema( return value && value.trim() ? value.trim() : null; }, }, + recentChecks: { + type: [checkSnapshotSchema], + default: [], + }, }, { timestamps: true, diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 5ce029634..9aeca380a 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -309,6 +309,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { selectedDisks: doc.selectedDisks ?? [], gameId: doc.gameId ?? undefined, group: doc.group ?? null, + recentChecks: (doc.recentChecks ?? []).map((check: any) => ({ ...check, createdAt: check.createdAt instanceof Date ? check.createdAt.toISOString() : check.createdAt })), createdAt: toDateString(doc.createdAt), updatedAt: toDateString(doc.updatedAt), }; @@ -392,6 +393,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { selectedDisks: doc.selectedDisks ?? [], gameId: doc.gameId ?? undefined, group: doc.group ?? null, + recentChecks: (doc.recentChecks ?? []).map((check: any) => ({ ...check, createdAt: check.createdAt instanceof Date ? check.createdAt.toISOString() : check.createdAt })), createdAt: toDateString(doc.createdAt), updatedAt: toDateString(doc.updatedAt), checks, diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 42788eba8..bd4684f11 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,4 +1,5 @@ import { requireTeamId } from "@/controllers/controllerUtils.js"; +import { Types } from "mongoose"; import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; @@ -39,6 +40,8 @@ class CheckService { const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse; const check: Partial = { + id: new Types.ObjectId().toString(), + createdAt: new Date().toISOString(), metadata: { monitorId, teamId, diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 38fcaef78..90a909991 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -360,16 +360,12 @@ export class MonitorService implements IMonitorService { const snapshotOnlyRequest = requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType)); - const limitPerMonitor = snapshotOnlyRequest ? 1 : 25; - const checksMap = await this.checksRepository.findLatestByMonitorIds( - monitorsList.map((monitor) => monitor.id), - { limitPerMonitor } - ); - const monitorsWithChecks = monitorsList.map((monitor: Monitor) => { - const rawChecks = checksMap[monitor.id] ?? []; + const rawChecks = monitor.recentChecks ?? []; const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type); - const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100); + // recentChecks are stored oldest-first, reverse to get newest-first for display + const reversedChecks = [...rawChecks].reverse(); + const checks = isSnapshotType ? reversedChecks.slice(0, 1) : NormalizeData(reversedChecks, 10, 100); return { ...monitor, checks, diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 3a83dbd28..64b14d795 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -9,6 +9,7 @@ import type { Check, HardwareStatusPayload, PageSpeedStatusPayload, + CheckSnapshot, } from "@/types/index.js"; const SERVICE_NAME = "StatusService"; @@ -24,13 +25,11 @@ export interface IStatusService { export class StatusService implements IStatusService { static SERVICE_NAME = SERVICE_NAME; - private db: any; private logger: any; private buffer: any; private monitorsRepository: IMonitorsRepository; - constructor({ db, logger, buffer, monitorsRepository }: { db: any; logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) { - this.db = db; + constructor({ logger, buffer, monitorsRepository }: { logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) { this.logger = logger; this.buffer = buffer; this.monitorsRepository = monitorsRepository; @@ -175,14 +174,41 @@ export class StatusService implements IStatusService { // Update running stats this.updateRunningStats({ monitor, networkResponse: statusResponse }); - // If the status window size has changed, empty - monitor.statusWindow = monitor.statusWindow || []; monitor.statusWindow.push(status); while (monitor.statusWindow.length > monitor.statusWindowSize) { monitor.statusWindow.shift(); } + // Update recentChecks with check snapshot (excludes metadata, ack, expiry, __v, updatedAt) + const checkSnapshot: CheckSnapshot = { + 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, + }; + monitor.recentChecks = monitor.recentChecks || []; + monitor.recentChecks.push(checkSnapshot); + const maxRecentChecks = 25; + while (monitor.recentChecks.length > maxRecentChecks) { + monitor.recentChecks.shift(); + } + if (monitor.status === undefined || monitor.status === null) { monitor.status = status; } diff --git a/server/src/types/check.ts b/server/src/types/check.ts index ee40ff6dc..6d38d538f 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -178,7 +178,11 @@ export interface ChecksSummary { cannotResolveChecks: number; } -export type NormalizedCheck = T & { +export interface HasResponseTime { + responseTime: number; +} + +export type NormalizedCheck = T & { originalResponseTime: number; }; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index c7d99a88a..96cd44a65 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -1,4 +1,7 @@ import type { Check } from "@/types/check.js"; + +export type CheckSnapshot = Omit; + export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const; export type MonitorType = (typeof MonitorTypes)[number]; @@ -42,6 +45,7 @@ export interface Monitor { selectedDisks: string[]; gameId?: string; group: string | null; + recentChecks: CheckSnapshot[]; createdAt: string; updatedAt: string; } @@ -54,7 +58,7 @@ export interface MonitorsSummary { } export interface MonitorWithChecks extends Monitor { - checks: Check[]; + checks: Check[] | CheckSnapshot[]; } export interface MonitorsWithChecksByTeamIdResult { diff --git a/server/src/utils/dataUtils.ts b/server/src/utils/dataUtils.ts index 0bf3e6416..b981365e9 100755 --- a/server/src/utils/dataUtils.ts +++ b/server/src/utils/dataUtils.ts @@ -1,6 +1,6 @@ -import type { Check, GroupedCheck, NormalizedCheck, NormalizedUptimeCheck } from "@/types/index.js"; +import type { GroupedCheck, NormalizedCheck, NormalizedUptimeCheck, HasResponseTime } from "@/types/index.js"; -const calculatePercentile = (arr: Check[], percentile: number): number => { +const calculatePercentile = (arr: T[], 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); @@ -20,7 +20,7 @@ const calculatePercentileUptimeDetails = (arr: GroupedCheck[], percentile: numbe return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight; }; -export const NormalizeData = (checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck[] => { +export const NormalizeData = (checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck[] => { if (checks.length > 1) { const min = calculatePercentile(checks, 0); const max = calculatePercentile(checks, 95); From 290ee7d2df5913620b27219392c01787561db4a7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 18:50:08 +0000 Subject: [PATCH 02/12] proper parsing --- .../monitors/MongoMonitorsRepository.ts | 31 +++++++++++++++++-- .../service/infrastructure/statusService.ts | 1 - 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 9aeca380a..979758308 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,6 +1,6 @@ import { MonitorModel } from "@/db/models/index.js"; import type { MonitorDocument } from "@/db/models/index.js"; -import type { Monitor, MonitorsSummary, MonitorWithChecks, Check } from "@/types/index.js"; +import type { Monitor, MonitorsSummary, MonitorWithChecks, CheckSnapshot, Check } from "@/types/index.js"; import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; @@ -309,7 +309,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { selectedDisks: doc.selectedDisks ?? [], gameId: doc.gameId ?? undefined, group: doc.group ?? null, - recentChecks: (doc.recentChecks ?? []).map((check: any) => ({ ...check, createdAt: check.createdAt instanceof Date ? check.createdAt.toISOString() : check.createdAt })), + recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)), createdAt: toDateString(doc.createdAt), updatedAt: toDateString(doc.updatedAt), }; @@ -393,12 +393,37 @@ class MongoMonitorsRepository implements IMonitorsRepository { selectedDisks: doc.selectedDisks ?? [], gameId: doc.gameId ?? undefined, group: doc.group ?? null, - recentChecks: (doc.recentChecks ?? []).map((check: any) => ({ ...check, createdAt: check.createdAt instanceof Date ? check.createdAt.toISOString() : check.createdAt })), + recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)), createdAt: toDateString(doc.createdAt), updatedAt: toDateString(doc.updatedAt), checks, }; }; + + private toCheckSnapshot = (doc: any): CheckSnapshot => { + const plain = typeof doc.toObject === "function" ? doc.toObject() : doc; + return { + id: plain.id ?? plain._id?.toString() ?? "", + status: plain.status, + responseTime: plain.responseTime, + timings: plain.timings, + statusCode: plain.statusCode, + message: plain.message, + cpu: plain.cpu, + memory: plain.memory, + disk: plain.disk, + host: plain.host, + errors: plain.errors, + capture: plain.capture, + net: plain.net, + accessibility: plain.accessibility, + bestPractices: plain.bestPractices, + seo: plain.seo, + performance: plain.performance, + audits: plain.audits, + createdAt: plain.createdAt instanceof Date ? plain.createdAt.toISOString() : plain.createdAt, + }; + }; } export default MongoMonitorsRepository; diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 64b14d795..0e1011278 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -180,7 +180,6 @@ export class StatusService implements IStatusService { monitor.statusWindow.shift(); } - // Update recentChecks with check snapshot (excludes metadata, ack, expiry, __v, updatedAt) const checkSnapshot: CheckSnapshot = { id: check.id, status: check.status, From 420ff292d9d7044802c49a253222f273b1df5e2d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 18:58:08 +0000 Subject: [PATCH 03/12] toEntity --- server/src/db/models/Monitor.ts | 2 +- .../monitors/MongoMonitorsRepository.ts | 47 ++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index 0eeca0ed7..24c8ef0bf 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -218,6 +218,6 @@ MonitorSchema.index({ teamId: 1, type: 1 }); const MonitorModel = model("Monitor", MonitorSchema); -export type { MonitorDocument }; +export type { MonitorDocument, CheckSnapshotDocument }; export { MonitorModel }; export default MonitorModel; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 979758308..e27fd4632 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,5 +1,5 @@ import { MonitorModel } from "@/db/models/index.js"; -import type { MonitorDocument } from "@/db/models/index.js"; +import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js"; import type { Monitor, MonitorsSummary, MonitorWithChecks, CheckSnapshot, Check } from "@/types/index.js"; import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; @@ -400,28 +400,31 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; }; - private toCheckSnapshot = (doc: any): CheckSnapshot => { - const plain = typeof doc.toObject === "function" ? doc.toObject() : doc; + private toCheckSnapshot = (doc: CheckSnapshotDocument): CheckSnapshot => { + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + return { - id: plain.id ?? plain._id?.toString() ?? "", - status: plain.status, - responseTime: plain.responseTime, - timings: plain.timings, - statusCode: plain.statusCode, - message: plain.message, - cpu: plain.cpu, - memory: plain.memory, - disk: plain.disk, - host: plain.host, - errors: plain.errors, - capture: plain.capture, - net: plain.net, - accessibility: plain.accessibility, - bestPractices: plain.bestPractices, - seo: plain.seo, - performance: plain.performance, - audits: plain.audits, - createdAt: plain.createdAt instanceof Date ? plain.createdAt.toISOString() : plain.createdAt, + 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), }; }; } From 5731283c46275ce4925cdecc56f1c3f42c37bd99 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 19:23:34 +0000 Subject: [PATCH 04/12] recent checks --- .../v2/common/charts/HeatmapResponseTime.tsx | 6 +- .../charts/HeatmapResponseTimeTooltip.tsx | 6 +- .../common/charts/HistogramResponseTime.tsx | 4 +- .../v2/monitors/charts/HistogramPageSpeed.tsx | 8 +-- .../charts/HistogramPageSpeedDetails.tsx | 4 +- .../v2/monitors/charts/PiePageSpeed.tsx | 4 +- .../v2/monitors/charts/PiePageSpeedLegend.tsx | 4 +- client/src/Pages/PageSpeed/Details/index.tsx | 6 +- .../Components/PageSpeedMonitorsTable.tsx | 2 +- .../Components/UptimeMonitorsTable.tsx | 4 +- client/src/Types/Check.ts | 2 + client/src/Types/Monitor.ts | 64 +++++++++++++------ 12 files changed, 72 insertions(+), 42 deletions(-) diff --git a/client/src/Components/v2/common/charts/HeatmapResponseTime.tsx b/client/src/Components/v2/common/charts/HeatmapResponseTime.tsx index 97289ba43..972291f57 100644 --- a/client/src/Components/v2/common/charts/HeatmapResponseTime.tsx +++ b/client/src/Components/v2/common/charts/HeatmapResponseTime.tsx @@ -1,6 +1,6 @@ import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; -import type { Check } from "@/Types/Check"; +import type { CheckSnapshot } from "@/Types/Check"; import { getResponseColor } from "@/Utils/DataUtils"; import { HeatmapResponseTimeTooltip } from "@/Components/v2/common/charts/HeatmapResponseTimeTooltip"; import type { SxProps } from "@mui/material/styles"; @@ -13,7 +13,7 @@ interface PlaceholderCheck { } interface HeatmapResponseTimeProps { - checks: Check[]; + checks: CheckSnapshot[]; gap?: ResponsiveStyleValue; availabilityCellSx?: SxProps; responseCellSx?: SxProps; @@ -34,7 +34,7 @@ export const HeatmapResponseTime = ({ const latestChecks = checks.slice(-25).reverse(); - let data: Array; + let data: Array; if (latestChecks.length !== 25) { const placeholders = Array(25 - latestChecks.length).fill({ status: "placeholder" as const, diff --git a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx b/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx index 7e9ae4470..e928269ce 100644 --- a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx +++ b/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx @@ -2,12 +2,14 @@ import Stack from "@mui/material/Stack"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import { formatDateWithTz } from "@/Utils/TimeUtils"; -import type { Check } from "@/Types/Check"; +import type { CheckSnapshot } from "@/Types/Check"; import { useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; import { useTranslation } from "react-i18next"; -type HeatmapCheck = Check | { status: "placeholder"; responseTime: 0; createdAt: "" }; +type HeatmapCheck = + | CheckSnapshot + | { status: "placeholder"; responseTime: 0; createdAt: "" }; export const HeatmapResponseTimeTooltip = ({ children, diff --git a/client/src/Components/v2/common/charts/HistogramResponseTime.tsx b/client/src/Components/v2/common/charts/HistogramResponseTime.tsx index bbe3bd9d4..e3b82cd50 100644 --- a/client/src/Components/v2/common/charts/HistogramResponseTime.tsx +++ b/client/src/Components/v2/common/charts/HistogramResponseTime.tsx @@ -1,11 +1,11 @@ import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import type { ResponsiveStyleValue } from "@mui/system"; -import type { Check } from "@/Types/Check"; +import type { CheckSnapshot } from "@/Types/Check"; import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip"; interface HistogramResponseTimeProps { - checks: Check[]; + checks: CheckSnapshot[]; height?: ResponsiveStyleValue; gap?: ResponsiveStyleValue; } diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx index 1bcceebd1..f19e0323c 100644 --- a/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx @@ -2,16 +2,16 @@ import Stack from "@mui/material/Stack"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { HistogramPageSpeedTooltip } from "@/Components/v2/monitors/charts/HistogramPageSpeedTooltip"; import { useTheme } from "@mui/material/styles"; -import type { Check } from "@/Types/Check"; +import type { CheckSnapshot } from "@/Types/Check"; import type { MonitorStatus } from "@/Types/Monitor"; import { getStatusColor } from "@/Utils/MonitorUtils"; -const processData = (checks: Check[]) => { +const processData = (checks: CheckSnapshot[]) => { if (checks.length === 0) return []; const formattedData: { score: number; date: string }[] = []; - const calculateScore = (entry: Check) => { + const calculateScore = (entry: CheckSnapshot) => { const accessibility = entry?.accessibility || 0; const bestPractices = entry?.bestPractices || 0; const performance = entry?.performance || 0; @@ -32,7 +32,7 @@ export const HistogramPageSpeed = ({ checks, status, }: { - checks: Check[]; + checks: CheckSnapshot[]; status: MonitorStatus; }) => { const theme = useTheme(); diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetails.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetails.tsx index 8fc39e16c..05bee8bc8 100644 --- a/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetails.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetails.tsx @@ -12,7 +12,7 @@ import { import { HistogramPageSpeedScoresTooltip } from "@/Components/v2/monitors"; import { useTheme } from "@mui/material/styles"; -import type { Check } from "@/Types/Check"; +import type { CheckSnapshot } from "@/Types/Check"; import type { Palette } from "@mui/material/styles"; type PaletteColorKey = Extract< keyof Palette, @@ -52,7 +52,7 @@ export const HistogramPageSpeedDetails = ({ checks, range, }: { - checks: Check[]; + checks: CheckSnapshot[]; range: string; }) => { const theme = useTheme(); diff --git a/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx b/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx index 1c58bb3f7..205ec9a96 100644 --- a/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx +++ b/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx @@ -1,6 +1,6 @@ import { BaseChart } from "@/Components/v2/design-elements"; import { FileText } from "lucide-react"; -import type { Check, CheckAudits } from "@/Types/Check"; +import type { CheckAudits, CheckSnapshot } from "@/Types/Check"; import { Pie, PieChart, ResponsiveContainer, Label } from "recharts"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; @@ -30,7 +30,7 @@ const CenterLabel = ({ viewBox, value }: any) => { ); }; -export const PiePageSpeed = ({ latestCheck }: { latestCheck?: Check }) => { +export const PiePageSpeed = ({ latestCheck }: { latestCheck?: CheckSnapshot }) => { const { t } = useTranslation(); const theme = useTheme(); const [hoverTitle, setHoverTitle] = useState(null); diff --git a/client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx b/client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx index 5d220ae4e..b98b09b33 100644 --- a/client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx +++ b/client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx @@ -4,7 +4,7 @@ import { BarChart3 } from "lucide-react"; import { BaseChart } from "@/Components/v2/design-elements"; import Typography from "@mui/material/Typography"; -import type { Check } from "@/Types/Check"; +import type { CheckSnapshot } from "@/Types/Check"; import { useTranslation } from "react-i18next"; import { getPageSpeedPalette } from "@/Utils/MonitorUtils"; import { useTheme } from "@mui/material/styles"; @@ -56,7 +56,7 @@ const MetricBox = ({ ); }; -export const PiePageSpeedLegend = ({ latestCheck }: { latestCheck?: Check }) => { +export const PiePageSpeedLegend = ({ latestCheck }: { latestCheck?: CheckSnapshot }) => { const theme = useTheme(); const { t } = useTranslation(); diff --git a/client/src/Pages/PageSpeed/Details/index.tsx b/client/src/Pages/PageSpeed/Details/index.tsx index 2028f47c0..f1e4df4ba 100644 --- a/client/src/Pages/PageSpeed/Details/index.tsx +++ b/client/src/Pages/PageSpeed/Details/index.tsx @@ -48,15 +48,15 @@ const PageSpeedDetails = () => { monitorStats={monitorStats} /> - - + + ); diff --git a/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx index f38e1c706..37093c7a2 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx @@ -208,7 +208,7 @@ export const PageSpeedMonitorsTable = ({ return ( diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index c7f5c4c7f..cc225ad81 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -199,9 +199,9 @@ export const MonitorTable = ({ content: t("pages.uptime.table.headers.responseTime"), render: (row) => { if (chartType === "histogram") { - return ; + return ; } else { - return ; + return ; } }, }, diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index 0aa953dbb..19b9dd966 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -144,3 +144,5 @@ export interface ChecksResponse { checks: Check[]; checksCount: number; } + +export type CheckSnapshot = Omit; diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 6baca2845..d4dcf5a38 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -1,4 +1,5 @@ -import type { Check, GroupedCheck } from "@/Types/Check"; +import type { GroupedCheck } from "@/Types/Check"; +import type { CheckSnapshot } from "@/Types/Check"; export type MonitorStatus = boolean | undefined; export const MonitorTypes = [ @@ -13,28 +14,53 @@ export const MonitorTypes = [ ] as const; export type MonitorType = (typeof MonitorTypes)[number]; -export interface Monitor { - checks: Check[]; - createdAt: string; - createdBy: string; - interval: number; - isActive: boolean; - latestChecks: Check[]; - n: number; - name: string; - status: MonitorStatus; - type: string; - updatedAt: string; - updatedBy: string; - url: string; - id: string; +export interface MonitorThresholds { + usage_cpu?: number; + usage_memory?: number; + usage_disk?: number; + usage_temperature?: number; } -export interface MonitorWithChecks extends Monitor { - checks: Check[]; +export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; + +export interface Monitor { + id: string; + userId: string; + teamId: string; + name: string; + description?: string; + status?: boolean; + statusWindow: boolean[]; + statusWindowSize: number; + statusWindowThreshold: number; + type: MonitorType; + ignoreTlsErrors: boolean; + jsonPath?: string; + expectedValue?: string; + matchMethod?: MonitorMatchMethod; + url: string; + port?: number; + isActive: boolean; + interval: number; uptimePercentage?: number; + notifications: string[]; + secret?: string; + thresholds?: MonitorThresholds; + alertThreshold: number; + cpuAlertThreshold: number; + memoryAlertThreshold: number; + diskAlertThreshold: number; + tempAlertThreshold: number; + selectedDisks: string[]; + gameId?: string; + group: string | null; + recentChecks: CheckSnapshot[]; + createdAt: string; + updatedAt: string; } +export type MonitorWithChecks = Monitor; + export interface MonitorsSummary { totalMonitors: number; upMonitors: number; @@ -45,7 +71,7 @@ export interface MonitorsSummary { export interface MonitorsWithChecksResponse { count: number; monitors: MonitorWithChecks[]; - summary: MonitorsSummary; + summary: MonitorsSummary | null; } export interface MonitorStats { From 6f9fefddd2e903221a609014a98b68fa3be219e2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 19:24:02 +0000 Subject: [PATCH 05/12] recent checks --- server/src/controllers/monitorController.ts | 1 - server/src/controllers/statusPageController.ts | 3 +-- .../src/repositories/monitors/IMonitorsRepository.ts | 4 ++-- .../repositories/monitors/MongoMonitorsRepository.ts | 7 +++---- server/src/service/business/monitorService.ts | 11 +++-------- server/src/types/check.ts | 2 ++ server/src/types/monitor.ts | 10 +++------- 7 files changed, 14 insertions(+), 24 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 30ceabbc5..721565f59 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -324,7 +324,6 @@ class MonitorController { try { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsWithChecksQueryValidation.validateAsync(req.query); - const explain = optionalBoolean(req?.query?.explain, "explain"); const limit = optionalNumber(req?.query?.limit, "limit"); const page = optionalNumber(req?.query?.page, "page"); diff --git a/server/src/controllers/statusPageController.ts b/server/src/controllers/statusPageController.ts index 3501a6592..866dd715a 100644 --- a/server/src/controllers/statusPageController.ts +++ b/server/src/controllers/statusPageController.ts @@ -7,7 +7,6 @@ import { IStatusPageService } from "@/service/business/statusPageService.js"; import { IMonitorsRepository } from "@/repositories/index.js"; import { ISettingsService } from "@/service/system/settingsService.js"; import { ParseBoolean } from "@/utils/utils.js"; -import { MonitorWithChecks } from "@/types/index.js"; import { NormalizeData } from "@/utils/dataUtils.js"; const SERVICE_NAME = "statusPageController"; @@ -84,7 +83,7 @@ class StatusPageController { const monitors = await this.monitorsRepository.findByIdsWithChecks(statusPage.monitors); const normalizedMonitors = monitors.map((monitor) => { - const normalizedChecks = NormalizeData(monitor.checks, 10, 100); + const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100); if (!showURL) { const { url, port, secret, notifications, ...rest } = monitor; return { ...rest, checks: normalizedChecks }; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 072772e9e..2fbce3406 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -1,4 +1,4 @@ -import { type MonitorType, type Monitor, type MonitorsSummary, type MonitorWithChecks } from "@/types/index.js"; +import { type MonitorType, type Monitor, type MonitorsSummary } from "@/types/index.js"; export interface TeamQueryConfig { limit?: number; @@ -25,7 +25,7 @@ export interface IMonitorsRepository { findAll(): Promise; findByTeamId(teamId: string, config: TeamQueryConfig): Promise; findByIds(monitorIds: string[]): Promise; - findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise; + findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise; // update updateById(monitorId: string, teamId: string, updates: Partial): Promise; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index e27fd4632..9cb519303 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,6 +1,6 @@ import { MonitorModel } from "@/db/models/index.js"; import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js"; -import type { Monitor, MonitorsSummary, MonitorWithChecks, CheckSnapshot, Check } from "@/types/index.js"; +import type { Monitor, MonitorsSummary, CheckSnapshot, Check } from "@/types/index.js"; import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; @@ -79,7 +79,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.mapDocuments(monitors); }; - findByIdsWithChecks = async (monitorIds: string[], checksCount: number = 25): Promise => { + findByIdsWithChecks = async (monitorIds: string[], checksCount: number = 25): Promise => { if (!monitorIds.length) { return []; } @@ -315,7 +315,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; }; - private toEntityWithChecks = (doc: any): MonitorWithChecks => { + private toEntityWithChecks = (doc: any): Monitor => { const toStringId = (value: unknown): string => { if (value instanceof mongoose.Types.ObjectId) { return value.toString(); @@ -396,7 +396,6 @@ class MongoMonitorsRepository implements IMonitorsRepository { recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)), createdAt: toDateString(doc.createdAt), updatedAt: toDateString(doc.updatedAt), - checks, }; }; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 90a909991..b950c9a32 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -363,15 +363,10 @@ export class MonitorService implements IMonitorService { const monitorsWithChecks = monitorsList.map((monitor: Monitor) => { const rawChecks = monitor.recentChecks ?? []; const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type); - // recentChecks are stored oldest-first, reverse to get newest-first for display - const reversedChecks = [...rawChecks].reverse(); - const checks = isSnapshotType ? reversedChecks.slice(0, 1) : NormalizeData(reversedChecks, 10, 100); - return { - ...monitor, - checks, - }; + const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100); + monitor.recentChecks = checks; + return monitor; }); - return { summary: summary ?? null, count, monitors: monitorsWithChecks }; }; diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 6d38d538f..a2c377f54 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -189,3 +189,5 @@ export type NormalizedCheck = T & { export type NormalizedUptimeCheck = T & { originalAvgResponseTime: number; }; + +export type CheckSnapshot = Omit; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 96cd44a65..cdbdb7c83 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -1,6 +1,6 @@ import type { Check } from "@/types/check.js"; - -export type CheckSnapshot = Omit; +import type { CheckSnapshot } from "@/types/check.js"; +export type { CheckSnapshot } from "@/types/check.js"; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const; export type MonitorType = (typeof MonitorTypes)[number]; @@ -57,14 +57,10 @@ export interface MonitorsSummary { pausedMonitors: number; } -export interface MonitorWithChecks extends Monitor { - checks: Check[] | CheckSnapshot[]; -} - export interface MonitorsWithChecksByTeamIdResult { summary: MonitorsSummary | null; count: number; - monitors: MonitorWithChecks[]; + monitors: Monitor[]; } export interface UptimeDetailsResult { From fcd9e7836779c037c3003e8e58e27f0447e36b88 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 19:39:17 +0000 Subject: [PATCH 06/12] infra migration --- .../src/Pages/Infrastructure/Create/index.jsx | 4 +-- .../Details/Components/GaugeBoxes/index.jsx | 3 +- .../Details/Components/StatusBoxes/index.jsx | 3 +- .../Pages/Infrastructure/Details/index.jsx | 2 +- .../Components/MonitorsTable/index.jsx | 8 ++--- .../checks/MongoChecksRepistory.ts | 31 +++++-------------- server/src/types/check.ts | 1 - server/src/types/monitor.ts | 1 - 8 files changed, 16 insertions(+), 37 deletions(-) diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index 0a09105b8..936d87ab0 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -92,9 +92,7 @@ const CreateInfrastructureMonitor = () => { initializeInfrastructureMonitorForUpdate(monitor); const fetchLastCheck = async () => { try { - const { stats } = monitor ?? {}; - let latestCheck = stats?.aggregateData?.latestCheck; - let disks = latestCheck?.disk || []; + let disks = monitor?.recentChecks?.[0]?.disk || []; if (disks.length > 0) { setAvailableDisks(disks); diff --git a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx index 7693fd32e..3859fe040 100644 --- a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx @@ -19,8 +19,7 @@ const Gauges = ({ isLoading = false, monitor }) => { return ; } - const { stats } = monitor ?? {}; - let latestCheck = stats?.aggregateData?.latestCheck; + const latestCheck = monitor?.recentChecks?.[0]; const memoryUsagePercent = latestCheck?.memory?.usage_percent ?? 0; const memoryUsedBytes = latestCheck?.memory?.used_bytes ?? 0; const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx index ef91709cf..d411886c7 100644 --- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx @@ -14,8 +14,7 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => { const { determineState } = useMonitorUtils(); const { t } = useTranslation(); - const { stats } = monitor ?? {}; - const latestCheck = stats?.aggregateData?.latestCheck; + const latestCheck = monitor?.recentChecks?.[0]; // Get data from latest check const physicalCores = latestCheck?.cpu?.physical_core ?? 0; diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx index 62a5c2642..6ae630822 100644 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ b/client/src/Pages/Infrastructure/Details/index.jsx @@ -127,7 +127,7 @@ const InfrastructureDetails = () => { )} {tab === "network" && ( { const processor = - ((monitor.checks[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz"; - const cpu = (monitor?.checks[0]?.cpu.usage_percent ?? 0) * 100; - const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100; - const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100; + ((monitor.recentChecks?.[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz"; + const cpu = (monitor?.recentChecks?.[0]?.cpu?.usage_percent ?? 0) * 100; + const mem = (monitor?.recentChecks?.[0]?.memory?.usage_percent ?? 0) * 100; + const disk = (monitor?.recentChecks?.[0]?.disk?.[0]?.usage_percent ?? 0) * 100; const status = determineState(monitor); const percentageColor = monitor.uptimePercentage < 0.25 diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 5c792a01c..16f00778a 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -29,7 +29,6 @@ const dateRangeLookup: Record = { export type LatestChecksMap = Record; type DateRange = { start: Date; end: Date }; -type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number }; type HardwareUpChecks = { totalChecks: number }; class MongoChecksRepository implements IChecksRepository { @@ -526,14 +525,13 @@ class MongoChecksRepository implements IChecksRepository { const monitorId = monitorObjectId.toHexString(); const dates = { start: startDate, end: endDate }; const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([ - this.getHardwareAggregateData(monitorId, dates), + this.getHardwareTotalChecks(monitorId, dates), this.getHardwareUpChecks(monitorId, dates), this.getHardwareStats(monitorId, dates, dateString), ]); const aggregateData = { - latestCheck: aggregateDataDoc?.latestCheck ? this.toEntity(aggregateDataDoc.latestCheck as CheckDocument) : null, - totalChecks: aggregateDataDoc?.totalChecks ?? 0, + totalChecks: aggregateDataDoc ?? 0, }; const upChecks = { @@ -589,25 +587,12 @@ class MongoChecksRepository implements IChecksRepository { }; }; - private getHardwareAggregateData = async (monitorId: string, dates: DateRange): Promise => { - const result = await CheckModel.aggregate([ - { - $match: { - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { $sort: { createdAt: -1 } }, - { - $group: { - _id: null, - latestCheck: { $first: "$$ROOT" }, - totalChecks: { $sum: 1 }, - }, - }, - ]); - return result[0] || { totalChecks: 0, latestCheck: null }; + private getHardwareTotalChecks = async (monitorId: string, dates: DateRange): Promise => { + return await CheckModel.countDocuments({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }); }; private getHardwareUpChecks = async (monitorId: string, dates: DateRange): Promise => { diff --git a/server/src/types/check.ts b/server/src/types/check.ts index a2c377f54..ce932d79d 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -121,7 +121,6 @@ export interface PageSpeedChecksResult { export interface HardwareChecksResult { monitorType: "hardware"; aggregateData: { - latestCheck: Check | null; totalChecks: number; }; upChecks: { diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index cdbdb7c83..b2bf360dc 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -78,7 +78,6 @@ export interface UptimeDetailsResult { export interface HardwareDetailsResult extends Monitor { stats: { aggregateData: { - latestCheck: import("./check.js").Check | null; totalChecks: number; }; upChecks: { From 7771a0ce3571774c06f97f9ff538b9b1b68e7b09 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 19:50:32 +0000 Subject: [PATCH 07/12] resolve errors --- .../charts/HeatmapResponseTimeTooltip.tsx | 4 ++-- client/src/Pages/Settings/DummyChart.tsx | 14 +++----------- client/src/Types/Check.ts | 19 ++++++++++++++++--- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx b/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx index e928269ce..ec9ad4d26 100644 --- a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx +++ b/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx @@ -55,9 +55,9 @@ export const HeatmapResponseTimeTooltip = ({ {formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)} - {check?.originalResponseTime && ( + {check?.responseTime && ( - {t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms + {t("common.labels.responseTime")}: {check.responseTime.toFixed()} ms )} diff --git a/client/src/Pages/Settings/DummyChart.tsx b/client/src/Pages/Settings/DummyChart.tsx index 6996af9c5..39323522e 100644 --- a/client/src/Pages/Settings/DummyChart.tsx +++ b/client/src/Pages/Settings/DummyChart.tsx @@ -2,31 +2,23 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { useTheme } from "@mui/material/styles"; import { HeatmapResponseTime, HistogramResponseTime } from "@/Components/v2/common"; -import type { Check } from "@/Types/Check"; +import type { CheckSnapshot } from "@/Types/Check"; interface DummyChartProps { chartType: string; } -const generateDummyChecks = (): Check[] => { - const checks: Check[] = []; +const generateDummyChecks = (): CheckSnapshot[] => { + const checks: CheckSnapshot[] = []; for (let i = 0; i < 25; i++) { const isUp = Math.random() > 0.1; checks.push({ id: `dummy-${i}`, - metadata: { - monitorId: "dummy-monitor", - teamId: "dummy-team", - type: "http", - }, status: isUp, statusCode: isUp ? 200 : 500, responseTime: Math.floor(Math.random() * 80) + 20, message: "", - ack: false, - expiry: new Date(Date.now() + 86400000).toISOString(), createdAt: new Date(Date.now() - i * 60000).toISOString(), - updatedAt: new Date(Date.now() - i * 60000).toISOString(), }); } return checks; diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index 19b9dd966..ae5515e87 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -1,7 +1,7 @@ export interface CheckMetadata { monitorId: string; teamId: string; - type: string; + type: "http" | "ping" | "pagespeed" | "hardware" | "docker" | "port" | "game" | "unknown"; } export interface CheckCpuInfo { @@ -83,6 +83,7 @@ export interface CheckTimings { lookup?: number; connect?: number; secureConnect?: number; + upload?: number; response?: number; end?: number; abort?: number; @@ -103,7 +104,6 @@ export interface Check { id: string; metadata: CheckMetadata; status: boolean; - originalResponseTime?: number; responseTime: number; timings?: CheckTimings; statusCode: number; @@ -123,6 +123,7 @@ export interface Check { seo?: number; performance?: number; audits?: CheckAudits; + __v: number; createdAt: string; updatedAt: string; } @@ -145,4 +146,16 @@ export interface ChecksResponse { checksCount: number; } -export type CheckSnapshot = Omit; +export type CheckSnapshot = Omit; + +export interface HasResponseTime { + responseTime: number; +} + +export type NormalizedCheck = T & { + originalResponseTime: number; +}; + +export type NormalizedUptimeCheck = T & { + originalAvgResponseTime: number; +}; From 1bf4c754188ca1b2eea8b2a3264650917c659532 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 19:52:44 +0000 Subject: [PATCH 08/12] format --- client/src/Types/Check.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index ae5515e87..09f851e3b 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -1,7 +1,15 @@ export interface CheckMetadata { monitorId: string; teamId: string; - type: "http" | "ping" | "pagespeed" | "hardware" | "docker" | "port" | "game" | "unknown"; + type: + | "http" + | "ping" + | "pagespeed" + | "hardware" + | "docker" + | "port" + | "game" + | "unknown"; } export interface CheckCpuInfo { @@ -146,7 +154,10 @@ export interface ChecksResponse { checksCount: number; } -export type CheckSnapshot = Omit; +export type CheckSnapshot = Omit< + Check, + "metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt" +>; export interface HasResponseTime { responseTime: number; From d190f5ab02b72fb46a4b466327bd5e7acd0a9a44 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 19:55:52 +0000 Subject: [PATCH 09/12] types --- client/src/Types/Check.ts | 73 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index 09f851e3b..05063291f 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -154,6 +154,79 @@ export interface ChecksResponse { checksCount: number; } +export type MonitorType = + | "http" + | "ping" + | "pagespeed" + | "hardware" + | "docker" + | "port" + | "game" + | "unknown"; + +export interface ChecksQueryResult { + checksCount: number; + checks: Check[]; +} + +export interface PageSpeedChecksResult { + monitorType: "pagespeed"; + checks: Check[]; +} + +export interface HardwareChecksResult { + monitorType: "hardware"; + aggregateData: { + 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 UptimeChecksResult { + monitorType: Exclude; + groupedChecks: GroupedCheck[]; + groupedUpChecks: GroupedCheck[]; + groupedDownChecks: GroupedCheck[]; + uptimePercentage: number; + avgResponseTime: number; +} + +export interface ChecksSummary { + totalChecks: number; + resolvedChecks: number; + downChecks: number; + cannotResolveChecks: number; +} + export type CheckSnapshot = Omit< Check, "metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt" From 0af7cfe714fc081a15d06a13908e3b92b2c46a76 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 20:04:47 +0000 Subject: [PATCH 10/12] generate checks --- server/scripts/generate-checks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/scripts/generate-checks.js b/server/scripts/generate-checks.js index d3a33b033..544770bb6 100644 --- a/server/scripts/generate-checks.js +++ b/server/scripts/generate-checks.js @@ -54,8 +54,8 @@ async function ensureMonitor({ monitorId, teamId, userId, type }) { async function run() { const mongoUri = process.env.MONGO_URI ?? "mongodb://localhost:27017/uptime_db"; const monitorId = parseObjectId(process.env.MONITOR_ID ?? DEFAULT_MONITOR_ID, DEFAULT_MONITOR_ID); - const teamId = parseObjectId("69648b0578209af45f9ffe30"); - const userId = parseObjectId("69648b0678209af45f9ffe32"); + const teamId = parseObjectId("6971546de1b2bc3de6498e6e"); + const userId = parseObjectId("6971546de1b2bc3de6498e70"); const monitorType = process.env.MONITOR_TYPE ?? DEFAULT_MONITOR_TYPE; const total = Number(process.env.CHECK_TOTAL ?? DEFAULT_TOTAL); const batchSize = Number(process.env.CHECK_BATCH_SIZE ?? DEFAULT_BATCH_SIZE); From 63d2bb97ee7e8a6dbc73a011909b34e6f884ed3e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 20:46:28 +0000 Subject: [PATCH 11/12] add dateRange to checks query --- client/src/Hooks/checkHooks.js | 6 +++--- client/src/Pages/Checks/Components/IncidentTable/index.jsx | 2 +- client/src/Pages/Checks/index.jsx | 4 ++-- client/src/Utils/NetworkService.js | 4 ++-- server/src/controllers/checkController.ts | 3 ++- server/src/repositories/checks/IChecksRepository.ts | 2 +- server/src/repositories/checks/MongoChecksRepistory.ts | 7 ++++++- server/src/service/business/checkService.ts | 4 ++-- 8 files changed, 19 insertions(+), 13 deletions(-) diff --git a/client/src/Hooks/checkHooks.js b/client/src/Hooks/checkHooks.js index 49a9b6ba3..2d7edd677 100644 --- a/client/src/Hooks/checkHooks.js +++ b/client/src/Hooks/checkHooks.js @@ -137,7 +137,7 @@ const useFetchChecksByMonitor = ({ return [checks, checksCount, isLoading, networkError]; }; -const useFetchChecksSummaryByTeamId = ({ updateTrigger } = {}) => { +const useFetchChecksSummaryByTeamId = ({ dateRange, updateTrigger } = {}) => { const [summary, setSummary] = useState(undefined); const [isLoading, setIsLoading] = useState(false); const [networkError, setNetworkError] = useState(false); @@ -147,7 +147,7 @@ const useFetchChecksSummaryByTeamId = ({ updateTrigger } = {}) => { try { setIsLoading(true); - const res = await networkService.getChecksAndSummaryByTeamId(); + const res = await networkService.getChecksAndSummaryByTeamId({ dateRange }); setSummary(res.data.data); } catch (error) { setNetworkError(true); @@ -158,7 +158,7 @@ const useFetchChecksSummaryByTeamId = ({ updateTrigger } = {}) => { }; fetchSummary(); - }, [updateTrigger]); + }, [dateRange, updateTrigger]); return [summary, isLoading, networkError]; }; diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx index 857f31bff..bac2a00cb 100644 --- a/client/src/Pages/Checks/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx @@ -19,7 +19,7 @@ import { useState } from "react"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; import { useFetchChecksTeam, useFetchChecksByMonitor } from "@/Hooks/checkHooks.js"; -import { Button, Typography, useTheme } from "@mui/material"; +import { Typography, useTheme } from "@mui/material"; import { lighten } from "@mui/material/styles"; const GetTooltip = (row) => { diff --git a/client/src/Pages/Checks/index.jsx b/client/src/Pages/Checks/index.jsx index aa8717c1a..bcd036a58 100644 --- a/client/src/Pages/Checks/index.jsx +++ b/client/src/Pages/Checks/index.jsx @@ -5,7 +5,6 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import IncidentTable from "./Components/IncidentTable/index.jsx"; import OptionsHeader from "./Components/OptionsHeader/index.jsx"; import StatusBoxes from "./Components/StatusBoxes/index.jsx"; -import { Box, Button } from "@mui/material"; //Utils import { useTheme } from "@emotion/react"; @@ -26,7 +25,7 @@ const Checks = () => { // Local state const [selectedMonitor, setSelectedMonitor] = useState("0"); const [filter, setFilter] = useState(undefined); - const [dateRange, setDateRange] = useState(undefined); + const [dateRange, setDateRange] = useState("hour"); const [monitorLookup, setMonitorLookup] = useState(undefined); const [updateTrigger, setUpdateTrigger] = useState(false); @@ -36,6 +35,7 @@ const Checks = () => { const theme = useTheme(); const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({}); const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({ + dateRange, updateTrigger, }); const { monitorId } = useParams(); diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index 4a096badd..f703b1043 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -594,8 +594,8 @@ class NetworkService { * @async * @returns {Promise} The response from the axios GET request. */ - getChecksAndSummaryByTeamId = async () => { - return this.axiosInstance.get(`/checks/team/summary`); + getChecksAndSummaryByTeamId = async ({ dateRange }) => { + return this.axiosInstance.get(`/checks/team/summary?dateRange=${dateRange}`); }; /** diff --git a/server/src/controllers/checkController.ts b/server/src/controllers/checkController.ts index 8277df3e7..b13663d97 100755 --- a/server/src/controllers/checkController.ts +++ b/server/src/controllers/checkController.ts @@ -66,7 +66,8 @@ class CheckController { getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId }); + const dateRange = req.query?.dateRange || "hour"; + const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId, dateRange }); return res.status(200).json({ success: true, msg: "Checks summary retrieved successfully", diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index ab5dc6ce5..74def03c2 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -33,7 +33,7 @@ export interface IChecksRepository { dateString: string, options?: { type?: MonitorType } ): Promise; - findSummaryByTeamId(teamId: string): Promise; + findSummaryByTeamId(teamId: string, dateRange: string): Promise; // update //delete deleteByMonitorId(monitorId: string): Promise; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 16f00778a..dbda4ad1d 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -365,9 +365,14 @@ class MongoChecksRepository implements IChecksRepository { return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString); }; - findSummaryByTeamId = async (teamId: string) => { + findSummaryByTeamId = async (teamId: string, dateRange: string) => { const matchStage = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), + ...(dateRangeLookup[dateRange] && { + createdAt: { + $gte: dateRangeLookup[dateRange], + }, + }), }; const checks = await CheckModel.aggregate([ { $match: matchStage }, diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index bd4684f11..5564d1d7f 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -145,11 +145,11 @@ class CheckService { return checkData; }; - getChecksSummaryByTeamId = async ({ teamId }: { teamId: string }) => { + getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: string }) => { if (!teamId) { throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksSummaryByTeamId", status: 400 }); } - const summary = await this.checksRepository.findSummaryByTeamId(teamId); + const summary = await this.checksRepository.findSummaryByTeamId(teamId, dateRange); return summary; }; From 7a84d06a79dcc11b06718d8261e9099ed38e0ec0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 27 Jan 2026 20:49:44 +0000 Subject: [PATCH 12/12] query tweak --- .../checks/MongoChecksRepistory.ts | 44 ++++--------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index dbda4ad1d..a6814fbf3 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -368,6 +368,7 @@ class MongoChecksRepository implements IChecksRepository { findSummaryByTeamId = async (teamId: string, dateRange: string) => { const matchStage = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), + status: false, ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], @@ -377,44 +378,17 @@ class MongoChecksRepository implements IChecksRepository { const checks = await CheckModel.aggregate([ { $match: matchStage }, { - $facet: { - summary: [ - { - $group: { - _id: null, - totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } }, - resolvedChecks: { - $sum: { - $cond: [{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] }, 1, 0], - }, - }, - downChecks: { - $sum: { - $cond: [{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, 1, 0], - }, - }, - cannotResolveChecks: { - $sum: { - $cond: [{ $eq: ["$statusCode", 5000] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ], - }, - }, - { - $project: { - summary: { $arrayElemAt: ["$summary", 0] }, + $group: { + _id: null, + totalChecks: { $sum: 1 }, + resolvedChecks: { $sum: { $cond: [{ $eq: ["$ack", true] }, 1, 0] } }, + downChecks: { $sum: { $cond: [{ $eq: ["$ack", false] }, 1, 0] } }, + cannotResolveChecks: { $sum: { $cond: [{ $eq: ["$statusCode", 5000] }, 1, 0] } }, }, }, + { $project: { _id: 0 } }, ]); - return checks[0].summary; + return checks[0] ?? { totalChecks: 0, resolvedChecks: 0, downChecks: 0, cannotResolveChecks: 0 }; }; deleteByMonitorId = async (monitorId: string): Promise => {