diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx
index b80cddaaa..a2601e302 100644
--- a/client/src/Pages/CreateMonitor/index.tsx
+++ b/client/src/Pages/CreateMonitor/index.tsx
@@ -302,35 +302,35 @@ const CreateMonitorPage = () => {
>
{
}}
>
- {monitor.name}
+ {`${monitor.name} (${t(`pages.common.monitors.monitorTypes.option${monitor?.type.charAt(0).toUpperCase() + monitor?.type.slice(1)}`)})`}
{
diff --git a/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx b/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx
index 46614a5ba..3dafdadc5 100644
--- a/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx
+++ b/client/src/Pages/StatusPage/Status/Components/InfrastructureMetrics.tsx
@@ -5,6 +5,9 @@ import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import prettyBytes from "pretty-bytes";
import type { Monitor } from "@/Types/Monitor";
+import Grid from "@mui/material/Grid";
+import useMediaQuery from "@mui/material/useMediaQuery";
+import Box from "@mui/material/Box";
interface StatusPageMonitor extends Monitor {
checks?: Monitor["recentChecks"];
@@ -13,34 +16,62 @@ interface StatusPageMonitor extends Monitor {
infrastructureDisk?: number;
}
-interface MetricItemProps {
+interface MetricDetailRowProps {
label: string;
- progress: number;
- upperLabel?: string;
- upperValue?: string;
- lowerLabel?: string;
- lowerValue?: string;
+ value: string;
}
-const MetricItem = ({
- label,
- progress,
- upperLabel,
- upperValue,
- lowerLabel,
- lowerValue,
-}: MetricItemProps) => {
+const MetricDetailRow = ({ label, value }: MetricDetailRowProps) => {
const theme = useTheme();
return (
-
+ {label}
+
+ {value}
+
+ );
+};
+
+interface MetricItemProps {
+ label: string;
+ progress: number;
+ details?: MetricDetailRowProps[];
+}
+
+const MetricItem = ({ label, progress, details }: MetricItemProps) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ return (
+
+
{label}
-
- {(upperLabel || lowerLabel) && (
-
+ {details && details.length > 0 && (
+
- {upperLabel && (
-
-
- {upperLabel}
-
- {upperValue}
-
- )}
- {lowerLabel && (
-
-
- {lowerLabel}
-
- {lowerValue}
-
- )}
-
+ {details.map((detail) => (
+
+ ))}
+
)}
-
+
);
};
@@ -99,54 +108,93 @@ export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor
return (
{t("pages.statusPages.monitorsList.noData")}
);
}
- const cpuUsage = (latestCheck?.cpu?.usage_percent || 0) * 100;
- const memoryUsage = (latestCheck?.memory?.usage_percent || 0) * 100;
- const memoryUsed = latestCheck?.memory?.used_bytes || 0;
- const memoryTotal = latestCheck?.memory?.total_bytes || 0;
-
- const disks = latestCheck?.disk ?? [];
- const totalDiskUsage = disks.reduce((acc, disk) => acc + (disk?.usage_percent || 0), 0);
- const diskCount = disks.length || 1;
- const diskUsage = (totalDiskUsage / diskCount) * 100;
- const diskUsed = disks.reduce((acc, disk) => acc + (disk?.used_bytes || 0), 0);
- const diskTotal = disks.reduce((acc, disk) => acc + (disk?.total_bytes || 0), 0);
+ const metrics = [
+ {
+ key: t("pages.statusPages.monitorsList.infrastructure.cpu"),
+ labelKey: t("pages.statusPages.monitorsList.infrastructure.cpu"),
+ hasData: latestCheck.cpu && typeof latestCheck.cpu.usage_percent === "number",
+ progress: latestCheck.cpu?.usage_percent ? latestCheck.cpu.usage_percent * 100 : 0,
+ details: [
+ {
+ label: t("pages.statusPages.monitorsList.infrastructure.usage"),
+ value: `${((latestCheck.cpu?.usage_percent || 0) * 100).toFixed(2)}%`,
+ },
+ ],
+ },
+ {
+ key: t("pages.statusPages.monitorsList.infrastructure.memoryText"),
+ labelKey: t("pages.statusPages.monitorsList.infrastructure.memory"),
+ hasData:
+ latestCheck.memory &&
+ typeof latestCheck.memory.usage_percent === "number" &&
+ typeof latestCheck.memory.used_bytes === "number" &&
+ typeof latestCheck.memory.total_bytes === "number",
+ progress: latestCheck.memory?.usage_percent
+ ? latestCheck.memory.usage_percent * 100
+ : 0,
+ details: [
+ {
+ label: t("pages.statusPages.monitorsList.infrastructure.used"),
+ value: prettyBytes(latestCheck.memory?.used_bytes || 0),
+ },
+ {
+ label: t("pages.statusPages.monitorsList.infrastructure.total"),
+ value: prettyBytes(latestCheck.memory?.total_bytes || 0),
+ },
+ ],
+ },
+ {
+ key: t("pages.statusPages.monitorsList.infrastructure.disk"),
+ labelKey: t("pages.statusPages.monitorsList.infrastructure.disk"),
+ hasData: latestCheck.disk && latestCheck.disk.length > 0,
+ progress: latestCheck.disk
+ ? (latestCheck.disk.reduce((acc, disk) => acc + (disk?.usage_percent || 0), 0) /
+ latestCheck.disk.length) *
+ 100
+ : 0,
+ details: [
+ {
+ label: t("pages.statusPages.monitorsList.infrastructure.used"),
+ value: prettyBytes(
+ latestCheck?.disk?.reduce((acc, disk) => acc + (disk?.used_bytes || 0), 0) ??
+ 0
+ ),
+ },
+ {
+ label: t("pages.statusPages.monitorsList.infrastructure.total"),
+ value: prettyBytes(
+ latestCheck?.disk?.reduce((acc, disk) => acc + (disk?.total_bytes || 0), 0) ??
+ 0
+ ),
+ },
+ ],
+ },
+ ];
return (
-
-
-
-
-
+ {metrics.map(
+ ({ key, labelKey, hasData, progress, details }) =>
+ hasData && (
+
+ )
+ )}
+
);
};
diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx
index 7e053c018..3cda8888e 100644
--- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx
+++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx
@@ -7,7 +7,7 @@ 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";
@@ -23,6 +23,87 @@ interface MonitorsListProps {
monitors: StatusPageMonitor[];
}
+const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
+ // for future: specific badge styles can be defined here based on monitor type
+ const badgeConfig: Record = {
+ hardware: {
+ color: theme.palette.background.paper,
+ bg: theme.palette.info.light,
+ },
+ http: {
+ color: theme.palette.background.paper,
+ bg: theme.palette.success.light,
+ },
+ ping: {
+ color: theme.palette.background.paper,
+ bg: theme.palette.success.light,
+ },
+ port: {
+ color: theme.palette.background.paper,
+ bg: theme.palette.success.light,
+ },
+ docker: {
+ color: theme.palette.background.paper,
+ bg: theme.palette.success.light,
+ },
+ pagespeed: {
+ color: theme.palette.background.paper,
+ bg: theme.palette.success.light,
+ },
+ game: {
+ color: theme.palette.background.paper,
+ bg: theme.palette.success.light,
+ },
+ };
+
+ const config = badgeConfig[monitorType] ?? badgeConfig.http;
+ return {
+ backgroundColor: config.bg,
+ color: config.color,
+ padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
+ borderRadius: theme.shape.borderRadius,
+ };
+};
+
+// Extracted component for monitor content below the header row
+const MonitorContent = ({
+ monitor,
+ statusPage,
+ chartType,
+}: {
+ monitor: Monitor & { checks?: Monitor["recentChecks"] };
+ statusPage: StatusPage;
+ chartType: string;
+}) => {
+ const theme = useTheme();
+ const isInfrastructureMonitor = monitor?.type === "hardware";
+
+ if (isInfrastructureMonitor) {
+ if (statusPage.showInfrastructure === false) {
+ return null;
+ }
+ return ;
+ }
+
+ if (statusPage.showCharts === false) {
+ return null;
+ }
+
+ return (
+
+ {chartType === "histogram" ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
const theme = useTheme();
const { t } = useTranslation();
@@ -55,7 +136,6 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
)}
{monitors?.map((monitor) => {
- const isInfrastructureMonitor = monitor?.type === "hardware";
return (
{
- {t(`pages.statusPages.monitorsList.badge.${monitor?.type}`)}
+ {t(
+ `pages.common.monitors.monitorTypes.option${monitor?.type.charAt(0).toUpperCase() + monitor?.type.slice(1)}`
+ )}
{showURL && monitor?.url && (
{monitor.url}
@@ -115,48 +190,11 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
-
- {!isInfrastructureMonitor && (
- <>
- {statusPage.showCharts !== false && (
-
- {chartType === "histogram" ? (
-
- ) : (
-
- )}
-
- )}
- >
- )}
-
- {isInfrastructureMonitor && statusPage.showInfrastructure !== false && (
-
-
-
-
- )}
+
);
})}
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 16b159398..4909030cd 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"
},
@@ -1184,6 +1188,7 @@
"title": "Infrastructure",
"cpu": "CPU",
"memory": "RAM",
+ "memoryText": "Memory",
"disk": "Disk",
"usage": "Usage",
"used": "Used",
@@ -1192,15 +1197,6 @@
"uptime": {
"title": "Uptime",
"responseTime": "Response time"
- },
- "badge": {
- "http": "HTTP",
- "ping": "Ping",
- "port": "Port",
- "docker": "Docker",
- "pagespeed": "PageSpeed",
- "hardware": "Infrastructure",
- "game": "Game"
}
},
"statusBar": {
diff --git a/server/src/db/migration/0005_migrateStatusPageTypeToArray.ts b/server/src/db/migration/0005_migrateStatusPageTypeToArray.ts
new file mode 100644
index 000000000..0e2d99ea8
--- /dev/null
+++ b/server/src/db/migration/0005_migrateStatusPageTypeToArray.ts
@@ -0,0 +1,45 @@
+import mongoose from "mongoose";
+import { logger } from "@/utils/logger.js";
+import StatusPageModel from "../models/StatusPage.js";
+
+/**
+ * Migration: Convert StatusPage `type` field from string to array of strings
+ *
+ * Old format: { type: "uptime" }
+ * New format: { type: ["uptime"] }
+ */
+export async function migrateStatusPageTypeToArray(): Promise {
+ const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray";
+
+ try {
+ logger.info({ service: SERVICE_NAME, message: "Starting migration of StatusPage type field from string to array" });
+
+ const db = mongoose.connection.db;
+ if (!db) {
+ throw new Error("Database connection is not initialized");
+ }
+
+ // Find all status pages where `type` is a string (not yet migrated)
+ const result = await StatusPageModel.updateMany({ type: { $type: "string" } }, [
+ {
+ $set: {
+ type: ["$type"],
+ },
+ },
+ ]);
+
+ if (result.modifiedCount === 0) {
+ logger.info({ service: SERVICE_NAME, message: "No StatusPage documents needed migration" });
+ return;
+ }
+
+ logger.info({
+ service: SERVICE_NAME,
+ message: `Migration complete. Converted ${result.modifiedCount} StatusPage document(s) from string type to array type`,
+ });
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ logger.error({ service: SERVICE_NAME, message: `Error during StatusPage type migration: ${errorMessage}` });
+ throw error;
+ }
+}
diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts
index 3b7b45ada..61f16ed4d 100644
--- a/server/src/db/migration/index.ts
+++ b/server/src/db/migration/index.ts
@@ -3,6 +3,7 @@ import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
import MigrationModel from "../models/Migration.js";
+import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js";
type MigrationEntry = {
name: string;
@@ -14,6 +15,7 @@ const migrations: MigrationEntry[] = [
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
+ { name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray },
];
const runMigrations = async (logger?: { info: Function; error: Function }) => {