diff --git a/.github/easypanel-deploy-screenshot.png b/.github/easypanel-deploy-screenshot.png
index 271273289..20f76e9e5 100644
Binary files a/.github/easypanel-deploy-screenshot.png and b/.github/easypanel-deploy-screenshot.png differ
diff --git a/client/src/Components/sidebar/Menu.tsx b/client/src/Components/sidebar/Menu.tsx
index be42fa6e5..300ce0346 100644
--- a/client/src/Components/sidebar/Menu.tsx
+++ b/client/src/Components/sidebar/Menu.tsx
@@ -20,12 +20,18 @@ import {
Tag,
Waypoints,
Container,
+ // LayoutDashboard,
} from "lucide-react";
export const getMenu = (t: Function) => {
return [
{
group: t("components.sidebar.menu.groups.monitoring"),
items: [
+ // {
+ // name: t("components.sidebar.menu.dashboard"),
+ // path: "dashboard",
+ // icon: ,
+ // },
{
name: t("components.sidebar.menu.uptime"),
path: "uptime",
diff --git a/client/src/Pages/Dashboard/components/DashboardCard.tsx b/client/src/Pages/Dashboard/components/DashboardCard.tsx
new file mode 100644
index 000000000..b075ca0f3
--- /dev/null
+++ b/client/src/Pages/Dashboard/components/DashboardCard.tsx
@@ -0,0 +1,54 @@
+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 { LAYOUT } from "@/Utils/Theme/constants";
+import { typographyLevels } from "@/Utils/Theme/Palette";
+
+interface DashboardCardProps {
+ title: string;
+ topRight?: React.ReactNode;
+ children: React.ReactNode;
+}
+
+export const DashboardCard = ({ title, topRight, children }: DashboardCardProps) => {
+ const theme = useTheme();
+
+ return (
+
+
+
+ {title}
+
+ {topRight && (
+
+ {topRight}
+
+ )}
+
+ {children}
+
+ );
+};
diff --git a/client/src/Pages/Dashboard/components/ProgressBar.tsx b/client/src/Pages/Dashboard/components/ProgressBar.tsx
new file mode 100644
index 000000000..6e0f86837
--- /dev/null
+++ b/client/src/Pages/Dashboard/components/ProgressBar.tsx
@@ -0,0 +1,28 @@
+import { LAYOUT } from "@/Utils/Theme/constants";
+import Box from "@mui/material/Box";
+import { useTheme } from "@mui/material/styles";
+
+interface ProgressBarProps {
+ fillRatio: number;
+}
+
+export const ProgressBar = ({ fillRatio }: ProgressBarProps) => {
+ const theme = useTheme();
+ const clamped = Math.min(1, Math.max(0, fillRatio));
+
+ return (
+
+
+
+ );
+};
diff --git a/client/src/Pages/Dashboard/components/cards/BarRow.tsx b/client/src/Pages/Dashboard/components/cards/BarRow.tsx
new file mode 100644
index 000000000..af87105e4
--- /dev/null
+++ b/client/src/Pages/Dashboard/components/cards/BarRow.tsx
@@ -0,0 +1,42 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import { typographyLevels } from "@/Utils/Theme/Palette";
+import { SPACING } from "@/Utils/Theme/constants";
+import { ProgressBar } from "@/Pages/Dashboard/components/ProgressBar";
+
+interface BarRowProps {
+ label: string;
+ value: string;
+ fillRatio: number;
+ valueColor?: string;
+}
+
+export const BarRow = ({ label, value, fillRatio, valueColor }: BarRowProps) => {
+ const theme = useTheme();
+
+ return (
+
+
+
+ {label}
+
+
+ {value}
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Dashboard/components/cards/MonitorStatusCard.tsx b/client/src/Pages/Dashboard/components/cards/MonitorStatusCard.tsx
new file mode 100644
index 000000000..10016e8f2
--- /dev/null
+++ b/client/src/Pages/Dashboard/components/cards/MonitorStatusCard.tsx
@@ -0,0 +1,49 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import { DashboardCard } from "@/Pages/Dashboard/components/DashboardCard";
+import { ProgressBar } from "@/Pages/Dashboard/components/ProgressBar";
+import { typographyLevels } from "@/Utils/Theme/Palette";
+import { SPACING } from "@/Utils/Theme/constants";
+import { useTranslation } from "react-i18next";
+import type { MonitorsSummary } from "@/Types/Monitor";
+
+interface MonitorStatusCardProps {
+ summary: MonitorsSummary;
+}
+
+export const MonitorStatusCard = ({ summary }: MonitorStatusCardProps) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+
+ const up = summary.upMonitors;
+ const total = summary.totalMonitors;
+ const ratio = total > 0 ? up / total : 0;
+
+ return (
+
+
+
+
+ {up}
+
+
+ {t("pages.dashboard.monitorStatus.ofUp", { total })}
+
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/Dashboard/components/cards/MonitorsByTypeCard.tsx b/client/src/Pages/Dashboard/components/cards/MonitorsByTypeCard.tsx
new file mode 100644
index 000000000..2922f708f
--- /dev/null
+++ b/client/src/Pages/Dashboard/components/cards/MonitorsByTypeCard.tsx
@@ -0,0 +1,35 @@
+import Stack from "@mui/material/Stack";
+import { DashboardCard } from "@/Pages/Dashboard/components/DashboardCard";
+import { BarRow } from "@/Pages/Dashboard/components/cards/BarRow";
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@mui/material/styles";
+import type { DashboardTypeCount } from "@/Pages/Dashboard/types";
+import { getMonitorTypeLabel } from "@/Types/Monitor";
+import { LAYOUT } from "@/Utils/Theme/constants";
+
+interface MonitorsByTypeCardProps {
+ monitorsByType: DashboardTypeCount[];
+}
+
+export const MonitorsByTypeCard = ({ monitorsByType }: MonitorsByTypeCardProps) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ const maxCount = monitorsByType[0]?.count ?? 1;
+
+ return (
+
+
+ {monitorsByType.map(({ type, count }) => (
+
+ ))}
+
+
+ );
+};
diff --git a/client/src/Pages/Dashboard/index.tsx b/client/src/Pages/Dashboard/index.tsx
new file mode 100644
index 000000000..9d48fb5a4
--- /dev/null
+++ b/client/src/Pages/Dashboard/index.tsx
@@ -0,0 +1,45 @@
+import Box from "@mui/material/Box";
+import { useTheme } from "@mui/material/styles";
+
+import { BasePage } from "@/Components/design-elements/BasePage";
+import { LAYOUT } from "@/Utils/Theme/constants";
+
+import { MonitorStatusCard } from "@/Pages/Dashboard/components/cards/MonitorStatusCard";
+import { MonitorsByTypeCard } from "@/Pages/Dashboard/components/cards/MonitorsByTypeCard";
+import type { MonitorsSummary } from "@/Types/Monitor";
+import type { DashboardTypeCount } from "@/Pages/Dashboard/types";
+
+const MOCK_SUMMARY: MonitorsSummary = {
+ totalMonitors: 12,
+ upMonitors: 10,
+ downMonitors: 1,
+ pausedMonitors: 1,
+ initializingMonitors: 0,
+ maintenanceMonitors: 0,
+ breachedMonitors: 0,
+};
+
+const MOCK_MONITORS_BY_TYPE: DashboardTypeCount[] = [
+ { type: "http", count: 7 },
+ { type: "ping", count: 3 },
+ { type: "port", count: 2 },
+];
+
+const Dashboard = () => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default Dashboard;
diff --git a/client/src/Pages/Dashboard/types.ts b/client/src/Pages/Dashboard/types.ts
new file mode 100644
index 000000000..1b9b75d60
--- /dev/null
+++ b/client/src/Pages/Dashboard/types.ts
@@ -0,0 +1,6 @@
+import type { MonitorType } from "@/Types/Monitor";
+
+export interface DashboardTypeCount {
+ type: MonitorType;
+ count: number;
+}
diff --git a/client/src/Routes/index.tsx b/client/src/Routes/index.tsx
index 37e49d1ee..937fd0f93 100644
--- a/client/src/Routes/index.tsx
+++ b/client/src/Routes/index.tsx
@@ -8,6 +8,9 @@ import AuthRegister from "@/Pages/Auth/Register";
import AuthForgotPassword from "@/Pages/Auth/Recovery";
import AuthSetNewPassword from "@/Pages/Auth/SetNewPassword";
+// Dashboard
+import Dashboard from "@/Pages/Dashboard";
+
// Uptime
import Uptime from "@/Pages/Uptime/Monitors";
import UptimeDetails from "@/Pages/Uptime/Details";
@@ -92,6 +95,10 @@ const Routes = () => {
path="/"
element={}
/>
+ }
+ />
}
diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts
index 892e2b549..664077ea0 100644
--- a/client/src/Types/Monitor.ts
+++ b/client/src/Types/Monitor.ts
@@ -60,6 +60,13 @@ export const monitorTypeLabelKey: Record &
pagespeed: "optionPagespeed",
};
+export const getMonitorTypeLabel = (
+ type: MonitorType,
+ t: (key: string) => string
+): string => {
+ const monitorLabel = monitorTypeLabelKey[type];
+ return monitorLabel ? t(`pages.common.monitors.monitorTypes.${monitorLabel}`) : type;
+};
export const MIN_MONITOR_INTERVAL_MS = 10000;
export const MonitorIntervalOptions = [
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 078722f53..e9ec4110e 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -226,7 +226,8 @@
"alerting": "Alerting",
"reporting": "Reporting",
"configuration": "Configuration"
- }
+ },
+ "dashboard": "Dashboard"
},
"bottomMenu": {
"support": "Support",
@@ -1908,6 +1909,19 @@
"title": "Uptime monitors",
"description": "Watch HTTP endpoints, pings, containers, and ports — and get alerted the moment something goes down."
}
+ },
+ "dashboard": {
+ "monitorStatus": {
+ "ofUp": "of {{total}} up"
+ },
+ "cards": {
+ "monitorStatus": "Monitor status",
+ "monitorsByType": "Monitors by type"
+ },
+ "header": {
+ "title": "Dashboard",
+ "description": "See every monitor type in one place and spot what needs attention."
+ }
}
}
}