diff --git a/server/lib/statusHistory.ts b/server/lib/statusHistory.ts index 315c776c2..f116d74e2 100644 --- a/server/lib/statusHistory.ts +++ b/server/lib/statusHistory.ts @@ -37,10 +37,11 @@ export async function getCachedStatusHistory( tzOffsetMinutes ); const cached = await cache.get(cacheKey); - if (cached !== undefined) { - return cached; - } + // if (cached !== undefined) { + // return cached; + // } + console.time(`[getCachedStatusHistory/${entityType}=${entityId}]`); // Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query // window aligns with stable calendar days for the requesting client const todayMidnightSec = localMidnightSec(tzOffsetMinutes); @@ -76,12 +77,14 @@ export async function getCachedStatusHistory( const priorStatus = lastKnownEvent?.status ?? null; + console.time(`[computeBuckets/${entityType}=${entityId}]`); const { buckets, totalDowntime } = computeBuckets( events, days, priorStatus, tzOffsetMinutes ); + console.timeEnd(`[computeBuckets/${entityType}=${entityId}]`); const totalWindow = days * 86400; const overallUptime = totalWindow > 0 @@ -96,6 +99,7 @@ export async function getCachedStatusHistory( totalDowntimeSeconds: totalDowntime }; + console.timeEnd(`[getCachedStatusHistory/${entityType}=${entityId}]`); await cache.set(cacheKey, result, STATUS_HISTORY_CACHE_TTL); return result; } @@ -299,7 +303,6 @@ export function computeBuckets( status }); } - return { buckets, totalDowntime }; } @@ -311,7 +314,8 @@ export type BatchedStatusHistoryResponse = Record< export async function getBatchedStatusHistory( entityType: string, entityIds: number[], - days: number + days: number, + tzOffsetMinutes: number = 0 ): Promise { // const cacheKey = statusHistoryCacheKey(entityType, entityId, days); // const cached = await cache.get(cacheKey); @@ -319,10 +323,13 @@ export async function getBatchedStatusHistory( // return cached; // } - // Anchor to UTC midnight so the query window aligns with stable calendar days - const utcToday = new Date(); - utcToday.setUTCHours(0, 0, 0, 0); - const todayMidnightSec = Math.floor(utcToday.getTime() / 1000); + console.time( + `[getBatchedStatusHistory/${entityType}=(${entityIds.join(" ,")})]` + ); + + // Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query + // window aligns with stable calendar days for the requesting client + const todayMidnightSec = localMidnightSec(tzOffsetMinutes); const startSec = todayMidnightSec - days * 86400; const events = await logsDb @@ -337,6 +344,10 @@ export async function getBatchedStatusHistory( ) .orderBy(asc(statusHistory.timestamp)); + console.log({ + events + }); + // Fetch the last known state before the window so that entities that // haven't changed status recently still show the correct status rather // than appearing as "no_data". @@ -375,6 +386,7 @@ export async function getBatchedStatusHistory( const result: BatchedStatusHistoryResponse = {}; + console.time(`[computeBuckets/${entityType}=(${entityIds.join(" ,")})]`); for (const entityId in eventStatusMap) { const event = eventStatusMap[Number(entityId)]; const priorStatus = event.lastKnownEvent?.status ?? null; @@ -401,7 +413,11 @@ export async function getBatchedStatusHistory( totalDowntimeSeconds: totalDowntime }; } + console.timeEnd(`[computeBuckets/${entityType}=(${entityIds.join(" ,")})]`); + console.timeEnd( + `[getBatchedStatusHistory/${entityType}=(${entityIds.join(" ,")})]` + ); // await cache.set(cacheKey, result, STATUS_HISTORY_CACHE_TTL); return result; } diff --git a/server/routers/external.ts b/server/routers/external.ts index 62ca15316..e6d487f56 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -319,6 +319,13 @@ authenticated.get( site.getSiteStatusHistory ); +authenticated.get( + "/org/:orgId/site-status-histories", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listSites), + site.getBatchedSiteStatusHistory +); + // Site Resource endpoints authenticated.put( "/org/:orgId/site-resource", diff --git a/server/routers/site/getBatchedStatusHistory.ts b/server/routers/site/getBatchedStatusHistory.ts index 507a556d4..f266c2840 100644 --- a/server/routers/site/getBatchedStatusHistory.ts +++ b/server/routers/site/getBatchedStatusHistory.ts @@ -1,15 +1,14 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; 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 { - getCachedStatusHistory, - statusHistoryQuerySchema, - StatusHistoryResponse + getBatchedStatusHistory, + type BatchedStatusHistoryResponse } from "@server/lib/statusHistory"; +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; const siteIdParamsSchema = z.object({ days: z @@ -59,12 +58,16 @@ export async function getBatchedSiteStatusHistory( } const entityType = "site"; - const entityId = parsedParams.data.siteId; - const { days } = parsedQuery.data; + const { days, siteIds, tzOffsetMinutes } = parsedQuery.data; - const data = await getCachedStatusHistory(entityType, entityId, days); + const data = await getBatchedStatusHistory( + entityType, + siteIds, + days, + tzOffsetMinutes + ); - return response(res, { + return response(res, { data, success: true, error: false, diff --git a/server/routers/site/index.ts b/server/routers/site/index.ts index c0c399789..7d5bcb698 100644 --- a/server/routers/site/index.ts +++ b/server/routers/site/index.ts @@ -1,5 +1,6 @@ export * from "./getSite"; export * from "./getStatusHistory"; +export * from "./getBatchedStatusHistory"; export * from "./createSite"; export * from "./deleteSite"; export * from "./updateSite"; diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 95d63cf9a..1f745269c 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -1,7 +1,7 @@ "use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import UptimeMiniBar from "@app/components/UptimeMiniBar"; +import { UptimeMiniBar } from "@app/components/UptimeMiniBar"; import { Credenza, @@ -56,8 +56,9 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; import { LabelsTableCell } from "./LabelsTableCell"; import { useQuery } from "@tanstack/react-query"; -import { productUpdatesQueries } from "@app/lib/queries"; +import { orgQueries, productUpdatesQueries } from "@app/lib/queries"; import semver from "semver"; +import { durationToMs } from "@app/lib/durationToMs"; export type SiteRow = { id: number; @@ -89,6 +90,8 @@ type SitesTableProps = { rowCount: number; }; +const SITE_STATUS_HISTORY_DAYS = 30; + export default function SitesTable({ sites, orgId, @@ -112,7 +115,14 @@ export default function SitesTable({ const [isRefreshing, startTransition] = useTransition(); const [isNavigatingToAddPage, startNavigation] = useTransition(); - const { isPaidUser } = usePaidStatus(); + const statusHistoryQuery = useQuery({ + ...orgQueries.batchedSiteStatusHistory({ + orgId, + siteIds: sites.map((s) => s.id), + days: SITE_STATUS_HISTORY_DAYS + }), + staleTime: durationToMs(5, "seconds") + }); const api = createApiClient(useEnvContext()); const t = useTranslations(); @@ -296,7 +306,14 @@ export default function SitesTable({ if (originalRow.type == "local") { return -; } - return ; + const data = statusHistoryQuery.data?.[row.original.id]; + return ( + + ); } }, { @@ -361,12 +378,9 @@ export default function SitesTable({ let updateAvailable = Boolean( latestNewtVersion && - originalRow.newtVersion && - semver.valid(originalRow.newtVersion) && - semver.lt( - originalRow.newtVersion, - latestNewtVersion - ) + originalRow.newtVersion && + semver.valid(originalRow.newtVersion) && + semver.lt(originalRow.newtVersion, latestNewtVersion) ); if (originalRow.type === "newt") { diff --git a/src/components/UptimeMiniBar.tsx b/src/components/UptimeMiniBar.tsx index b7e684c8b..454f44d66 100644 --- a/src/components/UptimeMiniBar.tsx +++ b/src/components/UptimeMiniBar.tsx @@ -1,15 +1,14 @@ "use client"; -import { useQuery } from "@tanstack/react-query"; -import { orgQueries } from "@app/lib/queries"; import { Tooltip, TooltipContent, TooltipTrigger } from "@app/components/ui/tooltip"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { createApiClient } from "@app/lib/api"; import { cn } from "@app/lib/cn"; +import { orgQueries } from "@app/lib/queries"; +import type { StatusHistoryResponse } from "@server/lib/statusHistory"; +import { useQuery } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; function formatDuration(seconds: number): string { @@ -46,21 +45,16 @@ type UptimeMiniBarProps = { days?: number; }; -export default function UptimeMiniBar({ +export default function UptimeMiniBarWrapper({ orgId, siteId, resourceId, healthCheckId, days = 30 }: UptimeMiniBarProps) { - const t = useTranslations(); - const api = createApiClient(useEnvContext()); - const siteQuery = useQuery({ ...orgQueries.siteStatusHistory({ siteId: siteId ?? 0, days }), - enabled: siteId != null, - meta: { api }, - staleTime: 5 * 60 * 1000 + enabled: siteId != null }); const hcQuery = useQuery({ @@ -69,16 +63,12 @@ export default function UptimeMiniBar({ healthCheckId: healthCheckId ?? 0, days }), - enabled: healthCheckId != null && siteId == null && resourceId == null, - meta: { api }, - staleTime: 5 * 60 * 1000 + enabled: healthCheckId != null && siteId == null && resourceId == null }); const resourceQuery = useQuery({ ...orgQueries.resourceStatusHistory({ resourceId, days }), - enabled: resourceId != null && siteId == null && healthCheckId == null, - meta: { api }, - staleTime: 5 * 60 * 1000 + enabled: resourceId != null && siteId == null && healthCheckId == null }); const { data, isLoading } = @@ -88,6 +78,22 @@ export default function UptimeMiniBar({ ? resourceQuery : hcQuery; + return ; +} + +type UptimeMiniBarUIProps = { + data?: StatusHistoryResponse; + isLoading?: boolean; + days?: number; +}; + +export function UptimeMiniBar({ + data, + isLoading, + days = 30 +}: UptimeMiniBarUIProps) { + const t = useTranslations(); + if (isLoading) { return (
@@ -138,7 +144,8 @@ export default function UptimeMiniBar({ {formatDate(day.date)}
- {day.status === "no_data" || day.status === "unknown" + {day.status === "no_data" || + day.status === "unknown" ? t("uptimeNoData") : `${day.uptimePercent.toFixed(1)}% ${t("uptimeSuffix")}`}
@@ -159,4 +166,4 @@ export default function UptimeMiniBar({ ); -} \ No newline at end of file +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index b552fd26c..a56271991 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -47,7 +47,10 @@ import { remote } from "./api"; import { durationToMs } from "./durationToMs"; import type { ListOrgLabelsResponse } from "@server/routers/labels/types"; import { ListHealthChecksResponse } from "@server/routers/healthChecks/types"; -import { StatusHistoryResponse } from "@server/lib/statusHistory"; +import { + StatusHistoryResponse, + type BatchedStatusHistoryResponse +} from "@server/lib/statusHistory"; import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; import type { GetResourcePolicyResponse } from "@server/routers/policy"; import type { @@ -642,24 +645,33 @@ export const orgQueries = { }), batchedSiteStatusHistory: ({ siteIds, + orgId, days = 90 }: { + orgId: string; siteIds: number[]; days?: number; }) => queryOptions({ queryKey: [ - "SITE_STATUS_HISTORY", - "BATCHED", + "ORG", + orgId, + "BATCHED_SITE_STATUS_HISTORY", siteIds, days ] as const, queryFn: async ({ signal, meta }) => { - // TODO - // const res = await meta!.api.get< - // AxiosResponse - // >(`/site/${siteId}/status-history?days=${days}`, { signal }); - // return res.data.data; + const sp = new URLSearchParams([ + ["days", days.toString()], + ...siteIds.map((id) => ["siteIds", id.toString()]) + ]); + + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/site-status-histories?${sp.toString()}`, { + signal + }); + return res.data.data; } }), siteStatusHistory: ({ @@ -671,6 +683,7 @@ export const orgQueries = { }) => queryOptions({ queryKey: ["SITE_STATUS_HISTORY", siteId, days] as const, + staleTime: durationToMs(5, "seconds"), queryFn: async ({ signal, meta }) => { const tzOffsetMinutes = -new Date().getTimezoneOffset(); const res = await meta!.api.get< @@ -692,6 +705,7 @@ export const orgQueries = { }) => queryOptions({ queryKey: ["RESOURCE_STATUS_HISTORY", resourceId, days] as const, + staleTime: durationToMs(5, "seconds"), queryFn: async ({ signal, meta }) => { const tzOffsetMinutes = -new Date().getTimezoneOffset(); const res = await meta!.api.get< @@ -714,6 +728,7 @@ export const orgQueries = { days?: number; }) => queryOptions({ + staleTime: durationToMs(5, "seconds"), queryKey: [ "HC_STATUS_HISTORY", orgId,