diff --git a/client/src/Components/design-elements/Gauge.tsx b/client/src/Components/design-elements/Gauge.tsx index fda42df3c..ff653489f 100644 --- a/client/src/Components/design-elements/Gauge.tsx +++ b/client/src/Components/design-elements/Gauge.tsx @@ -17,6 +17,7 @@ export const Gauge = ({ strokeWidth = 15, precision = 1, unit = "%", + colorFn, }: { isLoading?: boolean; progress?: number; @@ -24,6 +25,7 @@ export const Gauge = ({ strokeWidth?: number; precision?: number; unit?: string; + colorFn?: (val: number, theme: any) => string; }) => { const theme = useTheme(); const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE)); @@ -48,7 +50,8 @@ export const Gauge = ({ return () => clearTimeout(timer); }, [progress, circumference, strokeLength]); - const fillColor = getInfraGaugeColor(progressWithinRange, theme); + const resolvedColorFn = colorFn ?? getInfraGaugeColor; + const fillColor = resolvedColorFn(progressWithinRange, theme); if (isLoading) { return; diff --git a/client/src/Hooks/useStatusPageForm.ts b/client/src/Hooks/useStatusPageForm.ts index 0011ea761..64936dbf3 100644 --- a/client/src/Hooks/useStatusPageForm.ts +++ b/client/src/Hooks/useStatusPageForm.ts @@ -35,6 +35,7 @@ export const useStatusPageForm = ({ showUptimePercentage: data?.showUptimePercentage ?? true, showAdminLoginLink: data?.showAdminLoginLink ?? false, showInfrastructure: data?.showInfrastructure ?? false, + showPageSpeed: data?.showPageSpeed ?? false, customCSS: data?.customCSS || "", logo: transformLogo(data?.logo), }; diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index f09d9e47d..f8bef618d 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -37,9 +37,17 @@ import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusContr const monitorsUrl = (() => { const params = new URLSearchParams(); - ["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach( - (type) => params.append("type", type) - ); + [ + "http", + "ping", + "port", + "docker", + "game", + "grpc", + "websocket", + "hardware", + "pagespeed", + ].forEach((type) => params.append("type", type)); return `/monitors/team?${params.toString()}`; })(); @@ -59,7 +67,9 @@ const CreateStatusPage = () => { // Fetch existing status page data when configuring const { data: statusPageData, isLoading: isLoadingStatusPage } = useGet( - isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure` + isCreate + ? null + : `/status-page/${url}?type=uptime&type=infrastructure&type=pagespeed` ); const { data: monitorsResponse } = useGet(monitorsUrl); @@ -98,7 +108,9 @@ const CreateStatusPage = () => { const typesSet = new Set(); selectedMonitors.forEach((m) => { - typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime"); + if (m.type === "hardware") typesSet.add("infrastructure"); + else if (m.type === "pagespeed") typesSet.add("pagespeed"); + else typesSet.add("uptime"); }); return typesSet.size ? Array.from(typesSet) : ["uptime"]; @@ -139,6 +151,7 @@ const CreateStatusPage = () => { fd.append("showUptimePercentage", String(data.showUptimePercentage)); fd.append("showAdminLoginLink", String(data.showAdminLoginLink)); fd.append("showInfrastructure", String(data.showInfrastructure)); + fd.append("showPageSpeed", String(data.showPageSpeed)); data.monitors.forEach((monitorId) => { fd.append("monitors[]", monitorId); @@ -475,6 +488,21 @@ const CreateStatusPage = () => { /> )} /> + ( + + } + label={t("pages.statusPages.form.features.option.showPageSpeed.label")} + /> + )} + /> {/* { + const monitorColors = { + hardware: theme.palette.info.light, + pagespeed: theme.palette.warning.light, + }; + const bg = - monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light; + monitorColors[monitorType as keyof typeof monitorColors] || + theme.palette.success.light; return { backgroundColor: bg, color: theme.palette.background.paper, @@ -105,6 +112,11 @@ const MonitorContent = ({ return ; } + if (monitor.type === "pagespeed") { + if (statusPage.showPageSpeed === false) return null; + return ; + } + if (statusPage.showCharts === false) return null; const checks = monitor.checks?.slice().reverse() ?? []; diff --git a/client/src/Pages/StatusPage/Status/Components/PageSpeedMetrics.tsx b/client/src/Pages/StatusPage/Status/Components/PageSpeedMetrics.tsx new file mode 100644 index 000000000..2693ca751 --- /dev/null +++ b/client/src/Pages/StatusPage/Status/Components/PageSpeedMetrics.tsx @@ -0,0 +1,148 @@ +import Typography from "@mui/material/Typography"; +import { Gauge } from "@/Components/design-elements"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import type { Monitor } from "@/Types/Monitor"; +import Grid from "@mui/material/Grid"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import Box from "@mui/material/Box"; +import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; +import { getPageSpeedGaugeColor } from "@/Utils/MonitorUtils"; + +const GAUGE_RADIUS = 60; +const GAUGE_STROKE_WIDTH = 12; + +interface StatusPageMonitor extends Monitor { + checks?: Monitor["recentChecks"]; +} + +interface MetricConfig { + key: string; + label: string; + progress: number; +} + +const MetricItem = ({ label, progress }: MetricConfig) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); + return ( + + + + {label} + + + ); +}; + +const buildMetrics = ( + check: NonNullable[number], + t: (key: string) => string +): MetricConfig[] => { + const metrics: MetricConfig[] = []; + + if (typeof check.performance === "number") { + metrics.push({ + key: "performance", + label: t("pages.statusPages.monitorsList.pagespeed.performance"), + progress: check.performance, + }); + } + if (typeof check.accessibility === "number") { + metrics.push({ + key: "accessibility", + label: t("pages.statusPages.monitorsList.pagespeed.accessibility"), + progress: check.accessibility, + }); + } + if (typeof check.bestPractices === "number") { + metrics.push({ + key: "bestPractices", + label: t("pages.statusPages.monitorsList.pagespeed.bestPractices"), + progress: check.bestPractices, + }); + } + if (typeof check.seo === "number") { + metrics.push({ + key: "seo", + label: t("pages.statusPages.monitorsList.pagespeed.seo"), + progress: check.seo, + }); + } + + return metrics; +}; + +export const PageSpeedMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0]; + + if (!latestCheck) { + return ( + + {t("pages.statusPages.monitorsList.noData")} + + ); + } + + const metrics = buildMetrics(latestCheck, t); + + if (metrics.length === 0) { + return ( + + {t("pages.statusPages.monitorsList.noData")} + + ); + } + + return ( + + {metrics.map((metric) => ( + + ))} + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 78b7cba41..36028249d 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -23,7 +23,9 @@ const StatusPageView = () => { const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isPublic = location.pathname.startsWith("/status/public"); - const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null; + const apiUrl = url + ? `/status-page/${url}?type=uptime&type=infrastructure&type=pagespeed` + : null; const { data, isLoading, error } = useGet( apiUrl, diff --git a/client/src/Types/StatusPage.ts b/client/src/Types/StatusPage.ts index 6fe641210..225a5b74a 100644 --- a/client/src/Types/StatusPage.ts +++ b/client/src/Types/StatusPage.ts @@ -1,5 +1,5 @@ import type { Monitor, MonitorType } from "@/Types/Monitor"; -export type MonitorDisplayType = "uptime" | "infrastructure"; +export type MonitorDisplayType = "uptime" | "infrastructure" | "pagespeed"; export const MONITOR_TYPE_KEYS: Partial> = { http: "pages.common.monitors.monitorTypes.optionHttp", @@ -42,6 +42,7 @@ export interface StatusPage { showUptimePercentage: boolean; showAdminLoginLink: boolean; showInfrastructure: boolean; + showPageSpeed: boolean; customCSS: string; createdAt: string; updatedAt: string; diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts index d0ede67e1..934e28beb 100644 --- a/client/src/Utils/MonitorUtils.ts +++ b/client/src/Utils/MonitorUtils.ts @@ -78,6 +78,12 @@ export const getPageSpeedPalette = (score: number): PaletteKey => { else return "error"; }; +export const getPageSpeedGaugeColor = (val: number, theme: any) => { + if (val >= 90) return theme.palette.success.main; + else if (val >= 50) return theme.palette.warning.light; + else return theme.palette.error.light; +}; + export const formatUrl = (url: string, maxLength: number = 55) => { if (!url) return ""; diff --git a/client/src/Validation/statusPage.ts b/client/src/Validation/statusPage.ts index 43d7198f0..b5fe9b861 100644 --- a/client/src/Validation/statusPage.ts +++ b/client/src/Validation/statusPage.ts @@ -15,7 +15,7 @@ export const statusPageSchema = z.object({ ), timezone: z.string().optional(), type: z - .array(z.enum(["uptime", "infrastructure"])) + .array(z.enum(["uptime", "infrastructure", "pagespeed"])) .min(1, "At least one type is required"), color: z.string().min(1, "Color is required"), monitors: z.array(z.string()).min(1, "At least one monitor is required"), @@ -24,6 +24,7 @@ export const statusPageSchema = z.object({ showUptimePercentage: z.boolean(), showAdminLoginLink: z.boolean(), showInfrastructure: z.boolean(), + showPageSpeed: z.boolean(), customCSS: z.string().optional(), logo: z .object({ diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index c11543cad..11e3d5b1c 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -324,6 +324,7 @@ const statusPageValidation = joi.object({ showCharts: joi.boolean(), showAdminLoginLink: joi.boolean(), showInfrastructure: joi.boolean(), + showPageSpeed: joi.boolean(), }); const settingsValidation = joi.object({ diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 83ca4fa33..a199f2717 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1202,6 +1202,13 @@ "used": "Used", "total": "Total" }, + "pagespeed": { + "title": "PageSpeed", + "performance": "Performance", + "accessibility": "Accessibility", + "bestPractices": "Best Practices", + "seo": "SEO" + }, "uptime": { "title": "Uptime", "responseTime": "Response time" @@ -1300,6 +1307,9 @@ }, "showInfrastructure": { "label": "Show infrastructure metrics" + }, + "showPageSpeed": { + "label": "Show PageSpeed scores" } } } diff --git a/server/src/db/models/StatusPage.ts b/server/src/db/models/StatusPage.ts index e9b5b59aa..6fcd5cd10 100644 --- a/server/src/db/models/StatusPage.ts +++ b/server/src/db/models/StatusPage.ts @@ -104,6 +104,10 @@ const StatusPageSchema = new Schema( type: Boolean, default: false, }, + showPageSpeed: { + type: Boolean, + default: false, + }, customCSS: { type: String, default: "", diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts index d8e942271..25a6cbfa0 100644 --- a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -58,6 +58,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { showUptimePercentage: doc.showUptimePercentage, showAdminLoginLink: doc.showAdminLoginLink, showInfrastructure: doc.showInfrastructure, + showPageSpeed: doc.showPageSpeed, customCSS: doc.customCSS, createdAt: this.toDateString(doc.createdAt), updatedAt: this.toDateString(doc.updatedAt), diff --git a/server/src/types/statusPage.ts b/server/src/types/statusPage.ts index 0329f4e6f..112bba169 100644 --- a/server/src/types/statusPage.ts +++ b/server/src/types/statusPage.ts @@ -1,4 +1,4 @@ -export const StatusPageTypes = ["uptime", "infrastructure"] as const; +export const StatusPageTypes = ["uptime", "infrastructure", "pagespeed"] as const; export type StatusPageType = (typeof StatusPageTypes)[number]; export interface StatusPageLogo { @@ -29,6 +29,7 @@ export interface StatusPage { showUptimePercentage: boolean; showAdminLoginLink: boolean; showInfrastructure: boolean; + showPageSpeed: boolean; customCSS: string; createdAt: string; updatedAt: string; diff --git a/server/src/validation/statusPageValidation.ts b/server/src/validation/statusPageValidation.ts index 8b2ab3f20..8af932691 100644 --- a/server/src/validation/statusPageValidation.ts +++ b/server/src/validation/statusPageValidation.ts @@ -32,6 +32,7 @@ export const createStatusPageBodyValidation = z showUptimePercentage: booleanCoercion, showAdminLoginLink: booleanCoercion.optional(), showInfrastructure: booleanCoercion.optional(), + showPageSpeed: booleanCoercion.optional(), removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(), }) .strip();