From de4c528d55e7d8f0f88dc49044308617844b6cf6 Mon Sep 17 00:00:00 2001 From: nikhil2004 Date: Tue, 22 Sep 2026 03:31:00 +0530 Subject: [PATCH] fix: address PR feedback --- .../Status/themes/shared/BaseStatusPage.tsx | 1 + .../Status/themes/shared/IncidentModal.tsx | 27 ++++++----- .../Status/themes/shared/ThemedHeatmap.tsx | 12 ++--- .../Status/themes/shared/ThemedHistogram.tsx | 12 ++--- client/src/Utils/mergeSx.ts | 8 ---- server/src/domain/incidents/incident.model.ts | 1 + server/src/domain/incidents/incident.type.ts | 2 + .../status-pages/status-page.service.ts | 22 +++++++-- .../unit/services/statusPageService.test.ts | 45 +++++++++++++++++++ 9 files changed, 95 insertions(+), 35 deletions(-) delete mode 100644 client/src/Utils/mergeSx.ts diff --git a/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx b/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx index 11effdede..148633b73 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/BaseStatusPage.tsx @@ -348,6 +348,7 @@ export const BaseStatusPage = ({ url={statusPage.url} monitorId={selectedMonitorId} date={selectedDate} + timezone={bucketTimezone} onClose={() => { setSelectedMonitorId(null); setSelectedDate(null); diff --git a/client/src/Pages/StatusPage/Status/themes/shared/IncidentModal.tsx b/client/src/Pages/StatusPage/Status/themes/shared/IncidentModal.tsx index 45fc66312..ed3f5f33d 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/IncidentModal.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/IncidentModal.tsx @@ -7,6 +7,9 @@ import IconButton from "@mui/material/IconButton"; import { X as XIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useGet } from "@/Hooks/UseApi"; +import { useTheme } from "@mui/material"; +import { SPACING } from "@/Utils/Theme/constants"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; interface Incident { id: string; @@ -20,10 +23,12 @@ interface IncidentModalProps { url: string; monitorId: string | null; date: string | null; + timezone: string; onClose: () => void; } -export const IncidentModal = ({ url, monitorId, date, onClose }: IncidentModalProps) => { +export const IncidentModal = ({ url, monitorId, date, timezone, onClose }: IncidentModalProps) => { + const theme = useTheme(); const { t } = useTranslation(); const open = Boolean(monitorId && date); @@ -43,7 +48,7 @@ export const IncidentModal = ({ url, monitorId, date, onClose }: IncidentModalPr - {t("pages.statusPages.monitorsList.incidents.modalTitle", { date })} + {t("pages.statusPages.monitorsList.incidents.modalTitle", { date })} ({timezone}) {isLoading ? ( - + {t("pages.statusPages.monitorsList.incidents.loading")} ) : data?.incidents && data.incidents.length > 0 ? ( {data.incidents.map((incident) => ( - {new Date(incident.startTime).toLocaleTimeString()} -{" "} + {formatDateWithTz(incident.startTime, "h:mm A", timezone)} -{" "} {incident.endTime - ? new Date(incident.endTime).toLocaleTimeString() + ? formatDateWithTz(incident.endTime, "h:mm A", timezone) : 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 1f3c5334c..e213b097e 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHeatmap.tsx @@ -2,7 +2,7 @@ import Tooltip from "@mui/material/Tooltip"; import Box from "@mui/material/Box"; import type { SxProps, Theme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import { mergeSx } from "@/Utils/mergeSx"; + import type { ChartCell, HeatCellKind, @@ -20,7 +20,7 @@ export const ThemedHeatmap = ({ cells, containerSx, cellSx, onCellClick }: Props return ( @@ -29,10 +29,10 @@ export const ThemedHeatmap = ({ cells, containerSx, cellSx, onCellClick }: Props return ( { if (cell.date && onCellClick) { onCellClick(cell.date); @@ -49,10 +49,10 @@ export const ThemedHeatmap = ({ cells, containerSx, cellSx, onCellClick }: Props placement="top" > { if (cell.date && onCellClick) { diff --git a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx index cd9bb5045..a5ea74d0e 100644 --- a/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx +++ b/client/src/Pages/StatusPage/Status/themes/shared/ThemedHistogram.tsx @@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack"; import type { SxProps, Theme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; import { formatMs } from "@/Utils/TimeUtils"; -import { mergeSx } from "@/Utils/mergeSx"; + import type { BarKind, ChartCell, @@ -42,17 +42,17 @@ export const ThemedHistogram = ({ return ( {cells.map((cell) => { if (cell.barKind === "empty") { return ( { if (cell.date && onCellClick) { onCellClick(cell.date); @@ -69,10 +69,10 @@ export const ThemedHistogram = ({ placement="top" > { if (cell.date && onCellClick) { diff --git a/client/src/Utils/mergeSx.ts b/client/src/Utils/mergeSx.ts deleted file mode 100644 index 27fdd9833..000000000 --- a/client/src/Utils/mergeSx.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { SxProps, Theme } from "@mui/material/styles"; - -export const mergeSx = ( - ...styles: Array | false | null | undefined> -): SxProps => - styles - .flatMap((style) => (Array.isArray(style) ? style : [style])) - .filter(Boolean) as SxProps; diff --git a/server/src/domain/incidents/incident.model.ts b/server/src/domain/incidents/incident.model.ts index 9e252bb1d..468c65eba 100644 --- a/server/src/domain/incidents/incident.model.ts +++ b/server/src/domain/incidents/incident.model.ts @@ -82,6 +82,7 @@ IncidentSchema.index({ teamId: 1, startTime: -1 }); IncidentSchema.index({ status: 1, startTime: -1 }); IncidentSchema.index({ resolutionType: 1, status: 1 }); IncidentSchema.index({ resolvedBy: 1, status: 1 }); +IncidentSchema.index({ monitorId: 1, startTime: -1 }); IncidentSchema.index({ createdAt: -1 }); const IncidentModel = model("Incident", IncidentSchema); diff --git a/server/src/domain/incidents/incident.type.ts b/server/src/domain/incidents/incident.type.ts index 2b45e72c4..bdbc71ccb 100644 --- a/server/src/domain/incidents/incident.type.ts +++ b/server/src/domain/incidents/incident.type.ts @@ -20,6 +20,8 @@ export interface Incident { updatedAt: string; } +export type PublicIncident = Pick; + export interface IncidentSummaryTopMonitor { monitorId: string; monitorName: string | null; diff --git a/server/src/domain/status-pages/status-page.service.ts b/server/src/domain/status-pages/status-page.service.ts index 3922ddfb2..079fa8981 100644 --- a/server/src/domain/status-pages/status-page.service.ts +++ b/server/src/domain/status-pages/status-page.service.ts @@ -1,5 +1,5 @@ import { type IIncidentsRepository } from "@/domain/incidents/incident.repository.interface.js"; -import type { Incident } from "@/domain/incidents/incident.type.js"; +import type { Incident, PublicIncident } 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"; @@ -29,7 +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; + 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; @@ -105,6 +105,19 @@ export class StatusPageService implements IStatusPageService { return base; }; + private toPublicIncident = (incident: Incident): PublicIncident => ({ + id: incident.id, + monitorId: incident.monitorId, + status: incident.status, + startTime: incident.startTime, + endTime: incident.endTime, + resolutionType: incident.resolutionType, + message: incident.message ?? null, + statusCode: incident.statusCode ?? null, + createdAt: incident.createdAt, + }); + + createStatusPage = async ( userId: string, teamId: string, @@ -177,7 +190,7 @@ export class StatusPageService implements IStatusPageService { }; }; - getPublicMonitorIncidents = async (url: string, monitorId: string, date: string): Promise => { + getPublicMonitorIncidents = async (url: string, monitorId: string, date: string): Promise => { const statusPage = await this.getStatusPageByUrl(url); // Ensure the status page is public @@ -194,7 +207,8 @@ export class StatusPageService implements IStatusPageService { 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); + const incidents = await this.incidentsRepository.findByMonitorIdAndDate(monitorId, statusPage.teamId, dateStart, dateEnd); + return incidents.map(this.toPublicIncident); }; updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise => { diff --git a/server/test/unit/services/statusPageService.test.ts b/server/test/unit/services/statusPageService.test.ts index e5aeac92b..0c0c11c40 100644 --- a/server/test/unit/services/statusPageService.test.ts +++ b/server/test/unit/services/statusPageService.test.ts @@ -431,6 +431,51 @@ describe("StatusPageService", () => { }); }); + describe("getPublicMonitorIncidents", () => { + const publishedPage = () => makeStatusPage({ isPublished: true, url: "my-status-page", monitors: ["mon-1"], teamId: "team-1" }); + + it("rejects an unpublished page (403)", async () => { + const { service, repo } = createService(); + (repo.findByUrl as jest.Mock).mockResolvedValue(makeStatusPage({ isPublished: false })); + await expect(service.getPublicMonitorIncidents("my-status-page", "mon-1", "2026-08-01")).rejects.toMatchObject({ status: 403 }); + }); + + it("rejects if monitor is not on the page (404)", async () => { + const { service, repo } = createService(); + (repo.findByUrl as jest.Mock).mockResolvedValue(publishedPage()); + await expect(service.getPublicMonitorIncidents("my-status-page", "mon-99", "2026-08-01")).rejects.toMatchObject({ status: 404 }); + }); + + it("fetches and sanitizes incidents successfully", async () => { + const { service, repo, incidentsRepo } = createService(); + (repo.findByUrl as jest.Mock).mockResolvedValue(publishedPage()); + + const mockIncident = { + id: "inc-1", + monitorId: "mon-1", + teamId: "team-1", + resolvedBy: "user-99", + status: false, + startTime: "2026-08-01T10:00:00Z", + endTime: "2026-08-01T11:00:00Z", + resolutionType: "manual" as const, + message: "Test incident", + createdAt: "2026-08-01T10:00:00Z", + updatedAt: "2026-08-01T10:00:00Z", + }; + (incidentsRepo.findByMonitorIdAndDate as jest.Mock).mockResolvedValue([mockIncident]); + + const results = await service.getPublicMonitorIncidents("my-status-page", "mon-1", "2026-08-01"); + + expect(incidentsRepo.findByMonitorIdAndDate).toHaveBeenCalled(); + expect(results).toHaveLength(1); + expect(results[0]).toHaveProperty("id", "inc-1"); + expect(results[0]).not.toHaveProperty("teamId"); + expect(results[0]).not.toHaveProperty("resolvedBy"); + expect(results[0]).not.toHaveProperty("updatedAt"); + }); + }); + describe("deleteStatusPage", () => { it("delegates to repository and returns deleted page", async () => { const { service, repo } = createService(true);