diff --git a/server/src/domain/checks/check.repository.interface.ts b/server/src/domain/checks/check.repository.interface.ts index 6889d8d15..91ea5bb5b 100644 --- a/server/src/domain/checks/check.repository.interface.ts +++ b/server/src/domain/checks/check.repository.interface.ts @@ -2,6 +2,7 @@ import type { Check, ChecksQueryResult, ChecksSummary, + DailyCheckBucket, HardwareChecksResult, PageSpeedChecksResult, UptimeChecksResult, @@ -40,6 +41,8 @@ export interface IChecksRepository { ): Promise; findSummaryByTeamId(teamId: string, dateRange: DateRange): Promise; findUnevaluatedByMonitorId(monitorId: string, since: number): Promise; + getDailyStatusBuckets(monitorIds: string[], days: number, timezone: string | undefined): Promise; + // update //delete deleteByMonitorId(monitorId: string): Promise; diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index cb1c5bfba..2840907c9 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -329,6 +329,44 @@ class MongoChecksRepository implements IChecksRepository { return docs.map(this.toEntity); }; + getDailyStatusBuckets = async (monitorIds: string[], days: number, timezone: string | undefined) => { + const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + // One extra day so the oldest rendered day is complete; the client enumerates exactly `days` days and ignores the partial extra bucket + const windowStart = new Date(Date.now() - days * 24 * 60 * 60 * 1000); + const results = await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": { $in: objectIds }, + createdAt: { $gte: windowStart }, + }, + }, + { + $group: { + _id: { + monitorId: "$metadata.monitorId", + day: { $dateTrunc: { date: "$createdAt", unit: "day", timezone } }, + }, + totalChecks: { $sum: 1 }, + upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } }, + avgResponseTime: { $avg: "$responseTime" }, + }, + }, + { $sort: { "_id.day": 1 } }, + { + $project: { + _id: 0, + monitorId: { $toString: "$_id.monitorId" }, + date: { $dateToString: { date: "$_id.day", format: "%Y-%m-%d", timezone } }, + totalChecks: 1, + upChecks: 1, + downChecks: { $subtract: ["$totalChecks", "$upChecks"] }, + avgResponseTime: { $round: ["$avgResponseTime", 0] }, + }, + }, + ]); + return results; + }; + deleteByMonitorId = async (monitorId: string): Promise => { const result = await CheckModel.deleteMany({ "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) }); return result.deletedCount; diff --git a/server/src/domain/status-pages/status-page-repository.interface.ts b/server/src/domain/status-pages/status-page-repository.interface.ts index ed58b18a2..4b3eccd0a 100644 --- a/server/src/domain/status-pages/status-page-repository.interface.ts +++ b/server/src/domain/status-pages/status-page-repository.interface.ts @@ -1,4 +1,3 @@ -import { DailyCheckBucket } from "@/domain/checks/check.type.js"; import type { StatusPage } from "@/domain/status-pages/status-page.type.js"; export interface IStatusPagesRepository { @@ -9,7 +8,6 @@ export interface IStatusPagesRepository { findByCustomDomain(customDomain: string): Promise; findByTeamId(teamId: string): Promise; // collection fetch - getDailyStatusBuckets(monitorIds: string[], days: number, timezone: string | undefined): Promise; // update updateById(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; // delete diff --git a/server/src/domain/status-pages/status-page-repository.mongo.ts b/server/src/domain/status-pages/status-page-repository.mongo.ts index 326db1493..0b74a9374 100644 --- a/server/src/domain/status-pages/status-page-repository.mongo.ts +++ b/server/src/domain/status-pages/status-page-repository.mongo.ts @@ -114,44 +114,6 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { return this.mapDocuments(statusPages); }; - getDailyStatusBuckets = async (monitorIds: string[], days: number, timezone: string | undefined) => { - const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); - // One extra day so the oldest rendered day is complete; the client enumerates exactly `days` days and ignores the partial extra bucket - const windowStart = new Date(Date.now() - days * 24 * 60 * 60 * 1000); - const results = await CheckModel.aggregate([ - { - $match: { - "metadata.monitorId": { $in: objectIds }, - createdAt: { $gte: windowStart }, - }, - }, - { - $group: { - _id: { - monitorId: "$metadata.monitorId", - day: { $dateTrunc: { date: "$createdAt", unit: "day", timezone } }, - }, - totalChecks: { $sum: 1 }, - upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } }, - avgResponseTime: { $avg: "$responseTime" }, - }, - }, - { $sort: { "_id.day": 1 } }, - { - $project: { - _id: 0, - monitorId: { $toString: "$_id.monitorId" }, - date: { $dateToString: { date: "$_id.day", format: "%Y-%m-%d", timezone } }, - totalChecks: 1, - upChecks: 1, - downChecks: { $subtract: ["$totalChecks", "$upChecks"] }, - avgResponseTime: { $round: ["$avgResponseTime", 0] }, - }, - }, - ]); - return results; - }; - updateById = async ( id: string, teamId: string, diff --git a/server/src/domain/status-pages/status-page.service.ts b/server/src/domain/status-pages/status-page.service.ts index 7a2a0724b..7d953f8f3 100644 --- a/server/src/domain/status-pages/status-page.service.ts +++ b/server/src/domain/status-pages/status-page.service.ts @@ -5,11 +5,14 @@ import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE, PublicStatusPagePayload, + STATUS_PAGE_RANGE_DAYS, StatusPage, + StatusPageRange, } from "@/domain/status-pages/status-page.type.js"; import { AppError } from "@/utils/AppError.js"; import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js"; import { Monitor } from "@/domain/monitors/monitor.type.js"; +import { IChecksRepository } from "@/domain/checks/check.repository.interface.js"; export interface IStatusPageService { createStatusPage(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; @@ -26,7 +29,8 @@ export class StatusPageService implements IStatusPageService { constructor( private statusPagesRepository: IStatusPagesRepository, private settingsService: ISettingsService, - private monitorsRepository: IMonitorsRepository + private monitorsRepository: IMonitorsRepository, + private checksRepository: IChecksRepository ) {} private assertCustomDomainAllowed = (customDomain: string | null | undefined) => { @@ -116,7 +120,11 @@ export class StatusPageService implements IStatusPageService { return statusPages.map((sp) => this.normalizeTheme(sp)); }; - getPublicStatusPagePayload = async (statusPage: StatusPage, requesterTeamId: string | undefined): Promise => { + getPublicStatusPagePayload = async ( + statusPage: StatusPage, + requesterTeamId: string | undefined, + range: StatusPageRange = "latest" + ): Promise => { if (!statusPage.isPublished) { if (!requesterTeamId || statusPage.teamId !== requesterTeamId) { throw new AppError({ message: "Forbidden", status: 403 }); @@ -128,7 +136,26 @@ export class StatusPageService implements IStatusPageService { const order = new Map(statusPage.monitors.map((id, i) => [id, i])); const sorted = [...monitors].sort((a, b) => (order.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(b.id) ?? Number.MAX_SAFE_INTEGER)); - return { statusPage, monitors: sorted.map((monitor) => this.toPublicMonitor(monitor, showURL)) }; + if (range === "latest") { + return { statusPage, monitors: sorted.map((monitor) => this.toPublicMonitor(monitor, showURL)) }; + } + + const days = STATUS_PAGE_RANGE_DAYS[range]; + const bucketTimezone = statusPage.timezone ?? "Etc/UTC"; + const buckets = await this.checksRepository.getDailyStatusBuckets(statusPage.monitors, days, bucketTimezone); + const bucketsByMonitor = Map.groupBy(buckets, (bucket) => bucket.monitorId); + + return { + statusPage, + range, + bucketTimezone, + checkTTLDays: (await this.settingsService.getDBSettings()).checkTTL, + monitors: sorted.map((monitor) => ({ + ...this.toPublicMonitor(monitor, showURL), + recentChecks: [], + dailyChecks: bucketsByMonitor.get(monitor.id) ?? [], + })), + }; }; updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise => { diff --git a/server/src/domain/status-pages/status-page.type.ts b/server/src/domain/status-pages/status-page.type.ts index dc5977273..a8bb8fbc7 100644 --- a/server/src/domain/status-pages/status-page.type.ts +++ b/server/src/domain/status-pages/status-page.type.ts @@ -63,4 +63,7 @@ export type PublicStatusPageMonitor = Pick