diff --git a/client/package-lock.json b/client/package-lock.json
index 7f5397b7f..b4ad9f73c 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -26,7 +26,7 @@
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
- "pretty-ms": "9.3.0",
+ "pretty-ms": "^9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
"react-hook-form": "7.63.0",
@@ -38,7 +38,7 @@
"react-toastify": "^10.0.5",
"recharts": "2.15.2",
"redux-persist": "6.0.0",
- "swr": "2.3.6",
+ "swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0",
"zod": "4.1.11"
},
diff --git a/client/package.json b/client/package.json
index 7c5acb441..a5c17e343 100644
--- a/client/package.json
+++ b/client/package.json
@@ -31,7 +31,7 @@
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
- "pretty-ms": "9.3.0",
+ "pretty-ms": "^9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
"react-hook-form": "7.63.0",
diff --git a/client/src/Components/v2/design-elements/MonitorStatus.tsx b/client/src/Components/v2/design-elements/MonitorStatus.tsx
index 0ecb77df8..3d5e81845 100644
--- a/client/src/Components/v2/design-elements/MonitorStatus.tsx
+++ b/client/src/Components/v2/design-elements/MonitorStatus.tsx
@@ -34,7 +34,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
>
;
+
+export interface ConfigItem {
+ id: string;
+ text: string;
+ palette: PaletteColorKey;
+}
+
+const config: Record = {
+ seo: {
+ id: "seo",
+ text: "SEO",
+ palette: "primary",
+ },
+ performance: {
+ id: "performance",
+ text: "performance",
+ palette: "success",
+ },
+ bestPractices: {
+ id: "bestPractices",
+ text: "best practices",
+ palette: "warning",
+ },
+ accessibility: {
+ id: "accessibility",
+ text: "accessibility",
+ palette: "error",
+ },
+};
+
+export const HistogramPageSpeedDetails = ({
+ checks,
+ range,
+}: {
+ checks: Check[];
+ range: string;
+}) => {
+ const theme = useTheme();
+ return (
+
+ }
+ title="Score history"
+ >
+
+
+ (
+
+ )}
+ />
+
+ }
+ />
+
+ {Object.values(config).map(({ id, palette }) => {
+ const startColor = theme.palette[palette].main;
+ const endColor = theme.palette[palette].light;
+
+ return (
+
+
+
+
+ );
+ })}
+
+ {Object.keys(config).map((key) => {
+ const { palette } = config[key];
+ const strokeColor = theme.palette[palette].main;
+ const bgColor = theme.palette.primary.main;
+
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetailsTooltip.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetailsTooltip.tsx
new file mode 100644
index 000000000..7219ba821
--- /dev/null
+++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetailsTooltip.tsx
@@ -0,0 +1,78 @@
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import { formatDateWithTz } from "@/Utils/TimeUtils";
+import type { ConfigItem } from "@/Components/v2/monitors";
+import type { TooltipProps } from "recharts";
+import { useSelector } from "react-redux";
+import type { RootState } from "@/Types/state";
+
+interface HistogramPageSpeedScoresTooltipProps
+ extends Partial> {
+ config: Record;
+}
+export const HistogramPageSpeedScoresTooltip = ({
+ active,
+ payload,
+ label,
+ config,
+}: HistogramPageSpeedScoresTooltipProps) => {
+ const theme = useTheme();
+ const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
+
+ if (active && payload && payload.length) {
+ return (
+
+
+ {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
+
+ {Object.keys(config)
+ .reverse()
+ .map((key) => {
+ const { palette } = config[key];
+ const dotColor = theme.palette[palette].main;
+
+ return (
+
+
+
+ {config[key].text}
+
+ {Math.floor(payload[0].payload[key])}
+
+ );
+ })}
+
+ );
+ }
+ return null;
+};
diff --git a/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx b/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx
new file mode 100644
index 000000000..1c58bb3f7
--- /dev/null
+++ b/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx
@@ -0,0 +1,161 @@
+import { BaseChart } from "@/Components/v2/design-elements";
+import { FileText } from "lucide-react";
+import type { Check, CheckAudits } from "@/Types/Check";
+import { Pie, PieChart, ResponsiveContainer, Label } from "recharts";
+import Typography from "@mui/material/Typography";
+import Box from "@mui/material/Box";
+import { getPageSpeedPalette } from "@/Utils/MonitorUtils";
+import { useTheme } from "@mui/material/styles";
+import { alpha } from "@mui/material/styles";
+import { useState } from "react";
+import Tooltip from "@mui/material/Tooltip";
+import { useTranslation } from "react-i18next";
+
+const CenterLabel = ({ viewBox, value }: any) => {
+ const { cx, cy } = viewBox;
+ return (
+
+
+ {value}
+
+
+ );
+};
+
+export const PiePageSpeed = ({ latestCheck }: { latestCheck?: Check }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const [hoverTitle, setHoverTitle] = useState(null);
+
+ if (!latestCheck) return null;
+ const LABELS: Record = {
+ FCP: t("pages.pageSpeed.charts.common.fcp"),
+ SI: t("pages.pageSpeed.charts.common.si"),
+ LCP: t("pages.pageSpeed.charts.common.lcp"),
+ TBT: t("pages.pageSpeed.charts.common.tbt"),
+ CLS: t("pages.pageSpeed.charts.common.cls"),
+ };
+ const metrics: { key: keyof CheckAudits; color: string; weight: number }[] = [
+ { key: "fcp", color: alpha("#1DE9B6", 0.6), weight: 0.1 },
+ { key: "si", color: alpha("#7C4DFF", 0.6), weight: 0.1 },
+ { key: "lcp", color: alpha("#FFB200", 0.6), weight: 0.25 },
+ { key: "tbt", color: alpha("#00AFFE", 0.6), weight: 0.3 },
+ { key: "cls", color: alpha("#FF4181", 0.6), weight: 0.25 },
+ ];
+
+ const scores = metrics.flatMap(({ key, color, weight }) => {
+ const audit = latestCheck?.audits?.[key];
+ const val = Math.floor((audit?.score ?? 0) * 100);
+ const inverse = 100 - val;
+ return [
+ {
+ name: `${key.toUpperCase()} Inverse`,
+ value: inverse * weight,
+ fill: "transparent",
+ stroke: color,
+ weight,
+ },
+ {
+ name: key.toUpperCase(),
+ value: val * weight,
+ fill: color,
+ stroke: color,
+ weight,
+ },
+ ];
+ });
+
+ const totalScore =
+ (latestCheck.audits?.fcp?.score || 0) * 0.1 +
+ (latestCheck.audits?.si?.score || 0) * 0.1 +
+ (latestCheck.audits?.lcp?.score || 0) * 0.25 +
+ (latestCheck.audits?.tbt?.score || 0) * 0.3 +
+ (latestCheck.audits?.cls?.score || 0) * 0.25;
+ const pageSpeedPalette = getPageSpeedPalette(Math.floor(totalScore * 100));
+
+ const score = [
+ {
+ value: 100,
+ fill: alpha(theme.palette[pageSpeedPalette].light || "#ffffff", 0.6),
+ stroke: "none",
+ },
+ ];
+
+ return (
+
+ }
+ title={t("pages.pageSpeed.charts.pie.title")}
+ >
+
+
+
+
+
+ }
+ />
+
+ `${name}: ${Math.round(value)}`}
+ dataKey="value"
+ stroke="none"
+ onMouseEnter={(_, index: number) => {
+ const d = scores[index];
+ if (!d) return;
+ const isInverse = String(d.name).includes("Inverse");
+ const pair = isInverse && scores[index + 1] ? scores[index + 1] : d;
+ const base = String(pair.name).replace(" Inverse", "");
+ const full = LABELS[base] ?? base;
+ const displayVal = Math.round(Number(pair.value) || 0);
+ setHoverTitle(`${full}: ${displayVal}`);
+ }}
+ onMouseLeave={() => setHoverTitle(null)}
+ />
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx b/client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx
new file mode 100644
index 000000000..5d220ae4e
--- /dev/null
+++ b/client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx
@@ -0,0 +1,105 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import { BarChart3 } from "lucide-react";
+import { BaseChart } from "@/Components/v2/design-elements";
+import Typography from "@mui/material/Typography";
+
+import type { Check } from "@/Types/Check";
+import { useTranslation } from "react-i18next";
+import { getPageSpeedPalette } from "@/Utils/MonitorUtils";
+import { useTheme } from "@mui/material/styles";
+
+const MetricBox = ({
+ label,
+ value,
+ weight,
+}: {
+ label: string;
+ value: number;
+ weight: number;
+}) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const palette = getPageSpeedPalette(value);
+ return (
+
+
+ {label}
+
+ {`${value}%`}
+ {`${t("pages.pageSpeed.charts.legend.weight")}: ${weight}%`}
+
+
+
+
+ );
+};
+
+export const PiePageSpeedLegend = ({ latestCheck }: { latestCheck?: Check }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+
+ if (!latestCheck) {
+ return null;
+ }
+ return (
+
+ }
+ title={t("pages.pageSpeed.charts.legend.title")}
+ >
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx
index ca0aaa15e..4efaae65c 100644
--- a/client/src/Components/v2/monitors/index.tsx
+++ b/client/src/Components/v2/monitors/index.tsx
@@ -5,3 +5,7 @@ export * from "./charts/RadialAvgResponse";
export * from "./charts/HistogramDetails";
export * from "./charts/HistogramPageSpeed";
export * from "./charts/HistogramPageSpeedTooltip";
+export * from "./charts/PiePageSpeed";
+export * from "./charts/PiePageSpeedLegend";
+export * from "./charts/HistogramPageSpeedDetails";
+export * from "./charts/HistogramPageSpeedDetailsTooltip";
diff --git a/client/src/Pages/PageSpeed/Details/index.tsx b/client/src/Pages/PageSpeed/Details/index.tsx
new file mode 100644
index 000000000..2028f47c0
--- /dev/null
+++ b/client/src/Pages/PageSpeed/Details/index.tsx
@@ -0,0 +1,64 @@
+import { BasePage } from "@/Components/v2/design-elements";
+import type { PageSpeedDetailsResponse } from "@/Types/Monitor";
+import { HeaderMonitorControls } from "@/Components/v2/common";
+import Stack from "@mui/material/Stack";
+import {
+ HistogramPageSpeedDetails,
+ PiePageSpeed,
+ PiePageSpeedLegend,
+ MonitorStatBoxes,
+} from "@/Components/v2/monitors";
+
+import { useIsAdmin } from "@/Hooks/useIsAdmin";
+import { useGet } from "@/Hooks/UseApi";
+import { useParams } from "react-router-dom";
+import { useTheme } from "@mui/material";
+
+const PageSpeedDetails = () => {
+ const { monitorId } = useParams();
+ const isAdmin = useIsAdmin();
+ const theme = useTheme();
+ const {
+ data: monitorData,
+ isLoading,
+ error,
+ refetch,
+ } = useGet(
+ monitorId ? `/monitors/pagespeed/details/${monitorId}?dateRange=day` : null,
+ {},
+ { keepPreviousData: true, refreshInterval: 30000 }
+ );
+
+ const monitor = monitorData?.monitor;
+ const monitorStats = monitorData?.monitorStats || null;
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+export default PageSpeedDetails;
diff --git a/client/src/Pages/PageSpeed/Details/index.jsx b/client/src/Pages/PageSpeed/Details/old.jsx
similarity index 100%
rename from client/src/Pages/PageSpeed/Details/index.jsx
rename to client/src/Pages/PageSpeed/Details/old.jsx
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index c9a1084d6..45715a488 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -21,7 +21,7 @@ import UptimeCreate from "../Pages/Uptime/Create/index.jsx";
// PageSpeed
import PageSpeed from "../Pages/PageSpeed/Monitors/index";
-import PageSpeedDetails from "../Pages/PageSpeed/Details/index.jsx";
+import PageSpeedDetails from "../Pages/PageSpeed/Details/";
import PageSpeedCreate from "../Pages/PageSpeed/Create/index.jsx";
// Infrastructure
@@ -133,7 +133,13 @@ const Routes = () => {
/>
}
+ element={
+ <>
+
+
+
+ >
+ }
/>