diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx index 86cdc83d..a55be499 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx @@ -254,7 +254,9 @@ export default function Page() { ) : ( - {tempMonitor?.regions.length} {t("regions")}{" "} + {(tempMonitor?.regions.length ?? 0) + + (tempMonitor?.privateLocationCount ?? 0)}{" "} + {t("regions")}{" "} 0 - ? Object.keys(data[0]).filter((item) => item !== "timestamp") + ? Array.from( + new Set( + data + .flatMap((item) => Object.keys(item)) + .filter((key) => key !== "timestamp"), + ), + ) : []; return regions diff --git a/packages/api/src/router/statusPage.ts b/packages/api/src/router/statusPage.ts index 1659d28e..684acc4b 100644 --- a/packages/api/src/router/statusPage.ts +++ b/packages/api/src/router/statusPage.ts @@ -1,10 +1,11 @@ import { Events } from "@openstatus/analytics"; -import { and, eq, inArray, sql } from "@openstatus/db"; +import { and, eq, inArray, isNull, sql } from "@openstatus/db"; import { maintenance, page, pageComponent, pageConfigurationSchema, + privateLocationToMonitors, selectMaintenancePageSchema, selectPageComponentWithMonitorRelation, selectPageSchema, @@ -247,13 +248,46 @@ export const statusPageRouter = createTRPCRouter({ }; }); + // Add privateLocationCount to each monitor + const privateLocationCounts = new Map(); + if (monitors.length > 0) { + const monitorIds = monitors.map((m) => m.id); + const privateLocations = + await opts.ctx.db.query.privateLocationToMonitors.findMany({ + where: and( + inArray(privateLocationToMonitors.monitorId, monitorIds), + isNull(privateLocationToMonitors.deletedAt), + ), + columns: { + monitorId: true, + }, + }); + + // Count private locations per monitor + for (const pl of privateLocations) { + if (pl.monitorId === null) continue; + privateLocationCounts.set( + pl.monitorId, + (privateLocationCounts.get(pl.monitorId) ?? 0) + 1, + ); + } + } + + // Create new array with privateLocationCount included (no mutation/cast) + const monitorsWithPrivateLocationCount = monitors.map((m) => ({ + ...m, + privateLocationCount: privateLocationCounts.get(m.id) ?? 0, + })); + // no barType gate: incident-driven error is already suppressed per // monitor in manual mode; report-driven error (major_outage) must show - const status = monitors.some((m) => m.status === "error") + const status = monitorsWithPrivateLocationCount.some( + (m) => m.status === "error", + ) ? "error" - : monitors.some((m) => m.status === "degraded") + : monitorsWithPrivateLocationCount.some((m) => m.status === "degraded") ? "degraded" - : monitors.some((m) => m.status === "info") + : monitorsWithPrivateLocationCount.some((m) => m.status === "info") ? "info" : "success"; @@ -430,10 +464,11 @@ export const statusPageRouter = createTRPCRouter({ return selectPublicPageSchemaWithRelation.parse({ ..._page, customTheme, - monitors, + monitors: monitorsWithPrivateLocationCount, monitorGroups, trackers, - incidents: monitors.flatMap((m) => m.incidents) ?? [], + incidents: + monitorsWithPrivateLocationCount.flatMap((m) => m.incidents) ?? [], statusReports, maintenances, workspacePlan: _page.workspace.plan, diff --git a/packages/api/src/router/statusPage.utils.test.ts b/packages/api/src/router/statusPage.utils.test.ts index a9ae1c57..8b582653 100644 --- a/packages/api/src/router/statusPage.utils.test.ts +++ b/packages/api/src/router/statusPage.utils.test.ts @@ -59,10 +59,10 @@ function createStatusData( function createIncident(id: number, daysAgo: number, durationHours = 1): Event { const from = new Date(); from.setDate(from.getDate() - daysAgo); - from.setHours(from.getHours() - durationHours); + from.setUTCHours(0, 0, 0, 0); // Set to midnight UTC for full-day coverage const to = new Date(from); - to.setHours(to.getHours() + durationHours); + to.setUTCHours(from.getUTCHours() + durationHours); return { id, @@ -77,10 +77,10 @@ function createIncident(id: number, daysAgo: number, durationHours = 1): Event { function createReport(id: number, daysAgo: number, durationHours = 2): Event { const from = new Date(); from.setDate(from.getDate() - daysAgo); - from.setHours(from.getHours() - durationHours); + from.setUTCHours(0, 0, 0, 0); // Set to midnight UTC for full-day coverage const to = new Date(from); - to.setHours(to.getHours() + durationHours); + to.setUTCHours(from.getUTCHours() + durationHours); return { id, @@ -99,10 +99,10 @@ function createMaintenance( ): Event { const from = new Date(); from.setDate(from.getDate() - daysAgo); - from.setHours(from.getHours() - durationHours); + from.setUTCHours(0, 0, 0, 0); // Set to midnight UTC for full-day coverage const to = new Date(from); - to.setHours(to.getHours() + durationHours); + to.setUTCHours(from.getUTCHours() + durationHours); return { id, diff --git a/packages/db/src/schema/shared.ts b/packages/db/src/schema/shared.ts index 7d7855f9..8237a0cf 100644 --- a/packages/db/src/schema/shared.ts +++ b/packages/db/src/schema/shared.ts @@ -95,6 +95,7 @@ const selectPublicMonitorWithStatusSchema = selectPublicMonitorBaseSchema monitorGroupId: z.number().nullable().optional(), order: z.number().default(0).optional(), groupOrder: z.number().default(0).nullish(), + privateLocationCount: z.number().optional(), }) .transform((data) => ({ ...data,