From 879875508f8c0c62e57974a3172f9708b85e09db Mon Sep 17 00:00:00 2001 From: lendry Date: Tue, 30 Jun 2026 14:12:20 +0300 Subject: [PATCH] fix and update --- apps/frontend/app/data/page.tsx | 4 +-- .../family/family-sidebar-panel.tsx | 4 +-- .../components/family/mini-family-chat.tsx | 4 +-- apps/frontend/components/id/auth-provider.tsx | 25 +++++++++++++++---- .../id/public-settings-provider.tsx | 13 ++++++---- apps/frontend/lib/api.ts | 11 ++++---- install.sh | 9 ++++--- 7 files changed, 45 insertions(+), 25 deletions(-) diff --git a/apps/frontend/app/data/page.tsx b/apps/frontend/app/data/page.tsx index 838548e..d4fedb0 100644 --- a/apps/frontend/app/data/page.tsx +++ b/apps/frontend/app/data/page.tsx @@ -124,13 +124,13 @@ export default function DataPage() { }, [user]); useEffect(() => { - if (!user || !token || isPinLocked) return; + if (!isReady || !user || !token || isPinLocked) return; void apiFetch(`/profile/users/${user.id}`, {}, token) .then((profile) => { if (profile.birthDate) setBirthDate(profile.birthDate); }) .catch(() => undefined); - }, [isPinLocked, token, user]); + }, [isPinLocked, isReady, token, user]); useEffect(() => { if (isReady && user && !isPinLocked) void loadDocuments(); diff --git a/apps/frontend/components/family/family-sidebar-panel.tsx b/apps/frontend/components/family/family-sidebar-panel.tsx index d5e5bd3..b530c61 100644 --- a/apps/frontend/components/family/family-sidebar-panel.tsx +++ b/apps/frontend/components/family/family-sidebar-panel.tsx @@ -15,7 +15,7 @@ import { ChatRoom, fetchChatRooms, getApiErrorMessage, setChatRoomPinned } from import { useToast } from '@/components/id/toast-provider'; export function FamilySidebarPanel() { - const { user, token } = useAuth(); + const { user, token, isContentReady } = useAuth(); const { group, groupSummaries, @@ -25,7 +25,7 @@ export function FamilySidebarPanel() { setSelectedGroupId, presenceByUserId, refresh - } = useSelectedFamily(Boolean(user && token)); + } = useSelectedFamily(Boolean(user && token && isContentReady)); const { openChatWithMember } = useFamilyOverlay(); const { showToast } = useToast(); const [inviteOpen, setInviteOpen] = useState(false); diff --git a/apps/frontend/components/family/mini-family-chat.tsx b/apps/frontend/components/family/mini-family-chat.tsx index 8e9df0d..8df2c78 100644 --- a/apps/frontend/components/family/mini-family-chat.tsx +++ b/apps/frontend/components/family/mini-family-chat.tsx @@ -86,7 +86,7 @@ function formatMessageTime(value: string) { } export function MiniFamilyChat() { - const { user, token, isPinLocked } = useAuth(); + const { user, token, isPinLocked, isContentReady } = useAuth(); const { showToast } = useToast(); const { subscribe } = useRealtime(); const { @@ -96,7 +96,7 @@ export function MiniFamilyChat() { selectedGroupId, setSelectedGroupId, presenceByUserId - } = useSelectedFamily(Boolean(user && token && !isPinLocked)); + } = useSelectedFamily(Boolean(user && token && !isPinLocked && isContentReady)); const { miniChatOpen, openMiniChat, diff --git a/apps/frontend/components/id/auth-provider.tsx b/apps/frontend/components/id/auth-provider.tsx index e8c5834..1df35c5 100644 --- a/apps/frontend/components/id/auth-provider.tsx +++ b/apps/frontend/components/id/auth-provider.tsx @@ -105,17 +105,32 @@ function applySessionState( } } +function readInitialAuthState() { + if (typeof window === 'undefined') { + return { token: null as string | null, hasStoredSession: false, cachedUser: null as PublicUser | null }; + } + const token = window.localStorage.getItem(AUTH_TOKEN_KEY); + const refresh = window.localStorage.getItem(AUTH_REFRESH_KEY); + const cachedUser = readCachedUserProfile(); + return { + token, + hasStoredSession: Boolean(refresh || token), + cachedUser + }; +} + export function AuthProvider({ children }: { children: React.ReactNode }) { const router = useRouter(); const pathname = usePathname(); const { showToast } = useToast(); - const [token, setToken] = React.useState(null); - const [user, setUser] = React.useState(null); + const initialAuth = React.useMemo(() => readInitialAuthState(), []); + const [token, setToken] = React.useState(initialAuth.token); + const [user, setUser] = React.useState(initialAuth.cachedUser); const [isLoading, setIsLoading] = React.useState(true); const [isApiReady, setIsApiReady] = React.useState(false); const [isSessionReady, setIsSessionReady] = React.useState(false); const [isPinLocked, setIsPinLocked] = React.useState(false); - const [hasStoredSession, setHasStoredSession] = React.useState(false); + const [hasStoredSession, setHasStoredSession] = React.useState(initialAuth.hasStoredSession); const [lockedSessionId, setLockedSessionId] = React.useState(null); const [pinSubmitting, setPinSubmitting] = React.useState(false); const [pinError, setPinError] = React.useState(null); @@ -338,6 +353,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const message = getApiErrorMessage(error, 'Не удалось загрузить профиль'); if (isGatewayUnavailableError(error) && isInitialBootstrap) { setBootstrapError('Не удалось подключиться к серверу API. Подождите несколько секунд и нажмите «Повторить».'); + setIsApiReady(false); + setIsSessionReady(false); return; } if (message) showToast(message); @@ -348,8 +365,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } finally { if (isInitialBootstrap) { initialBootstrapDoneRef.current = true; - setIsApiReady(isApiGatewayReady()); - setIsSessionReady(isApiGatewayReady()); setIsLoading(false); } } diff --git a/apps/frontend/components/id/public-settings-provider.tsx b/apps/frontend/components/id/public-settings-provider.tsx index 1b12ada..469a5a9 100644 --- a/apps/frontend/components/id/public-settings-provider.tsx +++ b/apps/frontend/components/id/public-settings-provider.tsx @@ -1,7 +1,7 @@ 'use client'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { apiFetch, ensureApiGatewayReady } from '@/lib/api'; +import { apiFetch, ensureApiGatewayReady, isApiGatewayReady, subscribeApiReady } from '@/lib/api'; interface PublicSettingsContextValue { projectName: string; @@ -39,10 +39,13 @@ export function PublicSettingsProvider({ children }: { children: React.ReactNode }, []); useEffect(() => { - void refreshPublicSettings(); - const onFocus = () => void refreshPublicSettings(); - window.addEventListener('focus', onFocus); - return () => window.removeEventListener('focus', onFocus); + if (isApiGatewayReady()) { + void refreshPublicSettings(); + return; + } + return subscribeApiReady(() => { + void refreshPublicSettings(); + }); }, [refreshPublicSettings]); const value = useMemo( diff --git a/apps/frontend/lib/api.ts b/apps/frontend/lib/api.ts index d23cb8d..9efc54e 100644 --- a/apps/frontend/lib/api.ts +++ b/apps/frontend/lib/api.ts @@ -682,7 +682,8 @@ const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]); const GATEWAY_RETRY_ATTEMPTS = 4; const GATEWAY_PROBE_MAX_ATTEMPTS = 48; const GATEWAY_PROBE_BASE_DELAY_MS = 450; -const GATEWAY_STABLE_PROBES = 3; +const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800; +const GATEWAY_STABLE_MAX_ROUNDS = 18; const API_MAX_CONCURRENT = 6; const API_BURST_MAX_CONCURRENT = 2; const API_BURST_WINDOW_MS = 5000; @@ -742,24 +743,24 @@ export function subscribeApiNotReady(listener: () => void): () => void { } /** Несколько подряд успешных /health через nginx — защита от «один OK, потом 502». */ -export async function waitForStableGateway(stableProbes = GATEWAY_STABLE_PROBES): Promise { +export async function waitForStableGateway(stableProbes = 3): Promise { if (typeof window === 'undefined') return; gatewayReadyResolved = false; gatewayReadyAt = 0; let streak = 0; - for (let attempt = 0; attempt < 24 && streak < stableProbes; attempt += 1) { + for (let attempt = 0; attempt < GATEWAY_STABLE_MAX_ROUNDS && streak < stableProbes; attempt += 1) { if (await probeGatewayAlive()) { streak += 1; if (streak >= stableProbes) { markApiGatewayReady(); return; } - await sleep(500); + await sleep(GATEWAY_STABLE_PROBE_INTERVAL_MS); } else { streak = 0; - await sleep(Math.min(700 + attempt * 250, 2500)); + await sleep(Math.min(900 + attempt * 300, 2800)); } } diff --git a/install.sh b/install.sh index cde8d62..1fbf4ec 100644 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ set -euo pipefail -SCRIPT_VERSION="2.4.1" +SCRIPT_VERSION="2.4.2" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" @@ -2035,13 +2035,14 @@ nginx_proxy_headers=" proxy_set_header Host \$host; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header X-Forwarded-Host \$host; proxy_set_header Authorization \$http_authorization; + proxy_set_header Connection \"close\"; proxy_read_timeout 300s; proxy_connect_timeout 75s;" -# Встроенный DNS Docker. valid=2s — быстрее подхватывает новый IP после recreate api-gateway. -NGINX_DOCKER_RESOLVER=" resolver 127.0.0.11 ipv6=off valid=2s; +# Встроенный DNS Docker. valid=1s — быстрее подхватывает новый IP после recreate api-gateway. +NGINX_DOCKER_RESOLVER=" resolver 127.0.0.11 ipv6=off valid=1s; resolver_timeout 3s;" -NGINX_DOCKER_SERVER_DNS=" resolver 127.0.0.11 ipv6=off valid=2s; +NGINX_DOCKER_SERVER_DNS=" resolver 127.0.0.11 ipv6=off valid=1s; resolver_timeout 3s;" # Имя nginx-переменной из ключа апстрима (только [A-Za-z0-9_]).