diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index bc9a72c09..f064ed906 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -1092,6 +1092,20 @@ export const roundTripMessageTracker = pgTable("roundTripMessageTracker", { complete: boolean("complete").notNull().default(false) }); +export const statusHistory = pgTable("statusHistory", { + id: serial("id").primaryKey(), + entityType: varchar("entityType").notNull(), + entityId: integer("entityId").notNull(), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + status: varchar("status").notNull(), + timestamp: integer("timestamp").notNull(), +}, (table) => [ + index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp), + index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp), +]); + export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; @@ -1159,19 +1173,4 @@ export type RoundTripMessageTracker = InferSelectModel< typeof roundTripMessageTracker >; export type Network = InferSelectModel; - -export const statusHistory = pgTable("statusHistory", { - id: serial("id").primaryKey(), - entityType: varchar("entityType").notNull(), - entityId: integer("entityId").notNull(), - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - status: varchar("status").notNull(), - timestamp: integer("timestamp").notNull(), -}, (table) => [ - index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp), - index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp), -]); - export type StatusHistory = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index d7c947347..00994fa2a 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1181,6 +1181,16 @@ export const deviceWebAuthCodes = sqliteTable("deviceWebAuthCodes", { }) }); +export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", { + messageId: integer("messageId").primaryKey({ autoIncrement: true }), + wsClientId: text("clientId"), + messageType: text("messageType"), + sentAt: integer("sentAt").notNull(), + receivedAt: integer("receivedAt"), + error: text("error"), + complete: integer("complete", { mode: "boolean" }).notNull().default(false) +}); + export const statusHistory = sqliteTable("statusHistory", { id: integer("id").primaryKey({ autoIncrement: true }), entityType: text("entityType").notNull(), // "site" | "healthCheck" @@ -1195,16 +1205,6 @@ export const statusHistory = sqliteTable("statusHistory", { index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp), ]); -export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", { - messageId: integer("messageId").primaryKey({ autoIncrement: true }), - wsClientId: text("clientId"), - messageType: text("messageType"), - sentAt: integer("sentAt").notNull(), - receivedAt: integer("receivedAt"), - error: text("error"), - complete: integer("complete", { mode: "boolean" }).notNull().default(false) -}); - export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; diff --git a/server/lib/statusHistory.ts b/server/lib/statusHistory.ts new file mode 100644 index 000000000..001a0b93b --- /dev/null +++ b/server/lib/statusHistory.ts @@ -0,0 +1,133 @@ +import { z } from "zod"; + +export const statusHistoryQuerySchema = z + .object({ + days: z + .string() + .optional() + .transform((v) => (v ? parseInt(v, 10) : 90)), + }) + .pipe( + z.object({ + days: z.number().int().min(1).max(365), + }) + ); + +export interface StatusHistoryDayBucket { + date: string; // ISO date "YYYY-MM-DD" + uptimePercent: number; // 0-100 + totalDowntimeSeconds: number; + downtimeWindows: { start: number; end: number | null; status: string }[]; + status: "good" | "degraded" | "bad" | "no_data"; +} + +export interface StatusHistoryResponse { + entityType: string; + entityId: number; + days: StatusHistoryDayBucket[]; + overallUptimePercent: number; + totalDowntimeSeconds: number; +} + +export function computeBuckets( + events: { entityType: string; entityId: number; orgId: string; status: string; timestamp: number; id: number }[], + days: number +): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } { + const nowSec = Math.floor(Date.now() / 1000); + const buckets: StatusHistoryDayBucket[] = []; + let totalDowntime = 0; + + for (let d = 0; d < days; d++) { + const dayStartSec = nowSec - (days - d) * 86400; + const dayEndSec = dayStartSec + 86400; + + const dayEvents = events.filter( + (e) => e.timestamp >= dayStartSec && e.timestamp < dayEndSec + ); + + // Determine the status at the start of this day (last event before dayStart) + const lastBeforeDay = [...events] + .filter((e) => e.timestamp < dayStartSec) + .at(-1); + + const currentStatus = lastBeforeDay?.status ?? null; + + const windows: { start: number; end: number | null; status: string }[] = []; + let dayDowntime = 0; + + let windowStart = dayStartSec; + let windowStatus = currentStatus; + + for (const evt of dayEvents) { + if (windowStatus !== null && windowStatus !== evt.status) { + const windowEnd = evt.timestamp; + const isDown = + windowStatus === "offline" || + windowStatus === "unhealthy" || + windowStatus === "unknown"; + if (isDown) { + dayDowntime += windowEnd - windowStart; + windows.push({ + start: windowStart, + end: windowEnd, + status: windowStatus, + }); + } + } + windowStart = evt.timestamp; + windowStatus = evt.status; + } + + // Close the final window at the end of the day (or now if day hasn't ended) + if (windowStatus !== null) { + const finalEnd = Math.min(dayEndSec, nowSec); + const isDown = + windowStatus === "offline" || + windowStatus === "unhealthy" || + windowStatus === "unknown"; + if (isDown && finalEnd > windowStart) { + dayDowntime += finalEnd - windowStart; + windows.push({ + start: windowStart, + end: finalEnd, + status: windowStatus, + }); + } + } + + totalDowntime += dayDowntime; + + const effectiveDayLength = Math.max( + 0, + Math.min(dayEndSec, nowSec) - dayStartSec + ); + const uptimePct = + effectiveDayLength > 0 + ? Math.max( + 0, + ((effectiveDayLength - dayDowntime) / + effectiveDayLength) * + 100 + ) + : 100; + + const dateStr = new Date(dayStartSec * 1000).toISOString().slice(0, 10); + + let status: StatusHistoryDayBucket["status"] = "no_data"; + if (currentStatus !== null || dayEvents.length > 0) { + if (uptimePct >= 99) status = "good"; + else if (uptimePct >= 50) status = "degraded"; + else status = "bad"; + } + + buckets.push({ + date: dateStr, + uptimePercent: Math.round(uptimePct * 100) / 100, + totalDowntimeSeconds: dayDowntime, + downtimeWindows: windows, + status, + }); + } + + return { buckets, totalDowntime }; +} diff --git a/server/middlewares/verifyDomainAccess.ts b/server/middlewares/verifyDomainAccess.ts index c9ecf42e0..d37f6725d 100644 --- a/server/middlewares/verifyDomainAccess.ts +++ b/server/middlewares/verifyDomainAccess.ts @@ -15,7 +15,7 @@ export async function verifyDomainAccess( try { const userId = req.user!.userId; const domainId = - req.params.domainId || req.body.apiKeyId || req.query.apiKeyId; + req.params.domainId; const orgId = req.params.orgId; if (!userId) { diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 153f1e839..f7a4c71ab 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -657,6 +657,7 @@ authenticated.delete( authenticated.get( "/org/:orgId/event-streaming-destinations", + verifyValidLicense, verifyOrgAccess, verifyUserHasAction(ActionsEnum.listEventStreamingDestinations), eventStreamingDestination.listEventStreamingDestinations @@ -692,6 +693,7 @@ authenticated.delete( authenticated.get( "/org/:orgId/alert-rules", + verifyValidLicense, verifyOrgAccess, verifyUserHasAction(ActionsEnum.listAlertRules), alertRule.listAlertRules @@ -699,6 +701,7 @@ authenticated.get( authenticated.get( "/org/:orgId/alert-rule/:alertRuleId", + verifyValidLicense, verifyOrgAccess, verifyUserHasAction(ActionsEnum.getAlertRule), alertRule.getAlertRule @@ -706,6 +709,7 @@ authenticated.get( authenticated.get( "/org/:orgId/health-checks", + verifyValidLicense, verifyOrgAccess, verifyUserHasAction(ActionsEnum.listHealthChecks), healthChecks.listHealthChecks @@ -738,3 +742,11 @@ authenticated.delete( logActionAudit(ActionsEnum.deleteHealthCheck), healthChecks.deleteHealthCheck ); + +authenticated.get( + "/org/:orgId/health-check/:healthCheckId/status-history", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getTarget), + healthChecks.getHealthCheckStatusHistory +); diff --git a/server/private/routers/healthChecks/getStatusHistory.ts b/server/private/routers/healthChecks/getStatusHistory.ts new file mode 100644 index 000000000..f010c8ed7 --- /dev/null +++ b/server/private/routers/healthChecks/getStatusHistory.ts @@ -0,0 +1,93 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, statusHistory } from "@server/db"; +import { and, eq, gte, asc } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { + computeBuckets, + statusHistoryQuerySchema, + StatusHistoryResponse +} from "@server/lib/statusHistory"; + +const healthCheckParamsSchema = z.object({ + healthCheckId: z.string().transform((v) => parseInt(v, 10)) +}); + +export async function getHealthCheckStatusHistory( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = healthCheckParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + const parsedQuery = statusHistoryQuerySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const entityType = "healthCheck"; + const entityId = parsedParams.data.healthCheckId + const { days } = parsedQuery.data; + + const nowSec = Math.floor(Date.now() / 1000); + const startSec = nowSec - days * 86400; + + const events = await db + .select() + .from(statusHistory) + .where( + and( + eq(statusHistory.entityType, entityType), + eq(statusHistory.entityId, entityId), + gte(statusHistory.timestamp, startSec) + ) + ) + .orderBy(asc(statusHistory.timestamp)); + + const { buckets, totalDowntime } = computeBuckets(events, days); + const totalWindow = days * 86400; + const overallUptime = + totalWindow > 0 + ? Math.max( + 0, + ((totalWindow - totalDowntime) / totalWindow) * 100 + ) + : 100; + + return response(res, { + data: { + entityType, + entityId, + days: buckets, + overallUptimePercent: Math.round(overallUptime * 100) / 100, + totalDowntimeSeconds: totalDowntime + }, + success: true, + error: false, + message: "Status history retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/healthChecks/index.ts b/server/private/routers/healthChecks/index.ts index 5f5c796f3..665ae5cca 100644 --- a/server/private/routers/healthChecks/index.ts +++ b/server/private/routers/healthChecks/index.ts @@ -15,3 +15,4 @@ export * from "./listHealthChecks"; export * from "./createHealthCheck"; export * from "./updateHealthCheck"; export * from "./deleteHealthCheck"; +export * from "./getStatusHistory"; diff --git a/server/routers/external.ts b/server/routers/external.ts index 7f9f2bdc4..34bbe4f88 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -292,13 +292,6 @@ authenticated.get( site.getSiteStatusHistory ); -authenticated.get( - "/target/:targetId/health-check/status-history", - verifyTargetAccess, - verifyUserHasAction(ActionsEnum.getTarget), - site.getHealthCheckStatusHistory -); - // Site Resource endpoints authenticated.put( "/org/:orgId/site-resource", diff --git a/server/routers/site/getStatusHistory.ts b/server/routers/site/getStatusHistory.ts index d17fa83a9..f1717c8a9 100644 --- a/server/routers/site/getStatusHistory.ts +++ b/server/routers/site/getStatusHistory.ts @@ -7,147 +7,16 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; +import { + computeBuckets, + statusHistoryQuerySchema, + StatusHistoryResponse +} from "@server/lib/statusHistory"; const siteParamsSchema = z.object({ - siteId: z.string().transform((v) => parseInt(v, 10)), + siteId: z.string().transform((v) => parseInt(v, 10)) }); -const healthCheckParamsSchema = z.object({ - targetHealthCheckId: z.string().transform((v) => parseInt(v, 10)), -}); - -const querySchema = z - .object({ - days: z - .string() - .optional() - .transform((v) => (v ? parseInt(v, 10) : 90)), - }) - .pipe( - z.object({ - days: z.number().int().min(1).max(365), - }) - ); - -export interface DayBucket { - date: string; // ISO date "YYYY-MM-DD" - uptimePercent: number; // 0-100 - totalDowntimeSeconds: number; - downtimeWindows: { start: number; end: number | null; status: string }[]; - status: "good" | "degraded" | "bad" | "no_data"; -} - -export interface StatusHistoryResponse { - entityType: string; - entityId: number; - days: DayBucket[]; - overallUptimePercent: number; - totalDowntimeSeconds: number; -} - -function computeBuckets( - events: { entityType: string; entityId: number; orgId: string; status: string; timestamp: number; id: number }[], - days: number -): { buckets: DayBucket[]; totalDowntime: number } { - const nowSec = Math.floor(Date.now() / 1000); - const buckets: DayBucket[] = []; - let totalDowntime = 0; - - for (let d = 0; d < days; d++) { - const dayStartSec = nowSec - (days - d) * 86400; - const dayEndSec = dayStartSec + 86400; - - const dayEvents = events.filter( - (e) => e.timestamp >= dayStartSec && e.timestamp < dayEndSec - ); - - // Determine the status at the start of this day (last event before dayStart) - const lastBeforeDay = [...events] - .filter((e) => e.timestamp < dayStartSec) - .at(-1); - - let currentStatus = lastBeforeDay?.status ?? null; - - const windows: { start: number; end: number | null; status: string }[] = []; - let dayDowntime = 0; - - let windowStart = dayStartSec; - let windowStatus = currentStatus; - - for (const evt of dayEvents) { - if (windowStatus !== null && windowStatus !== evt.status) { - const windowEnd = evt.timestamp; - const isDown = - windowStatus === "offline" || - windowStatus === "unhealthy" || - windowStatus === "unknown"; - if (isDown) { - dayDowntime += windowEnd - windowStart; - windows.push({ - start: windowStart, - end: windowEnd, - status: windowStatus, - }); - } - } - windowStart = evt.timestamp; - windowStatus = evt.status; - } - - // Close the final window at the end of the day (or now if day hasn't ended) - if (windowStatus !== null) { - const finalEnd = Math.min(dayEndSec, nowSec); - const isDown = - windowStatus === "offline" || - windowStatus === "unhealthy" || - windowStatus === "unknown"; - if (isDown && finalEnd > windowStart) { - dayDowntime += finalEnd - windowStart; - windows.push({ - start: windowStart, - end: finalEnd, - status: windowStatus, - }); - } - } - - totalDowntime += dayDowntime; - - const effectiveDayLength = Math.max( - 0, - Math.min(dayEndSec, nowSec) - dayStartSec - ); - const uptimePct = - effectiveDayLength > 0 - ? Math.max( - 0, - ((effectiveDayLength - dayDowntime) / - effectiveDayLength) * - 100 - ) - : 100; - - const dateStr = new Date(dayStartSec * 1000).toISOString().slice(0, 10); - - let status: DayBucket["status"] = "no_data"; - if (currentStatus !== null || dayEvents.length > 0) { - if (uptimePct >= 99) status = "good"; - else if (uptimePct >= 50) status = "degraded"; - else status = "bad"; - } - - buckets.push({ - date: dateStr, - uptimePercent: Math.round(uptimePct * 100) / 100, - totalDowntimeSeconds: dayDowntime, - downtimeWindows: windows, - status, - }); - } - - return { buckets, totalDowntime }; -} - export async function getSiteStatusHistory( req: Request, res: Response, @@ -163,7 +32,7 @@ export async function getSiteStatusHistory( ) ); } - const parsedQuery = querySchema.safeParse(req.query); + const parsedQuery = statusHistoryQuerySchema.safeParse(req.query); if (!parsedQuery.success) { return next( createHttpError( @@ -208,98 +77,17 @@ export async function getSiteStatusHistory( entityId, days: buckets, overallUptimePercent: Math.round(overallUptime * 100) / 100, - totalDowntimeSeconds: totalDowntime, + totalDowntimeSeconds: totalDowntime }, success: true, error: false, message: "Status history retrieved successfully", - status: HttpCode.OK, + status: HttpCode.OK }); } catch (error) { logger.error(error); return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred" - ) + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } } - -export async function getHealthCheckStatusHistory( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = healthCheckParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - const parsedQuery = querySchema.safeParse(req.query); - if (!parsedQuery.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedQuery.error).toString() - ) - ); - } - - const entityType = "healthCheck"; - const entityId = parsedParams.data.targetHealthCheckId; - const { days } = parsedQuery.data; - - const nowSec = Math.floor(Date.now() / 1000); - const startSec = nowSec - days * 86400; - - const events = await db - .select() - .from(statusHistory) - .where( - and( - eq(statusHistory.entityType, entityType), - eq(statusHistory.entityId, entityId), - gte(statusHistory.timestamp, startSec) - ) - ) - .orderBy(asc(statusHistory.timestamp)); - - const { buckets, totalDowntime } = computeBuckets(events, days); - const totalWindow = days * 86400; - const overallUptime = - totalWindow > 0 - ? Math.max( - 0, - ((totalWindow - totalDowntime) / totalWindow) * 100 - ) - : 100; - - return response(res, { - data: { - entityType, - entityId, - days: buckets, - overallUptimePercent: Math.round(overallUptime * 100) / 100, - totalDowntimeSeconds: totalDowntime, - }, - success: true, - error: false, - message: "Status history retrieved successfully", - status: HttpCode.OK, - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred" - ) - ); - } -} \ No newline at end of file diff --git a/src/components/HealthChecksTable.tsx b/src/components/HealthChecksTable.tsx index 81299fcb4..a314070d4 100644 --- a/src/components/HealthChecksTable.tsx +++ b/src/components/HealthChecksTable.tsx @@ -227,7 +227,7 @@ export default function HealthChecksTable({ header: () => Uptime (30d), cell: ({ row }) => { return ( - + ); } }, diff --git a/src/components/UptimeBar.tsx b/src/components/UptimeBar.tsx index be88a4a21..d2f29b760 100644 --- a/src/components/UptimeBar.tsx +++ b/src/components/UptimeBar.tsx @@ -45,16 +45,18 @@ const barColorClass: Record = { }; type UptimeBarProps = { + orgId?: string; siteId?: number; - targetId?: number; + healthCheckId?: number; days?: number; title?: string; className?: string; }; export default function UptimeBar({ + orgId, siteId, - targetId, + healthCheckId, days = 90, title, className @@ -68,8 +70,8 @@ export default function UptimeBar({ }); const hcQuery = useQuery({ - ...orgQueries.healthCheckStatusHistory({ targetId: targetId ?? 0, days }), - enabled: targetId != null && siteId == null, + ...orgQueries.healthCheckStatusHistory({ orgId: orgId ?? "", healthCheckId: healthCheckId ?? 0, days }), + enabled: healthCheckId != null && siteId == null, meta: { api } }); @@ -205,4 +207,4 @@ export default function UptimeBar({ ); -} \ No newline at end of file +} diff --git a/src/components/UptimeMiniBar.tsx b/src/components/UptimeMiniBar.tsx index b92a9d765..f668ac023 100644 --- a/src/components/UptimeMiniBar.tsx +++ b/src/components/UptimeMiniBar.tsx @@ -37,14 +37,16 @@ const barColorClass: Record = { }; type UptimeMiniBarProps = { + orgId?: string; siteId?: number; - targetId?: number; + healthCheckId?: number; days?: number; }; export default function UptimeMiniBar({ + orgId, siteId, - targetId, + healthCheckId, days = 30 }: UptimeMiniBarProps) { const api = createApiClient(useEnvContext()); @@ -56,8 +58,8 @@ export default function UptimeMiniBar({ }); const hcQuery = useQuery({ - ...orgQueries.healthCheckStatusHistory({ targetId: targetId ?? 0, days }), - enabled: targetId != null && siteId == null, + ...orgQueries.healthCheckStatusHistory({ orgId: orgId ?? "", healthCheckId: healthCheckId ?? 0, days }), + enabled: healthCheckId != null && siteId == null, meta: { api } }); @@ -125,4 +127,4 @@ export default function UptimeMiniBar({ ); -} \ No newline at end of file +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 5e1d6ea38..dbd1e0bfb 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -1,5 +1,4 @@ import { build } from "@server/build"; -import type { StatusHistoryResponse } from "@server/routers/site/getStatusHistory"; import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs"; import type { ListClientsResponse } from "@server/routers/client"; import type { ListDomainsResponse } from "@server/routers/domain"; @@ -29,6 +28,7 @@ import z from "zod"; import { remote } from "./api"; import { durationToMs } from "./durationToMs"; import { ListHealthChecksResponse } from "@server/routers/healthChecks/types"; +import { StatusHistoryResponse } from "@server/middlewares/statusHistory"; export type ProductUpdate = { link: string | null; @@ -306,7 +306,13 @@ export const orgQueries = { return res.data.data.healthChecks; } }), - siteStatusHistory: ({ siteId, days = 90 }: { siteId: number; days?: number }) => + siteStatusHistory: ({ + siteId, + days = 90 + }: { + siteId: number; + days?: number; + }) => queryOptions({ queryKey: ["SITE_STATUS_HISTORY", siteId, days] as const, queryFn: async ({ signal, meta }) => { @@ -314,21 +320,35 @@ export const orgQueries = { AxiosResponse >(`/site/${siteId}/status-history?days=${days}`, { signal }); return res.data.data; - }, - refetchInterval: 60_000, + } }), - healthCheckStatusHistory: ({ targetId, days = 90 }: { targetId: number; days?: number }) => + healthCheckStatusHistory: ({ + orgId, + healthCheckId, + days = 90 + }: { + orgId: string; + healthCheckId: number; + days?: number; + }) => queryOptions({ - queryKey: ["HC_STATUS_HISTORY", targetId, days] as const, + queryKey: [ + "HC_STATUS_HISTORY", + orgId, + healthCheckId, + days + ] as const, queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse - >(`/target/${targetId}/health-check/status-history?days=${days}`, { signal }); + >( + `/org/${orgId}/health-check/${healthCheckId}/status-history?days=${days}`, + { signal } + ); return res.data.data; - }, - refetchInterval: 60_000, - }), + } + }) }; export const logAnalyticsFiltersSchema = z.object({