diff --git a/server/openapi.json b/server/openapi.json index 3fc6568b6..0ea728a61 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -1648,7 +1648,7 @@ }, "range": { "type": "string", - "enum": ["latest", "30d", "60d", "90d"] + "enum": ["30d", "60d", "90d"] }, "bucketTimezone": { "type": "string" diff --git a/server/src/api/controllers/statusPageController.ts b/server/src/api/controllers/statusPageController.ts index 322f74c8d..9153c152c 100644 --- a/server/src/api/controllers/statusPageController.ts +++ b/server/src/api/controllers/statusPageController.ts @@ -69,7 +69,6 @@ class StatusPageController implements IStatusPageController { getStatusPageByUrl = catchAsync(async (req: Request, res: Response) => { getStatusPageParamValidation.parse(req.params); - getStatusPageQueryValidation.parse(req.query); const { range } = getStatusPageQueryValidation.parse(req.query); if (!req.params.url) { diff --git a/server/src/api/validation/statusPageValidation.ts b/server/src/api/validation/statusPageValidation.ts index b363efd15..ad2277797 100644 --- a/server/src/api/validation/statusPageValidation.ts +++ b/server/src/api/validation/statusPageValidation.ts @@ -1,6 +1,12 @@ import { z } from "zod"; import { booleanCoercion, dnsHostnameRegex, timezoneValidation } from "./shared.js"; -import { StatusPageTypes, StatusPageThemes, StatusPageThemeModes, StatusPageRanges } from "@/domain/status-pages/status-page.type.js"; +import { + StatusPageTypes, + StatusPageThemes, + StatusPageThemeModes, + StatusPageRanges, + StatusPageDayRanges, +} from "@/domain/status-pages/status-page.type.js"; import { MonitorTypes, MonitorStatuses } from "@/domain/monitors/monitor.type.js"; import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js"; import { cssReferencesExternalResource } from "@/utils/customCss.js"; @@ -156,7 +162,7 @@ export const publicStatusPageMonitorResponseSchema = z.object({ export const publicStatusPagePayloadResponseSchema = z.object({ statusPage: statusPageResponseSchema, monitors: z.array(publicStatusPageMonitorResponseSchema), - range: z.enum(StatusPageRanges).optional(), + range: z.enum(StatusPageDayRanges).optional(), bucketTimezone: z.string().optional(), checkTTLDays: z.number().optional(), }); diff --git a/server/src/domain/checks/check.repository.interface.ts b/server/src/domain/checks/check.repository.interface.ts index 91ea5bb5b..1767b2ea1 100644 --- a/server/src/domain/checks/check.repository.interface.ts +++ b/server/src/domain/checks/check.repository.interface.ts @@ -41,7 +41,7 @@ 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; + getDailyStatusBuckets(monitorIds: string[], days: number, timezone: string): Promise; // update //delete diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index 2840907c9..de144ea80 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -329,7 +329,7 @@ class MongoChecksRepository implements IChecksRepository { return docs.map(this.toEntity); }; - getDailyStatusBuckets = async (monitorIds: string[], days: number, timezone: string | undefined) => { + getDailyStatusBuckets = async (monitorIds: string[], days: number, timezone: string) => { 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); diff --git a/server/src/domain/status-pages/status-page.service.ts b/server/src/domain/status-pages/status-page.service.ts index 6aab311ee..25e6d3e08 100644 --- a/server/src/domain/status-pages/status-page.service.ts +++ b/server/src/domain/status-pages/status-page.service.ts @@ -132,8 +132,9 @@ export class StatusPageService implements IStatusPageService { } } - const showURL = (await this.settingsService.getDBSettings()).showURL; - const monitors = await this.monitorsRepository.findByIds(statusPage.monitors); + const dbSettings = await this.settingsService.getDBSettings(); + const showURL = dbSettings.showURL; + const monitors = await this.monitorsRepository.findByIds(statusPage.monitors, { includeRecentChecks: range === "latest" }); 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)); @@ -158,7 +159,7 @@ export class StatusPageService implements IStatusPageService { statusPage, range, bucketTimezone, - checkTTLDays: (await this.settingsService.getDBSettings()).checkTTL, + checkTTLDays: dbSettings.checkTTL, monitors: sorted.map((monitor) => ({ ...this.toPublicMonitor(monitor, showURL), recentChecks: [], diff --git a/server/src/domain/status-pages/status-page.type.ts b/server/src/domain/status-pages/status-page.type.ts index a8bb8fbc7..630a22ce2 100644 --- a/server/src/domain/status-pages/status-page.type.ts +++ b/server/src/domain/status-pages/status-page.type.ts @@ -11,9 +11,11 @@ export const StatusPageThemeModes = ["auto", "light", "dark"] as const; export type StatusPageThemeMode = (typeof StatusPageThemeModes)[number]; export const DEFAULT_STATUS_PAGE_THEME_MODE: StatusPageThemeMode = "auto"; -export const StatusPageRanges = ["latest", "30d", "60d", "90d"] as const; +export const StatusPageDayRanges = ["30d", "60d", "90d"] as const; +export type StatusPageDayRange = (typeof StatusPageDayRanges)[number]; +export const StatusPageRanges = ["latest", ...StatusPageDayRanges] as const; export type StatusPageRange = (typeof StatusPageRanges)[number]; -export const STATUS_PAGE_RANGE_DAYS: Record, number> = { +export const STATUS_PAGE_RANGE_DAYS: Record = { "30d": 30, "60d": 60, "90d": 90, @@ -63,7 +65,7 @@ export type PublicStatusPageMonitor = Pick { describe("range", () => { it("omits range-mode fields and never queries buckets for the default (latest) range", async () => { - const { service, checksRepo } = createService(); + const { service, checksRepo, monitorsRepo } = createService(); const implicitLatest = await service.getPublicStatusPagePayload(publishedPage(), undefined); const explicitLatest = await service.getPublicStatusPagePayload(publishedPage(), undefined, "latest"); @@ -376,6 +376,7 @@ describe("StatusPageService", () => { expect(payload.monitors[0]).not.toHaveProperty("dailyChecks"); } expect(checksRepo.getDailyStatusBuckets).not.toHaveBeenCalled(); + expect(monitorsRepo.findByIds).toHaveBeenCalledWith(["mon-1"], { includeRecentChecks: true }); }); it("attaches dailyChecks, empties recentChecks, and echoes range metadata for a day range", async () => { @@ -388,6 +389,7 @@ describe("StatusPageService", () => { const payload = await service.getPublicStatusPagePayload(page, undefined, "30d"); expect(checksRepo.getDailyStatusBuckets).toHaveBeenCalledWith(["mon-1"], 30, "America/Toronto"); + expect(monitorsRepo.findByIds).toHaveBeenCalledWith(["mon-1"], { includeRecentChecks: false }); expect(payload).toMatchObject({ range: "30d", bucketTimezone: "America/Toronto", checkTTLDays: 30 }); expect(payload.monitors[0].recentChecks).toEqual([]); expect(payload.monitors[0].dailyChecks).toEqual([bucket]);