fix and update

This commit is contained in:
lendry
2026-06-30 14:12:20 +03:00
parent 2a88c87e94
commit 879875508f
7 changed files with 45 additions and 25 deletions

View File

@@ -124,13 +124,13 @@ export default function DataPage() {
}, [user]);
useEffect(() => {
if (!user || !token || isPinLocked) return;
if (!isReady || !user || !token || isPinLocked) return;
void apiFetch<UserProfileResponse>(`/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();

View File

@@ -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);

View File

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

View File

@@ -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<string | null>(null);
const [user, setUser] = React.useState<PublicUser | null>(null);
const initialAuth = React.useMemo(() => readInitialAuthState(), []);
const [token, setToken] = React.useState<string | null>(initialAuth.token);
const [user, setUser] = React.useState<PublicUser | null>(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<string | null>(null);
const [pinSubmitting, setPinSubmitting] = React.useState(false);
const [pinError, setPinError] = React.useState<string | null>(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);
}
}

View File

@@ -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(

View File

@@ -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<void> {
export async function waitForStableGateway(stableProbes = 3): Promise<void> {
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));
}
}

View File

@@ -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_]).