diff --git a/client/src/Hooks/useStatusPageForm.ts b/client/src/Hooks/useStatusPageForm.ts
index a079095cc..0011ea761 100644
--- a/client/src/Hooks/useStatusPageForm.ts
+++ b/client/src/Hooks/useStatusPageForm.ts
@@ -27,12 +27,14 @@ export const useStatusPageForm = ({
companyName: data?.companyName || "",
url: data?.url || generateDefaultUrl(),
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
+ type: data?.type || ["uptime"],
color: data?.color || "#4169E1",
monitors: data?.monitors || [],
isPublished: data?.isPublished ?? false,
showCharts: data?.showCharts ?? true,
showUptimePercentage: data?.showUptimePercentage ?? true,
showAdminLoginLink: data?.showAdminLoginLink ?? false,
+ showInfrastructure: data?.showInfrastructure ?? false,
customCSS: data?.customCSS || "",
logo: transformLogo(data?.logo),
};
diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx
index b80cddaaa..15b76eab3 100644
--- a/client/src/Pages/CreateMonitor/index.tsx
+++ b/client/src/Pages/CreateMonitor/index.tsx
@@ -302,49 +302,49 @@ const CreateMonitorPage = () => {
>
{
+ const params = new URLSearchParams();
+ ["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach(
+ (type) => params.append("type", type)
+ );
+ return `/monitors/team?${params.toString()}`;
+})();
+
interface TimezoneOption {
_id: string;
name: string;
@@ -49,11 +58,11 @@ const CreateStatusPage = () => {
// Fetch existing status page data when configuring
const { data: statusPageData, isLoading: isLoadingStatusPage } =
- useGet(isCreate ? null : `/status-page/${url}?type=uptime`);
+ useGet(
+ isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure`
+ );
- const { data: monitorsResponse } = useGet(
- "/monitors/team?type=http&type=ping&type=port&type=docker"
- );
+ const { data: monitorsResponse } = useGet(monitorsUrl);
const monitors = monitorsResponse ?? [];
const { post, loading: isSubmittingPost } = usePost();
@@ -81,6 +90,24 @@ const CreateStatusPage = () => {
reset(defaults);
}, [defaults, reset]);
+ const watchedMonitorIds: string[] = form.watch("monitors") ?? [];
+ const computedTypes: MonitorDisplayType[] = useMemo(() => {
+ const selectedMonitors = (watchedMonitorIds ?? [])
+ .map((id) => monitors.find((m) => m.id === id))
+ .filter((m): m is Monitor => m !== undefined);
+
+ const typesSet = new Set();
+ selectedMonitors.forEach((m) => {
+ typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime");
+ });
+
+ return typesSet.size ? Array.from(typesSet) : ["uptime"];
+ }, [watchedMonitorIds, monitors]);
+
+ useEffect(() => {
+ form.setValue("type", computedTypes);
+ }, [computedTypes]);
+
const onError = (errors: any) => {
logger.debug("Status page validation errors", errors);
};
@@ -103,7 +130,6 @@ const CreateStatusPage = () => {
const onSubmit = async (data: StatusPageFormData) => {
const fd = new FormData();
- fd.append("type", "uptime");
fd.append("isPublished", String(data.isPublished));
if (data.companyName) fd.append("companyName", data.companyName);
if (data.url) fd.append("url", data.url);
@@ -112,11 +138,16 @@ const CreateStatusPage = () => {
fd.append("showCharts", String(data.showCharts));
fd.append("showUptimePercentage", String(data.showUptimePercentage));
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
+ fd.append("showInfrastructure", String(data.showInfrastructure));
data.monitors.forEach((monitorId) => {
fd.append("monitors[]", monitorId);
});
+ data.type.forEach((type) => {
+ fd.append("type[]", type);
+ });
+
// Handle logo upload
if (data.logo === null) {
// Signal to remove the logo
@@ -303,7 +334,9 @@ const CreateStatusPage = () => {
}}
>
- {monitor.name}
+ {`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`}
{
@@ -425,6 +458,23 @@ const CreateStatusPage = () => {
/>
)}
/>
+ (
+
+ }
+ label={t(
+ "pages.statusPages.form.features.option.showInfrastructure.label"
+ )}
+ />
+ )}
+ />
{/* {
+ const theme = useTheme();
+ return (
+
+
+ {label}
+
+ {value}
+
+ );
+};
+
+interface MetricItemProps {
+ label: string;
+ progress: number;
+ details?: MetricDetail[];
+}
+
+const MetricItem = ({ label, progress, details }: MetricItemProps) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ return (
+
+
+
+ {label}
+
+ {details && details.length > 0 && (
+
+ {details.map((detail) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+type LatestCheck = NonNullable[number];
+
+const buildCpuMetric = (
+ check: LatestCheck,
+ t: (key: string) => string
+): MetricConfig | null => {
+ if (!check.cpu || typeof check.cpu.usage_percent !== "number") {
+ return null;
+ }
+ const usagePercent = (check.cpu.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER;
+ return {
+ key: "cpu",
+ label: t("pages.statusPages.monitorsList.infrastructure.cpu"),
+ hasData: true,
+ progress: usagePercent,
+ details: [
+ {
+ label: t("pages.statusPages.monitorsList.infrastructure.usage"),
+ value: `${usagePercent.toFixed(2)}%`,
+ },
+ ],
+ };
+};
+
+const buildMemoryMetric = (
+ check: LatestCheck,
+ t: (key: string) => string
+): MetricConfig | null => {
+ if (
+ !check.memory ||
+ typeof check.memory.usage_percent !== "number" ||
+ typeof check.memory.used_bytes !== "number" ||
+ typeof check.memory.total_bytes !== "number"
+ ) {
+ return null;
+ }
+ return {
+ key: "memory",
+ label: t("pages.statusPages.monitorsList.infrastructure.memory"),
+ hasData: true,
+ progress: (check.memory.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER,
+ details: [
+ {
+ label: t("pages.statusPages.monitorsList.infrastructure.used"),
+ value: prettyBytes(check.memory.used_bytes ?? 0),
+ },
+ {
+ label: t("pages.statusPages.monitorsList.infrastructure.total"),
+ value: prettyBytes(check.memory.total_bytes ?? 0),
+ },
+ ],
+ };
+};
+
+const buildDiskMetric = (
+ check: LatestCheck,
+ t: (key: string) => string
+): MetricConfig | null => {
+ if (!check.disk || check.disk.length === 0) {
+ return null;
+ }
+ const disks = check.disk;
+ const avgUsagePercent =
+ (disks.reduce((acc, disk) => acc + (disk?.usage_percent ?? 0), 0) / disks.length) *
+ PERCENTAGE_MULTIPLIER;
+ const totalUsedBytes = disks.reduce((acc, disk) => acc + (disk?.used_bytes ?? 0), 0);
+ const totalTotalBytes = disks.reduce((acc, disk) => acc + (disk?.total_bytes ?? 0), 0);
+
+ return {
+ key: "disk",
+ label: t("pages.statusPages.monitorsList.infrastructure.disk"),
+ hasData: true,
+ progress: avgUsagePercent,
+ details: [
+ {
+ label: t("pages.statusPages.monitorsList.infrastructure.used"),
+ value: prettyBytes(totalUsedBytes),
+ },
+ {
+ label: t("pages.statusPages.monitorsList.infrastructure.total"),
+ value: prettyBytes(totalTotalBytes),
+ },
+ ],
+ };
+};
+
+export const InfrastructureMetrics = ({ 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: MetricConfig[] = [
+ buildCpuMetric(latestCheck, t),
+ buildMemoryMetric(latestCheck, t),
+ buildDiskMetric(latestCheck, t),
+ ].filter((m): m is MetricConfig => m !== null);
+
+ if (metrics.length === 0) {
+ return (
+
+ {t("pages.statusPages.monitorsList.noData")}
+
+ );
+ }
+
+ return (
+
+ {metrics.map(({ key, label, progress, details }) => (
+
+ ))}
+
+ );
+};
diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx
index b94b20a80..248a69c9b 100644
--- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx
+++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx
@@ -5,13 +5,16 @@ import Typography from "@mui/material/Typography";
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
import { StatusLabel, BaseBox } from "@/Components/design-elements";
import { SwitchComponent } from "@/Components/inputs";
+import { InfrastructureMetrics } from "@/Pages/StatusPage/Status/Components/InfrastructureMetrics";
-import { useTheme } from "@mui/material/styles";
+import { useTheme, type Theme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import { useState } from "react";
import type { Monitor } from "@/Types/Monitor";
import type { StatusPage } from "@/Types/StatusPage";
+import { getMonitorTypeLabel } from "@/Types/StatusPage";
import type { RootState } from "@/Types/state";
+import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
interface StatusPageMonitor extends Monitor {
checks?: Monitor["recentChecks"];
@@ -22,29 +25,124 @@ interface MonitorsListProps {
monitors: StatusPageMonitor[];
}
-export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
+const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
+ const bg =
+ monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light;
+ return {
+ backgroundColor: bg,
+ color: theme.palette.background.paper,
+ padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`,
+ borderRadius: theme.shape.borderRadius,
+ };
+};
+
+const MonitorHeader = ({
+ monitor,
+ showURL,
+}: {
+ monitor: StatusPageMonitor;
+ showURL: boolean;
+}) => {
const theme = useTheme();
const { t } = useTranslation();
- const showURL = useSelector((state: RootState) => state.ui?.showURL);
+ return (
+
+
+
+
+ {monitor.name}
+
+
+ {getMonitorTypeLabel(monitor.type, t)}
+
+
+ {showURL && monitor.url && (
+
+ {monitor.url}
+
+ )}
+
+
+
+ );
+};
+const MonitorContent = ({
+ monitor,
+ statusPage,
+ chartType,
+}: {
+ monitor: StatusPageMonitor;
+ statusPage: StatusPage;
+ chartType: string;
+}) => {
+ const theme = useTheme();
+
+ if (monitor.type === "hardware") {
+ if (statusPage.showInfrastructure === false) return null;
+ return ;
+ }
+
+ if (statusPage.showCharts === false) return null;
+
+ const checks = monitor.checks?.slice().reverse() ?? [];
+ return (
+
+ {chartType === "histogram" ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const showURL = useSelector((state: RootState) => state.ui?.showURL);
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
return (
-
- {statusPage.showCharts !== false && (
+
+ {statusPage.showCharts && (
{t("pages.statusPages.monitorsList.chartTypeHeatmap")}
{
- setChartType(e.target.checked ? "histogram" : "heatmap");
- }}
+ onChange={(e) => setChartType(e.target.checked ? "histogram" : "heatmap")}
/>
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
@@ -52,59 +150,22 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
)}
- {monitors?.map((monitor) => {
- return (
-
-
-
-
- {monitor.name}
-
- {showURL && (
-
- {monitor.url}
-
- )}
-
-
-
- {statusPage.showCharts !== false && (
-
- {chartType === "histogram" ? (
-
- ) : (
-
- )}
-
- )}
-
- );
- })}
+ {monitors.map((monitor) => (
+
+
+
+
+ ))}
);
};
diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx
index 0ca629581..3bb14261f 100644
--- a/client/src/Pages/StatusPage/Status/index.tsx
+++ b/client/src/Pages/StatusPage/Status/index.tsx
@@ -23,7 +23,7 @@ const StatusPageView = () => {
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isPublic = location.pathname.startsWith("/status/public");
- const apiUrl = url ? `/status-page/${url}?type=uptime` : null;
+ const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
const { data, isLoading, error } = useGet(apiUrl);
@@ -32,7 +32,7 @@ const StatusPageView = () => {
if (!statusPage) return null;
- if (monitors?.length === 0) {
+ if (monitors.length === 0) {
return (
row.type,
+ render: (row) => row.type.join(", "),
},
{
id: "status",
diff --git a/client/src/Types/StatusPage.ts b/client/src/Types/StatusPage.ts
index 900373f3d..6fe641210 100644
--- a/client/src/Types/StatusPage.ts
+++ b/client/src/Types/StatusPage.ts
@@ -1,10 +1,31 @@
-import type { Monitor } from "@/Types/Monitor";
+import type { Monitor, MonitorType } from "@/Types/Monitor";
+export type MonitorDisplayType = "uptime" | "infrastructure";
+
+export const MONITOR_TYPE_KEYS: Partial> = {
+ http: "pages.common.monitors.monitorTypes.optionHttp",
+ ping: "pages.common.monitors.monitorTypes.optionPing",
+ docker: "pages.common.monitors.monitorTypes.optionDocker",
+ port: "pages.common.monitors.monitorTypes.optionPort",
+ game: "pages.common.monitors.monitorTypes.optionGame",
+ grpc: "pages.common.monitors.monitorTypes.optionGrpc",
+ websocket: "pages.common.monitors.monitorTypes.optionWebSocket",
+ hardware: "pages.common.monitors.monitorTypes.optionHardware",
+ pagespeed: "pages.common.monitors.monitorTypes.optionPagespeed",
+};
+
+export const getMonitorTypeLabel = (
+ type: MonitorType,
+ t: (key: string) => string
+): string => {
+ const key = MONITOR_TYPE_KEYS[type];
+ return key ? t(key) : type;
+};
export interface StatusPage {
id: string;
userId: string;
teamId: string;
- type: string;
+ type: MonitorDisplayType[];
companyName: string;
url: string;
timezone?: string;
@@ -20,6 +41,7 @@ export interface StatusPage {
showCharts: boolean;
showUptimePercentage: boolean;
showAdminLoginLink: boolean;
+ showInfrastructure: boolean;
customCSS: string;
createdAt: string;
updatedAt: string;
diff --git a/client/src/Validation/statusPage.ts b/client/src/Validation/statusPage.ts
index fbe6a0690..43d7198f0 100644
--- a/client/src/Validation/statusPage.ts
+++ b/client/src/Validation/statusPage.ts
@@ -14,12 +14,16 @@ export const statusPageSchema = z.object({
"URL can only contain lowercase letters, numbers, and hyphens"
),
timezone: z.string().optional(),
+ type: z
+ .array(z.enum(["uptime", "infrastructure"]))
+ .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"),
isPublished: z.boolean(),
showCharts: z.boolean(),
showUptimePercentage: z.boolean(),
showAdminLoginLink: z.boolean(),
+ showInfrastructure: z.boolean(),
customCSS: z.string().optional(),
logo: z
.object({
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index fa92f2e99..c11543cad 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -288,7 +288,13 @@ const logoImageValidation = joi
.optional(); // Make entire object optional
const statusPageValidation = joi.object({
- type: joi.string().valid("uptime").required(),
+ type: joi
+ .alternatives()
+ .try(
+ joi.string().valid("uptime", "infrastructure"),
+ joi.array().items(joi.string().valid("uptime", "infrastructure"))
+ )
+ .required(),
isPublished: joi.bool(),
companyName: joi
.string()
@@ -317,6 +323,7 @@ const statusPageValidation = joi.object({
showUptimePercentage: joi.boolean(),
showCharts: joi.boolean(),
showAdminLoginLink: joi.boolean(),
+ showInfrastructure: joi.boolean(),
});
const settingsValidation = joi.object({
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 04e18e3dc..92a21939f 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -407,6 +407,17 @@
"paused": "paused",
"total": "total",
"up": "up"
+ },
+ "monitorTypes": {
+ "optionDocker": "Docker",
+ "optionGame": "Game",
+ "optionHttp": "HTTP(S)",
+ "optionPing": "Ping",
+ "optionPort": "Port",
+ "optionPagespeed": "PageSpeed",
+ "optionHardware": "Infrastructure",
+ "optionGrpc": "gRPC",
+ "optionWebSocket": "WebSocket"
}
}
},
@@ -534,19 +545,12 @@
},
"type": {
"description": "Select the type of check to perform",
- "optionDocker": "Docker",
"optionDockerDescription": "Use Docker to monitor if a container is running.",
- "optionGame": "Game",
"optionGameDescription": "Monitor if a specific game server is online.",
- "optionGrpc": "gRPC",
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
- "optionWebSocket": "WebSocket",
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
- "optionHttp": "HTTP(S)",
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
- "optionPing": "Ping",
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
- "optionPort": "Port",
"optionPortDescription": "Monitor if a specific port on a server is open.",
"title": "Type"
},
@@ -1178,7 +1182,22 @@
},
"monitorsList": {
"chartTypeHeatmap": "Heatmap",
- "chartTypeHistogram": "Histogram"
+ "chartTypeHistogram": "Histogram",
+ "noData": "No data available",
+ "infrastructure": {
+ "title": "Infrastructure",
+ "cpu": "CPU",
+ "memory": "RAM",
+ "memoryText": "Memory",
+ "disk": "Disk",
+ "usage": "Usage",
+ "used": "Used",
+ "total": "Total"
+ },
+ "uptime": {
+ "title": "Uptime",
+ "responseTime": "Response time"
+ }
},
"statusBar": {
"allDown": "All systems are down",
@@ -1270,6 +1289,9 @@
},
"showAdminLoginLink": {
"label": "Show admin login link"
+ },
+ "showInfrastructure": {
+ "label": "Show infrastructure metrics"
}
}
}