Skip to content

Commit 412193e

Browse files
authored
feat(pulse): Add uptime overview and regional breakdown to pulse page
1 parent 8b998e6 commit 412193e

File tree

2 files changed

+255
-0
lines changed

2 files changed

+255
-0
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
"use client";
2+
3+
import {
4+
ArrowClockwiseIcon,
5+
ClockIcon,
6+
GlobeIcon,
7+
LockSimpleIcon,
8+
ShieldCheckIcon,
9+
} from "@phosphor-icons/react";
10+
import dayjs from "dayjs";
11+
import { useParams } from "next/navigation";
12+
import { useState } from "react";
13+
import { Button } from "@/components/ui/button";
14+
import { useManualInvoke } from "../../agent/_components/hooks/use-manual-invoke";
15+
16+
type OverviewData = {
17+
total_checks: number;
18+
successful_checks: number;
19+
failed_checks: number;
20+
pending_checks: number;
21+
uptime_percentage: number;
22+
avg_response_time: number;
23+
p50_response_time: number;
24+
p95_response_time: number;
25+
p99_response_time: number;
26+
max_response_time: number;
27+
min_response_time: number;
28+
avg_ttfb: number;
29+
ssl_expiry: string | null;
30+
ssl_valid: number;
31+
};
32+
33+
type RegionData = {
34+
region: string;
35+
total_checks: number;
36+
successful_checks: number;
37+
failed_checks: number;
38+
uptime_percentage: number;
39+
avg_response_time: number;
40+
p95_response_time: number;
41+
};
42+
43+
export function UptimeOverview() {
44+
const params = useParams();
45+
const websiteId = params.id as string;
46+
const { invoke, isLoading } = useManualInvoke();
47+
const [overviewData, setOverviewData] = useState<OverviewData | null>(null);
48+
const [regionData, setRegionData] = useState<RegionData[]>([]);
49+
const [error, setError] = useState<string | null>(null);
50+
const [lastFetched, setLastFetched] = useState<Date | null>(null);
51+
52+
const fetchOverview = async () => {
53+
setError(null);
54+
try {
55+
const [overviewResult, regionResult] = await Promise.all([
56+
invoke({
57+
tool: "uptime_overview",
58+
params: {
59+
from: dayjs().subtract(30, "day").format("YYYY-MM-DD"),
60+
to: dayjs().format("YYYY-MM-DD"),
61+
},
62+
}),
63+
invoke({
64+
tool: "uptime_by_region",
65+
params: {
66+
from: dayjs().subtract(30, "day").format("YYYY-MM-DD"),
67+
to: dayjs().format("YYYY-MM-DD"),
68+
},
69+
}),
70+
]);
71+
72+
if (overviewResult.success && overviewResult.data?.[0]) {
73+
setOverviewData(overviewResult.data[0] as OverviewData);
74+
}
75+
if (regionResult.success && regionResult.data) {
76+
setRegionData(regionResult.data as RegionData[]);
77+
}
78+
setLastFetched(new Date());
79+
} catch (err) {
80+
setError(err instanceof Error ? err.message : "Failed to fetch overview");
81+
}
82+
};
83+
84+
const formatMs = (ms: number) => {
85+
if (ms < 1000) return `${Math.round(ms)}ms`;
86+
return `${(ms / 1000).toFixed(2)}s`;
87+
};
88+
89+
return (
90+
<div className="border-b bg-sidebar">
91+
<div className="flex items-center justify-between border-b px-4 py-3">
92+
<div className="flex items-center gap-2">
93+
<h3 className="font-semibold text-lg text-sidebar-foreground tracking-tight">
94+
Uptime Overview
95+
</h3>
96+
{lastFetched && (
97+
<span className="text-muted-foreground text-xs">
98+
Last updated {dayjs(lastFetched).format("HH:mm:ss")}
99+
</span>
100+
)}
101+
</div>
102+
<Button
103+
disabled={isLoading}
104+
onClick={fetchOverview}
105+
size="sm"
106+
variant="outline"
107+
>
108+
<ArrowClockwiseIcon
109+
className={isLoading ? "animate-spin" : ""}
110+
size={16}
111+
/>
112+
{overviewData ? "Refresh" : "Load Stats"}
113+
</Button>
114+
</div>
115+
116+
{error && (
117+
<div className="border-b border-destructive/20 bg-destructive/10 px-4 py-2">
118+
<p className="text-destructive text-sm">{error}</p>
119+
</div>
120+
)}
121+
122+
{overviewData ? (
123+
<div className="p-4">
124+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
125+
{/* Uptime Percentage */}
126+
<div className="rounded-lg border bg-background p-3">
127+
<div className="flex items-center gap-2 text-muted-foreground text-xs">
128+
<ShieldCheckIcon size={14} />
129+
Uptime (30d)
130+
</div>
131+
<p
132+
className={`font-mono font-semibold text-2xl ${
133+
overviewData.uptime_percentage >= 99.9
134+
? "text-emerald-600"
135+
: overviewData.uptime_percentage >= 99
136+
? "text-amber-600"
137+
: "text-red-600"
138+
}`}
139+
>
140+
{overviewData.uptime_percentage.toFixed(2)}%
141+
</p>
142+
<p className="text-muted-foreground text-xs">
143+
{overviewData.successful_checks}/{overviewData.total_checks}{" "}
144+
checks passed
145+
</p>
146+
</div>
147+
148+
{/* Average Response Time */}
149+
<div className="rounded-lg border bg-background p-3">
150+
<div className="flex items-center gap-2 text-muted-foreground text-xs">
151+
<ClockIcon size={14} />
152+
Avg Response
153+
</div>
154+
<p className="font-mono font-semibold text-2xl">
155+
{formatMs(overviewData.avg_response_time)}
156+
</p>
157+
<p className="text-muted-foreground text-xs">
158+
p95: {formatMs(overviewData.p95_response_time)}
159+
</p>
160+
</div>
161+
162+
{/* TTFB */}
163+
<div className="rounded-lg border bg-background p-3">
164+
<div className="flex items-center gap-2 text-muted-foreground text-xs">
165+
<ClockIcon size={14} />
166+
Avg TTFB
167+
</div>
168+
<p className="font-mono font-semibold text-2xl">
169+
{formatMs(overviewData.avg_ttfb)}
170+
</p>
171+
<p className="text-muted-foreground text-xs">
172+
Time to first byte
173+
</p>
174+
</div>
175+
176+
{/* SSL Status */}
177+
<div className="rounded-lg border bg-background p-3">
178+
<div className="flex items-center gap-2 text-muted-foreground text-xs">
179+
<LockSimpleIcon size={14} />
180+
SSL Certificate
181+
</div>
182+
<p
183+
className={`font-semibold text-2xl ${
184+
overviewData.ssl_valid === 1
185+
? "text-emerald-600"
186+
: "text-red-600"
187+
}`}
188+
>
189+
{overviewData.ssl_valid === 1 ? "Valid" : "Invalid"}
190+
</p>
191+
{overviewData.ssl_expiry && (
192+
<p className="text-muted-foreground text-xs">
193+
Expires {dayjs(overviewData.ssl_expiry).format("MMM D, YYYY")}
194+
</p>
195+
)}
196+
</div>
197+
</div>
198+
199+
{/* Region breakdown */}
200+
{regionData.length > 0 && (
201+
<div className="mt-4">
202+
<h4 className="mb-2 flex items-center gap-2 font-medium text-muted-foreground text-sm">
203+
<GlobeIcon size={14} />
204+
Performance by Region
205+
</h4>
206+
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
207+
{regionData.map((region) => (
208+
<div
209+
className="flex items-center justify-between rounded border bg-background px-3 py-2"
210+
key={region.region}
211+
>
212+
<div>
213+
<span className="font-mono text-sm">
214+
{region.region || "Global"}
215+
</span>
216+
<span className="ml-2 text-muted-foreground text-xs">
217+
({region.total_checks} checks)
218+
</span>
219+
</div>
220+
<div className="text-right">
221+
<span
222+
className={`font-mono text-sm ${
223+
region.uptime_percentage >= 99.9
224+
? "text-emerald-600"
225+
: region.uptime_percentage >= 99
226+
? "text-amber-600"
227+
: "text-red-600"
228+
}`}
229+
>
230+
{region.uptime_percentage.toFixed(1)}%
231+
</span>
232+
<span className="ml-2 text-muted-foreground text-xs">
233+
{formatMs(region.avg_response_time)}
234+
</span>
235+
</div>
236+
</div>
237+
))}
238+
</div>
239+
</div>
240+
)}
241+
</div>
242+
) : (
243+
<div className="p-4 text-center">
244+
<p className="text-muted-foreground text-sm">
245+
Click &quot;Load Stats&quot; to fetch 30-day uptime overview and
246+
performance metrics.
247+
</p>
248+
</div>
249+
)}
250+
</div>
251+
);
252+
}

apps/dashboard/app/(main)/websites/[id]/pulse/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { WebsitePageHeader } from "../_components/website-page-header";
2323
import { MonitorDialog } from "./_components/monitor-dialog";
2424
import { RecentActivity } from "./_components/recent-activity";
2525
import { UptimeHeatmap } from "./_components/uptime-heatmap";
26+
import { UptimeOverview } from "./_components/uptime-overview";
2627

2728
dayjs.extend(relativeTime);
2829

@@ -298,6 +299,8 @@ export default function PulsePage() {
298299
</div>
299300
) : schedule ? (
300301
<>
302+
<UptimeOverview />
303+
301304
<div className="border-b bg-sidebar">
302305
<UptimeHeatmap
303306
data={heatmapData}

0 commit comments

Comments
 (0)