diff --git a/client/src/Components/v2/inputs/Radio.tsx b/client/src/Components/v2/inputs/Radio.tsx
new file mode 100644
index 000000000..ad8dc3374
--- /dev/null
+++ b/client/src/Components/v2/inputs/Radio.tsx
@@ -0,0 +1,88 @@
+import Radio from "@mui/material/Radio";
+import type { RadioProps } from "@mui/material/Radio";
+import { useTheme } from "@mui/material/styles";
+import { Circle, CircleDot } from "lucide-react";
+import FormControlLabel from "@mui/material/FormControlLabel";
+import Typography from "@mui/material/Typography";
+
+interface RadioInputProps extends RadioProps {}
+
+export const RadioInput = ({ ...props }: RadioInputProps) => {
+ const theme = useTheme();
+ return (
+
+ }
+ checkedIcon={
+
+ }
+ sx={{
+ padding: 0,
+ mt: theme.spacing(0.5),
+ color: theme.palette.text.secondary,
+ "&.Mui-checked": {
+ color: theme.palette.primary.main,
+ "& svg circle": {
+ fill: theme.palette.primary.main,
+ },
+ },
+ "& .MuiSvgIcon-root": {
+ fontSize: 16,
+ },
+ "& svg": {
+ stroke: "currentColor",
+ },
+ "& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
+ stroke: "currentColor",
+ fill: "none",
+ },
+ }}
+ />
+ );
+};
+
+export const RadioWithDescription = ({
+ label,
+ description,
+ ...props
+}: RadioInputProps & { label: string; description: string }) => {
+ const theme = useTheme();
+ return (
+ }
+ label={
+ <>
+ {label}
+
+ {description}
+
+ >
+ }
+ sx={{
+ alignItems: "flex-start",
+ p: theme.spacing(2.5),
+ m: theme.spacing(-2.5),
+ borderRadius: theme.shape.borderRadius,
+
+ "&:hover": {
+ backgroundColor: theme.palette.background.paper,
+ },
+ "& .MuiButtonBase-root": {
+ p: 0,
+ mr: theme.spacing(6),
+ },
+ }}
+ />
+ );
+};
diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx
index 70e740dbd..a0646d394 100644
--- a/client/src/Components/v2/inputs/index.tsx
+++ b/client/src/Components/v2/inputs/index.tsx
@@ -9,3 +9,4 @@ export {
ToggleButtonGroupInput as ToggleButtonGroup,
} from "./ToggleButton";
export { DialogInput as Dialog } from "./Dialog";
+export * from "./Radio";
diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts
new file mode 100644
index 000000000..2d7df97ab
--- /dev/null
+++ b/client/src/Hooks/useMonitorForm.ts
@@ -0,0 +1,105 @@
+import { useMemo } from "react";
+import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
+import type { Monitor, MonitorType } from "@/Types/Monitor";
+
+interface UseMonitorFormOptions {
+ data?: Monitor | null;
+ defaultType?: MonitorType;
+}
+
+const getBaseDefaults = (data?: Monitor | null) => ({
+ name: data?.name || "",
+ interval: data?.interval || 60000,
+ notifications: data?.notifications || [],
+ statusWindowSize: data?.statusWindowSize || 5,
+ statusWindowThreshold: data?.statusWindowThreshold || 60,
+});
+
+export const useMonitorForm = ({
+ data = null,
+ defaultType = "http",
+}: UseMonitorFormOptions = {}) => {
+ return useMemo(() => {
+ const type = data?.type || defaultType;
+ const base = getBaseDefaults(data);
+
+ let defaults: MonitorFormData;
+
+ switch (type) {
+ case "http":
+ defaults = {
+ ...base,
+ type: "http",
+ url: data?.url || "",
+ ignoreTlsErrors: data?.ignoreTlsErrors || false,
+ matchMethod: data?.matchMethod || "",
+ expectedValue: data?.expectedValue || "",
+ jsonPath: data?.jsonPath || "",
+ };
+ break;
+ case "ping":
+ defaults = {
+ ...base,
+ type: "ping",
+ url: data?.url || "",
+ };
+ break;
+ case "port":
+ defaults = {
+ ...base,
+ type: "port",
+ url: data?.url || "",
+ port: data?.port || 80,
+ };
+ break;
+ case "docker":
+ defaults = {
+ ...base,
+ type: "docker",
+ url: data?.url || "",
+ };
+ break;
+ case "game":
+ defaults = {
+ ...base,
+ type: "game",
+ url: data?.url || "",
+ port: data?.port || 27015,
+ gameId: data?.gameId || "",
+ };
+ break;
+ case "pagespeed":
+ defaults = {
+ ...base,
+ type: "pagespeed",
+ url: data?.url || "",
+ };
+ break;
+ case "hardware":
+ defaults = {
+ ...base,
+ type: "hardware",
+ url: data?.url || "",
+ secret: data?.secret || "",
+ cpuAlertThreshold: data?.cpuAlertThreshold ?? 80,
+ memoryAlertThreshold: data?.memoryAlertThreshold ?? 80,
+ diskAlertThreshold: data?.diskAlertThreshold ?? 80,
+ tempAlertThreshold: data?.tempAlertThreshold ?? 80,
+ selectedDisks: data?.selectedDisks || [],
+ };
+ break;
+ default:
+ defaults = {
+ ...base,
+ type: "http",
+ url: "",
+ ignoreTlsErrors: false,
+ matchMethod: "",
+ expectedValue: "",
+ jsonPath: "",
+ };
+ }
+
+ return { schema: monitorSchema, defaults };
+ }, [data, defaultType]);
+};
diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx
new file mode 100644
index 000000000..80f818407
--- /dev/null
+++ b/client/src/Pages/CreateMonitor/index.tsx
@@ -0,0 +1,266 @@
+import { useMemo } from "react";
+import { useEffect } from "react";
+import { useParams } from "react-router";
+import { useForm, Controller } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useTheme } from "@mui/material";
+import Stack from "@mui/material/Stack";
+import RadioGroup from "@mui/material/RadioGroup";
+import FormControl from "@mui/material/FormControl";
+import { useTranslation } from "react-i18next";
+
+import { BasePage, ConfigBox } from "@/Components/v2/design-elements";
+import { RadioWithDescription, Button, TextField, Select } from "@/Components/v2/inputs";
+import { useGet } from "@/Hooks/UseApi";
+import { useMonitorForm } from "@/Hooks/useMonitorForm";
+import type { Monitor, MonitorType } from "@/Types/Monitor";
+import type { MonitorFormData } from "@/Validation/monitor";
+import MenuItem from "@mui/material/MenuItem";
+
+interface GeneralSettingsConfig {
+ urlLabel: string;
+ urlPlaceholder: string;
+ namePlaceholder: string;
+ showPort: boolean;
+ showGameSelect: boolean;
+}
+
+const getGeneralSettingsConfig = (
+ type: MonitorType,
+ t: (key: string) => string
+): GeneralSettingsConfig => {
+ const configs: Record = {
+ http: {
+ urlLabel: t("monitorType.http.label"),
+ urlPlaceholder: t("monitorType.http.placeholder"),
+ namePlaceholder: t("monitorType.http.namePlaceholder"),
+ showPort: false,
+ showGameSelect: false,
+ },
+ ping: {
+ urlLabel: t("monitorType.ping.label"),
+ urlPlaceholder: t("monitorType.ping.placeholder"),
+ namePlaceholder: t("monitorType.ping.namePlaceholder"),
+ showPort: false,
+ showGameSelect: false,
+ },
+ docker: {
+ urlLabel: t("monitorType.docker.label"),
+ urlPlaceholder: t("monitorType.docker.placeholder"),
+ namePlaceholder: t("monitorType.docker.namePlaceholder"),
+ showPort: false,
+ showGameSelect: false,
+ },
+ port: {
+ urlLabel: t("monitorType.port.label"),
+ urlPlaceholder: t("monitorType.port.placeholder"),
+ namePlaceholder: t("monitorType.port.namePlaceholder"),
+ showPort: true,
+ showGameSelect: false,
+ },
+ game: {
+ urlLabel: t("monitorType.game.label"),
+ urlPlaceholder: t("monitorType.game.placeholder"),
+ namePlaceholder: t("monitorType.game.namePlaceholder"),
+ showPort: true,
+ showGameSelect: true,
+ },
+ };
+ return configs[type] || configs.http;
+};
+
+const CreateMonitorPage = () => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const { monitorId } = useParams();
+ const isEditMode = Boolean(monitorId);
+
+ const { data: existingMonitor } = useGet(
+ isEditMode ? `/monitors/${monitorId}` : null
+ );
+
+ const { schema, defaults } = useMonitorForm({ data: existingMonitor ?? null });
+
+ const form = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: defaults,
+ });
+ const { control, watch, handleSubmit, clearErrors } = form;
+
+ useEffect(() => {
+ form.reset(defaults);
+ }, [defaults, form]);
+
+ const watchedType = watch("type") as MonitorType;
+
+ useEffect(() => {
+ clearErrors();
+ }, [watchedType, clearErrors]);
+
+ const generalSettingsConfig = useMemo(
+ () => getGeneralSettingsConfig(watchedType, t),
+ [watchedType, t]
+ );
+
+ const onSubmit = async (data: MonitorFormData) => {
+ console.log(data);
+ };
+
+ const onError = (errors: unknown) => {
+ console.log(errors);
+ };
+
+ return (
+
+ {/* Monitor Type Selection */}
+ (
+
+
+
+
+
+
+
+
+
+ )}
+ />
+ }
+ />
+
+ {/* General Settings - Dynamic based on type */}
+
+ {/* URL/Host/Container field */}
+ (
+
+ )}
+ />
+
+ {/* Port field - only for port and game types */}
+ {generalSettingsConfig.showPort && (
+ (
+ field.onChange(Number(e.target.value) || 0)}
+ type="number"
+ fieldLabel={t("portToMonitor")}
+ placeholder="5173"
+ fullWidth
+ error={!!fieldState.error}
+ helperText={fieldState.error?.message ?? ""}
+ />
+ )}
+ />
+ )}
+
+ {/* Game select - only for game type */}
+ {generalSettingsConfig.showGameSelect && (
+ (
+
+ )}
+ />
+ )}
+
+ {/* Display name field - common to all types */}
+ (
+
+ )}
+ />
+
+ }
+ />
+
+ {/* Submit Button */}
+
+
+
+
+ );
+};
+
+export default CreateMonitorPage;
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 498be4ba7..c2613a16c 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -60,6 +60,8 @@ import withAdminCheck from "@/Components/v1/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport/index.jsx";
import Logs from "../Pages/Logs/index.jsx";
+import CreateMonitor from "@/Pages/CreateMonitor";
+
const Routes = () => {
const mode = useSelector((state) => state.ui.mode);
const AdminCheckedRegister = withAdminCheck(AuthRegister);
@@ -78,6 +80,16 @@ const Routes = () => {
path="/"
element={}
/>
+
+
+
+
+ >
+ }
+ />
;
+
+// Type-specific schemas exported for individual use
+export {
+ httpSchema,
+ pingSchema,
+ portSchema,
+ dockerSchema,
+ gameSchema,
+ pagespeedSchema,
+ hardwareSchema,
+};
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 80da1835c..d9f2e7ac6 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -220,6 +220,37 @@
}
}
},
+ "createMonitor": {
+ "form": {
+ "type": {
+ "description": "Select the type of check to perform",
+ "optionType": "Type",
+ "title": "Type",
+ "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.",
+ "optionDocker": "Docker",
+ "optionDockerDescription": "Use Docker to monitor if a container is running.",
+ "optionPort": "Port",
+ "optionPortDescription": "Monitor if a specific port on a server is open.",
+ "optionGame": "Game",
+ "optionGameDescription": "Monitor if a specific game server is online."
+ },
+ "general": {
+ "title": "General settings",
+ "description": {
+ "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.",
+ "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.",
+ "docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
+ "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.",
+ "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
+ "pagespeed": "Here you can select the URL of the host, together with the type of monitor.",
+ "infrastructure": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent."
+ }
+ }
+ }
+ },
"uptime": {
"table": {
"headers": {