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"