diff --git a/apps/frontend/components/id/app-bootstrap-screen.tsx b/apps/frontend/components/id/app-bootstrap-screen.tsx index eb39153..630ec46 100644 --- a/apps/frontend/components/id/app-bootstrap-screen.tsx +++ b/apps/frontend/components/id/app-bootstrap-screen.tsx @@ -1,15 +1,38 @@ 'use client'; import { Loader2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; -export function AppBootstrapScreen() { +export function AppBootstrapScreen({ + error, + onRetry +}: { + error?: string | null; + onRetry?: () => void; +}) { return (
- -

Подключение к серверу

-

- Подготавливаем ваш профиль и сервисы. Это займёт несколько секунд. -

+ {error ? ( + <> +

{error}

+

+ Сервер авторизации временно недоступен. Обычно помогает повтор через несколько секунд. +

+ {onRetry ? ( + + ) : null} + + ) : ( + <> + +

Подключение к серверу

+

+ Подготавливаем ваш профиль и сервисы. Это займёт несколько секунд. +

+ + )}
); } diff --git a/apps/frontend/components/id/auth-provider.tsx b/apps/frontend/components/id/auth-provider.tsx index fde0cde..e8c5834 100644 --- a/apps/frontend/components/id/auth-provider.tsx +++ b/apps/frontend/components/id/auth-provider.tsx @@ -102,7 +102,6 @@ function applySessionState( setters.activatePinLock(session.sessionId ?? ''); } else { setters.clearPinLock(); - scheduleFedcmSessionSync(); } } @@ -120,6 +119,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const [lockedSessionId, setLockedSessionId] = React.useState(null); const [pinSubmitting, setPinSubmitting] = React.useState(false); const [pinError, setPinError] = React.useState(null); + const [bootstrapError, setBootstrapError] = React.useState(null); const isPinLockedRef = React.useRef(false); const refreshInFlightRef = React.useRef | null>(null); const initialBootstrapDoneRef = React.useRef(false); @@ -182,6 +182,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { async (accessToken: string) => { setIsSessionReady(false); setIsLoading(true); + setBootstrapError(null); invalidateApiGatewayReady(); setIsApiReady(false); @@ -192,6 +193,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { initialBootstrapDoneRef.current = true; setIsApiReady(true); setIsSessionReady(true); + } catch (error) { + setBootstrapError( + isGatewayUnavailableError(error) + ? 'Не удалось подключиться к серверу API. Подождите несколько секунд и нажмите «Повторить».' + : (getApiErrorMessage(error, 'Не удалось подключиться к серверу API') ?? 'Ошибка подключения') + ); + throw error; } finally { setIsLoading(false); } @@ -213,7 +221,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { await warmUpAndApplySession(auth.accessToken); if (auth.pinVerified !== false) { - scheduleFedcmSessionSync(auth.accessToken, 2500); + scheduleFedcmSessionSync(auth.accessToken, 8000); } }, [clearPinLock, warmUpAndApplySession] @@ -255,6 +263,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const isInitialBootstrap = !initialBootstrapDoneRef.current; if (isInitialBootstrap) { setIsLoading(true); + setBootstrapError(null); } try { @@ -327,6 +336,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return; } const message = getApiErrorMessage(error, 'Не удалось загрузить профиль'); + if (isGatewayUnavailableError(error) && isInitialBootstrap) { + setBootstrapError('Не удалось подключиться к серверу API. Подождите несколько секунд и нажмите «Повторить».'); + return; + } if (message) showToast(message); logout(); return; @@ -613,7 +626,22 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { logout }} > - {isBootstrapping ? : children} + {isBootstrapping ? ( + { + setBootstrapError(null); + initialBootstrapDoneRef.current = false; + void refreshProfile(); + } + : undefined + } + /> + ) : ( + children + )} ); diff --git a/apps/frontend/lib/api.ts b/apps/frontend/lib/api.ts index bcf8ae1..d23cb8d 100644 --- a/apps/frontend/lib/api.ts +++ b/apps/frontend/lib/api.ts @@ -565,9 +565,11 @@ let fedcmSyncTimer: ReturnType | null = null; let fedcmSyncInFlight = false; /** Откладывает FedCM sync после OAuth/bootstrap, чтобы не конкурировать с consent/session. */ -export function scheduleFedcmSessionSync(accessToken?: string | null, delayMs = 1500) { +export function scheduleFedcmSessionSync(accessToken?: string | null, delayMs = 8000) { if (typeof window === 'undefined') return; if (window.location.pathname.startsWith('/auth/oauth/authorize')) return; + if (window.location.pathname.startsWith('/auth/login')) return; + if (window.location.pathname.startsWith('/auth/register')) return; if (fedcmSyncTimer) { clearTimeout(fedcmSyncTimer); } @@ -747,17 +749,17 @@ export async function waitForStableGateway(stableProbes = GATEWAY_STABLE_PROBES) gatewayReadyAt = 0; let streak = 0; - for (let attempt = 0; attempt < 40 && streak < stableProbes; attempt += 1) { + for (let attempt = 0; attempt < 24 && streak < stableProbes; attempt += 1) { if (await probeGatewayAlive()) { streak += 1; if (streak >= stableProbes) { markApiGatewayReady(); return; } - await sleep(400); + await sleep(500); } else { streak = 0; - await sleep(Math.min(500 + attempt * 220, 2200)); + await sleep(Math.min(700 + attempt * 250, 2500)); } } diff --git a/docker-compose.yml b/docker-compose.yml index 08c6969..87a93fe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -154,6 +154,10 @@ services: timeout: 10s retries: 12 start_period: 60s + networks: + default: {} + idp_static: + ipv4_address: 172.28.50.10 media-ws: pull_policy: build @@ -302,6 +306,9 @@ services: interval: 30s timeout: 5s retries: 3 + networks: + default: {} + idp_static: {} # После recreate api-gateway nginx держит старый IP upstream → 502. Этот watcher # делает nginx -s reload когда gateway снова поднялся (~55 с на Nest bootstrap). @@ -323,3 +330,9 @@ volumes: redis_data: rabbitmq_data: minio_data: + +networks: + idp_static: + ipam: + config: + - subnet: 172.28.50.0/24 diff --git a/install.sh b/install.sh index a80df6b..2ba8a40 100644 --- a/install.sh +++ b/install.sh @@ -33,6 +33,7 @@ INSTALL_LOCAL=0 INSTALL_INTRANET=0 OFFLINE="${OFFLINE:-0}" NGINX_MODE="${NGINX_MODE:-auto}" +IDP_API_GATEWAY_IP="${IDP_API_GATEWAY_IP:-172.28.50.10}" SKIP_DOCKER_PRUNE="${SKIP_DOCKER_PRUNE:-0}" DOCKER_PRUNE_AGGRESSIVE="${DOCKER_PRUNE_AGGRESSIVE:-0}" # Принудительный Docker Nginx (контейнер lendry-id-nginx) — включается в --fix-all @@ -1565,8 +1566,16 @@ ensure_host_upstreams() { nginx_upstream() { case "$1" in api) - # container_name стабильнее service name при пересоздании compose-стека - [[ "$NGINX_MODE" == "docker" ]] && echo "http://lendry-id-api-gateway:3000" || echo "http://127.0.0.1:3000" + if [[ "$NGINX_MODE" == "docker" ]]; then + # Статический IP — nginx не кеширует DNS; переживает recreate api-gateway без reload. + if [[ -n "${IDP_API_GATEWAY_IP:-}" ]]; then + echo "http://${IDP_API_GATEWAY_IP}:3000" + else + echo "http://lendry-id-api-gateway:3000" + fi + else + echo "http://127.0.0.1:3000" + fi ;; frontend) [[ "$NGINX_MODE" == "docker" ]] && echo "http://lendry-id-frontend:3000" || echo "http://127.0.0.1:3002" @@ -1991,13 +2000,20 @@ nginx_upstream_var() { printf 'idp_up_%s' "$(printf '%s' "$1" | tr -c 'A-Za-z0-9' '_')" } +nginx_api_uses_static_ip() { + [[ "$NGINX_MODE" == "docker" && -n "${IDP_API_GATEWAY_IP:-}" ]] +} + # proxy_pass к КОРНЮ апстрима ($request_uri сохраняет путь и query). -# docker: через resolver+переменную (переживает смену IP контейнера). -# host: статический proxy_pass на 127.0.0.1 (DNS-кеша нет, IP не меняется). +# docker + IDP_API_GATEWAY_IP: прямой proxy_pass без DNS (надёжно после recreate). +# docker без static IP: resolver+переменная. +# host: статический proxy_pass на 127.0.0.1. render_proxy_root() { local key="$1" up var up="$(nginx_upstream "$key")" - if [[ "$NGINX_MODE" == "docker" ]]; then + if nginx_api_uses_static_ip && [[ "$key" == "api" ]]; then + printf ' proxy_pass %s$request_uri;' "$up" + elif [[ "$NGINX_MODE" == "docker" ]]; then var="$(nginx_upstream_var "$key")" printf '%s\n set $%s %s;\n proxy_pass $%s$request_uri;' \ "$NGINX_DOCKER_RESOLVER" "$var" "$up" "$var" @@ -2024,7 +2040,9 @@ render_proxy_rewrite() { render_proxy_idp_api() { local key="api" up var up="$(nginx_upstream "$key")" - if [[ "$NGINX_MODE" == "docker" ]]; then + if nginx_api_uses_static_ip; then + printf ' rewrite ^/idp-api/?(.*)$ /$1 break;\n proxy_pass %s;' "$up" + elif [[ "$NGINX_MODE" == "docker" ]]; then var="$(nginx_upstream_var "$key")" printf '%s\n set $%s %s;\n rewrite ^/idp-api/?(.*)$ /$1 break;\n proxy_pass $%s;' \ "$NGINX_DOCKER_RESOLVER" "$var" "$up" "$var"