From 7ef10b4fcd8ac6af1f29b98c3e8b2de3b3d7269f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 17:53:00 +0000 Subject: [PATCH 01/26] initial commit --- client/src/App.tsx | 2 +- .../Components/v1/Layouts/AppLayout/index.jsx | 31 -------- .../v2/design-elements/OfflineBanner.tsx | 76 +++++++++++++++++++ .../Components/v2/design-elements/index.tsx | 1 + client/src/Components/v2/layout/AppLayout.tsx | 39 ++++++++++ .../src/Components/v2/layout/RootLayout.tsx | 0 client/src/Components/v2/sidebar/index.tsx | 0 client/src/Utils/ApiClient.ts | 15 ++++ client/src/locales/en.json | 6 ++ 9 files changed, 138 insertions(+), 32 deletions(-) delete mode 100644 client/src/Components/v1/Layouts/AppLayout/index.jsx create mode 100644 client/src/Components/v2/design-elements/OfflineBanner.tsx create mode 100644 client/src/Components/v2/layout/AppLayout.tsx create mode 100644 client/src/Components/v2/layout/RootLayout.tsx create mode 100644 client/src/Components/v2/sidebar/index.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index ef3625cad..0a6b364f8 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -8,7 +8,7 @@ import darkTheme from "./Utils/Theme/darkTheme"; import { CssBaseline, GlobalStyles } from "@mui/material"; import { logger } from "./Utils/Logger"; // Import the logger import { Routes } from "./Routes"; -import AppLayout from "@/Components/v1/Layouts/AppLayout"; +import AppLayout from "@/Components/v2/layout/AppLayout"; import type { RootState } from "@/Types/state"; function App() { diff --git a/client/src/Components/v1/Layouts/AppLayout/index.jsx b/client/src/Components/v1/Layouts/AppLayout/index.jsx deleted file mode 100644 index 0dbfb1e7a..000000000 --- a/client/src/Components/v1/Layouts/AppLayout/index.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import Box from "@mui/material/Box"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import BackgroundSVG from "../../../../assets/Images/background.svg"; -import { useSelector } from "react-redux"; - -const AppLayout = ({ children }) => { - const theme = useTheme(); - const ui = useSelector((state) => state.ui); - return ( - - {children} - - ); -}; - -AppLayout.propTypes = { - children: PropTypes.node, -}; - -export default AppLayout; diff --git a/client/src/Components/v2/design-elements/OfflineBanner.tsx b/client/src/Components/v2/design-elements/OfflineBanner.tsx new file mode 100644 index 000000000..fa5690c12 --- /dev/null +++ b/client/src/Components/v2/design-elements/OfflineBanner.tsx @@ -0,0 +1,76 @@ +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { WifiOff } from "lucide-react"; + +interface OfflineBannerProps { + visible: boolean; + onRetry?: () => void; + isRetrying?: boolean; +} + +export const OfflineBanner = ({ visible, onRetry, isRetrying }: OfflineBannerProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + + if (!visible) { + return null; + } + + return ( + + + + + {t("components.offlineBanner.serverUnreachable")} + + {onRetry && ( + + )} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index b7b1d484e..9a4b97a44 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -19,3 +19,4 @@ export * from "./Gauge"; export * from "./Tabs"; export * from "./SplitBox"; export * from "./TextLink"; +export * from "./OfflineBanner"; diff --git a/client/src/Components/v2/layout/AppLayout.tsx b/client/src/Components/v2/layout/AppLayout.tsx new file mode 100644 index 000000000..1310863f9 --- /dev/null +++ b/client/src/Components/v2/layout/AppLayout.tsx @@ -0,0 +1,39 @@ +import Box from "@mui/material/Box"; +import { ThemeProvider, useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import BackgroundSVG from "@/assets/Images/background.svg"; +import type { RootState } from "@/Types/state"; +import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; +import { OfflineBanner } from "@/Components/v2/design-elements"; + +interface AppLayoutProps { + children: React.ReactNode; +} + +const AppLayout = ({ children }: AppLayoutProps) => { + const theme = useTheme(); + const mode = useSelector((state: RootState) => state.ui.mode); + const v2theme = mode === "dark" ? darkTheme : lightTheme; + + return ( + + + + {children} + + + ); +}; + +export default AppLayout; diff --git a/client/src/Components/v2/layout/RootLayout.tsx b/client/src/Components/v2/layout/RootLayout.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts index c39839558..4b2f4971f 100644 --- a/client/src/Utils/ApiClient.ts +++ b/client/src/Utils/ApiClient.ts @@ -15,6 +15,15 @@ type StoreType = { let storeInstance: StoreType | null = null; let interceptorsInitialized = false; +type ServerUnreachableCallback = (unreachable: boolean) => void; +let serverUnreachableCallback: ServerUnreachableCallback | null = null; + +export const setServerUnreachableCallback = ( + callback: ServerUnreachableCallback +): void => { + serverUnreachableCallback = callback; +}; + export const initApiClient = (store: StoreType): void => { storeInstance = store; @@ -41,6 +50,12 @@ export const initApiClient = (store: StoreType): void => { const onSuccess = (response: AxiosResponse) => response; const onError = (error: AxiosError) => { + // Handle network errors (server unreachable) + if (error.code === "ERR_NETWORK") { + serverUnreachableCallback?.(true); + return Promise.reject(error); + } + if (error.response?.status === 401) { if (window.location.pathname !== "/login") { window.location.href = "/login"; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index cbd9ea7b7..096fabd9d 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -325,6 +325,12 @@ "week": "Showing statistics for past 7 days.", "month": "Showing statistics for past 30 days." } + }, + "offlineBanner": { + "serverUnreachable": "Unable to reach server", + "retry": "Retry", + "retrying": "Retrying...", + "reconnected": "Connection restored" } }, "configure": "Configure", From d1d96bd2baddfb0eb0cde646e2ec77b765091b43 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 19:21:57 +0000 Subject: [PATCH 02/26] sidebar --- .../v1/Layouts/HomeLayout/index.jsx | 1 - client/src/Components/v2/layout/AppLayout.tsx | 32 ++--- .../src/Components/v2/layout/RootLayout.tsx | 46 ++++++++ client/src/Components/v2/sidebar/Logo.tsx | 60 ++++++++++ client/src/Components/v2/sidebar/Menu.tsx | 41 +++++++ client/src/Components/v2/sidebar/NavItem.tsx | 110 ++++++++++++++++++ client/src/Components/v2/sidebar/index.tsx | 69 +++++++++++ client/src/Hooks/useSidebar.js | 29 ----- client/src/Hooks/useSidebar.ts | 25 ++++ client/src/Routes/index.jsx | 4 +- 10 files changed, 369 insertions(+), 48 deletions(-) create mode 100644 client/src/Components/v2/sidebar/Logo.tsx create mode 100644 client/src/Components/v2/sidebar/Menu.tsx create mode 100644 client/src/Components/v2/sidebar/NavItem.tsx delete mode 100644 client/src/Hooks/useSidebar.js create mode 100644 client/src/Hooks/useSidebar.ts diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.jsx b/client/src/Components/v1/Layouts/HomeLayout/index.jsx index b078fe048..adadf638f 100644 --- a/client/src/Components/v1/Layouts/HomeLayout/index.jsx +++ b/client/src/Components/v1/Layouts/HomeLayout/index.jsx @@ -14,7 +14,6 @@ const HomeLayout = () => { flexDirection="row" > - {/* Spacer for fixed sidebar */} { const v2theme = mode === "dark" ? darkTheme : lightTheme; return ( - - + + - {children} - - + + {children} + ); }; diff --git a/client/src/Components/v2/layout/RootLayout.tsx b/client/src/Components/v2/layout/RootLayout.tsx index e69de29bb..b0c0effe1 100644 --- a/client/src/Components/v2/layout/RootLayout.tsx +++ b/client/src/Components/v2/layout/RootLayout.tsx @@ -0,0 +1,46 @@ +import { Sidebar } from "@/Components/v2/sidebar"; +import { Outlet } from "react-router"; +import Stack from "@mui/material/Stack"; + +// TEMP +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; +import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; +import { ThemeProvider, useTheme } from "@mui/material"; + +const RootLayout = () => { + const mode = useSelector((state: RootState) => state.ui.mode); + const v2theme = mode === "dark" ? darkTheme : lightTheme; + const theme = useTheme(); + return ( + + + + + + + + + + + ); +}; + +export default RootLayout; diff --git a/client/src/Components/v2/sidebar/Logo.tsx b/client/src/Components/v2/sidebar/Logo.tsx new file mode 100644 index 000000000..131ac03bc --- /dev/null +++ b/client/src/Components/v2/sidebar/Logo.tsx @@ -0,0 +1,60 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { toggleSidebar } from "@/Features/UI/uiSlice.js"; +import { useDispatch } from "react-redux"; +import { useTranslation } from "react-i18next"; +import useSidebar from "@/Hooks/useSidebar"; + +export const Logo = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const dispatch = useDispatch(); + const { collapsed } = useSidebar(); + return ( + { + dispatch(toggleSidebar()); + }} + sx={{ cursor: "pointer" }} + > + + C + + + {" "} + + {t("common.appName")} + + + + ); +}; diff --git a/client/src/Components/v2/sidebar/Menu.tsx b/client/src/Components/v2/sidebar/Menu.tsx new file mode 100644 index 000000000..ad58f4455 --- /dev/null +++ b/client/src/Components/v2/sidebar/Menu.tsx @@ -0,0 +1,41 @@ +import { Icon } from "@/Components/v2/design-elements"; + +import { + Globe, + Gauge, + Link, + Bell, + FileText, + AlertTriangle, + Wifi, + Wrench, + Database, + Settings, +} from "lucide-react"; + +export const getMenu = (t: Function) => { + return [ + { name: t("menu.uptime"), path: "uptime", icon: }, + { name: t("menu.pagespeed"), path: "pagespeed", icon: }, + { + name: t("menu.infrastructure"), + path: "infrastructure", + icon: , + }, + { + name: t("menu.notifications"), + path: "notifications", + icon: , + }, + { name: t("menu.checks"), path: "checks", icon: }, + { name: t("menu.incidents"), path: "incidents", icon: }, + { name: t("menu.statusPages"), path: "status", icon: }, + { name: t("menu.maintenance"), path: "maintenance", icon: }, + { name: t("menu.logs"), path: "logs", icon: }, + { + name: t("menu.settings"), + icon: , + path: "settings", + }, + ]; +}; diff --git a/client/src/Components/v2/sidebar/NavItem.tsx b/client/src/Components/v2/sidebar/NavItem.tsx new file mode 100644 index 000000000..756b10d60 --- /dev/null +++ b/client/src/Components/v2/sidebar/NavItem.tsx @@ -0,0 +1,110 @@ +import Tooltip from "@mui/material/Tooltip"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import useSidebar from "@/Hooks/useSidebar"; + +export interface NavData { + name: string; + icon: React.ReactElement; +} + +export const NavItem = ({ + item, + selected, + onClick, +}: { + item: NavData; + selected: boolean; + onClick: (event: React.MouseEvent) => void; +}) => { + const { collapsed } = useSidebar(); + const theme = useTheme(); + const iconStroke = selected ? theme.palette.primary.main : theme.palette.text.secondary; + + const buttonBgColor = selected ? theme.palette.action.selected : "transparent"; + const buttonBgHoverColor = selected + ? theme.palette.action.selected + : theme.palette.action.hover; + const fontWeight = selected ? 600 : 400; + return ( + + + + {item.icon} + + + + {item.name} + + + + + ); +}; diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx index e69de29bb..10725c6a0 100644 --- a/client/src/Components/v2/sidebar/index.tsx +++ b/client/src/Components/v2/sidebar/index.tsx @@ -0,0 +1,69 @@ +import Stack from "@mui/material/Stack"; +import List from "@mui/material/List"; +import { useSidebar } from "@/Hooks/useSidebar.js"; +import { Logo } from "@/Components/v2/sidebar/Logo"; +import { getMenu } from "@/Components/v2/sidebar/Menu"; +import { NavItem } from "@/Components/v2/sidebar/NavItem"; + +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; +import { useTheme, useMediaQuery } from "@mui/material"; +import { useDispatch } from "react-redux"; +import { toggleSidebar } from "@/Features/UI/uiSlice"; + +export const Sidebar = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { t } = useTranslation(); + const { width, transition } = useSidebar(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const menu = getMenu(t); + + return ( + t.zIndex.drawer : "auto", + }} + > + + + {menu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + { + navigate(`/${item.path}`); + if (isSmall) { + dispatch(toggleSidebar()); + } + }} + /> + ); + })} + + + ); +}; diff --git a/client/src/Hooks/useSidebar.js b/client/src/Hooks/useSidebar.js deleted file mode 100644 index 97724b417..000000000 --- a/client/src/Hooks/useSidebar.js +++ /dev/null @@ -1,29 +0,0 @@ -import { useSelector } from "react-redux"; - -// CSS variable names for sidebar widths -const SIDEBAR_WIDTH_VAR = "var(--env-var-side-bar-width)"; -const SIDEBAR_COLLAPSED_WIDTH_VAR = "var(--env-var-side-bar-collapsed-width)"; - -// Transition timing for sidebar width changes -const SIDEBAR_TRANSITION = "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)"; - -/** - * Hook to get sidebar state and computed width - * Centralizes sidebar width logic to avoid duplication between Sidebar and HomeLayout - * - * @returns {Object} Sidebar state and styles - * @returns {boolean} collapsed - Whether the sidebar is collapsed - * @returns {string} width - CSS width value based on collapsed state - * @returns {string} transition - CSS transition for width changes - */ -export const useSidebar = () => { - const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); - - return { - collapsed, - width: collapsed ? SIDEBAR_COLLAPSED_WIDTH_VAR : SIDEBAR_WIDTH_VAR, - transition: SIDEBAR_TRANSITION, - }; -}; - -export default useSidebar; diff --git a/client/src/Hooks/useSidebar.ts b/client/src/Hooks/useSidebar.ts new file mode 100644 index 000000000..1cf705dd1 --- /dev/null +++ b/client/src/Hooks/useSidebar.ts @@ -0,0 +1,25 @@ +import type { RootState } from "@/Types/state"; +import { useSelector } from "react-redux"; + +// CSS variable names for sidebar widths +const SIDEBAR_WIDTH_VAR = 250; +const SIDEBAR_COLLAPSED_WIDTH_VAR = 64; + +// Transition timing for sidebar width changes +const SIDEBAR_TRANSITION = "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)"; + +export const useSidebar = () => { + const collapsed = useSelector( + (state: RootState) => state.ui.sidebar?.collapsed ?? false + ); + + return { + collapsed, + collapsedWidth: SIDEBAR_COLLAPSED_WIDTH_VAR, + expandedWidth: SIDEBAR_WIDTH_VAR, + width: collapsed ? SIDEBAR_COLLAPSED_WIDTH_VAR : SIDEBAR_WIDTH_VAR, + transition: SIDEBAR_TRANSITION, + }; +}; + +export default useSidebar; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 07dc1793c..0a66b0cf3 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -4,7 +4,7 @@ import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; import { useSelector } from "react-redux"; import { Navigate, Route, Routes as LibRoutes } from "react-router"; -import HomeLayout from "@/Components/v1/Layouts/HomeLayout"; +import RootLayout from "@/Components/v2/layout/RootLayout"; import NotFound from "@/Pages/NotFound"; // Auth @@ -67,7 +67,7 @@ const Routes = () => { path="/" element={ - + } > From 1db5f4b70f946e0f69ad0a631667cb5d0be46aa1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 21:54:31 +0000 Subject: [PATCH 03/26] sidebar --- .../src/Components/v2/layout/RootLayout.tsx | 7 ++++++- client/src/Components/v2/sidebar/Logo.tsx | 8 ++++---- client/src/Components/v2/sidebar/index.tsx | 19 ++++++++++++++----- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/client/src/Components/v2/layout/RootLayout.tsx b/client/src/Components/v2/layout/RootLayout.tsx index b0c0effe1..0b6defb91 100644 --- a/client/src/Components/v2/layout/RootLayout.tsx +++ b/client/src/Components/v2/layout/RootLayout.tsx @@ -1,8 +1,9 @@ import { Sidebar } from "@/Components/v2/sidebar"; import { Outlet } from "react-router"; import Stack from "@mui/material/Stack"; +import { useMediaQuery } from "@mui/material"; +import { useSidebar } from "@/Hooks/useSidebar"; -// TEMP import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; @@ -12,6 +13,9 @@ const RootLayout = () => { const mode = useSelector((state: RootState) => state.ui.mode); const v2theme = mode === "dark" ? darkTheme : lightTheme; const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const { collapsedWidth } = useSidebar(); + return ( @@ -28,6 +32,7 @@ const RootLayout = () => { : "rgba(0, 0, 0, 0.01)", display: "flex", alignItems: "center", + paddingLeft: isSmall ? `${collapsedWidth + 12}px` : 12, }} > { +export const Logo = (props: StackProps) => { const { t } = useTranslation(); const theme = useTheme(); const dispatch = useDispatch(); const { collapsed } = useSidebar(); return ( { dispatch(toggleSidebar()); }} sx={{ cursor: "pointer" }} + {...props} > { const dispatch = useDispatch(); const navigate = useNavigate(); + const location = useLocation(); const { t } = useTranslation(); const { width, transition } = useSidebar(); const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const menu = getMenu(t); + useEffect(() => { + dispatch(setCollapsed({ collapsed: isSmall })); + }, [isSmall, dispatch]); + return ( { height: "100%", }} > - + {menu.map((item) => { const selected = location.pathname.startsWith(`/${item.path}`); return ( @@ -57,7 +66,7 @@ export const Sidebar = () => { onClick={() => { navigate(`/${item.path}`); if (isSmall) { - dispatch(toggleSidebar()); + dispatch(setCollapsed({ collapsed: true })); } }} /> From 0f6c273dad4cae9d594edbf8d777bbdbdcde02d0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 22:17:13 +0000 Subject: [PATCH 04/26] bottom menu --- client/src/Components/v2/sidebar/Menu.tsx | 19 ++++++++ client/src/Components/v2/sidebar/index.tsx | 50 ++++++++++++++++++---- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/client/src/Components/v2/sidebar/Menu.tsx b/client/src/Components/v2/sidebar/Menu.tsx index ad58f4455..8292ea490 100644 --- a/client/src/Components/v2/sidebar/Menu.tsx +++ b/client/src/Components/v2/sidebar/Menu.tsx @@ -11,6 +11,12 @@ import { Wrench, Database, Settings, + HelpCircle, + MessageCircle, + Code, + User, + Lock, + Users, } from "lucide-react"; export const getMenu = (t: Function) => { @@ -39,3 +45,16 @@ export const getMenu = (t: Function) => { }, ]; }; + +export const getBottomMenu = (t: Function) => { + return [ + { name: t("menu.support"), path: "support", icon: }, + { + name: t("menu.discussions"), + path: "discussions", + icon: , + }, + { name: t("menu.docs"), path: "docs", icon: }, + { name: t("menu.changelog"), path: "changelog", icon: }, + ]; +}; diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx index 568703791..e278092eb 100644 --- a/client/src/Components/v2/sidebar/index.tsx +++ b/client/src/Components/v2/sidebar/index.tsx @@ -1,9 +1,10 @@ import { useEffect } from "react"; import Stack from "@mui/material/Stack"; import List from "@mui/material/List"; +import Divider from "@mui/material/Divider"; import { useSidebar } from "@/Hooks/useSidebar.js"; import { Logo } from "@/Components/v2/sidebar/Logo"; -import { getMenu } from "@/Components/v2/sidebar/Menu"; +import { getMenu, getBottomMenu } from "@/Components/v2/sidebar/Menu"; import { NavItem } from "@/Components/v2/sidebar/NavItem"; import { useNavigate, useLocation } from "react-router"; @@ -12,6 +13,13 @@ import { useTheme, useMediaQuery } from "@mui/material"; import { useDispatch } from "react-redux"; import { setCollapsed } from "@/Features/UI/uiSlice"; +const URL_MAP: Record = { + support: "https://discord.com/invite/NAb6H3UTjK", + discussions: "https://github.com/bluewave-labs/checkmate/discussions", + docs: "https://bluewavelabs.gitbook.io/checkmate", + changelog: "https://github.com/bluewave-labs/checkmate/releases", +}; + export const Sidebar = () => { const dispatch = useDispatch(); const navigate = useNavigate(); @@ -21,11 +29,24 @@ export const Sidebar = () => { const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const menu = getMenu(t); + const bottomMenu = getBottomMenu(t); useEffect(() => { dispatch(setCollapsed({ collapsed: isSmall })); }, [isSmall, dispatch]); + const handleNavClick = (path: string) => { + const url = URL_MAP[path]; + if (url) { + window.open(url, "_blank", "noreferrer"); + } else { + navigate(`/${path}`); + } + if (isSmall) { + dispatch(setCollapsed({ collapsed: true })); + } + }; + return ( { disablePadding sx={{ px: theme.spacing(6), - height: "100%", + flex: 1, }} > { key={item.path} item={item} selected={selected} - onClick={() => { - navigate(`/${item.path}`); - if (isSmall) { - dispatch(setCollapsed({ collapsed: true })); - } - }} + onClick={() => handleNavClick(item.path)} /> ); })} + + {bottomMenu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + handleNavClick(item.path)} + /> + ); + })} + + ); }; From a289a197a11b8a760ec43da8dd6eb3db70f9fbd6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 22:24:35 +0000 Subject: [PATCH 05/26] starprompt --- .../src/Components/v2/sidebar/StarPrompt.tsx | 115 ++++++++++++++++++ client/src/Components/v2/sidebar/index.tsx | 2 + 2 files changed, 117 insertions(+) create mode 100644 client/src/Components/v2/sidebar/StarPrompt.tsx diff --git a/client/src/Components/v2/sidebar/StarPrompt.tsx b/client/src/Components/v2/sidebar/StarPrompt.tsx new file mode 100644 index 000000000..cacc442cc --- /dev/null +++ b/client/src/Components/v2/sidebar/StarPrompt.tsx @@ -0,0 +1,115 @@ +import { Typography, IconButton, Stack, Box } from "@mui/material"; +import { Icon } from "@/Components/v2/design-elements"; +import { X } from "lucide-react"; + +import { useTheme } from "@mui/material"; +import { useSelector, useDispatch } from "react-redux"; +import { useTranslation } from "react-i18next"; +import { setStarPromptOpen } from "@/Features/UI/uiSlice.js"; +import type { RootState } from "@/Types/state.js"; +import useSidebar from "@/Hooks/useSidebar.js"; + +export const StarPrompt = ({ + repoUrl = "https://github.com/bluewave-labs/checkmate", +}: { + repoUrl?: string; +}) => { + const theme = useTheme(); + const dispatch = useDispatch(); + const { t } = useTranslation(); + const isOpen = useSelector((state: RootState) => state.ui?.starPromptOpen ?? true); + const mode = useSelector((state: RootState) => state.ui.mode); + const { collapsed } = useSidebar(); + const handleClose = () => { + dispatch(setStarPromptOpen(false)); + }; + + const handleStarClick = () => { + window.open(repoUrl, "_blank"); + }; + + if (collapsed) return null; + if (!isOpen) return null; + + return ( + + + + {t("starPromptTitle")} + + + + + + + + {t("starPromptDescription")} + + + + + ); +}; diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx index e278092eb..e3056c9f4 100644 --- a/client/src/Components/v2/sidebar/index.tsx +++ b/client/src/Components/v2/sidebar/index.tsx @@ -6,6 +6,7 @@ import { useSidebar } from "@/Hooks/useSidebar.js"; import { Logo } from "@/Components/v2/sidebar/Logo"; import { getMenu, getBottomMenu } from "@/Components/v2/sidebar/Menu"; import { NavItem } from "@/Components/v2/sidebar/NavItem"; +import { StarPrompt } from "@/Components/v2/sidebar/StarPrompt"; import { useNavigate, useLocation } from "react-router"; import { useTranslation } from "react-i18next"; @@ -89,6 +90,7 @@ export const Sidebar = () => { ); })} + Date: Tue, 10 Feb 2026 22:28:47 +0000 Subject: [PATCH 06/26] styling --- client/src/Components/v2/sidebar/StarPrompt.tsx | 12 +----------- client/src/locales/en.json | 7 ++++++- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/client/src/Components/v2/sidebar/StarPrompt.tsx b/client/src/Components/v2/sidebar/StarPrompt.tsx index cacc442cc..665f0a440 100644 --- a/client/src/Components/v2/sidebar/StarPrompt.tsx +++ b/client/src/Components/v2/sidebar/StarPrompt.tsx @@ -52,13 +52,7 @@ export const StarPrompt = ({ > {t("starPromptTitle")} @@ -68,7 +62,6 @@ export const StarPrompt = ({ sx={{ color: theme.palette.text.primary, padding: 0, - marginTop: theme.spacing(-5), "&:hover": { backgroundColor: "transparent", opacity: 0.8, @@ -83,10 +76,7 @@ export const StarPrompt = ({ Date: Tue, 10 Feb 2026 22:47:39 +0000 Subject: [PATCH 07/26] auth footer --- .../Components/v2/design-elements/Avatar.tsx | 48 +++ .../Components/v2/design-elements/index.tsx | 1 + .../src/Components/v2/sidebar/Authfooter.tsx | 279 ++++++++++++++++++ client/src/Components/v2/sidebar/Menu.tsx | 88 +++++- .../src/Components/v2/sidebar/StarPrompt.tsx | 4 +- client/src/Components/v2/sidebar/index.tsx | 20 +- client/src/locales/en.json | 34 ++- 7 files changed, 454 insertions(+), 20 deletions(-) create mode 100644 client/src/Components/v2/design-elements/Avatar.tsx create mode 100644 client/src/Components/v2/sidebar/Authfooter.tsx diff --git a/client/src/Components/v2/design-elements/Avatar.tsx b/client/src/Components/v2/design-elements/Avatar.tsx new file mode 100644 index 000000000..1adec4741 --- /dev/null +++ b/client/src/Components/v2/design-elements/Avatar.tsx @@ -0,0 +1,48 @@ +import { Avatar as MuiAvatar } from "@mui/material"; +import { useSelector } from "react-redux"; +import { useEffect, useState } from "react"; +import { useTheme } from "@mui/material"; +import type { RootState } from "@/Types/state"; + +interface AvatarProps { + src?: string; + small?: boolean; + sx?: object; + onClick?: Function; +} + +export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => { + const { user } = useSelector((state: RootState) => state.auth); + const theme = useTheme(); + if (!user) return null; + + const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 }; + const border = small ? 1 : 3; + + const [image, setImage] = useState(); + useEffect(() => { + if (user.avatarImage) { + setImage(`data:image/png;base64,${user.avatarImage}`); + } + }, [user?.avatarImage]); + + return ( + + {user.firstName?.charAt(0)} + {user.lastName?.charAt(0) || ""} + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index 9a4b97a44..ed6a885f6 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -20,3 +20,4 @@ export * from "./Tabs"; export * from "./SplitBox"; export * from "./TextLink"; export * from "./OfflineBanner"; +export * from "./Avatar"; diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx new file mode 100644 index 000000000..cc84dc0e2 --- /dev/null +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -0,0 +1,279 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Tooltip from "@mui/material/Tooltip"; +import IconButton from "@mui/material/IconButton"; +import { Avatar, Icon } from "@/Components/v2/design-elements"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import Divider from "@mui/material/Divider"; +import { MoreVertical, LogOut } from "lucide-react"; + +import { useTheme } from "@mui/material"; +import { useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import { useNavigate } from "react-router"; +import { clearAuthState } from "@/Features/Auth/authSlice.js"; +import { useDispatch } from "react-redux"; +import PropTypes from "prop-types"; +import type { RootState } from "@/Types/state.js"; + +const getFilteredAccountMenuItems = (user: any, items: any[]) => { + if (!user) return []; + + let filtered = [...items]; + + if (user.role?.includes("demo")) { + filtered = filtered.filter((item) => item.name !== "Password"); + } + + if (!user.role?.includes("superadmin")) { + filtered = filtered.filter((item) => item.name !== "Team"); + } + + return filtered; +}; + +const getRoleDisplayText = (user: any, t: Function) => { + if (!user?.role) return ""; + + if (user.role.includes("superadmin")) return t("roles.superAdmin"); + if (user.role.includes("admin")) return t("roles.admin"); + if (user.role.includes("user")) return t("roles.teamMember"); + if (user.role.includes("demo")) return t("roles.demoUser"); + + return user.role; +}; + +const AuthFooter = ({ + collapsed, + accountMenuItems, +}: { + collapsed: boolean; + accountMenuItems: any[]; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const authState = useSelector((state: RootState) => state.auth); + const navigate = useNavigate(); + const dispatch = useDispatch(); + + const [anchorEl, setAnchorEl] = useState(null); + + const openPopup = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const closePopup = () => { + setAnchorEl(null); + }; + + const logout = async () => { + dispatch(clearAuthState()); + navigate("/login"); + }; + const renderAccountMenuItems = (user: any, items: any[]) => { + const filteredItems = getFilteredAccountMenuItems(user, items); + + return filteredItems.map((item) => ( + { + closePopup(); + navigate(item.path); + }} + sx={{ + gap: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + pl: theme.spacing(4), + }} + > + {item.icon} + {item.name} + + )); + }; + return ( + + { + openPopup(e); + }} + sx={{ + cursor: collapsed ? "pointer" : "default", + }} + /> + + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {getRoleDisplayText(authState.user, t)} + + + + openPopup(event)} + > + + + + + + {collapsed && ( + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {authState.user?.role} + + + + )} + {/* TODO Do we need two dividers? */} + {collapsed && } + {/* */} + {renderAccountMenuItems(authState.user, accountMenuItems)} + + + {t("menu.logOut", "Log out")} + + + + ); +}; + +AuthFooter.propTypes = { + collapsed: PropTypes.bool, + accountMenuItems: PropTypes.array, +}; + +export default AuthFooter; diff --git a/client/src/Components/v2/sidebar/Menu.tsx b/client/src/Components/v2/sidebar/Menu.tsx index 8292ea490..918a82858 100644 --- a/client/src/Components/v2/sidebar/Menu.tsx +++ b/client/src/Components/v2/sidebar/Menu.tsx @@ -21,25 +21,53 @@ import { export const getMenu = (t: Function) => { return [ - { name: t("menu.uptime"), path: "uptime", icon: }, - { name: t("menu.pagespeed"), path: "pagespeed", icon: }, { - name: t("menu.infrastructure"), + name: t("components.sidebar.menu.uptime"), + path: "uptime", + icon: , + }, + { + name: t("components.sidebar.menu.pagespeed"), + path: "pagespeed", + icon: , + }, + { + name: t("components.sidebar.menu.infrastructure"), path: "infrastructure", icon: , }, { - name: t("menu.notifications"), + name: t("components.sidebar.menu.notifications"), path: "notifications", icon: , }, - { name: t("menu.checks"), path: "checks", icon: }, - { name: t("menu.incidents"), path: "incidents", icon: }, - { name: t("menu.statusPages"), path: "status", icon: }, - { name: t("menu.maintenance"), path: "maintenance", icon: }, - { name: t("menu.logs"), path: "logs", icon: }, { - name: t("menu.settings"), + name: t("components.sidebar.menu.checks"), + path: "checks", + icon: , + }, + { + name: t("components.sidebar.menu.incidents"), + path: "incidents", + icon: , + }, + { + name: t("components.sidebar.menu.statusPages"), + path: "status", + icon: , + }, + { + name: t("components.sidebar.menu.maintenance"), + path: "maintenance", + icon: , + }, + { + name: t("components.sidebar.menu.logs"), + path: "logs", + icon: , + }, + { + name: t("components.sidebar.menu.settings"), icon: , path: "settings", }, @@ -48,13 +76,45 @@ export const getMenu = (t: Function) => { export const getBottomMenu = (t: Function) => { return [ - { name: t("menu.support"), path: "support", icon: }, { - name: t("menu.discussions"), + name: t("components.sidebar.bottomMenu.support"), + path: "support", + icon: , + }, + { + name: t("components.sidebar.bottomMenu.discussions"), path: "discussions", icon: , }, - { name: t("menu.docs"), path: "docs", icon: }, - { name: t("menu.changelog"), path: "changelog", icon: }, + { + name: t("components.sidebar.bottomMenu.docs"), + path: "docs", + icon: , + }, + { + name: t("components.sidebar.bottomMenu.changelog"), + path: "changelog", + icon: , + }, + ]; +}; + +export const getAccountMenu = (t: Function) => { + return [ + { + name: t("components.sidebar.accountMenu.profile"), + path: "account/profile", + icon: , + }, + { + name: t("components.sidebar.accountMenu.password"), + path: "account/password", + icon: , + }, + { + name: t("components.sidebar.accountMenu.team"), + path: "account/team", + icon: , + }, ]; }; diff --git a/client/src/Components/v2/sidebar/StarPrompt.tsx b/client/src/Components/v2/sidebar/StarPrompt.tsx index 665f0a440..5319f6bcc 100644 --- a/client/src/Components/v2/sidebar/StarPrompt.tsx +++ b/client/src/Components/v2/sidebar/StarPrompt.tsx @@ -54,7 +54,7 @@ export const StarPrompt = ({ variant="subtitle2" mt={theme.spacing(3)} > - {t("starPromptTitle")} + {t("components.sidebar.starPrompt.title")} - {t("starPromptDescription")} + {t("components.sidebar.starPrompt.description")} { const isSmall = useMediaQuery(theme.breakpoints.down("md")); const menu = getMenu(t); const bottomMenu = getBottomMenu(t); + const accountMenu = getAccountMenu(t); useEffect(() => { dispatch(setCollapsed({ collapsed: isSmall })); @@ -109,6 +110,23 @@ export const Sidebar = () => { })} + + {accountMenu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + handleNavClick(item.path)} + /> + ); + })} + ); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index a9090e5bc..dd747b5e4 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -333,10 +333,38 @@ "reconnected": "Connection restored" }, "sidebar": { - "menu": {}, - "bottomMenu": {} + "menu": { + "uptime": "Uptime", + "pagespeed": "Pagespeed", + "infrastructure": "Infrastructure", + "notifications": "Notifications", + "checks": "Checks", + "incidents": "Incidents", + "statusPages": "Status pages", + "maintenance": "Maintenance", + "logs": "Logs", + "settings": "Settings" + }, + "bottomMenu": { + "support": "Support", + "discussions": "Discussions", + "docs": "Docs", + "changelog": "Changelog" + }, + "accountMenu": { + "profile": "Profile", + "password": "Password", + "team": "Team" + }, + "starPrompt": { + "title": "Star Checkmate", + "description": "See the latest releases and help grow the community on GitHub" + } }, - "starPrompt": {} + "starPrompt": { + "title": "Star Checkmate", + "description": "See the latest releases and help grow the community on GitHub" + } }, "configure": "Configure", "confirmPassword": "Confirm password", From a390ee5f21343d308fbfb48ebbecde5ae8a67d32 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 22:54:23 +0000 Subject: [PATCH 08/26] auth footer --- .../Components/v2/design-elements/Avatar.tsx | 3 +-- .../src/Components/v2/sidebar/Authfooter.tsx | 11 -------- client/src/Components/v2/sidebar/index.tsx | 25 ++++++------------- 3 files changed, 8 insertions(+), 31 deletions(-) diff --git a/client/src/Components/v2/design-elements/Avatar.tsx b/client/src/Components/v2/design-elements/Avatar.tsx index 1adec4741..74c06f8f3 100644 --- a/client/src/Components/v2/design-elements/Avatar.tsx +++ b/client/src/Components/v2/design-elements/Avatar.tsx @@ -8,7 +8,7 @@ interface AvatarProps { src?: string; small?: boolean; sx?: object; - onClick?: Function; + onClick?: (event: React.MouseEvent) => void; } export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => { @@ -17,7 +17,6 @@ export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => { if (!user) return null; const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 }; - const border = small ? 1 : 3; const [image, setImage] = useState(); useEffect(() => { diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index cc84dc0e2..3194cf44a 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -6,7 +6,6 @@ import IconButton from "@mui/material/IconButton"; import { Avatar, Icon } from "@/Components/v2/design-elements"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -import Divider from "@mui/material/Divider"; import { MoreVertical, LogOut } from "lucide-react"; import { useTheme } from "@mui/material"; @@ -97,7 +96,6 @@ const AuthFooter = ({ return ( @@ -240,7 +233,6 @@ const AuthFooter = ({ fontSize: 12, whiteSpace: "nowrap", overflow: "visible", - // wordBreak: "break-word", }} > {authState.user?.role} @@ -248,9 +240,6 @@ const AuthFooter = ({ )} - {/* TODO Do we need two dividers? */} - {collapsed && } - {/* */} {renderAccountMenuItems(authState.user, accountMenuItems)} { const navigate = useNavigate(); const location = useLocation(); const { t } = useTranslation(); - const { width, transition } = useSidebar(); + const { width, transition, collapsed } = useSidebar(); const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const menu = getMenu(t); @@ -110,23 +111,11 @@ export const Sidebar = () => { })} - - {accountMenu.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - return ( - handleNavClick(item.path)} - /> - ); - })} - + + ); }; From 4c5c2d2d5e74ad275270a77d4a081cf412523a09 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 23:12:33 +0000 Subject: [PATCH 09/26] auth footer --- .../Components/v2/design-elements/Icon.tsx | 1 + .../src/Components/v2/sidebar/Authfooter.tsx | 27 +++++++++++-------- client/src/Components/v2/sidebar/index.tsx | 2 +- client/src/locales/en.json | 3 ++- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/client/src/Components/v2/design-elements/Icon.tsx b/client/src/Components/v2/design-elements/Icon.tsx index b32cd84b2..22f939750 100644 --- a/client/src/Components/v2/design-elements/Icon.tsx +++ b/client/src/Components/v2/design-elements/Icon.tsx @@ -4,6 +4,7 @@ interface IconProps { icon: LucideIcon; size?: number; strokeWidth?: number; + stroke?: string; } const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => { diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index 3194cf44a..d63663a01 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -15,7 +15,6 @@ import { useState } from "react"; import { useNavigate } from "react-router"; import { clearAuthState } from "@/Features/Auth/authSlice.js"; import { useDispatch } from "react-redux"; -import PropTypes from "prop-types"; import type { RootState } from "@/Types/state.js"; const getFilteredAccountMenuItems = (user: any, items: any[]) => { @@ -45,7 +44,7 @@ const getRoleDisplayText = (user: any, t: Function) => { return user.role; }; -const AuthFooter = ({ +export const AuthFooter = ({ collapsed, accountMenuItems, }: { @@ -86,6 +85,9 @@ const AuthFooter = ({ gap: theme.spacing(2), borderRadius: theme.shape.borderRadius, pl: theme.spacing(4), + "& svg": { + stroke: theme.palette.text.secondary, + }, }} > {item.icon} @@ -140,6 +142,7 @@ const AuthFooter = ({ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", + color: theme.palette.text.primary, }} > {authState.user?.firstName} {authState.user?.lastName} @@ -148,7 +151,10 @@ const AuthFooter = ({ textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap" - sx={{ textTransform: "capitalize", opacity: 0.8 }} + sx={{ + textTransform: "capitalize", + color: theme.palette.text.secondary, + }} > {getRoleDisplayText(authState.user, t)} @@ -162,6 +168,9 @@ const AuthFooter = ({ ml: "50px", "&:focus": { outline: "none" }, alignSelf: "center", + "& svg": { + stroke: theme.palette.text.secondary, + }, }} onClick={(event) => openPopup(event)} > @@ -244,9 +253,12 @@ const AuthFooter = ({ ); }; - -AuthFooter.propTypes = { - collapsed: PropTypes.bool, - accountMenuItems: PropTypes.array, -}; - -export default AuthFooter; diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx index deb2730e2..9bd36a853 100644 --- a/client/src/Components/v2/sidebar/index.tsx +++ b/client/src/Components/v2/sidebar/index.tsx @@ -7,7 +7,7 @@ import { Logo } from "@/Components/v2/sidebar/Logo"; import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/v2/sidebar/Menu"; import { NavItem } from "@/Components/v2/sidebar/NavItem"; import { StarPrompt } from "@/Components/v2/sidebar/StarPrompt"; -import AuthFooter from "@/Components/v2/sidebar/Authfooter"; +import { AuthFooter } from "@/Components/v2/sidebar/Authfooter"; import { useNavigate, useLocation } from "react-router"; import { useTranslation } from "react-i18next"; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index dd747b5e4..314ba7e50 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -359,7 +359,8 @@ "starPrompt": { "title": "Star Checkmate", "description": "See the latest releases and help grow the community on GitHub" - } + }, + "authFooter": {} }, "starPrompt": { "title": "Star Checkmate", From 71f3c904722da8f82c326fd32300661d81fc2c89 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 23:16:12 +0000 Subject: [PATCH 10/26] strings --- client/src/Components/v2/sidebar/Authfooter.tsx | 15 +++++++++------ client/src/locales/en.json | 11 ++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index d63663a01..2bebb49b2 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -36,10 +36,13 @@ const getFilteredAccountMenuItems = (user: any, items: any[]) => { const getRoleDisplayText = (user: any, t: Function) => { if (!user?.role) return ""; - if (user.role.includes("superadmin")) return t("roles.superAdmin"); - if (user.role.includes("admin")) return t("roles.admin"); - if (user.role.includes("user")) return t("roles.teamMember"); - if (user.role.includes("demo")) return t("roles.demoUser"); + if (user.role.includes("superadmin")) + return t("components.sidebar.authFooter.roles.superAdmin"); + if (user.role.includes("admin")) return t("components.sidebar.authFooter.roles.admin"); + if (user.role.includes("user")) + return t("components.sidebar.authFooter.roles.teamMember"); + if (user.role.includes("demo")) + return t("components.sidebar.authFooter.roles.demoUser"); return user.role; }; @@ -160,7 +163,7 @@ export const AuthFooter = ({ - {t("menu.logOut", "Log out")} + {t("components.sidebar.authFooter.logOut")} diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 314ba7e50..b49ebaa43 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -360,7 +360,16 @@ "title": "Star Checkmate", "description": "See the latest releases and help grow the community on GitHub" }, - "authFooter": {} + "authFooter": { + "navControls": "Controls", + "logOut": "Log out", + "roles": { + "superAdmin": "Super admin", + "admin": "Admin", + "teamMember": "Team member", + "demoUser": "Demo user" + } + } }, "starPrompt": { "title": "Star Checkmate", From 442fc2ce22d37f1c77561f16bedb5ed17912ce9e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 23:24:51 +0000 Subject: [PATCH 11/26] account tab fix --- .../src/Components/v2/sidebar/Authfooter.tsx | 315 ++++++------------ client/src/Pages/Account/index.tsx | 7 +- 2 files changed, 117 insertions(+), 205 deletions(-) diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index 2bebb49b2..4b369cb67 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -1,103 +1,75 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import Tooltip from "@mui/material/Tooltip"; import IconButton from "@mui/material/IconButton"; -import { Avatar, Icon } from "@/Components/v2/design-elements"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -import { MoreVertical, LogOut } from "lucide-react"; - import { useTheme } from "@mui/material"; -import { useSelector } from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; import { useState } from "react"; import { useNavigate } from "react-router"; +import { MoreVertical, LogOut } from "lucide-react"; +import { Avatar, Icon } from "@/Components/v2/design-elements"; import { clearAuthState } from "@/Features/Auth/authSlice.js"; -import { useDispatch } from "react-redux"; import type { RootState } from "@/Types/state.js"; -const getFilteredAccountMenuItems = (user: any, items: any[]) => { - if (!user) return []; - - let filtered = [...items]; - - if (user.role?.includes("demo")) { - filtered = filtered.filter((item) => item.name !== "Password"); - } - - if (!user.role?.includes("superadmin")) { - filtered = filtered.filter((item) => item.name !== "Team"); - } - - return filtered; -}; - -const getRoleDisplayText = (user: any, t: Function) => { - if (!user?.role) return ""; - - if (user.role.includes("superadmin")) - return t("components.sidebar.authFooter.roles.superAdmin"); - if (user.role.includes("admin")) return t("components.sidebar.authFooter.roles.admin"); - if (user.role.includes("user")) - return t("components.sidebar.authFooter.roles.teamMember"); - if (user.role.includes("demo")) - return t("components.sidebar.authFooter.roles.demoUser"); - - return user.role; -}; - -export const AuthFooter = ({ - collapsed, - accountMenuItems, -}: { +interface AuthFooterProps { collapsed: boolean; - accountMenuItems: any[]; -}) => { + accountMenuItems: Array<{ + name: string; + path: string; + icon: React.ReactNode; + }>; +} + +export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) => { const { t } = useTranslation(); const theme = useTheme(); - const authState = useSelector((state: RootState) => state.auth); + const user = useSelector((state: RootState) => state.auth.user); const navigate = useNavigate(); const dispatch = useDispatch(); - const [anchorEl, setAnchorEl] = useState(null); - const openPopup = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); + const menuOpen = Boolean(anchorEl); + const handleMenuOpen = (e: React.MouseEvent) => + setAnchorEl(e.currentTarget); + const handleMenuClose = () => setAnchorEl(null); + + const handleNavigate = (path: string) => { + handleMenuClose(); + navigate(path); }; - const closePopup = () => { - setAnchorEl(null); - }; - - const logout = async () => { + const handleLogout = () => { dispatch(clearAuthState()); navigate("/login"); }; - const renderAccountMenuItems = (user: any, items: any[]) => { - const filteredItems = getFilteredAccountMenuItems(user, items); - return filteredItems.map((item) => ( - { - closePopup(); - navigate(item.path); - }} - sx={{ - gap: theme.spacing(2), - borderRadius: theme.shape.borderRadius, - pl: theme.spacing(4), - "& svg": { - stroke: theme.palette.text.secondary, - }, - }} - > - {item.icon} - {item.name} - - )); + const getRoleText = () => { + const role = user?.role ?? ""; + if (role.includes("superadmin")) + return t("components.sidebar.authFooter.roles.superAdmin"); + if (role.includes("admin")) return t("components.sidebar.authFooter.roles.admin"); + if (role.includes("user")) return t("components.sidebar.authFooter.roles.teamMember"); + if (role.includes("demo")) return t("components.sidebar.authFooter.roles.demoUser"); + return role; }; + + const filteredMenuItems = accountMenuItems.filter((item) => { + if (!user) return false; + if (item.name === "Password" && user.role?.includes("demo")) return false; + if (item.name === "Team" && !user.role?.includes("superadmin")) return false; + return true; + }); + + const menuItemSx = { + gap: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + pl: theme.spacing(4), + "& svg": { stroke: theme.palette.text.secondary }, + }; + return ( { - openPopup(e); - }} - sx={{ - cursor: collapsed ? "pointer" : "default", - }} + small + onClick={collapsed ? handleMenuOpen : undefined} + sx={{ cursor: collapsed ? "pointer" : "default" }} /> - + {!collapsed && ( - - {authState.user?.firstName} {authState.user?.lastName} - - - {getRoleDisplayText(authState.user, t)} - - - + + {user?.firstName} {user?.lastName} + + + {getRoleText()} + + openPopup(event)} > - - + + )} + {collapsed && ( - - + - - {authState.user?.firstName} {authState.user?.lastName} - - - {authState.user?.role} - - - + {user?.firstName} {user?.lastName} + + + {getRoleText()} + + )} - {renderAccountMenuItems(authState.user, accountMenuItems)} + {filteredMenuItems.map((item) => ( + handleNavigate(item.path)} + sx={menuItemSx} + > + {item.icon} + {item.name} + + ))} { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(TAB_MAP[open]); + // Sync activeTab when open prop changes (e.g., navigating from sidebar) + useEffect(() => { + setActiveTab(TAB_MAP[open]); + }, [open]); + return ( Date: Tue, 10 Feb 2026 23:32:58 +0000 Subject: [PATCH 12/26] cleanup --- client/src/Components/v1/ArrowLeft/index.jsx | 36 --- client/src/Components/v1/Avatar/index.css | 0 client/src/Components/v1/Avatar/index.jsx | 73 ----- client/src/Components/v1/Label/index.css | 6 - client/src/Components/v1/Label/index.jsx | 172 ----------- .../v1/Layouts/HomeLayout/index.css | 29 -- .../v1/Layouts/HomeLayout/index.jsx | 31 -- .../v1/Sidebar/components/authFooter.jsx | 276 ------------------ .../v1/Sidebar/components/collapseButton.jsx | 55 ---- .../Components/v1/Sidebar/components/logo.jsx | 67 ----- .../v1/Sidebar/components/navItem.jsx | 96 ------ client/src/Components/v1/Sidebar/index.jsx | 152 ---------- client/src/Components/v1/StarPrompt/index.jsx | 110 ------- .../src/Components/v2/sidebar/Authfooter.tsx | 2 +- client/src/locales/en.json | 2 +- 15 files changed, 2 insertions(+), 1105 deletions(-) delete mode 100644 client/src/Components/v1/ArrowLeft/index.jsx delete mode 100644 client/src/Components/v1/Avatar/index.css delete mode 100644 client/src/Components/v1/Avatar/index.jsx delete mode 100644 client/src/Components/v1/Label/index.css delete mode 100644 client/src/Components/v1/Label/index.jsx delete mode 100644 client/src/Components/v1/Layouts/HomeLayout/index.css delete mode 100644 client/src/Components/v1/Layouts/HomeLayout/index.jsx delete mode 100644 client/src/Components/v1/Sidebar/components/authFooter.jsx delete mode 100644 client/src/Components/v1/Sidebar/components/collapseButton.jsx delete mode 100644 client/src/Components/v1/Sidebar/components/logo.jsx delete mode 100644 client/src/Components/v1/Sidebar/components/navItem.jsx delete mode 100644 client/src/Components/v1/Sidebar/index.jsx delete mode 100644 client/src/Components/v1/StarPrompt/index.jsx diff --git a/client/src/Components/v1/ArrowLeft/index.jsx b/client/src/Components/v1/ArrowLeft/index.jsx deleted file mode 100644 index e18a7f2d5..000000000 --- a/client/src/Components/v1/ArrowLeft/index.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import Icon from "../Icon"; -import PropTypes from "prop-types"; - -const ArrowLeft = ({ type, color = "#667085", ...props }) => { - if (type === "double") { - return ( - - ); - } else if (type === "long") { - return ( - - ); - } else { - return ( - - ); - } -}; - -ArrowLeft.propTypes = { - color: PropTypes.string, - type: PropTypes.oneOf(["double", "long", "default"]), -}; -export default ArrowLeft; diff --git a/client/src/Components/v1/Avatar/index.css b/client/src/Components/v1/Avatar/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Components/v1/Avatar/index.jsx b/client/src/Components/v1/Avatar/index.jsx deleted file mode 100644 index d089080ff..000000000 --- a/client/src/Components/v1/Avatar/index.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Avatar as MuiAvatar } from "@mui/material"; -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import { useEffect, useState } from "react"; -import { useTheme } from "@emotion/react"; -/** - * @component - * @param {Object} props - * @param {string} props.src - Path to image for avatar - * @param {boolean} props.small - Specifies if avatar should be large - * @param {Object} [props.sx] - Additional styles to apply to the button. - * @returns {JSX.Element} - * @example - * // Render a red label - * - */ - -const Avatar = ({ src, small, sx, onClick = () => {} }) => { - const { user } = useSelector((state) => state.auth); - const theme = useTheme(); - - const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 }; - const border = small ? 1 : 3; - - const [image, setImage] = useState(); - useEffect(() => { - if (user.avatarImage) { - setImage(`data:image/png;base64,${user.avatarImage}`); - } - }, [user?.avatarImage]); - - return ( - - {user.firstName?.charAt(0)} - {user.lastName?.charAt(0) || ""} - - ); -}; - -Avatar.propTypes = { - src: PropTypes.string, - small: PropTypes.bool, - sx: PropTypes.object, - onClick: PropTypes.func, -}; - -export default Avatar; diff --git a/client/src/Components/v1/Label/index.css b/client/src/Components/v1/Label/index.css deleted file mode 100644 index d3fe53da5..000000000 --- a/client/src/Components/v1/Label/index.css +++ /dev/null @@ -1,6 +0,0 @@ -.label { - display: inline-flex; - justify-content: center; - align-items: center; - line-height: normal; -} diff --git a/client/src/Components/v1/Label/index.jsx b/client/src/Components/v1/Label/index.jsx deleted file mode 100644 index 2ed9b93bf..000000000 --- a/client/src/Components/v1/Label/index.jsx +++ /dev/null @@ -1,172 +0,0 @@ -import PropTypes from "prop-types"; -import { Box } from "@mui/material"; -import { useTheme } from "@mui/material"; -import "./index.css"; - -/** - * @typedef {Object} Styles - * @param {string} [color] - The text color - * @param {string} [backgroundColor] - The background color - * @param {string} [borderColor] - The border color - */ - -/** - * @component - * @param {Object} props - * @param {string} props.label - The label of the label - * @param {Styles} props.styles - CSS Styles passed from parent component - * @param {React.ReactNode} children - Children passed from parent component - * @returns {JSX.Element} - */ - -const BaseLabel = ({ label, styles, children }) => { - const theme = useTheme(); - // Grab the default borderRadius from the theme to match button style - const { borderRadius } = theme.shape; - // Calculate padding for the label to mimic button. Appears to scale correctly, not 100% sure though. - const padding = theme.spacing(3, 5); - - return ( - - {children} - {label} - - ); -}; - -BaseLabel.propTypes = { - label: PropTypes.string.isRequired, - styles: PropTypes.shape({ - color: PropTypes.string, - backgroundColor: PropTypes.string, - }), - children: PropTypes.node, -}; - -// Produces a lighter color based on a hex color and a percent -// lightenColor("#067647", 20) will produce a color 20% lighter than #067647 -const lightenColor = (color, percent) => { - let r = parseInt(color.substring(1, 3), 16); - let g = parseInt(color.substring(3, 5), 16); - let b = parseInt(color.substring(5, 7), 16); - - const amt = Math.round((255 * percent) / 100); - - r = r + amt <= 255 ? r + amt : 255; - g = g + amt <= 255 ? g + amt : 255; - b = b + amt <= 255 ? b + amt : 255; - - r = r.toString(16).padStart(2, "0"); - g = g.toString(16).padStart(2, "0"); - b = b.toString(16).padStart(2, "0"); - - return `#${r}${g}${b}`; -}; - -/** - * @component - * @param {Object} props - * @param {string} props.label - The label of the label - * @param {string} props.color - The color of the label, specified in #RRGGBB format - * @returns {JSX.Element} - * @example - * // Render a red label - * - */ - -const ColoredLabel = ({ label, color }) => { - const theme = useTheme(); - // If an invalid color is passed, default to the labelGray color - if (typeof color !== "string" || !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) { - color = theme.palette.primary.lowContrast; - } - - // Calculate lighter shades for border and bg - const borderColor = lightenColor(color, 20); - const bgColor = lightenColor(color, 75); - - return ( - - ); -}; - -ColoredLabel.propTypes = { - label: PropTypes.string.isRequired, - color: PropTypes.string.isRequired, -}; - -/** - * @component - * @param {Object} props - * @param {'up' | 'down' | 'paused' | 'pending' | 'cannot resolve' | 'published' | 'unpublished'} props.status - The status for the label - * @param {string} props.text - The text of the label - * @returns {JSX.Element} - * @example - * // Render an active label - * - */ - -const statusToTheme = { - up: "success", - down: "error", - paused: "warning", - pending: "warning", - "cannot resolve": "error", - published: "success", - unpublished: "error", -}; - -const StatusLabel = ({ status, text, customStyles }) => { - const theme = useTheme(); - - const themeColor = statusToTheme[status]; - - return ( - - - - ); -}; - -StatusLabel.propTypes = { - status: PropTypes.oneOf([ - "up", - "down", - "paused", - "pending", - "cannot resolve", - "published", - "unpublished", - ]), - text: PropTypes.string, - customStyles: PropTypes.object, -}; - -export { BaseLabel, ColoredLabel, StatusLabel }; diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.css b/client/src/Components/v1/Layouts/HomeLayout/index.css deleted file mode 100644 index 61808aa57..000000000 --- a/client/src/Components/v1/Layouts/HomeLayout/index.css +++ /dev/null @@ -1,29 +0,0 @@ -.home-layout { - position: relative; - min-height: 100vh; - margin: 0 auto; - padding: 0; - overflow-x: hidden; - width: 100%; -} - -/* TODO go for this approach for responsiveness. The aside needs to be taken care of */ -/* @media (max-width: 1000px) { - .home-layout { - flex-direction: column !important; - } -} */ - -.home-layout > .home-content-wrapper { - min-height: calc(100vh - var(--env-var-spacing-2) * 2); - flex: 1; -} - -.home-content-wrapper { - padding: var(--env-var-spacing-2); - max-width: 1400px; - margin: 0 auto; - flex: 1; - min-width: 0; - overflow-x: hidden; -} diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.jsx b/client/src/Components/v1/Layouts/HomeLayout/index.jsx deleted file mode 100644 index adadf638f..000000000 --- a/client/src/Components/v1/Layouts/HomeLayout/index.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import Sidebar from "../../Sidebar/index.jsx"; -import { Outlet } from "react-router"; -import { Box, Stack } from "@mui/material"; -import { useSidebar } from "@/Hooks/useSidebar.js"; - -import "./index.css"; - -const HomeLayout = () => { - const { width, transition } = useSidebar(); - - return ( - - - - - - - - ); -}; - -export default HomeLayout; diff --git a/client/src/Components/v1/Sidebar/components/authFooter.jsx b/client/src/Components/v1/Sidebar/components/authFooter.jsx deleted file mode 100644 index 16b60d9f2..000000000 --- a/client/src/Components/v1/Sidebar/components/authFooter.jsx +++ /dev/null @@ -1,276 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import Tooltip from "@mui/material/Tooltip"; -import IconButton from "@mui/material/IconButton"; -import Avatar from "../../Avatar/index.jsx"; -import Menu from "@mui/material/Menu"; -import MenuItem from "@mui/material/MenuItem"; -import Divider from "@mui/material/Divider"; -import Icon from "../../Icon"; - -import { useTheme } from "@emotion/react"; -import { useSelector } from "react-redux"; -import { useTranslation } from "react-i18next"; -import { useState } from "react"; -import { useNavigate } from "react-router"; -import { clearAuthState } from "../../../../Features/Auth/authSlice.js"; -import { useDispatch } from "react-redux"; -import PropTypes from "prop-types"; - -const getFilteredAccountMenuItems = (user, items) => { - if (!user) return []; - - let filtered = [...items]; - - if (user.role?.includes("demo")) { - filtered = filtered.filter((item) => item.name !== "Password"); - } - - if (!user.role?.includes("superadmin")) { - filtered = filtered.filter((item) => item.name !== "Team"); - } - - return filtered; -}; - -const getRoleDisplayText = (user, t) => { - if (!user?.role) return ""; - - if (user.role.includes("superadmin")) return t("roles.superAdmin"); - if (user.role.includes("admin")) return t("roles.admin"); - if (user.role.includes("user")) return t("roles.teamMember"); - if (user.role.includes("demo")) return t("roles.demoUser"); - - return user.role; -}; - -const AuthFooter = ({ collapsed, accountMenuItems }) => { - const { t } = useTranslation(); - const theme = useTheme(); - const authState = useSelector((state) => state.auth); - const navigate = useNavigate(); - const dispatch = useDispatch(); - - const [anchorEl, setAnchorEl] = useState(null); - - const openPopup = (event) => { - setAnchorEl(event.currentTarget); - }; - - const closePopup = () => { - setAnchorEl(null); - }; - - const logout = async () => { - dispatch(clearAuthState()); - navigate("/login"); - }; - const renderAccountMenuItems = (user, items) => { - const filteredItems = getFilteredAccountMenuItems(user, items); - - return filteredItems.map((item) => ( - { - closePopup(); - navigate(item.path); - }} - sx={{ - gap: theme.spacing(2), - borderRadius: theme.shape.borderRadius, - pl: theme.spacing(4), - color: theme.palette.primary.contrastTextTertiary, - "& svg": { - stroke: theme.palette.primary.contrastTextTertiary, - }, - }} - > - {item.icon} - {item.name} - - )); - }; - return ( - - collapsed && openPopup(e)} - sx={{ - cursor: collapsed ? "pointer" : "default", - }} - /> - - - - - {authState.user?.firstName} {authState.user?.lastName} - - - {getRoleDisplayText(authState.user, t)} - - - - openPopup(event)} - > - - - - - - {collapsed && ( - - - - {authState.user?.firstName} {authState.user?.lastName} - - - {authState.user?.role} - - - - )} - {/* TODO Do we need two dividers? */} - {collapsed && } - {/* */} - {renderAccountMenuItems(authState.user, accountMenuItems)} - - - {t("menu.logOut", "Log out")} - - - - ); -}; - -AuthFooter.propTypes = { - collapsed: PropTypes.bool, - accountMenuItems: PropTypes.array, -}; - -export default AuthFooter; diff --git a/client/src/Components/v1/Sidebar/components/collapseButton.jsx b/client/src/Components/v1/Sidebar/components/collapseButton.jsx deleted file mode 100644 index e3a9c91f5..000000000 --- a/client/src/Components/v1/Sidebar/components/collapseButton.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import IconButton from "@mui/material/IconButton"; -import ArrowRight from "../../ArrowRight/index.jsx"; -import ArrowLeft from "../../ArrowLeft/index.jsx"; -import { useTheme } from "@mui/material/styles"; -import { useDispatch } from "react-redux"; -import { toggleSidebar } from "../../../../Features/UI/uiSlice.js"; -import PropTypes from "prop-types"; - -const CollapseButton = ({ collapsed }) => { - const theme = useTheme(); - const dispatch = useDispatch(); - const arrowIcon = collapsed ? ( - - ) : ( - - ); - return ( - { - dispatch(toggleSidebar()); - }} - > - {arrowIcon} - - ); -}; - -CollapseButton.propTypes = { - collapsed: PropTypes.bool.isRequired, -}; -export default CollapseButton; diff --git a/client/src/Components/v1/Sidebar/components/logo.jsx b/client/src/Components/v1/Sidebar/components/logo.jsx deleted file mode 100644 index a0c0ff84e..000000000 --- a/client/src/Components/v1/Sidebar/components/logo.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import { useTheme } from "@mui/material/styles"; -import { useNavigate } from "react-router"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; - -const Logo = ({ collapsed }) => { - const { t } = useTranslation(); - const theme = useTheme(); - const navigate = useNavigate(); - - return ( - navigate("/")} - sx={{ cursor: "pointer" }} - > - - C - - - {" "} - - {t("common.appName")} - - - - ); -}; - -Logo.propTypes = { - collapsed: PropTypes.bool, -}; - -export default Logo; diff --git a/client/src/Components/v1/Sidebar/components/navItem.jsx b/client/src/Components/v1/Sidebar/components/navItem.jsx deleted file mode 100644 index 170d39bee..000000000 --- a/client/src/Components/v1/Sidebar/components/navItem.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import Tooltip from "@mui/material/Tooltip"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; - -const NavItem = ({ item, collapsed, selected, onClick }) => { - const theme = useTheme(); - const iconStroke = selected - ? theme.palette.primary.contrastText - : theme.palette.primary.contrastTextTertiary; - - const buttonBgColor = selected ? theme.palette.secondary.main : "transparent"; - const buttonBgHoverColor = selected - ? theme.palette.secondary.main - : theme.palette.tertiary.main; - const fontWeight = selected ? 600 : 400; - return ( - - - - {item.icon} - - - - {item.name} - - - - - ); -}; - -NavItem.propTypes = { - item: PropTypes.object, - collapsed: PropTypes.bool, - selected: PropTypes.bool, - onClick: PropTypes.func, -}; -export default NavItem; diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx deleted file mode 100644 index 763a4123c..000000000 --- a/client/src/Components/v1/Sidebar/index.jsx +++ /dev/null @@ -1,152 +0,0 @@ -import Stack from "@mui/material/Stack"; - -import List from "@mui/material/List"; -import Logo from "./components/logo.jsx"; -import CollapseButton from "./components/collapseButton.jsx"; -import Divider from "@mui/material/Divider"; -import NavItem from "./components/navItem.jsx"; -import AuthFooter from "./components/authFooter.jsx"; - -import StarPrompt from "../StarPrompt/index.jsx"; -import Icon from "../Icon"; - -// Utils -import { useTheme } from "@mui/material/styles"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router"; -import { useSidebar } from "@/Hooks/useSidebar.js"; - -const URL_MAP = { - support: "https://discord.com/invite/NAb6H3UTjK", - discussions: "https://github.com/bluewave-labs/checkmate/discussions", - docs: "https://bluewavelabs.gitbook.io/checkmate", - changelog: "https://github.com/bluewave-labs/checkmate/releases", -}; - -const getMenu = (t) => [ - { name: t("menu.uptime"), path: "uptime", icon: }, - { name: t("menu.pagespeed"), path: "pagespeed", icon: }, - { name: t("menu.infrastructure"), path: "infrastructure", icon: }, - { - name: t("menu.notifications"), - path: "notifications", - icon: , - }, - { name: t("menu.checks"), path: "checks", icon: }, - { name: t("menu.incidents"), path: "incidents", icon: }, - { name: t("menu.statusPages"), path: "status", icon: }, - { name: t("menu.maintenance"), path: "maintenance", icon: }, - { name: t("menu.logs"), path: "logs", icon: }, - { - name: t("menu.settings"), - icon: , - path: "settings", - }, -]; - -const getOtherMenuItems = (t) => [ - { name: t("menu.support"), path: "support", icon: }, - { - name: t("menu.discussions"), - path: "discussions", - icon: , - }, - { name: t("menu.docs"), path: "docs", icon: }, - { name: t("menu.changelog"), path: "changelog", icon: }, -]; - -const getAccountMenuItems = (t) => [ - { name: t("menu.profile"), path: "account/profile", icon: }, - { name: t("menu.password"), path: "account/password", icon: }, - { name: t("menu.team"), path: "account/team", icon: }, -]; - -const Sidebar = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const { collapsed, width, transition } = useSidebar(); - - const menu = getMenu(t); - const otherMenuItems = getOtherMenuItems(t); - const accountMenuItems = getAccountMenuItems(t); - - return ( - - - - - {menu.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - return ( - navigate(`/${item.path}`)} - /> - ); - })} - - {!collapsed && } - - {otherMenuItems.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - - return ( - { - const url = URL_MAP[item.path]; - if (url) { - window.open(url, "_blank", "noreferrer"); - } else { - navigate(`/${item.path}`); - } - }} - /> - ); - })} - - - - - ); -}; - -export default Sidebar; diff --git a/client/src/Components/v1/StarPrompt/index.jsx b/client/src/Components/v1/StarPrompt/index.jsx deleted file mode 100644 index f1dd5e136..000000000 --- a/client/src/Components/v1/StarPrompt/index.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from "react"; -import { Typography, IconButton, Stack, Box } from "@mui/material"; -import Icon from "../Icon"; -import { useTheme } from "@emotion/react"; -import { useSelector, useDispatch } from "react-redux"; -import { useTranslation } from "react-i18next"; -import { setStarPromptOpen } from "../../../Features/UI/uiSlice.js"; - -const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" }) => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - const isOpen = useSelector((state) => state.ui?.starPromptOpen ?? true); - const mode = useSelector((state) => state.ui.mode); - - const handleClose = () => { - dispatch(setStarPromptOpen(false)); - }; - - const handleStarClick = () => { - window.open(repoUrl, "_blank"); - }; - - if (!isOpen) return null; - - return ( - - - - {t("starPromptTitle")} - - - - - - - - {t("starPromptDescription")} - - - - - ); -}; - -export default StarPrompt; diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index 4b369cb67..62ea5610b 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -51,7 +51,7 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) => if (role.includes("superadmin")) return t("components.sidebar.authFooter.roles.superAdmin"); if (role.includes("admin")) return t("components.sidebar.authFooter.roles.admin"); - if (role.includes("user")) return t("components.sidebar.authFooter.roles.teamMember"); + if (role.includes("user")) return t("components.sidebar.authFooter.roles.user"); if (role.includes("demo")) return t("components.sidebar.authFooter.roles.demoUser"); return role; }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index b49ebaa43..86b220998 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -366,7 +366,7 @@ "roles": { "superAdmin": "Super admin", "admin": "Admin", - "teamMember": "Team member", + "user": "User", "demoUser": "Demo user" } } From 10bfdc9da13de9ebd2c406638f972f001aeefe3c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Feb 2026 23:53:06 +0000 Subject: [PATCH 13/26] hook up offline banner --- .../v2/design-elements/OfflineBanner.tsx | 49 ++---- client/src/Components/v2/layout/AppLayout.tsx | 32 +++- client/src/Pages/ServerUnreachable.jsx | 166 ------------------ client/src/Routes/index.jsx | 7 - client/src/Utils/ApiClient.ts | 6 +- client/src/locales/en.json | 10 +- 6 files changed, 59 insertions(+), 211 deletions(-) delete mode 100644 client/src/Pages/ServerUnreachable.jsx diff --git a/client/src/Components/v2/design-elements/OfflineBanner.tsx b/client/src/Components/v2/design-elements/OfflineBanner.tsx index fa5690c12..062b1ca0d 100644 --- a/client/src/Components/v2/design-elements/OfflineBanner.tsx +++ b/client/src/Components/v2/design-elements/OfflineBanner.tsx @@ -1,30 +1,39 @@ import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; import { WifiOff } from "lucide-react"; +import { useState, useEffect } from "react"; interface OfflineBannerProps { visible: boolean; - onRetry?: () => void; - isRetrying?: boolean; } -export const OfflineBanner = ({ visible, onRetry, isRetrying }: OfflineBannerProps) => { +export const OfflineBanner = ({ visible }: OfflineBannerProps) => { const theme = useTheme(); const { t } = useTranslation(); + const [shouldRender, setShouldRender] = useState(visible); + const [isAnimating, setIsAnimating] = useState(false); - if (!visible) { - return null; - } + useEffect(() => { + if (visible) { + setShouldRender(true); + requestAnimationFrame(() => setIsAnimating(true)); + } else { + setIsAnimating(false); + const timer = setTimeout(() => setShouldRender(false), 1000); + return () => clearTimeout(timer); + } + }, [visible]); + + if (!shouldRender) return null; return ( {t("components.offlineBanner.serverUnreachable")} - {onRetry && ( - - )} ); diff --git a/client/src/Components/v2/layout/AppLayout.tsx b/client/src/Components/v2/layout/AppLayout.tsx index 3ae2729e3..7c9c3c903 100644 --- a/client/src/Components/v2/layout/AppLayout.tsx +++ b/client/src/Components/v2/layout/AppLayout.tsx @@ -1,10 +1,12 @@ import Box from "@mui/material/Box"; +import { useState, useEffect, useRef } from "react"; import { ThemeProvider, useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import BackgroundSVG from "@/assets/Images/background.svg"; import type { RootState } from "@/Types/state"; import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; import { OfflineBanner } from "@/Components/v2/design-elements"; +import { setServerUnreachableCallback, get } from "@/Utils/ApiClient"; interface AppLayoutProps { children: React.ReactNode; @@ -15,6 +17,34 @@ const AppLayout = ({ children }: AppLayoutProps) => { const mode = useSelector((state: RootState) => state.ui.mode); const v2theme = mode === "dark" ? darkTheme : lightTheme; + const [serverUnreachable, setServerUnreachable] = useState(false); + const retryIntervalRef = useRef(null); + + useEffect(() => { + setServerUnreachableCallback(setServerUnreachable); + }, []); + + useEffect(() => { + if (serverUnreachable) { + retryIntervalRef.current = window.setInterval(async () => { + try { + await get("/health", { timeout: 5000 }); + } catch { + // NO_OP + } + }, 5000); + } else if (retryIntervalRef.current) { + clearInterval(retryIntervalRef.current); + retryIntervalRef.current = null; + } + + return () => { + if (retryIntervalRef.current) { + clearInterval(retryIntervalRef.current); + } + }; + }, [serverUnreachable]); + return ( { }} > - + {children} diff --git a/client/src/Pages/ServerUnreachable.jsx b/client/src/Pages/ServerUnreachable.jsx deleted file mode 100644 index 5051ee95f..000000000 --- a/client/src/Pages/ServerUnreachable.jsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useState } from "react"; -import { Box, Typography, Button, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { get } from "@/Utils/ApiClient"; -import Alert from "@/Components/v1/Alert/index.jsx"; -import { createToast } from "@/Utils/toastUtils.jsx"; -import { useTranslation } from "react-i18next"; -import Background from "@/assets/Images/background-grid.svg?react"; -import Logo from "@/assets/icons/checkmate-icon.svg?react"; -import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; -import LanguageSelector from "@/Components/LanguageSelector.jsx"; - -const ServerUnreachable = () => { - const theme = useTheme(); - const navigate = useNavigate(); - const { t } = useTranslation(); - - // State for tracking connection check status - const [isCheckingConnection, setIsCheckingConnection] = useState(false); - - const handleRetry = React.useCallback(async () => { - setIsCheckingConnection(true); - try { - // Try to connect to the backend with a simple API call - // We'll use any lightweight endpoint that doesn't require authentication - await get("/health", { timeout: 5000 }); - - // If successful, show toast and navigate to login page - createToast({ - body: t("errorPages.serverUnreachable.toasts.reconnected"), - }); - navigate("/login"); - } catch (error) { - // If still unreachable, stay on this page and show toast - createToast({ - body: t("errorPages.serverUnreachable.toasts.stillUnreachable"), - }); - } finally { - setIsCheckingConnection(false); - } - }, [navigate, t]); - - return ( - - - - - - {/* Header with logo */} - - - - {t("common.appName")} - - - - - - - .MuiStack-root": { - border: 1, - borderRadius: theme.spacing(5), - borderColor: theme.palette.primary.lowContrast, - backgroundColor: theme.palette.primary.main, - padding: { - xs: theme.spacing(12), - sm: theme.spacing(20), - }, - }, - }} - > - - - - - - - {t("errorPages.serverUnreachable.description")} - - - - - - - - - ); -}; - -export default ServerUnreachable; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 0a66b0cf3..7d9163c43 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -25,9 +25,6 @@ import PageSpeedDetails from "@/Pages/PageSpeed/Details/"; import Infrastructure from "@/Pages/Infrastructure/Monitors"; import InfrastructureDetails from "@/Pages/Infrastructure/Details/index"; -// Server Status -import ServerUnreachable from "../Pages/ServerUnreachable.jsx"; - // Checks import Checks from "../Pages/Checks/index"; @@ -432,10 +429,6 @@ const Routes = () => { } /> - } - /> { } ); - const onSuccess = (response: AxiosResponse) => response; + const onSuccess = (response: AxiosResponse) => { + // Server is reachable, hide offline banner if shown + serverUnreachableCallback?.(false); + return response; + }; const onError = (error: AxiosError) => { // Handle network errors (server unreachable) if (error.code === "ERR_NETWORK") { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 86b220998..d2a066c11 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -582,7 +582,7 @@ "notFoundButton": "Go to the main dashboard", "notifications": { "fallback": { - "actionButton": "Let's create your first notification channel!", + "actionButton": "Create notification channel!", "checks": [ "Alert teams about downtime or performance issues", "Let engineers know when incidents happen", @@ -1019,7 +1019,7 @@ } }, "fallback": { - "actionButton": "Let's create your first infrastructure monitor!", + "actionButton": "Create a monitor!", "checks": [ "Track the performance of your servers", "Identify bottlenecks and optimize usage", @@ -1087,7 +1087,7 @@ }, "maintenanceWindow": { "fallback": { - "actionButton": "Let's create your first maintenance window!", + "actionButton": "Create a maintenance window!", "checks": [ "Mark your maintenance periods", "Eliminate any misunderstandings", @@ -1228,7 +1228,7 @@ } }, "fallback": { - "actionButton": "Let's create your first PageSpeed monitor!", + "actionButton": "Create a monitor!", "checks": [ "Report on the user experience of a page", "Help analyze webpage speed", @@ -1247,7 +1247,7 @@ "Build trust with transparent service monitoring", "Reduce support requests during incidents" ], - "actionButton": "Let's create your first status page!" + "actionButton": "Create a status page!" }, "monitorsList": { "chartTypeHeatmap": "Heatmap", From 64438020243d4671a526af0f7b1ef9d8eb14d4ef Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 17:21:32 +0000 Subject: [PATCH 14/26] cleanup imports --- client/src/Routes/index.jsx | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 7d9163c43..0022187fe 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -11,7 +11,7 @@ import NotFound from "@/Pages/NotFound"; import AuthLogin from "@/Pages/Auth/Login"; import AuthRegister from "@/Pages/Auth/Register"; import AuthForgotPassword from "@/Pages/Auth/Recovery"; -import AuthSetNewPassword from "../Pages/Auth/SetNewPassword"; +import AuthSetNewPassword from "@/Pages/Auth/SetNewPassword"; // Uptime import Uptime from "@/Pages/Uptime/Monitors"; @@ -23,34 +23,34 @@ import PageSpeedDetails from "@/Pages/PageSpeed/Details/"; // Infrastructure import Infrastructure from "@/Pages/Infrastructure/Monitors"; -import InfrastructureDetails from "@/Pages/Infrastructure/Details/index"; +import InfrastructureDetails from "@/Pages/Infrastructure/Details"; // Checks -import Checks from "../Pages/Checks/index"; +import Checks from "@/Pages/Checks"; // Incidents -import Incidents from "../Pages/Incidents/"; +import Incidents from "@/Pages/Incidents"; // Status pages -import CreateStatus from "../Pages/StatusPage/Create/"; -import StatusPages from "../Pages/StatusPage/StatusPages"; -import Status from "../Pages/StatusPage/Status"; +import CreateStatus from "@/Pages/StatusPage/Create/"; +import StatusPages from "@/Pages/StatusPage/StatusPages"; +import Status from "@/Pages/StatusPage/Status"; -import Notifications from "../Pages/Notifications"; -import CreateNotifications from "../Pages/Notifications/create"; +import Notifications from "@/Pages/Notifications"; +import CreateNotifications from "@/Pages/Notifications/create"; // Settings import Account from "@/Pages/Account"; -import EditUser from "../Pages/Account/EditUser"; -import Settings from "../Pages/Settings"; +import EditUser from "@/Pages/Account/EditUser"; +import Settings from "@/Pages/Settings"; -import Maintenance from "../Pages/Maintenance"; +import Maintenance from "@/Pages/Maintenance"; import CreateNewMaintenanceWindow from "@/Pages/Maintenance/create"; -import ProtectedRoute from "../Components/v1/ProtectedRoute"; -import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute"; +import ProtectedRoute from "@/Components/v1/ProtectedRoute"; +import RoleProtectedRoute from "@/Components/v1/RoleProtectedRoute"; import withAdminCheck from "@/Components/v1/HOC/withAdminCheck"; -import Logs from "../Pages/Logs"; +import Logs from "@/Pages/Logs"; import CreateMonitor from "@/Pages/CreateMonitor"; From ee3062fcb495035bb653e68888103d7df4780272 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 17:37:50 +0000 Subject: [PATCH 15/26] remove unused components --- client/src/Components/v1/Alert/index.css | 9 -- client/src/Components/v1/Alert/index.jsx | 138 ------------------ .../v1/ThemeSwitch/SunAndMoonIcon.jsx | 98 ------------- .../src/Components/v1/ThemeSwitch/index.css | 64 -------- .../src/Components/v1/ThemeSwitch/index.jsx | 53 ------- .../Components/v2/routing/RouteProtected.tsx | 0 .../v2/routing/RouteRoleProtected.tsx | 0 client/src/Routes/index.jsx | 4 +- 8 files changed, 1 insertion(+), 365 deletions(-) delete mode 100644 client/src/Components/v1/Alert/index.css delete mode 100644 client/src/Components/v1/Alert/index.jsx delete mode 100644 client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx delete mode 100644 client/src/Components/v1/ThemeSwitch/index.css delete mode 100644 client/src/Components/v1/ThemeSwitch/index.jsx create mode 100644 client/src/Components/v2/routing/RouteProtected.tsx create mode 100644 client/src/Components/v2/routing/RouteRoleProtected.tsx diff --git a/client/src/Components/v1/Alert/index.css b/client/src/Components/v1/Alert/index.css deleted file mode 100644 index 233b5c466..000000000 --- a/client/src/Components/v1/Alert/index.css +++ /dev/null @@ -1,9 +0,0 @@ -.alert { - margin: 0; - width: fit-content; -} -.alert, -.alert button, -.alert .MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} diff --git a/client/src/Components/v1/Alert/index.jsx b/client/src/Components/v1/Alert/index.jsx deleted file mode 100644 index dd3b2f9f2..000000000 --- a/client/src/Components/v1/Alert/index.jsx +++ /dev/null @@ -1,138 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Button, IconButton, Stack, Typography } from "@mui/material"; -import Icon from "../Icon"; -import "./index.css"; - -/** - * Icons mapping for different alert variants. - * @type {Object} - */ - -const icons = { - info: ( - - ), - error: ( - - ), - warning: ( - - ), -}; - -/** - * @param {Object} props - * @param {'info' | 'error' | 'warning'} props.variant - The type of alert. - * @param {string} [props.title] - The title of the alert. - * @param {string} [props.body] - The body text of the alert. - * @param {boolean} [props.isToast] - Indicates if the alert is used as a toast notification. - * @param {boolean} [props.hasIcon] - Whether to display an icon in the alert. - * @param {function} props.onClick - Toast dismiss function. - * @returns {JSX.Element} - */ - -const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { - const theme = useTheme(); - /* TODO - Do we need other variants for alert? - */ - - const text = theme.palette.secondary.contrastText; - const border = theme.palette.alert.contrastText; - const bg = theme.palette.alert.main; - const icon = icons[variant]; - - return ( - - {hasIcon && {icon}} - - {title && ( - {title} - )} - {body && ( - {body} - )} - {hasIcon && isToast && ( - - )} - - {isToast && ( - - - - )} - - ); -}; - -Alert.propTypes = { - variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired, - title: PropTypes.string, - body: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - isToast: PropTypes.bool, - hasIcon: PropTypes.bool, - onClick: function (props, propName, componentName) { - if (props.isToast && !props[propName]) { - return new Error( - `Prop '${propName}' is required when 'isToast' is true in '${componentName}'.` - ); - } - return null; - }, -}; - -export default Alert; diff --git a/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx b/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx deleted file mode 100644 index 21256e114..000000000 --- a/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useTheme } from "@mui/material"; -import "./index.css"; - -const SunAndMoonIcon = () => { - const theme = useTheme(); - - return ( - - ); -}; - -export default SunAndMoonIcon; diff --git a/client/src/Components/v1/ThemeSwitch/index.css b/client/src/Components/v1/ThemeSwitch/index.css deleted file mode 100644 index d5db1c1ca..000000000 --- a/client/src/Components/v1/ThemeSwitch/index.css +++ /dev/null @@ -1,64 +0,0 @@ -.sun-and-moon > :is(.moon, .sun, .sun-beams) { - transform-origin: center; -} - -.theme-toggle .sun-and-moon > .sun-beams { - stroke-width: 2px; -} - -.theme-dark .sun-and-moon > .sun { - transform: scale(1.75); -} - -.theme-dark .sun-and-moon > .sun-beams { - opacity: 0; -} - -.theme-dark .sun-and-moon > .moon > circle { - transform: translateX(-7px); -} - -@supports (cx: 1) { - .theme-dark .sun-and-moon > .moon > circle { - cx: 17; - transform: translateX(0); - } -} - -@media (prefers-reduced-motion: no-preference) { - .sun-and-moon > .sun { - transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55); - } - - .sun-and-moon > .sun-beams { - transition: - transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55), - opacity 0.5s cubic-bezier(0.25, 0.1, 0.25, 1); - } - - .sun-and-moon .moon > circle { - transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); - } - - @supports (cx: 1) { - .sun-and-moon .moon > circle { - transition: cx 0.25s cubic-bezier(0.4, 0, 0.2, 1); - } - } - - .theme-dark .sun-and-moon > .sun { - transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); - transition-duration: 0.25s; - transform: scale(1.75); - } - - .theme-dark .sun-and-moon > .sun-beams { - transition-duration: 0.15s; - transform: rotateZ(-25deg); - } - - .theme-dark .sun-and-moon > .moon > circle { - transition-duration: 0.5s; - transition-delay: 0.25s; - } -} diff --git a/client/src/Components/v1/ThemeSwitch/index.jsx b/client/src/Components/v1/ThemeSwitch/index.jsx deleted file mode 100644 index 60763da90..000000000 --- a/client/src/Components/v1/ThemeSwitch/index.jsx +++ /dev/null @@ -1,53 +0,0 @@ -/** - * ThemeSwitch Component - * Dark and Light Theme Switch - * Original Code: https://web.dev/patterns/theming/theme-switch - * License: Apache License 2.0 - * Copyright © Google LLC - * - * This code has been adapted for use in this project. - * Apache License: https://www.apache.org/licenses/LICENSE-2.0 - */ - -import { IconButton } from "@mui/material"; -import SunAndMoonIcon from "./SunAndMoonIcon.jsx"; -import { useDispatch, useSelector } from "react-redux"; -import { setMode } from "../../../Features/UI/uiSlice.js"; -import "./index.css"; -import { useTranslation } from "react-i18next"; - -const ThemeSwitch = ({ width = 48, height = 48, color }) => { - const mode = useSelector((state) => state.ui.mode); - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const toggleTheme = () => { - dispatch(setMode(mode === "light" ? "dark" : "light")); - }; - - return ( - :is(circle, g)": { - fill: color, - stroke: color, - }, - }} - > - - - ); -}; - -export default ThemeSwitch; diff --git a/client/src/Components/v2/routing/RouteProtected.tsx b/client/src/Components/v2/routing/RouteProtected.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/Components/v2/routing/RouteRoleProtected.tsx b/client/src/Components/v2/routing/RouteRoleProtected.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 0022187fe..178d00b2d 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -49,14 +49,12 @@ import CreateNewMaintenanceWindow from "@/Pages/Maintenance/create"; import ProtectedRoute from "@/Components/v1/ProtectedRoute"; import RoleProtectedRoute from "@/Components/v1/RoleProtectedRoute"; -import withAdminCheck from "@/Components/v1/HOC/withAdminCheck"; import Logs from "@/Pages/Logs"; import CreateMonitor from "@/Pages/CreateMonitor"; const Routes = () => { const mode = useSelector((state) => state.ui.mode); - const AdminCheckedRegister = withAdminCheck(AuthRegister); const v2theme = mode === "light" ? lightTheme : darkTheme; return ( @@ -380,7 +378,7 @@ const Routes = () => { element={ <> - + } From de447d7be2a90e0a77125674c534fab34e92affd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 17:52:26 +0000 Subject: [PATCH 16/26] move admin check to regsiter page --- .../src/Components/v1/HOC/withAdminCheck.jsx | 49 ------------------- .../Components/v2/routing/RouteProtected.tsx | 41 ++++++++++++++++ .../v2/routing/RouteRoleProtected.tsx | 0 client/src/Pages/Auth/Register/index.tsx | 14 +++++- client/src/Routes/index.jsx | 9 +++- 5 files changed, 61 insertions(+), 52 deletions(-) delete mode 100644 client/src/Components/v1/HOC/withAdminCheck.jsx delete mode 100644 client/src/Components/v2/routing/RouteRoleProtected.tsx diff --git a/client/src/Components/v1/HOC/withAdminCheck.jsx b/client/src/Components/v1/HOC/withAdminCheck.jsx deleted file mode 100644 index 6343b2e4e..000000000 --- a/client/src/Components/v1/HOC/withAdminCheck.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import { logger } from "../../../Utils/Logger.js"; -import { useLazyGet } from "@/Hooks/UseApi"; - -const withAdminCheck = (WrappedComponent) => { - const WithAdminCheck = (props) => { - const navigate = useNavigate(); - const [superAdminExists, setSuperAdminExists] = useState(false); - const [hasChecked, setHasChecked] = useState(false); - const { get: checkSuperAdmin, loading: isChecking } = useLazyGet(); - - useEffect(() => { - checkSuperAdmin("/auth/users/superadmin") - .then((response) => { - if (response?.data === true) { - navigate("/login"); - } else { - setSuperAdminExists(false); - } - }) - .catch((error) => { - logger.error(error); - }) - .finally(() => { - setHasChecked(true); - }); - }, [navigate, checkSuperAdmin]); - - if (!hasChecked || isChecking) { - return null; - } - - return ( - - ); - }; - const wrappedComponentName = - WrappedComponent.displayName || WrappedComponent.name || "Component"; - WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`; - - return WithAdminCheck; -}; - -export default withAdminCheck; diff --git a/client/src/Components/v2/routing/RouteProtected.tsx b/client/src/Components/v2/routing/RouteProtected.tsx index e69de29bb..7064c26a4 100644 --- a/client/src/Components/v2/routing/RouteProtected.tsx +++ b/client/src/Components/v2/routing/RouteProtected.tsx @@ -0,0 +1,41 @@ +import { Navigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; +import type { UserRole } from "@/Types/User"; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export const ProtectedRoute = ({ children }: ProtectedRouteProps) => { + const authState = useSelector((state: RootState) => state.auth); + + return authState.authToken ? ( + children + ) : ( + + ); +}; + +interface RoleProtectedRouteProps { + roles: UserRole[]; + children: React.ReactNode; +} + +export const RoleProtectedRoute = ({ roles, children }: RoleProtectedRouteProps) => { + const authState = useSelector((state: RootState) => state.auth); + const userRoles = authState?.user?.role || []; + const canAccess = userRoles.some((role) => roles.includes(role)); + + return canAccess ? ( + children + ) : ( + + ); +}; diff --git a/client/src/Components/v2/routing/RouteRoleProtected.tsx b/client/src/Components/v2/routing/RouteRoleProtected.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Pages/Auth/Register/index.tsx b/client/src/Pages/Auth/Register/index.tsx index 75d27649f..3c8a4e4bc 100644 --- a/client/src/Pages/Auth/Register/index.tsx +++ b/client/src/Pages/Auth/Register/index.tsx @@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"; import { useRegisterForm } from "@/Hooks/useRegisterForm"; import type { RegisterFormData } from "@/Validation/register"; import { useTranslation } from "react-i18next"; -import { usePost } from "@/Hooks/UseApi"; +import { usePost, useGet } from "@/Hooks/UseApi"; import { setAuthState } from "@/Features/Auth/authSlice"; import { useDispatch } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; @@ -32,11 +32,21 @@ const RegisterPage = () => { const { post: verifyToken } = usePost<{ token: string }, InviteVerifyResponse>(); const hasVerified = useRef(false); + const { data: superAdminExists, isLoading: isCheckingAdmin } = useGet( + token ? null : "/auth/users/superadmin" + ); + const { control, handleSubmit, setError, reset } = useForm({ resolver: zodResolver(schema), defaultValues: defaults, }); + useEffect(() => { + if (superAdminExists === true) { + navigate("/login", { replace: true }); + } + }, [superAdminExists, navigate]); + useEffect(() => { if (!token || hasVerified.current) return; hasVerified.current = true; @@ -53,6 +63,8 @@ const RegisterPage = () => { }); }, [token]); + if (isCheckingAdmin) return null; + const onSubmit = async (data: RegisterFormData) => { if (loading) return; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 178d00b2d..3b0b76fc6 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -47,10 +47,15 @@ import Settings from "@/Pages/Settings"; import Maintenance from "@/Pages/Maintenance"; import CreateNewMaintenanceWindow from "@/Pages/Maintenance/create"; -import ProtectedRoute from "@/Components/v1/ProtectedRoute"; -import RoleProtectedRoute from "@/Components/v1/RoleProtectedRoute"; +// Logs & Diagnostics import Logs from "@/Pages/Logs"; +// Routing +import { + ProtectedRoute, + RoleProtectedRoute, +} from "@/Components/v2/routing/RouteProtected"; + import CreateMonitor from "@/Pages/CreateMonitor"; const Routes = () => { From 6f6920f3b25425c244146a9b6ee09f610709bc36 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 18:15:26 +0000 Subject: [PATCH 17/26] migrate protected routes --- .../Components/v1/ProtectedRoute/index.jsx | 32 ----------------- .../v1/RoleProtectedRoute/index.jsx | 35 ------------------- 2 files changed, 67 deletions(-) delete mode 100644 client/src/Components/v1/ProtectedRoute/index.jsx delete mode 100644 client/src/Components/v1/RoleProtectedRoute/index.jsx diff --git a/client/src/Components/v1/ProtectedRoute/index.jsx b/client/src/Components/v1/ProtectedRoute/index.jsx deleted file mode 100644 index 313500338..000000000 --- a/client/src/Components/v1/ProtectedRoute/index.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Navigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; - -/** - * ProtectedRoute is a wrapper component that ensures only authenticated users - * can access the wrapped content. It checks authentication status (e.g., from Redux or Context). - * If the user is authenticated, it renders the children; otherwise, it redirects to the login page. - * - * @param {Object} props - The props passed to the ProtectedRoute component. - * @param {React.ReactNode} props.children - The children to render if the user is authenticated. - * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page. - */ - -const ProtectedRoute = ({ children }) => { - const authState = useSelector((state) => state.auth); - - return authState.authToken ? ( - children - ) : ( - - ); -}; - -ProtectedRoute.propTypes = { - children: PropTypes.element.isRequired, -}; - -export default ProtectedRoute; diff --git a/client/src/Components/v1/RoleProtectedRoute/index.jsx b/client/src/Components/v1/RoleProtectedRoute/index.jsx deleted file mode 100644 index c7b7dfc12..000000000 --- a/client/src/Components/v1/RoleProtectedRoute/index.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Navigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; - -/** - * ProtectedRoute is a wrapper component that ensures only authenticated users - * can access the wrapped content. It checks authentication status (e.g., from Redux or Context). - * If the user is authenticated, it renders the children; otherwise, it redirects to the login page. - * - * @param {Object} props - The props passed to the ProtectedRoute component. - * @param {React.ReactNode} props.children - The children to render if the user is authenticated. - * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page. - */ - -const RoleProtectedRoute = ({ roles, children }) => { - const authState = useSelector((state) => state.auth); - const userRoles = authState?.user?.role || []; - const canAccess = userRoles.some((role) => roles.includes(role)); - - return canAccess ? ( - children - ) : ( - - ); -}; - -RoleProtectedRoute.propTypes = { - children: PropTypes.element.isRequired, - roles: PropTypes.array, -}; - -export default RoleProtectedRoute; From e214d9a54ce96bb1972fd4cb8934550caf45d6a1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 18:34:55 +0000 Subject: [PATCH 18/26] sidebar tweak --- .../src/Components/v2/sidebar/Authfooter.tsx | 2 +- client/src/Components/v2/sidebar/index.tsx | 134 ++++++++++-------- 2 files changed, 73 insertions(+), 63 deletions(-) diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index 62ea5610b..4724ed97d 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -76,7 +76,7 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) => alignItems="center" py={theme.spacing(4)} px={theme.spacing(8)} - gap={theme.spacing(2)} + gap={theme.spacing(4)} > { }; return ( - t.zIndex.drawer : "auto", - }} - > - + dispatch(setCollapsed({ collapsed: true }))} + sx={{ zIndex: 999 }} + /> + - - {menu.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - return ( - handleNavClick(item.path)} - /> - ); - })} - - - - {bottomMenu.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - return ( - handleNavClick(item.path)} - /> - ); - })} - - + + + {menu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + handleNavClick(item.path)} + /> + ); + })} + + + + {bottomMenu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + handleNavClick(item.path)} + /> + ); + })} + + - - + + + ); }; From 446c791f88b7d1899add15e623512058ed00e838 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 18:36:20 +0000 Subject: [PATCH 19/26] avatar text color --- client/src/Components/v2/design-elements/Avatar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Components/v2/design-elements/Avatar.tsx b/client/src/Components/v2/design-elements/Avatar.tsx index 74c06f8f3..2dd1160ad 100644 --- a/client/src/Components/v2/design-elements/Avatar.tsx +++ b/client/src/Components/v2/design-elements/Avatar.tsx @@ -31,6 +31,7 @@ export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => { alt={`${user?.firstName} ${user?.lastName}`} src={src ? src : user?.avatarImage ? image : undefined} sx={{ + color: theme.palette.primary.contrastText, fontSize: small ? "16px" : "22px", fontWeight: 400, backgroundColor: theme.palette.primary.main, From d296aec02ea0f6a48b35aa82b553aee5c40c8ed1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 18:44:21 +0000 Subject: [PATCH 20/26] add monitor statuses --- server/src/types/monitor.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index f6c0f83a7..6d1f9a422 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -1,10 +1,12 @@ -import type { Check } from "@/types/check.js"; import type { CheckSnapshot } from "@/types/check.js"; export type { CheckSnapshot } from "@/types/check.js"; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const; export type MonitorType = (typeof MonitorTypes)[number]; +export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance"] as const; +export type MonitorStatus = (typeof MonitorStatuses)[number]; + export interface MonitorThresholds { usage_cpu?: number; usage_memory?: number; @@ -20,7 +22,7 @@ export interface Monitor { teamId: string; name: string; description?: string; - status?: boolean; + status?: MonitorStatus; statusWindow: boolean[]; statusWindowSize: number; statusWindowThreshold: number; From 7b658a9e52e50986562ce7417bbd1af77ef88f33 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 18:58:12 +0000 Subject: [PATCH 21/26] initial commit --- server/src/db/models/Monitor.ts | 10 ++-- .../monitors/MongoMonitorsRepository.ts | 56 ++++++------------- server/src/types/monitor.ts | 2 +- 3 files changed, 22 insertions(+), 46 deletions(-) diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index d4cc9a872..9111ce47f 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -1,9 +1,6 @@ import { Schema, model, Types, type UpdateQuery } from "mongoose"; import type { Monitor, MonitorMatchMethod, MonitorThresholds, CheckSnapshot } from "@/types/monitor.js"; -import { MonitorTypes } from "@/types/monitor.js"; -import Check from "./Check.js"; -import MonitorStats from "./MonitorStats.js"; -import StatusPage from "./StatusPage.js"; +import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js"; type CheckSnapshotDocument = Omit & { createdAt: Date }; @@ -68,8 +65,9 @@ const MonitorSchema = new Schema( type: String, }, status: { - type: Boolean, - default: undefined, + type: String, + enum: MonitorStatuses, + default: "initializing", }, statusWindow: { type: [Boolean], diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 9b30e5ac2..71bdca752 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -55,7 +55,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { query.isActive = filter === "true"; break; case "status": - query.status = filter === "true"; + query.status = filter; break; case "type": query.type = filter; @@ -223,17 +223,27 @@ class MongoMonitorsRepository implements IMonitorsRepository { totalMonitors: { $sum: 1 }, upMonitors: { $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], + $cond: [{ $eq: ["$status", "up"] }, 1, 0], }, }, downMonitors: { $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], + $cond: [{ $eq: ["$status", "down"] }, 1, 0], }, }, pausedMonitors: { $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], + $cond: [{ $eq: ["$status", "paused"] }, 1, 0], + }, + }, + initializingMonitors: { + $sum: { + $cond: [{ $eq: ["$status", "initializing"] }, 1, 0], + }, + }, + maintenanceMonitors: { + $sum: { + $cond: [{ $eq: ["$status", "maintenance"] }, 1, 0], }, }, }, @@ -242,7 +252,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { ]; const [summary] = await MonitorModel.aggregate(pipeline); - return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; + return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0, initializingMonitors: 0, maintenanceMonitors: 0 }; }; findGroupsByTeamId = async (teamId: string): Promise => { @@ -284,7 +294,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { teamId: toStringId(doc.teamId), name: doc.name, description: doc.description ?? undefined, - status: doc.status ?? undefined, + status: doc.status ?? "initializing", statusWindow: doc.statusWindow ?? [], statusWindowSize: doc.statusWindowSize, statusWindowThreshold: doc.statusWindowThreshold, @@ -331,45 +341,13 @@ class MongoMonitorsRepository implements IMonitorsRepository { const notificationIds = (doc.notifications ?? []).map((notification: unknown) => toStringId(notification)); - const checks: Check[] = (doc.checks ?? []).map((check: any) => ({ - id: toStringId(check._id), - metadata: { - monitorId: toStringId(check.metadata?.monitorId), - teamId: toStringId(check.metadata?.teamId), - type: check.metadata?.type, - }, - status: check.status, - responseTime: check.responseTime, - timings: check.timings, - statusCode: check.statusCode, - message: check.message, - ack: check.ack, - ackAt: check.ackAt ?? null, - expiry: toDateString(check.expiry), - cpu: check.cpu, - memory: check.memory, - disk: check.disk, - host: check.host, - errors: check.errors, - capture: check.capture, - net: check.net, - accessibility: check.accessibility, - bestPractices: check.bestPractices, - seo: check.seo, - performance: check.performance, - audits: check.audits, - __v: check.__v, - createdAt: toDateString(check.createdAt), - updatedAt: toDateString(check.updatedAt), - })); - return { id: toStringId(doc._id), userId: toStringId(doc.userId), teamId: toStringId(doc.teamId), name: doc.name, description: doc.description ?? undefined, - status: doc.status ?? undefined, + status: doc.status ?? "initializing", statusWindow: doc.statusWindow ?? [], statusWindowSize: doc.statusWindowSize, statusWindowThreshold: doc.statusWindowThreshold, diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 6d1f9a422..f3cd40898 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -22,7 +22,7 @@ export interface Monitor { teamId: string; name: string; description?: string; - status?: MonitorStatus; + status: MonitorStatus; statusWindow: boolean[]; statusWindowSize: number; statusWindowThreshold: number; From d622bce6c141cad8986e89623355f0c4c7854ee1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 19:00:04 +0000 Subject: [PATCH 22/26] update summary type --- client/src/Types/Monitor.ts | 2 ++ server/src/types/monitor.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 2a0272fdc..dde2c4841 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -66,6 +66,8 @@ export interface MonitorsSummary { upMonitors: number; downMonitors: number; pausedMonitors: number; + initializingMonitors: number; + maintenanceMonitors: number; } export interface MonitorsWithChecksResponse { diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index f3cd40898..384e14fe0 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -58,6 +58,8 @@ export interface MonitorsSummary { upMonitors: number; downMonitors: number; pausedMonitors: number; + initializingMonitors: number; + maintenanceMonitors: number; } export interface MonitorsWithChecksByTeamIdResult { From 94afe19b373ae401d10b743b01a360775206a3d2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 19:22:35 +0000 Subject: [PATCH 23/26] update services/repositories to reflect new monitorstatus type --- .../monitors/MongoMonitorsRepository.ts | 8 ++++++- .../src/service/business/incidentService.ts | 2 +- .../SuperSimpleQueueHelper.ts | 3 ++- .../notificationProviders/utils.ts | 2 +- .../infrastructure/notificationsService.ts | 12 ++++------ .../service/infrastructure/statusService.ts | 24 ++++++------------- server/src/types/network.ts | 3 ++- 7 files changed, 24 insertions(+), 30 deletions(-) diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 71bdca752..65d93421e 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -181,7 +181,13 @@ class MongoMonitorsRepository implements IMonitorsRepository { { $set: { isActive: { $not: "$isActive" }, - status: "$$REMOVE", + status: { + $cond: { + if: { $eq: ["$status", "paused"] }, + then: "initializing", + else: "paused", + }, + }, }, }, ], diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 8116c893a..881f98f56 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -46,7 +46,7 @@ class IncidentService { handleIncident = async (monitor: Monitor, code: number): Promise => { const activeIncident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId); // Monitor is down, create an incident - if (monitor.status === false) { + if (monitor.status === "down") { if (activeIncident) { return activeIncident; } else { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index cf3824d60..c5a3453b6 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -59,7 +59,7 @@ class SuperSimpleQueueHelper { throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" }); } - // Step 1. Check for maintenacne window, if found, skip the check + // Step 1. Check for maintenance window, if found, skip the check const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId); if (maintenanceWindowActive) { this.logger.debug({ @@ -67,6 +67,7 @@ class SuperSimpleQueueHelper { service: SERVICE_NAME, method: "getMonitorJob", }); + // TODO update monitor status return; } diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index f71933425..2fe22e834 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -163,7 +163,7 @@ export const buildHardwareEmail = async (emailService: any, monitor: Monitor, al }; export const buildEmail = async (emailService: any, monitor: Monitor): Promise => { - const template = monitor.status === true ? "serverIsUpTemplate" : "serverIsDownTemplate"; + const template = monitor.status === "up" ? "serverIsUpTemplate" : "serverIsDownTemplate"; const context = { monitor: monitor.name, url: monitor.url }; const html = await emailService.buildEmail(template, context); return html; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 78b1dc8ab..531014a7a 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,7 +1,8 @@ -import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; +import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification, MonitorStatus } from "@/types/index.js"; import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js"; import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; + export interface INotificationsService { createNotification: (notificationData: Partial) => Promise; findById: (id: string, teamId: string) => Promise; @@ -11,7 +12,7 @@ export interface INotificationsService { handleNotifications: ( monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, - prevStatus: boolean | undefined, + prevStatus: MonitorStatus, statusChanged: boolean ) => Promise; @@ -228,12 +229,7 @@ export class NotificationsService implements INotificationsService { return await this.emailProvider.sendAlert(notification, syntheticMonitor, baseStatus); }; - handleNotifications = async ( - monitor: Monitor, - monitorStatusResponse: MonitorStatusResponse, - prevStatus: boolean | undefined, - statusChanged: boolean - ) => { + handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, prevStatus: MonitorStatus, statusChanged: boolean) => { const { type } = monitor; const payload = monitorStatusResponse.payload as HardwareStatusPayload; // If this is a non-hardeware type monitor and status did not change, we're done diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 0e1011278..506ad5396 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -2,7 +2,6 @@ import { IMonitorsRepository } from "@/repositories/index.js"; import MonitorStats from "../../db/models/MonitorStats.js"; import { CheckModel } from "@/db/models/index.js"; import type { - CheckErrorInfo, Monitor, MonitorStatusResponse, StatusChangeResult, @@ -15,7 +14,6 @@ const SERVICE_NAME = "StatusService"; export interface IStatusService { updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise; - getStatusString(status: boolean | undefined): string; handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise; updateMonitorStatus( statusResponse: MonitorStatusResponse, @@ -39,7 +37,7 @@ export class StatusService implements IStatusService { return StatusService.SERVICE_NAME; } - async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }) { + async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: MonitorStatusResponse }) { try { const monitorId = monitor.id; const { responseTime, status } = networkResponse; @@ -60,7 +58,7 @@ export class StatusService implements IStatusService { // Update stats // Last response time - stats.lastResponseTime = responseTime; + stats.lastResponseTime = responseTime ?? 0; // Avg response time: let avgResponseTime = stats.avgResponseTime; @@ -111,12 +109,6 @@ export class StatusService implements IStatusService { } } - getStatusString = (status: boolean | undefined) => { - if (status === true) return "up"; - if (status === false) return "down"; - return "unknown"; - }; - handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => { try { let savedCheck; @@ -208,9 +200,7 @@ export class StatusService implements IStatusService { monitor.recentChecks.shift(); } - if (monitor.status === undefined || monitor.status === null) { - monitor.status = status; - } + monitor.status = status === true ? "up" : "down"; const prevStatus = monitor.status; let newStatus = monitor.status; @@ -233,13 +223,13 @@ export class StatusService implements IStatusService { const failureRate = (failures / monitor.statusWindow.length) * 100; // If threshold has been met and the monitor is not already down, mark down: - if (failureRate >= monitor.statusWindowThreshold && monitor.status !== false) { - newStatus = false; + if (failureRate >= monitor.statusWindowThreshold && monitor.status !== "down") { + newStatus = "down"; statusChanged = true; } // If the failure rate is below the threshold and the monitor is down, recover: - else if (failureRate < monitor.statusWindowThreshold && monitor.status === false) { - newStatus = true; + else if (failureRate < monitor.statusWindowThreshold && monitor.status === "down") { + newStatus = "up"; statusChanged = true; } diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 6bed40316..7fbfb79fe 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -11,6 +11,7 @@ import type { Monitor, MonitorMatchMethod, MonitorType, + MonitorStatus, } from "@/types/index.js"; export interface MonitorStatusResponse { @@ -106,7 +107,7 @@ export interface MonitorPayloadMap { export type StatusChangeResult = { monitor: Monitor; statusChanged: boolean; - prevStatus: boolean | undefined; + prevStatus: MonitorStatus; code: number; timestamp: number; }; From cbd8b5db47536cfc7cd779da28221168aa2f5006 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 19:38:32 +0000 Subject: [PATCH 24/26] update monitor status on maintenance --- server/src/config/services.ts | 1 + .../SuperSimpleQueue/SuperSimpleQueueHelper.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 4437e9176..9bf18ff1d 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -206,6 +206,7 @@ export const initializeServices = async ({ buffer: bufferService, incidentService, maintenanceWindowsRepository, + monitorsRepository, }); const superSimpleQueue = await SuperSimpleQueue.create({ diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index c5a3453b6..d5d275e59 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -3,7 +3,7 @@ import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; import IncidentService from "@/service/business/incidentService.js"; -import { IMaintenanceWindowsRepository } from "@/repositories/index.js"; +import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; @@ -16,6 +16,7 @@ class SuperSimpleQueueHelper { private buffer: any; private incidentService: IncidentService; private maintenanceWindowsRepository: IMaintenanceWindowsRepository; + private monitorsRepository: IMonitorsRepository; constructor({ logger, @@ -26,6 +27,7 @@ class SuperSimpleQueueHelper { buffer, incidentService, maintenanceWindowsRepository, + monitorsRepository, }: { logger: any; networkService: INetworkService; @@ -35,6 +37,7 @@ class SuperSimpleQueueHelper { buffer: any; incidentService: IncidentService; maintenanceWindowsRepository: IMaintenanceWindowsRepository; + monitorsRepository: IMonitorsRepository; }) { this.logger = logger; this.networkService = networkService; @@ -44,6 +47,7 @@ class SuperSimpleQueueHelper { this.notificationsService = notificationsService; this.incidentService = incidentService; this.maintenanceWindowsRepository = maintenanceWindowsRepository; + this.monitorsRepository = monitorsRepository; } get serviceName() { @@ -60,6 +64,7 @@ class SuperSimpleQueueHelper { } // Step 1. Check for maintenance window, if found, skip the check + const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId); if (maintenanceWindowActive) { this.logger.debug({ @@ -67,7 +72,9 @@ class SuperSimpleQueueHelper { service: SERVICE_NAME, method: "getMonitorJob", }); - // TODO update monitor status + if (monitor.status !== "maintenance") { + await this.monitorsRepository.updateById(monitorId, teamId, { status: "maintenance" }); + } return; } From 4b1c588f5ad5c02dfeaba676bc11917e0a26c220 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 20:11:22 +0000 Subject: [PATCH 25/26] extract header --- .../v2/design-elements/StatusBox.tsx | 35 ++++++++++++------- .../v2/design-elements/StatusLabel.tsx | 35 +++++++------------ .../v2/monitors/HeaderMonitorsSummary.tsx | 29 +++++++++++++++ client/src/Components/v2/monitors/index.tsx | 1 + .../Pages/Infrastructure/Monitors/index.tsx | 20 +++-------- client/src/Pages/PageSpeed/Monitors/index.tsx | 20 +++-------- .../Status/Components/MonitorsList.tsx | 7 +--- .../Status/Components/StatusBar.tsx | 6 ++-- .../Components/UptimeMonitorsTable.tsx | 7 +--- client/src/Pages/Uptime/Monitors/index.tsx | 25 +++++-------- client/src/Types/Monitor.ts | 12 +++++-- client/src/Utils/MonitorUtils.ts | 21 ++++------- client/src/locales/en.json | 1 + 13 files changed, 105 insertions(+), 114 deletions(-) create mode 100644 client/src/Components/v2/monitors/HeaderMonitorsSummary.tsx diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx index bf7979cca..a83ebcf5d 100644 --- a/client/src/Components/v2/design-elements/StatusBox.tsx +++ b/client/src/Components/v2/design-elements/StatusBox.tsx @@ -105,6 +105,29 @@ export const PausedStatusBox = ({ n }: { n: number }) => { /> ); }; +export const MaintenanceStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; +export const InitializingStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; + export const TotalChecksBox = ({ n }: { n: number }) => { const theme = useTheme(); const { t } = useTranslation(); @@ -138,15 +161,3 @@ export const UpChecksBox = ({ n }: { n: number }) => { /> ); }; - -export const InitializingStatusBox = ({ n }: { n: number }) => { - const theme = useTheme(); - const { t } = useTranslation(); - return ( - - ); -}; diff --git a/client/src/Components/v2/design-elements/StatusLabel.tsx b/client/src/Components/v2/design-elements/StatusLabel.tsx index d7212ddff..cf404421e 100644 --- a/client/src/Components/v2/design-elements/StatusLabel.tsx +++ b/client/src/Components/v2/design-elements/StatusLabel.tsx @@ -11,33 +11,24 @@ import { useTranslation } from "react-i18next"; export const ValueTypes = ["positive", "negative", "neutral"] as const; export type ValueType = (typeof ValueTypes)[number]; -export const StatusLabel = ({ - status, - isActive, - sx, -}: { - status: MonitorStatus; - isActive?: boolean; - sx?: SxProps; -}) => { +export const StatusLabel = ({ status, sx }: { status: MonitorStatus; sx?: SxProps }) => { const { t } = useTranslation(); const theme = useTheme(); const palette = getStatusPalette(status); - const determineStatus = ( - isActive: boolean | undefined, - status: MonitorStatus - ): string => { - if (isActive === false) { + const determineStatus = (status: MonitorStatus): string => { + if (status === "up") { + return t("pages.common.monitors.status.up"); + } else if (status === "down") { + return t("pages.common.monitors.status.down"); + } else if (status === "maintenance") { + return t("pages.common.monitors.status.maintenance"); + } else if (status === "paused") { return t("pages.common.monitors.status.paused"); + } else if (status === "initializing") { + return t("pages.common.monitors.status.initializing"); } - if (status === true) { - return t("pages.common.monitors.status.up"); - } - if (status === false) { - return t("pages.common.monitors.status.down"); - } return t("pages.common.monitors.status.initializing"); }; @@ -64,9 +55,7 @@ export const StatusLabel = ({ borderRadius="50%" marginRight="5px" /> - - {determineStatus(isActive, status)} - + {determineStatus(status)} ); }; diff --git a/client/src/Components/v2/monitors/HeaderMonitorsSummary.tsx b/client/src/Components/v2/monitors/HeaderMonitorsSummary.tsx new file mode 100644 index 000000000..b2a93a807 --- /dev/null +++ b/client/src/Components/v2/monitors/HeaderMonitorsSummary.tsx @@ -0,0 +1,29 @@ +import { + UpStatusBox, + DownStatusBox, + PausedStatusBox, + InitializingStatusBox, +} from "@/Components/v2/design-elements"; +import Stack from "@mui/material/Stack"; + +import type { MonitorsSummary } from "@/Types/Monitor"; +import { useTheme } from "@mui/material"; + +interface MonitorsSummaryProps { + summary: MonitorsSummary | null; +} + +export const HeaderMonitorsSummary = ({ summary }: MonitorsSummaryProps) => { + const theme = useTheme(); + return ( + + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index e2856d6a9..9496395f6 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -11,3 +11,4 @@ export * from "./charts/PiePageSpeedLegend"; export * from "./charts/HistogramPageSpeedDetails"; export * from "./charts/HistogramPageSpeedDetailsTooltip"; export * from "./charts/HistogramInfrastructure"; +export * from "./HeaderMonitorsSummary"; diff --git a/client/src/Pages/Infrastructure/Monitors/index.tsx b/client/src/Pages/Infrastructure/Monitors/index.tsx index 1d38dbdd3..01de1c09c 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.tsx +++ b/client/src/Pages/Infrastructure/Monitors/index.tsx @@ -1,13 +1,8 @@ import Stack from "@mui/material/Stack"; import useMediaQuery from "@mui/material/useMediaQuery"; -import { - MonitorBasePageWithStates, - UpStatusBox, - DownStatusBox, - PausedStatusBox, -} from "@/Components/v2/design-elements"; +import { MonitorBasePageWithStates } from "@/Components/v2/design-elements"; import { HeaderCreate } from "@/Components/v2/common"; -import { ControlsFilter } from "@/Components/v2/monitors"; +import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/v2/monitors"; import { TextField, Dialog } from "@/Components/v2/inputs"; import { useGet, useDelete } from "@/Hooks/UseApi"; @@ -99,7 +94,7 @@ const InfrastructureMonitors = () => { { refreshInterval: 5000, keepPreviousData: true } ); - const { summary, count } = monitorsWithChecksData ?? {}; + const { summary, count } = monitorsWithChecksData ?? { summary: null, count: 0 }; const isLoading = monitorsWithChecksLoading; // Check if any filters are active @@ -138,14 +133,7 @@ const InfrastructureMonitors = () => { isLoading={isLoading} isAdmin={isAdmin} /> - - - - - + { const { t } = useTranslation(); - const theme = useTheme(); const isAdmin = useIsAdmin(); const { deleteFn, loading: isDeleting } = useDelete(); @@ -48,8 +43,8 @@ const PageSpeedMonitorsPage = () => { const [rowsPerPage, setRowsPerPage] = useState(10); const monitors = monitorsData?.monitors; - const monitorsCount = monitorsData?.count; - const summary = monitorsData?.summary; + const monitorsCount = monitorsData?.count ?? 0; + const summary = monitorsData?.summary ?? null; const isLoading = monitorsIsLoading || settingsIsLoading; @@ -80,14 +75,7 @@ const PageSpeedMonitorsPage = () => { isLoading={isLoading} isAdmin={isAdmin} /> - - - - - + { )} {monitors?.map((monitor) => { - const status = determineState(monitor); return ( { )} - + {statusPage.showCharts !== false && ( diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx index 8b3343249..b5c52b32b 100644 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx @@ -12,16 +12,16 @@ const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => { icon: , }; - if (monitors.every((monitor) => monitor.status === true)) { + if (monitors.every((monitor) => monitor.status === "up")) { monitorsStatus.msg = t("pages.statusPages.statusBar.allUp"); monitorsStatus.color = theme.palette.success.main; monitorsStatus.icon = ; return monitorsStatus; - } else if (monitors.every((monitor) => monitor.status === false)) { + } else if (monitors.every((monitor) => monitor.status === "down")) { monitorsStatus.msg = t("pages.statusPages.statusBar.allDown"); monitorsStatus.color = theme.palette.error.main; return monitorsStatus; - } else if (monitors.some((monitor) => monitor.status === false)) { + } else if (monitors.some((monitor) => monitor.status === "down")) { monitorsStatus.msg = t("pages.statusPages.statusBar.degraded"); monitorsStatus.color = theme.palette.warning.main; return monitorsStatus; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index 94c619672..75738e6ad 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -186,12 +186,7 @@ export const MonitorTable = ({ ), render: (row) => { - return ( - - ); + return ; }, }, { diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 702bbd928..9e36ccc86 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -1,10 +1,5 @@ -import { ControlsFilter } from "@/Components/v2/monitors"; -import { - MonitorBasePageWithStates, - UpStatusBox, - DownStatusBox, - PausedStatusBox, -} from "@/Components/v2/design-elements"; +import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/v2/monitors"; +import { MonitorBasePageWithStates } from "@/Components/v2/design-elements"; import { TextField, Dialog } from "@/Components/v2/inputs"; import Stack from "@mui/material/Stack"; import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable"; @@ -99,7 +94,11 @@ const UptimeMonitorsPage = () => { { refreshInterval: 5000, keepPreviousData: true } ); - const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {}; + const { + monitors: monitorsWithChecks, + summary, + count, + } = monitorsWithChecksData ?? { monitors: null, summary: null, count: 0 }; // Delete hook const { deleteFn, loading: isDeleting } = useDelete(); @@ -150,14 +149,8 @@ const UptimeMonitorsPage = () => { isLoading={isLoading} isAdmin={isAdmin} /> - - - - - + + { - if (typeof monitor === "undefined") return "pending"; - if (monitor?.isActive === false) return "paused"; - if (monitor?.status === undefined) return "pending"; - return monitor?.status == true ? "up" : "down"; -}; - export const getMonitorPath = (type: MonitorType): string => { const pathMap: Record = { http: "uptime", @@ -24,10 +17,10 @@ export const getMonitorPath = (type: MonitorType): string => { }; export const getStatusPalette = (status: MonitorStatus): PaletteKey => { - if (status === true) { + if (status === "up") { return "success"; } - if (status === false) { + if (status === "down") { return "error"; } return "warning"; @@ -43,11 +36,11 @@ export const getValuePalette = (value: ValueType): PaletteKey => { }; export const getStatusColor = (status: MonitorStatus, theme: any): string => { - if (status === true) { + if (status === "up") { return theme.palette.success.light; } - if (status === false) { + if (status === "down") { return theme.palette.error.light; } @@ -101,9 +94,9 @@ export const getStatusPageHeaderConfig = ( return { paletteKey: "error", message: "No monitors available" }; } - const allUp = monitors.every((monitor) => monitor.status === true); - const anyDown = monitors.some((monitor) => monitor.status === false); - const allDown = monitors.every((monitor) => monitor.status === false); + const allUp = monitors.every((monitor) => monitor.status === "up"); + const anyDown = monitors.some((monitor) => monitor.status === "down"); + const allDown = monitors.every((monitor) => monitor.status === "down"); if (allUp) return { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index d2a066c11..7eb833308 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -775,6 +775,7 @@ "status": { "down": "down", "initializing": "initializing", + "maintenance": "maintenance", "paused": "paused", "total": "total", "up": "up" From 71cdd05b152a64daad9fd18b9e28142221f401df Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Feb 2026 20:29:50 +0000 Subject: [PATCH 26/26] type errors --- client/src/Pages/Checks/Components/ChecksTable.tsx | 4 ++-- .../Infrastructure/Monitors/Components/MonitorsTable.tsx | 7 +------ client/src/Pages/Uptime/Details/Components/ChecksTable.tsx | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/client/src/Pages/Checks/Components/ChecksTable.tsx b/client/src/Pages/Checks/Components/ChecksTable.tsx index 6464be25c..bb1920ab2 100644 --- a/client/src/Pages/Checks/Components/ChecksTable.tsx +++ b/client/src/Pages/Checks/Components/ChecksTable.tsx @@ -6,7 +6,7 @@ import { } from "@/Components/v2/design-elements"; import Box from "@mui/material/Box"; import type { Header } from "@/Components/v2/design-elements/Table"; -import type { Monitor, MonitorStatus } from "@/Types/Monitor"; +import type { Monitor } from "@/Types/Monitor"; import { useTranslation } from "react-i18next"; import { formatDateWithTz } from "@/Utils/TimeUtils"; @@ -52,7 +52,7 @@ export const ChecksTable = ({ id: "status", content: "Status", render: (row) => { - return ; + return ; }, }, { diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx index ca236f45f..b2bcadc1a 100644 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx +++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx @@ -195,12 +195,7 @@ export const InfraMonitorsTable = ({ ), render: (row) => { - return ( - - ); + return ; }, }, { diff --git a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx index 2cfdc1f15..ceb8e1a28 100644 --- a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx +++ b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx @@ -15,7 +15,7 @@ const getHeaders = (t: Function, uiTimezone: string) => { id: "status", content: t("common.table.headers.status"), render: (row) => { - return ; + return ; }, }, {