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",