fix and update
This commit is contained in:
@@ -124,13 +124,13 @@ export default function DataPage() {
|
|||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user || !token || isPinLocked) return;
|
if (!isReady || !user || !token || isPinLocked) return;
|
||||||
void apiFetch<UserProfileResponse>(`/profile/users/${user.id}`, {}, token)
|
void apiFetch<UserProfileResponse>(`/profile/users/${user.id}`, {}, token)
|
||||||
.then((profile) => {
|
.then((profile) => {
|
||||||
if (profile.birthDate) setBirthDate(profile.birthDate);
|
if (profile.birthDate) setBirthDate(profile.birthDate);
|
||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}, [isPinLocked, token, user]);
|
}, [isPinLocked, isReady, token, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady && user && !isPinLocked) void loadDocuments();
|
if (isReady && user && !isPinLocked) void loadDocuments();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { ChatRoom, fetchChatRooms, getApiErrorMessage, setChatRoomPinned } from
|
|||||||
import { useToast } from '@/components/id/toast-provider';
|
import { useToast } from '@/components/id/toast-provider';
|
||||||
|
|
||||||
export function FamilySidebarPanel() {
|
export function FamilySidebarPanel() {
|
||||||
const { user, token } = useAuth();
|
const { user, token, isContentReady } = useAuth();
|
||||||
const {
|
const {
|
||||||
group,
|
group,
|
||||||
groupSummaries,
|
groupSummaries,
|
||||||
@@ -25,7 +25,7 @@ export function FamilySidebarPanel() {
|
|||||||
setSelectedGroupId,
|
setSelectedGroupId,
|
||||||
presenceByUserId,
|
presenceByUserId,
|
||||||
refresh
|
refresh
|
||||||
} = useSelectedFamily(Boolean(user && token));
|
} = useSelectedFamily(Boolean(user && token && isContentReady));
|
||||||
const { openChatWithMember } = useFamilyOverlay();
|
const { openChatWithMember } = useFamilyOverlay();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [inviteOpen, setInviteOpen] = useState(false);
|
const [inviteOpen, setInviteOpen] = useState(false);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function formatMessageTime(value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MiniFamilyChat() {
|
export function MiniFamilyChat() {
|
||||||
const { user, token, isPinLocked } = useAuth();
|
const { user, token, isPinLocked, isContentReady } = useAuth();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const { subscribe } = useRealtime();
|
const { subscribe } = useRealtime();
|
||||||
const {
|
const {
|
||||||
@@ -96,7 +96,7 @@ export function MiniFamilyChat() {
|
|||||||
selectedGroupId,
|
selectedGroupId,
|
||||||
setSelectedGroupId,
|
setSelectedGroupId,
|
||||||
presenceByUserId
|
presenceByUserId
|
||||||
} = useSelectedFamily(Boolean(user && token && !isPinLocked));
|
} = useSelectedFamily(Boolean(user && token && !isPinLocked && isContentReady));
|
||||||
const {
|
const {
|
||||||
miniChatOpen,
|
miniChatOpen,
|
||||||
openMiniChat,
|
openMiniChat,
|
||||||
|
|||||||
@@ -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 }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [token, setToken] = React.useState<string | null>(null);
|
const initialAuth = React.useMemo(() => readInitialAuthState(), []);
|
||||||
const [user, setUser] = React.useState<PublicUser | null>(null);
|
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 [isLoading, setIsLoading] = React.useState(true);
|
||||||
const [isApiReady, setIsApiReady] = React.useState(false);
|
const [isApiReady, setIsApiReady] = React.useState(false);
|
||||||
const [isSessionReady, setIsSessionReady] = React.useState(false);
|
const [isSessionReady, setIsSessionReady] = React.useState(false);
|
||||||
const [isPinLocked, setIsPinLocked] = 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 [lockedSessionId, setLockedSessionId] = React.useState<string | null>(null);
|
||||||
const [pinSubmitting, setPinSubmitting] = React.useState(false);
|
const [pinSubmitting, setPinSubmitting] = React.useState(false);
|
||||||
const [pinError, setPinError] = React.useState<string | null>(null);
|
const [pinError, setPinError] = React.useState<string | null>(null);
|
||||||
@@ -338,6 +353,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const message = getApiErrorMessage(error, 'Не удалось загрузить профиль');
|
const message = getApiErrorMessage(error, 'Не удалось загрузить профиль');
|
||||||
if (isGatewayUnavailableError(error) && isInitialBootstrap) {
|
if (isGatewayUnavailableError(error) && isInitialBootstrap) {
|
||||||
setBootstrapError('Не удалось подключиться к серверу API. Подождите несколько секунд и нажмите «Повторить».');
|
setBootstrapError('Не удалось подключиться к серверу API. Подождите несколько секунд и нажмите «Повторить».');
|
||||||
|
setIsApiReady(false);
|
||||||
|
setIsSessionReady(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (message) showToast(message);
|
if (message) showToast(message);
|
||||||
@@ -348,8 +365,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
} finally {
|
} finally {
|
||||||
if (isInitialBootstrap) {
|
if (isInitialBootstrap) {
|
||||||
initialBootstrapDoneRef.current = true;
|
initialBootstrapDoneRef.current = true;
|
||||||
setIsApiReady(isApiGatewayReady());
|
|
||||||
setIsSessionReady(isApiGatewayReady());
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
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 {
|
interface PublicSettingsContextValue {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
@@ -39,10 +39,13 @@ export function PublicSettingsProvider({ children }: { children: React.ReactNode
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isApiGatewayReady()) {
|
||||||
void refreshPublicSettings();
|
void refreshPublicSettings();
|
||||||
const onFocus = () => void refreshPublicSettings();
|
return;
|
||||||
window.addEventListener('focus', onFocus);
|
}
|
||||||
return () => window.removeEventListener('focus', onFocus);
|
return subscribeApiReady(() => {
|
||||||
|
void refreshPublicSettings();
|
||||||
|
});
|
||||||
}, [refreshPublicSettings]);
|
}, [refreshPublicSettings]);
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
|
|||||||
@@ -682,7 +682,8 @@ const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]);
|
|||||||
const GATEWAY_RETRY_ATTEMPTS = 4;
|
const GATEWAY_RETRY_ATTEMPTS = 4;
|
||||||
const GATEWAY_PROBE_MAX_ATTEMPTS = 48;
|
const GATEWAY_PROBE_MAX_ATTEMPTS = 48;
|
||||||
const GATEWAY_PROBE_BASE_DELAY_MS = 450;
|
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_MAX_CONCURRENT = 6;
|
||||||
const API_BURST_MAX_CONCURRENT = 2;
|
const API_BURST_MAX_CONCURRENT = 2;
|
||||||
const API_BURST_WINDOW_MS = 5000;
|
const API_BURST_WINDOW_MS = 5000;
|
||||||
@@ -742,24 +743,24 @@ export function subscribeApiNotReady(listener: () => void): () => void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Несколько подряд успешных /health через nginx — защита от «один OK, потом 502». */
|
/** Несколько подряд успешных /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;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
gatewayReadyResolved = false;
|
gatewayReadyResolved = false;
|
||||||
gatewayReadyAt = 0;
|
gatewayReadyAt = 0;
|
||||||
|
|
||||||
let streak = 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()) {
|
if (await probeGatewayAlive()) {
|
||||||
streak += 1;
|
streak += 1;
|
||||||
if (streak >= stableProbes) {
|
if (streak >= stableProbes) {
|
||||||
markApiGatewayReady();
|
markApiGatewayReady();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await sleep(500);
|
await sleep(GATEWAY_STABLE_PROBE_INTERVAL_MS);
|
||||||
} else {
|
} else {
|
||||||
streak = 0;
|
streak = 0;
|
||||||
await sleep(Math.min(700 + attempt * 250, 2500));
|
await sleep(Math.min(900 + attempt * 300, 2800));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_VERSION="2.4.1"
|
SCRIPT_VERSION="2.4.2"
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$ROOT_DIR"
|
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-Proto \$scheme;
|
||||||
proxy_set_header X-Forwarded-Host \$host;
|
proxy_set_header X-Forwarded-Host \$host;
|
||||||
proxy_set_header Authorization \$http_authorization;
|
proxy_set_header Authorization \$http_authorization;
|
||||||
|
proxy_set_header Connection \"close\";
|
||||||
proxy_read_timeout 300s;
|
proxy_read_timeout 300s;
|
||||||
proxy_connect_timeout 75s;"
|
proxy_connect_timeout 75s;"
|
||||||
|
|
||||||
# Встроенный DNS Docker. valid=2s — быстрее подхватывает новый IP после recreate api-gateway.
|
# Встроенный DNS Docker. valid=1s — быстрее подхватывает новый IP после recreate api-gateway.
|
||||||
NGINX_DOCKER_RESOLVER=" resolver 127.0.0.11 ipv6=off valid=2s;
|
NGINX_DOCKER_RESOLVER=" resolver 127.0.0.11 ipv6=off valid=1s;
|
||||||
resolver_timeout 3s;"
|
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;"
|
resolver_timeout 3s;"
|
||||||
|
|
||||||
# Имя nginx-переменной из ключа апстрима (только [A-Za-z0-9_]).
|
# Имя nginx-переменной из ключа апстрима (только [A-Za-z0-9_]).
|
||||||
|
|||||||
Reference in New Issue
Block a user