add range buckets for status pages

This commit is contained in:
Alex Holliday committed 2026-08-17 11:48:44 -07:00
1 parent 697574a2a9
commit 3a22d3e819
5 files changed
+64 -4

No files matched your search

@@ -1,6 +1,6 @@
import { z } from "zod";
import { booleanCoercion, dnsHostnameRegex } from "./shared.js";
import { StatusPageTypes, StatusPageThemes, StatusPageThemeModes } from "@/domain/status-pages/status-page.type.js";
import { StatusPageTypes, StatusPageThemes, StatusPageThemeModes, StatusPageRanges } from "@/domain/status-pages/status-page.type.js";
import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js";
import { cssReferencesExternalResource } from "@/utils/customCss.js";
import { ImageMimeTypes } from "@/types/upload.js";
@@ -15,7 +15,7 @@ export const getStatusPageParamValidation = z.object({
export const getStatusPageQueryValidation = z.object({
type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])),
timeFrame: z.coerce.number().optional(),
range: z.enum(StatusPageRanges).optional().default("latest"),
});
export const resolveStatusPageQueryValidation = getStatusPageQueryValidation.extend({
+9
View File
@@ -243,3 +243,12 @@ export interface HardwareStats {
export interface HardwareChecksResult extends HardwareStats {
monitorType: "hardware";
}
export type DailyCheckBucket = {
monitorId: string;
date: string;
totalChecks: number;
upChecks: number;
downChecks: number;
avgResponseTime: number;
};
@@ -1,3 +1,4 @@
import { DailyCheckBucket } from "@/domain/checks/check.type.js";
import type { StatusPage } from "@/domain/status-pages/status-page.type.js";
export interface IStatusPagesRepository {
@@ -8,6 +9,7 @@ export interface IStatusPagesRepository {
findByCustomDomain(customDomain: string): Promise<StatusPage>;
findByTeamId(teamId: string): Promise<StatusPage[]>;
// collection fetch
getDailyStatusBuckets(monitorIds: string[], days: number, timezone: string | undefined): Promise<DailyCheckBucket[]>;
// update
updateById(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
// delete
@@ -5,6 +5,7 @@ import mongoose from "mongoose";
import { AppError } from "@/utils/AppError.js";
import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js";
import { toStringId, toDateString } from "@/utils/mongoMappers.js";
import CheckModel from "@/domain/checks/check.model.js";
// Type for update data that can include document-level fields (Buffer for logo)
type StatusPageUpdateData = Partial<Omit<StatusPage, "id" | "userId" | "teamId" | "logo" | "createdAt" | "updatedAt">> & {
logo?: StatusPageLogoDocument | null;
@@ -113,6 +114,43 @@ 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));
const windowStart = new Date(Date.now() - (days - 1) * 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: { $ifNull: ["$responseTime", 0] } },
},
},
{ $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,
@@ -1,3 +1,4 @@
import { DailyCheckBucket } from "@/domain/checks/check.type.js";
import type { Monitor } from "@/domain/monitors/monitor.type.js";
export const StatusPageTypes = ["uptime", "infrastructure"] as const;
export type StatusPageType = (typeof StatusPageTypes)[number];
@@ -10,6 +11,13 @@ 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 type StatusPageRange = (typeof StatusPageRanges)[number];
export const STATUS_PAGE_RANGE_DAYS: Record<Exclude<StatusPageRange, "latest">, number> = {
"30d": 30,
"60d": 60,
"90d": 90,
};
export interface StatusPageLogo {
data: string;
contentType: string;
@@ -46,8 +54,11 @@ export interface StatusPage {
updatedAt: string;
}
export type PublicStatusPageMonitor = Pick<Monitor, "id" | "name" | "type" | "status" | "uptimePercentage" | "recentChecks"> &
Partial<Pick<Monitor, "url" | "port">>;
export type PublicStatusPageMonitor = Pick<Monitor, "id" | "name" | "type" | "status" | "uptimePercentage" | "recentChecks"> & {
url?: string;
port?: number;
dailyChecks?: DailyCheckBucket[]; // Only present when range !== "latest"
};
export interface PublicStatusPagePayload {
statusPage: StatusPage;