Skip to content

Commit 65b45cc

Browse files
committed
fix: downtime tracking
1 parent 505bcf5 commit 65b45cc

File tree

16 files changed

+755
-187
lines changed

16 files changed

+755
-187
lines changed

apps/api/src/query/builders/uptime.ts

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,31 +58,70 @@ export const UptimeBuilders: Record<string, SimpleQueryConfig> = {
5858
const granularity = _granularity ?? "hour";
5959
const timeGroup =
6060
granularity === "minute"
61-
? "toStartOfMinute(timestamp)"
61+
? "toStartOfMinute(ts)"
6262
: granularity === "hour"
63-
? "toStartOfHour(timestamp)"
63+
? "toStartOfHour(ts)"
6464
: granularity === "day"
65-
? "toDate(timestamp)"
66-
: "toStartOfHour(timestamp)";
65+
? "toDate(ts)"
66+
: "toStartOfHour(ts)";
67+
68+
const windowSec =
69+
granularity === "day" ? 86_400 : granularity === "hour" ? 3600 : 60;
70+
71+
const uptimePercentageExpr =
72+
granularity === "minute"
73+
? "if(total_checks = 0, 0, round(100 * successful_checks / total_checks, 2))"
74+
: `round(100 * (1 - least(downtime_seconds, ${windowSec}) / ${windowSec}), 2)`;
6775

6876
return {
6977
sql: `
7078
SELECT
71-
${timeGroup} as date,
72-
if((countIf(status = 1) + countIf(status = 0)) = 0, 0, round((countIf(status = 1) / (countIf(status = 1) + countIf(status = 0))) * 100, 2)) as uptime_percentage,
73-
avg(total_ms) as avg_response_time,
74-
quantile(0.50)(total_ms) as p50_response_time,
75-
quantile(0.95)(total_ms) as p95_response_time,
76-
max(total_ms) as max_response_time,
77-
avg(ttfb_ms) as avg_ttfb,
78-
quantile(0.50)(ttfb_ms) as p50_ttfb,
79-
quantile(0.95)(ttfb_ms) as p95_ttfb
80-
FROM ${UPTIME_TABLE}
81-
WHERE
82-
site_id = {websiteId:String}
83-
AND timestamp >= toDateTime({startDate:String})
84-
AND timestamp <= toDateTime(concat({endDate:String}, ' 23:59:59'))
85-
GROUP BY date
79+
date,
80+
${uptimePercentageExpr} as uptime_percentage,
81+
total_checks,
82+
successful_checks,
83+
downtime_seconds,
84+
avg_response_time,
85+
p50_response_time,
86+
p95_response_time,
87+
max_response_time,
88+
avg_ttfb,
89+
p50_ttfb,
90+
p95_ttfb
91+
FROM (
92+
SELECT
93+
${timeGroup} as date,
94+
toUInt32(countIf(status = 1) + countIf(status = 0)) as total_checks,
95+
toUInt32(countIf(status = 1)) as successful_checks,
96+
toUInt32(sumIf(
97+
least(dateDiff('second', ts, next_ts), 86400),
98+
status = 0
99+
)) as downtime_seconds,
100+
avg(total_ms) as avg_response_time,
101+
quantile(0.50)(total_ms) as p50_response_time,
102+
quantile(0.95)(total_ms) as p95_response_time,
103+
max(total_ms) as max_response_time,
104+
avg(ttfb_ms) as avg_ttfb,
105+
quantile(0.50)(ttfb_ms) as p50_ttfb,
106+
quantile(0.95)(ttfb_ms) as p95_ttfb
107+
FROM (
108+
SELECT
109+
timestamp as ts,
110+
status,
111+
total_ms,
112+
ttfb_ms,
113+
leadInFrame(timestamp, 1, now()) OVER (
114+
ORDER BY timestamp ASC
115+
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
116+
) as next_ts
117+
FROM ${UPTIME_TABLE}
118+
WHERE
119+
site_id = {websiteId:String}
120+
AND timestamp >= toDateTime({startDate:String})
121+
AND timestamp <= toDateTime(concat({endDate:String}, ' 23:59:59'))
122+
)
123+
GROUP BY date
124+
)
86125
ORDER BY date ASC
87126
`,
88127
params: { websiteId, startDate, endDate },

apps/dashboard/app/(main)/monitors/status-pages/page.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,9 @@ export default function StatusPagesListPage() {
3333
useOrganizationsContext();
3434
const queryClient = useQueryClient();
3535
const [isSheetOpen, setIsSheetOpen] = useState(false);
36-
const [editingStatusPage, setEditingStatusPage] = useState<{
37-
id: string;
38-
name: string;
39-
slug: string;
40-
description: string | null;
41-
} | null>(null);
36+
const [editingStatusPage, setEditingStatusPage] = useState<StatusPage | null>(
37+
null
38+
);
4239
const [statusPageToDelete, setStatusPageToDelete] =
4340
useState<StatusPage | null>(null);
4441

@@ -68,12 +65,7 @@ export default function StatusPagesListPage() {
6865
};
6966

7067
const handleEdit = (statusPage: StatusPage) => {
71-
setEditingStatusPage({
72-
id: statusPage.id,
73-
name: statusPage.name,
74-
slug: statusPage.slug,
75-
description: statusPage.description,
76-
});
68+
setEditingStatusPage(statusPage);
7769
setIsSheetOpen(true);
7870
};
7971

apps/dashboard/app/status/[slug]/_components/status-navbar.tsx

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,70 @@
11
import { ThemeToggle } from "@/components/layout/theme-toggle";
2+
import { LifebuoyIcon } from "@phosphor-icons/react/ssr";
3+
import Image from "next/image";
4+
5+
interface StatusNavbarProps {
6+
name?: string;
7+
logoUrl?: string | null;
8+
websiteUrl?: string | null;
9+
supportUrl?: string | null;
10+
}
11+
12+
export function StatusNavbar({
13+
name,
14+
logoUrl,
15+
websiteUrl,
16+
supportUrl,
17+
}: StatusNavbarProps) {
18+
const displayName = name ?? "System Status";
19+
20+
const brandContent = (
21+
<span className="flex items-center gap-2">
22+
{logoUrl ? (
23+
<Image
24+
alt=""
25+
className="shrink-0 rounded object-contain"
26+
height={20}
27+
src={logoUrl}
28+
unoptimized
29+
width={20}
30+
/>
31+
) : null}
32+
<span className="font-medium text-foreground text-sm tracking-tight">
33+
{displayName}
34+
</span>
35+
</span>
36+
);
237

3-
export function StatusNavbar() {
438
return (
539
<div className="sticky top-0 z-30 border-b bg-background/80 backdrop-blur-lg">
640
<nav className="mx-auto flex h-12 max-w-2xl items-center justify-between px-4 sm:px-6">
7-
<p className="font-medium text-foreground text-sm tracking-tight">
8-
System Status
9-
</p>
41+
{websiteUrl ? (
42+
<a
43+
className="transition-opacity hover:opacity-80"
44+
href={websiteUrl}
45+
rel="noopener noreferrer"
46+
target="_blank"
47+
>
48+
{brandContent}
49+
</a>
50+
) : (
51+
brandContent
52+
)}
1053

11-
<ThemeToggle className="flex" />
54+
<div className="flex items-center gap-1">
55+
{supportUrl ? (
56+
<a
57+
className="flex items-center gap-1.5 rounded px-2 py-1 text-muted-foreground text-xs transition-colors hover:text-foreground"
58+
href={supportUrl}
59+
rel="noopener noreferrer"
60+
target="_blank"
61+
>
62+
<LifebuoyIcon className="size-3.5" weight="duotone" />
63+
<span className="hidden sm:inline">Get Support</span>
64+
</a>
65+
) : null}
66+
<ThemeToggle className="flex" />
67+
</div>
1268
</nav>
1369
</div>
1470
);

apps/dashboard/app/status/[slug]/_components/status-page.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
WarningCircleIcon,
55
XCircleIcon,
66
} from "@phosphor-icons/react/ssr";
7+
import Image from "next/image";
78
import type { ReactNode } from "react";
89
import { cn } from "@/lib/utils";
910
import { LastChecked } from "./last-checked";
@@ -29,25 +30,54 @@ function StatusRoot({ children, className }: StatusRootProps) {
2930
interface StatusHeaderProps {
3031
name: string;
3132
description?: string;
33+
logoUrl?: string | null;
34+
websiteUrl?: string | null;
3235
children?: ReactNode;
3336
className?: string;
3437
}
3538

3639
function StatusHeader({
3740
name,
3841
description = "System status and uptime",
42+
logoUrl,
43+
websiteUrl,
3944
children,
4045
className,
4146
}: StatusHeaderProps) {
47+
const heading = (
48+
<h1 className="text-balance font-semibold text-2xl tracking-tight">
49+
{name}
50+
</h1>
51+
);
52+
4253
return (
4354
<div
4455
className={cn("flex items-center gap-3.5", className)}
4556
data-slot="status-header"
4657
>
47-
<div>
48-
<h1 className="text-balance font-semibold text-2xl tracking-tight">
49-
{name}
50-
</h1>
58+
{logoUrl ? (
59+
<Image
60+
alt=""
61+
className="size-10 shrink-0 rounded object-contain"
62+
height={40}
63+
src={logoUrl}
64+
unoptimized
65+
width={40}
66+
/>
67+
) : null}
68+
<div className="min-w-0 flex-1">
69+
{websiteUrl ? (
70+
<a
71+
className="transition-opacity hover:opacity-80"
72+
href={websiteUrl}
73+
rel="noopener noreferrer"
74+
target="_blank"
75+
>
76+
{heading}
77+
</a>
78+
) : (
79+
heading
80+
)}
5181
<p className="mt-0.5 text-pretty text-muted-foreground text-sm">
5282
{description}
5383
</p>
Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import type { Metadata } from "next";
2-
import { ThemeProvider } from "next-themes";
31
import { TooltipProvider } from "@/components/ui/tooltip";
4-
import { StatusNavbar } from "./_components/status-navbar";
2+
import type { Metadata } from "next";
53

64
export const metadata: Metadata = {
75
title: {
@@ -20,35 +18,5 @@ export default function StatusLayout({
2018
}: {
2119
children: React.ReactNode;
2220
}) {
23-
return (
24-
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
25-
<TooltipProvider>
26-
<div className="flex h-dvh flex-col overflow-hidden bg-background">
27-
<StatusNavbar />
28-
29-
<main className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
30-
<div className="mx-auto max-w-2xl px-4 py-8 sm:px-6">
31-
{children}
32-
</div>
33-
</main>
34-
35-
<footer className="shrink-0 border-t bg-background">
36-
<div className="mx-auto flex max-w-2xl items-center justify-center px-4 py-3 sm:px-6">
37-
<p className="text-muted-foreground/60 text-xs">
38-
Powered by{" "}
39-
<a
40-
className="text-muted-foreground transition-colors hover:text-foreground"
41-
href="https://www.databuddy.cc"
42-
rel="noopener noreferrer dofollow"
43-
target="_blank"
44-
>
45-
Databuddy
46-
</a>
47-
</p>
48-
</div>
49-
</footer>
50-
</div>
51-
</TooltipProvider>
52-
</ThemeProvider>
53-
);
21+
return <TooltipProvider>{children}</TooltipProvider>;
5422
}

apps/dashboard/app/status/[slug]/loading.tsx

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,41 @@ function MonitorRowSkeleton() {
2626

2727
export default function StatusLoading() {
2828
return (
29-
<div className="space-y-6">
30-
<div>
31-
<Skeleton className="h-7 w-48 rounded" />
32-
<Skeleton className="mt-2 h-4 w-40 rounded" />
29+
<div className="flex h-dvh flex-col overflow-hidden bg-background">
30+
<div className="sticky top-0 z-30 border-b bg-background/80 backdrop-blur-lg">
31+
<nav className="mx-auto flex h-12 max-w-2xl items-center justify-between px-4 sm:px-6">
32+
<div className="flex items-center gap-2">
33+
<Skeleton className="size-5 rounded" />
34+
<Skeleton className="h-4 w-28 rounded" />
35+
</div>
36+
<Skeleton className="size-8 rounded" />
37+
</nav>
3338
</div>
3439

35-
<Skeleton className="h-14 w-full rounded" />
40+
<main className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
41+
<div className="mx-auto max-w-2xl space-y-6 px-4 py-8 sm:px-6">
42+
<div>
43+
<Skeleton className="h-7 w-48 rounded" />
44+
<Skeleton className="mt-2 h-4 w-40 rounded" />
45+
</div>
46+
47+
<Skeleton className="h-14 w-full rounded" />
3648

37-
<div className="space-y-3">
38-
<MonitorRowSkeleton />
39-
<MonitorRowSkeleton />
40-
<MonitorRowSkeleton />
41-
</div>
49+
<div className="space-y-3">
50+
<MonitorRowSkeleton />
51+
<MonitorRowSkeleton />
52+
<MonitorRowSkeleton />
53+
</div>
54+
55+
<Skeleton className="h-3.5 w-52 rounded" />
56+
</div>
57+
</main>
4258

43-
<Skeleton className="h-3.5 w-52 rounded" />
59+
<footer className="shrink-0 border-t bg-background">
60+
<div className="mx-auto flex max-w-2xl items-center justify-center px-4 py-3 sm:px-6">
61+
<Skeleton className="h-3 w-36 rounded" />
62+
</div>
63+
</footer>
4464
</div>
4565
);
4666
}

0 commit comments

Comments
 (0)