diff --git a/client/package-lock.json b/client/package-lock.json index ef226f507..659cde440 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -93,7 +93,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -409,7 +408,6 @@ "node_modules/@emotion/react": { "version": "11.14.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -447,7 +445,6 @@ "node_modules/@emotion/styled": { "version": "11.14.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1236,7 +1233,6 @@ "node_modules/@mui/material": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1341,7 +1337,6 @@ "node_modules/@mui/system": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -2386,7 +2381,6 @@ "version": "24.5.2", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2402,7 +2396,6 @@ "node_modules/@types/react": { "version": "18.3.27", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2493,7 +2486,6 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -2774,7 +2766,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3131,7 +3122,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001800", @@ -3575,8 +3565,7 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -3975,7 +3964,6 @@ "version": "8.57.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4788,7 +4776,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5600,7 +5587,6 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz", "integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -6382,7 +6368,6 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6394,7 +6379,6 @@ "node_modules/react-hook-form": { "version": "7.71.1", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -6598,8 +6582,7 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-persist": { "version": "6.0.0", @@ -6749,7 +6732,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -7591,7 +7573,6 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7729,7 +7710,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx b/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx index a355e8774..11effdede 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx @@ -14,6 +14,7 @@ import { import type { StatusPageThemeTokens } from "@/Pages/StatusPage/Status/themes/tokens"; import { ThemedHeatmap } from "@/Pages/StatusPage/Status/themes/shared/ThemedHeatmap"; import { ThemedHistogram } from "@/Pages/StatusPage/Status/themes/shared/ThemedHistogram"; +import { IncidentModal } from "@/Pages/StatusPage/Status/themes/shared/IncidentModal"; import { ThemedInfrastructure, type GaugeFillLevel, @@ -116,6 +117,8 @@ export const BaseStatusPage = ({ [config, tokens, mode] ); const [chartMode, setChartMode] = useState<"heatmap" | "histogram">("heatmap"); + const [selectedMonitorId, setSelectedMonitorId] = useState(null); + const [selectedDate, setSelectedDate] = useState(null); const overall = resolveOverallStatus(monitors, t, config.overallStatusOptions); const logoSrc = statusPage.logo?.data @@ -306,6 +309,10 @@ export const BaseStatusPage = ({ cells={cells} containerSx={styles.heatmap} cellSx={styles.heatmapCell} + onCellClick={(date) => { + setSelectedMonitorId(monitor.id); + setSelectedDate(date); + }} /> ) : ( { + setSelectedMonitorId(monitor.id); + setSelectedDate(date); + }} /> ))} @@ -333,6 +344,15 @@ export const BaseStatusPage = ({ Checkmate + { + setSelectedMonitorId(null); + setSelectedDate(null); + }} + /> ); }; diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ChartCells.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ChartCells.tsx index 8d8a6a675..924e758b9 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ChartCells.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ChartCells.tsx @@ -25,6 +25,7 @@ export interface ChartCell { responseTime: number; // 0 for empty cells tooltip: React.ReactNode | null; ariaLabel: string; + date: string | null; } const DAY_DOWN_THRESHOLD = 0.5; @@ -61,7 +62,7 @@ export const classifyResponseTime = ( return "fast"; }; -const emptyCell = (key: string): ChartCell => ({ +const emptyCell = (key: string, date: string | null = null): ChartCell => ({ key, barKind: "empty", heatKind: "empty", @@ -70,6 +71,7 @@ const emptyCell = (key: string): ChartCell => ({ tooltip: null, ariaLabel: "", severity: 0, + date, }); const padCells = (cells: ChartCell[], length: number): ChartCell[] => [ @@ -92,6 +94,7 @@ export const checksToCells = (checks: CheckSnapshot[]): ChartCell[] => { tooltip: , ariaLabel: `${formatMs(check.responseTime)}, ${check.status ? "up" : "down"}`, severity: 0, + date: null, }) ); return padCells(cells, MAX_RECENT_CHECKS); @@ -108,7 +111,7 @@ export const dailyBucketsToCells = ( const enumerated = enumerateDays(days, bucketTimezone); return enumerated.map((date) => { const bucket = byDate.get(date); - if (!bucket) return emptyCell(`day-${date}`); + if (!bucket) return emptyCell(`day-${date}`, date); const upRatio = bucket.upChecks / bucket.totalChecks; const barKind = classifyDay(bucket, upRatio); @@ -132,6 +135,7 @@ export const dailyBucketsToCells = ( date, uptime: formatPercentage(bucket.upChecks / bucket.totalChecks), }), + date, }; }); }; diff --git a/client/src/Pages/StatusPage/Status/themes/shared/IncidentModal.tsx b/client/src/Pages/StatusPage/Status/themes/shared/IncidentModal.tsx new file mode 100644 index 000000000..4523ed596 --- /dev/null +++ b/client/src/Pages/StatusPage/Status/themes/shared/IncidentModal.tsx @@ -0,0 +1,97 @@ +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import IconButton from "@mui/material/IconButton"; +import { X as XIcon } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { useGet } from "@/Hooks/UseApi"; + +interface Incident { + id: string; + message: string | null; + startTime: string; + endTime: string | null; + statusCode: number | null; +} + +interface IncidentModalProps { + url: string; + monitorId: string | null; + date: string | null; + onClose: () => void; +} + +export const IncidentModal = ({ url, monitorId, date, onClose }: IncidentModalProps) => { + const { t } = useTranslation(); + const open = Boolean(monitorId && date); + + const { data, isLoading } = useGet<{ incidents: Incident[] }>( + open ? `/status-page/${url}/incidents/${monitorId}?date=${date}` : null, + {}, + { revalidateOnFocus: false } + ); + + return ( + + + {t("pages.statusPages.monitorsList.incidents.modalTitle", { date })} + + + + + + {isLoading ? ( + + {t("pages.statusPages.monitorsList.incidents.loading")} + + ) : data?.incidents && data.incidents.length > 0 ? ( + + {data.incidents.map((incident) => ( + + + {incident.message || + (incident.statusCode ? `Error ${incident.statusCode}` : "Incident")} + + + {new Date(incident.startTime).toLocaleTimeString()} -{" "} + {incident.endTime + ? new Date(incident.endTime).toLocaleTimeString() + : t("pages.statusPages.monitorsList.incidents.ongoing")} + + + ))} + + ) : ( + + {t("pages.statusPages.monitorsList.incidents.noIncidents")} + + )} + + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx index 7ddc7424d..556c170e9 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx @@ -11,9 +11,10 @@ interface Props { cells: ChartCell[]; containerSx: SxProps; cellSx: (kind: HeatCellKind, severity?: number) => SxProps; + onCellClick?: (date: string) => void; } -export const ThemedHeatmap = ({ cells, containerSx, cellSx }: Props) => { +export const ThemedHeatmap = ({ cells, containerSx, cellSx, onCellClick }: Props) => { const { t } = useTranslation(); return ( @@ -42,8 +43,16 @@ export const ThemedHeatmap = ({ cells, containerSx, cellSx }: Props) => { placement="top" > { + if (cell.date && onCellClick) { + onCellClick(cell.date); + } + }} /> ); diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx index ea5fa649a..d38bc68e6 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx @@ -16,6 +16,7 @@ interface Props { barSx: (kind: BarKind, heightPct: number, severity?: number) => SxProps; statsSx: SxProps; statsGap?: number; + onCellClick?: (date: string) => void; } export const ThemedHistogram = ({ @@ -24,6 +25,7 @@ export const ThemedHistogram = ({ barSx, statsSx, statsGap = 1, + onCellClick, }: Props) => { const { t } = useTranslation(); @@ -61,8 +63,16 @@ export const ThemedHistogram = ({ placement="top" > { + if (cell.date && onCellClick) { + onCellClick(cell.date); + } + }} /> ); diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 75e695c70..89466409a 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1712,6 +1712,12 @@ "uptime": { "title": "Uptime", "responseTime": "Response time" + }, + "incidents": { + "modalTitle": "Incidents for {{date}}", + "loading": "Loading...", + "noIncidents": "No incidents reported for this day.", + "ongoing": "Ongoing" } }, "statusBar": { diff --git a/server/src/api/controllers/statusPageController.ts b/server/src/api/controllers/statusPageController.ts index 94783b5a9..60f31617d 100644 --- a/server/src/api/controllers/statusPageController.ts +++ b/server/src/api/controllers/statusPageController.ts @@ -7,6 +7,8 @@ import { getStatusPageQueryValidation, imageValidation, resolveStatusPageQueryValidation, + getPublicMonitorIncidentsParamValidation, + getPublicMonitorIncidentsQueryValidation, } from "@/api/validation/statusPageValidation.js"; import { AppError } from "@/utils/AppError.js"; import { requireTeamId, requireUserId } from "@/api/controllers/controllerUtils.js"; @@ -17,6 +19,7 @@ export interface IStatusPageController { createStatusPage: RequestHandler; updateStatusPage: RequestHandler; getStatusPageByUrl: RequestHandler; + getPublicMonitorIncidents: RequestHandler; resolveStatusPageByDomain: RequestHandler; getStatusPagesByTeamId: RequestHandler; deleteStatusPage: RequestHandler; @@ -85,6 +88,19 @@ class StatusPageController implements IStatusPageController { }); }); + getPublicMonitorIncidents = catchAsync(async (req: Request, res: Response) => { + const { url, monitorId } = getPublicMonitorIncidentsParamValidation.parse(req.params); + const { date } = getPublicMonitorIncidentsQueryValidation.parse(req.query); + + const incidents = await this.statusPageService.getPublicMonitorIncidents(url, monitorId, date); + + return res.status(200).json({ + success: true, + msg: "Incidents retrieved successfully", + data: { incidents }, + }); + }); + resolveStatusPageByDomain = catchAsync(async (req: Request, res: Response) => { const { range } = resolveStatusPageQueryValidation.parse(req.query); const domain = resolveStatusPageDomainFromRequest(req.hostname, req.query.domain as string | undefined); diff --git a/server/src/api/routes/statusPageRoutes.ts b/server/src/api/routes/statusPageRoutes.ts index b37ae5499..07848f41c 100644 --- a/server/src/api/routes/statusPageRoutes.ts +++ b/server/src/api/routes/statusPageRoutes.ts @@ -14,6 +14,7 @@ export const createStatusPageRoutes = ( router.put("/:id", imageUpload.single("logo"), verifyJWT, isAllowed(["admin", "superadmin"]), statusPageController.updateStatusPage); router.get("/resolve", statusPageController.resolveStatusPageByDomain); router.get("/:url", verifyStatusPageAccess, statusPageController.getStatusPageByUrl); + router.get("/:url/incidents/:monitorId", verifyStatusPageAccess, statusPageController.getPublicMonitorIncidents); router.delete("/:id", verifyJWT, isAllowed(["admin", "superadmin"]), statusPageController.deleteStatusPage); return router; }; diff --git a/server/src/api/validation/statusPageValidation.ts b/server/src/api/validation/statusPageValidation.ts index e92f28a57..a332205d2 100644 --- a/server/src/api/validation/statusPageValidation.ts +++ b/server/src/api/validation/statusPageValidation.ts @@ -20,6 +20,15 @@ export const getStatusPageParamValidation = z.object({ url: z.string().min(1, "URL is required"), }); +export const getPublicMonitorIncidentsParamValidation = z.object({ + url: z.string().min(1, "URL is required"), + monitorId: z.string().regex(/^[0-9a-fA-F]{24}$/, "Must be a valid monitor ID"), +}); + +export const getPublicMonitorIncidentsQueryValidation = z.object({ + date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format"), +}); + export const getStatusPageQueryValidation = z.object({ type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])), range: z.enum(StatusPageRanges).optional().default("latest"), diff --git a/server/src/config/services.api.ts b/server/src/config/services.api.ts index 68b722f0a..a72fa50e5 100644 --- a/server/src/config/services.api.ts +++ b/server/src/config/services.api.ts @@ -99,7 +99,7 @@ export const buildApi = (shared: SharedServices, jobScheduler: IJobScheduler): A emailService, }); - const statusPageService = new StatusPageService(statusPagesRepository, settingsService, monitorsRepository, checksRepository); + const statusPageService = new StatusPageService(statusPagesRepository, settingsService, monitorsRepository, checksRepository, incidentsRepository); const tagsService = new TagsService(tagsRepository, monitorsRepository); const diagnosticService = new DiagnosticService(db); const proxiesService = new ProxiesService(proxiesRepository, monitorsRepository, settingsService); diff --git a/server/src/domain/incidents/incident.repository.interface.ts b/server/src/domain/incidents/incident.repository.interface.ts index 549a33b85..561ab7a41 100644 --- a/server/src/domain/incidents/incident.repository.interface.ts +++ b/server/src/domain/incidents/incident.repository.interface.ts @@ -17,6 +17,7 @@ export interface IIncidentsRepository { monitorId?: string, resolutionType?: string ): Promise; + findByMonitorIdAndDate(monitorId: string, teamId: string, dateStart: Date, dateEnd: Date): Promise; findSummaryByTeamId(teamId: string, limit?: number): Promise; countByTeamId(teamId: string, startDate: Date | undefined, status?: boolean, monitorId?: string, resolutionType?: string): Promise; diff --git a/server/src/domain/incidents/incident.repository.mongo.ts b/server/src/domain/incidents/incident.repository.mongo.ts index 61cea96b8..3f40b68cd 100644 --- a/server/src/domain/incidents/incident.repository.mongo.ts +++ b/server/src/domain/incidents/incident.repository.mongo.ts @@ -120,6 +120,17 @@ class MongoIncidentsRepository implements IIncidentsRepository { return this.mapDocuments(incidents); }; + findByMonitorIdAndDate = async (monitorId: string, teamId: string, dateStart: Date, dateEnd: Date): Promise => { + const matchStage = { + monitorId: new mongoose.Types.ObjectId(monitorId), + teamId: new mongoose.Types.ObjectId(teamId), + startTime: { $lt: dateEnd }, + $or: [{ endTime: null }, { endTime: { $gte: dateStart } }], + }; + const incidents = await IncidentModel.find(matchStage).sort({ startTime: -1 }); + return this.mapDocuments(incidents); + }; + updateById = async (incidentId: string, teamId: string, patch: Partial) => { const updatedIncident = await IncidentModel.findOneAndUpdate( { _id: new mongoose.Types.ObjectId(incidentId), teamId: new mongoose.Types.ObjectId(teamId) }, diff --git a/server/src/domain/status-pages/status-page.service.ts b/server/src/domain/status-pages/status-page.service.ts index 5f700857f..3922ddfb2 100644 --- a/server/src/domain/status-pages/status-page.service.ts +++ b/server/src/domain/status-pages/status-page.service.ts @@ -1,6 +1,13 @@ +import { type IIncidentsRepository } from "@/domain/incidents/incident.repository.interface.js"; +import type { Incident } from "@/domain/incidents/incident.type.js"; import { type IStatusPagesRepository } from "@/domain/status-pages/status-page-repository.interface.js"; import { ISettingsService } from "@/domain/app-settings/app-settings.service.js"; import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js"; +import { IChecksRepository } from "@/domain/checks/check.repository.interface.js"; +import type { DailyCheckBucket } from "@/domain/checks/check.type.js"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc.js"; +import timezone from "dayjs/plugin/timezone.js"; import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE, @@ -12,8 +19,9 @@ import { 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"; -import type { DailyCheckBucket } from "@/domain/checks/check.type.js"; + +dayjs.extend(utc); +dayjs.extend(timezone); export interface IStatusPageService { createStatusPage(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; @@ -21,6 +29,7 @@ export interface IStatusPageService { getStatusPageByCustomDomain(customDomain: string): Promise; getStatusPagesByTeamId(teamId: string): Promise; getPublicStatusPagePayload(statusPage: StatusPage, requesterTeamId: string | undefined, range: StatusPageRange): Promise; + getPublicMonitorIncidents(url: string, monitorId: string, date: string): Promise; updateStatusPage(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; deleteStatusPage(statusPageId: string, teamId: string): Promise; @@ -31,7 +40,8 @@ export class StatusPageService implements IStatusPageService { private statusPagesRepository: IStatusPagesRepository, private settingsService: ISettingsService, private monitorsRepository: IMonitorsRepository, - private checksRepository: IChecksRepository + private checksRepository: IChecksRepository, + private incidentsRepository: IIncidentsRepository ) {} private assertCustomDomainAllowed = (customDomain: string | null | undefined) => { @@ -167,6 +177,26 @@ export class StatusPageService implements IStatusPageService { }; }; + getPublicMonitorIncidents = async (url: string, monitorId: string, date: string): Promise => { + const statusPage = await this.getStatusPageByUrl(url); + + // Ensure the status page is public + if (!statusPage.isPublished) { + throw new AppError({ message: "Forbidden", status: 403 }); + } + + // Ensure the requested monitor actually belongs to this status page + if (!statusPage.monitors.includes(monitorId)) { + throw new AppError({ message: "Monitor not found on this status page", status: 404 }); + } + + const tz = statusPage.timezone ?? "Etc/UTC"; + const dateStart = dayjs.tz(date, tz).startOf("day").toDate(); + const dateEnd = dayjs.tz(date, tz).endOf("day").toDate(); + + return this.incidentsRepository.findByMonitorIdAndDate(monitorId, statusPage.teamId, dateStart, dateEnd); + }; + updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise => { const normalizedData = this.normalizeCustomDomainInput(this.normalizeInput(data)); const updated = await this.statusPagesRepository.updateById(id, teamId, image, normalizedData); diff --git a/server/test/unit/services/statusPageService.test.ts b/server/test/unit/services/statusPageService.test.ts index ba3ac298e..e5aeac92b 100644 --- a/server/test/unit/services/statusPageService.test.ts +++ b/server/test/unit/services/statusPageService.test.ts @@ -4,6 +4,7 @@ import type { IStatusPagesRepository } from "../../../src/domain/status-pages/st import type { ISettingsService } from "../../../src/domain/app-settings/app-settings.service.ts"; import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts"; import type { IChecksRepository } from "../../../src/domain/checks/check.repository.interface.ts"; +import type { IIncidentsRepository } from "../../../src/domain/incidents/incident.repository.interface.ts"; import type { CheckSnapshot } from "../../../src/domain/checks/check.type.ts"; import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { StatusPage } from "../../../src/domain/status-pages/status-page.type.ts"; @@ -84,13 +85,19 @@ const createChecksRepo = () => getDailyStatusBuckets: jest.fn().mockResolvedValue([]), }) as unknown as jest.Mocked; +const createIncidentsRepo = () => + ({ + findByMonitorIdAndDate: jest.fn().mockResolvedValue([]), + }) as unknown as jest.Mocked; + const createService = (themesEnabled = true, clientHost = "http://localhost:5173", showURL = false) => { const repo = createRepo(); const settingsService = createSettingsService(themesEnabled, clientHost, showURL); const monitorsRepo = createMonitorsRepo(); const checksRepo = createChecksRepo(); - const service = new StatusPageService(repo, settingsService, monitorsRepo, checksRepo); - return { service, repo, settingsService, monitorsRepo, checksRepo }; + const incidentsRepo = createIncidentsRepo(); + const service = new StatusPageService(repo, settingsService, monitorsRepo, checksRepo, incidentsRepo); + return { service, repo, settingsService, monitorsRepo, checksRepo, incidentsRepo }; }; // ── Tests ────────────────────────────────────────────────────────────────────