From ca21a8f5bc48ce6f20b79cd99eb79aed79edc627 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 8 Aug 2026 11:44:40 +0300 Subject: [PATCH] =?UTF-8?q?FIX=20-=20TASKER=20STARTUP:=20=D0=B0=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B0=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../instance/maintenance-message.tsx | 42 +++++++++---- .../components/instance/maintenance-view.tsx | 28 ++++++++- .../core/lib/wrappers/instance-wrapper.tsx | 61 +++++++++++++++++-- 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/plane-src/apps/web/ce/components/instance/maintenance-message.tsx b/plane-src/apps/web/ce/components/instance/maintenance-message.tsx index 6843325..58d5bf8 100644 --- a/plane-src/apps/web/ce/components/instance/maintenance-message.tsx +++ b/plane-src/apps/web/ce/components/instance/maintenance-message.tsx @@ -4,7 +4,16 @@ * See the LICENSE file for details. */ -export function MaintenanceMessage() { +export type TMaintenanceReason = "starting" | "offline" | "unavailable"; + +type TMaintenanceMessageProps = { + autoRetry?: boolean; + reason?: TMaintenanceReason; + statusCode?: number; +}; + +export function MaintenanceMessage(props: TMaintenanceMessageProps) { + const { autoRetry = false, reason = "unavailable", statusCode } = props; const linkMap = [ { key: "mail_to", @@ -13,24 +22,33 @@ export function MaintenanceMessage() { }, ]; + const title = + reason === "starting" + ? "Сервисы NODE.DC запускаются." + : reason === "offline" + ? "Нет связи с сервером NODE.DC." + : "NODE.DC временно недоступен."; + + const description = + reason === "starting" + ? "Сервер ещё не готов после обновления или перезапуска. NODE.DC подключится автоматически сразу после завершения запуска. Перезагружать страницу не нужно." + : reason === "offline" + ? "Не удалось связаться с сервером. NODE.DC проверяет подключение автоматически и продолжит работу после восстановления связи." + : autoRetry + ? "Сервер временно не может обработать запрос. NODE.DC повторяет подключение автоматически и продолжит работу после восстановления." + : "Сервер вернул ошибку, которую нельзя исправить автоматическим повтором. Попробуйте ещё раз или обратитесь в службу поддержки."; + return ( <>
-

🚧 NODE.DC запустился с ошибкой.

- - Часть сервисов могла не подняться. Проверьте логи контейнеров и устраните причину. Если нужна помощь, - переходите в службу поддержки. - +

🚧 {title}

+ {description} + {statusCode && Код ответа сервера: {statusCode}}
{linkMap.map((link) => (
- + {link.label}
diff --git a/plane-src/apps/web/core/components/instance/maintenance-view.tsx b/plane-src/apps/web/core/components/instance/maintenance-view.tsx index eaa3bba..6c7d5d7 100644 --- a/plane-src/apps/web/core/components/instance/maintenance-view.tsx +++ b/plane-src/apps/web/core/components/instance/maintenance-view.tsx @@ -5,6 +5,7 @@ */ import { useTheme } from "next-themes"; +import { Button } from "@plane/propel/button"; // assets import maintenanceModeDarkModeImage from "@/app/assets/instance/maintenance-mode-dark.svg?url"; import maintenanceModeLightModeImage from "@/app/assets/instance/maintenance-mode-light.svg?url"; @@ -12,8 +13,18 @@ import maintenanceModeLightModeImage from "@/app/assets/instance/maintenance-mod import DefaultLayout from "@/layouts/default-layout"; // components import { MaintenanceMessage } from "@/plane-web/components/instance"; +import type { TMaintenanceReason } from "@/plane-web/components/instance"; -export function MaintenanceView() { +type TMaintenanceViewProps = { + autoRetry?: boolean; + isRetrying?: boolean; + reason?: TMaintenanceReason; + statusCode?: number; + onRetry?: () => void; +}; + +export function MaintenanceView(props: TMaintenanceViewProps) { + const { autoRetry = false, isRetrying = false, reason = "unavailable", statusCode, onRetry } = props; // hooks const { resolvedTheme } = useTheme(); // derived values @@ -31,7 +42,20 @@ export function MaintenanceView() { />
- + + {onRetry && ( +
+ +
+ )}
diff --git a/plane-src/apps/web/core/lib/wrappers/instance-wrapper.tsx b/plane-src/apps/web/core/lib/wrappers/instance-wrapper.tsx index c5beae4..ce429c8 100644 --- a/plane-src/apps/web/core/lib/wrappers/instance-wrapper.tsx +++ b/plane-src/apps/web/core/lib/wrappers/instance-wrapper.tsx @@ -5,6 +5,7 @@ */ import type { ReactNode } from "react"; +import { isAxiosError } from "axios"; import { observer } from "mobx-react"; import useSWR from "swr"; // components @@ -17,16 +18,44 @@ type TInstanceWrapper = { children: ReactNode; }; +const INSTANCE_RETRY_BASE_INTERVAL = 2_000; +const INSTANCE_RETRY_MAX_INTERVAL = 30_000; + +export const getInstanceRetryInterval = (retryCount: number) => + Math.min(INSTANCE_RETRY_MAX_INTERVAL, INSTANCE_RETRY_BASE_INTERVAL * 2 ** Math.min(Math.max(retryCount - 1, 0), 4)); + +export const getInstanceErrorStatus = (error: unknown) => (isAxiosError(error) ? error.response?.status : undefined); + +export const shouldRetryInstanceRequest = (error: unknown) => { + if (!isAxiosError(error) || !error.response) return true; + + const { status } = error.response; + return status === 408 || status === 429 || status >= 500; +}; + const InstanceWrapper = observer(function InstanceWrapper(props: TInstanceWrapper) { const { children } = props; // store const { isLoading, instance, error, fetchInstanceInfo } = useInstance(); - const { isLoading: isInstanceSWRLoading, error: instanceSWRError } = useSWR( - "INSTANCE_INFORMATION", - async () => await fetchInstanceInfo(), - { revalidateOnFocus: false } - ); + const { + isLoading: isInstanceSWRLoading, + isValidating: isInstanceSWRValidating, + error: instanceSWRError, + mutate: retryInstanceInfo, + } = useSWR("INSTANCE_INFORMATION", async () => await fetchInstanceInfo(), { + revalidateOnFocus: true, + revalidateOnReconnect: true, + shouldRetryOnError: true, + onErrorRetry: (requestError, _key, _config, revalidate, { retryCount }) => { + if (typeof navigator !== "undefined" && !navigator.onLine) return; + if (!shouldRetryInstanceRequest(requestError)) return; + + setTimeout(() => { + void revalidate({ retryCount }); + }, getInstanceRetryInterval(retryCount)); + }, + }); // loading state if ((isLoading || isInstanceSWRLoading) && !instance) @@ -36,7 +65,27 @@ const InstanceWrapper = observer(function InstanceWrapper(props: TInstanceWrappe ); - if (instanceSWRError) return ; + if (instanceSWRError) { + const statusCode = getInstanceErrorStatus(instanceSWRError); + const reason = + statusCode === 502 || statusCode === 503 || statusCode === 504 + ? "starting" + : statusCode === undefined + ? "offline" + : "unavailable"; + + return ( + { + void retryInstanceInfo(); + }} + /> + ); + } // something went wrong while in the request if (error && error?.status === "error") return <>{children};