fix and update

This commit is contained in:
lendry
2026-06-29 21:36:32 +03:00
parent 8369abb023
commit 4cd75cb0b1
4 changed files with 65 additions and 18 deletions

View File

@@ -1699,7 +1699,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
> >
{activeRoom ? ( {activeRoom ? (
<> <>
<div className="flex items-center justify-between gap-2 border-b border-[#dce3ec] bg-white px-3 py-3 sm:px-4"> <div className="flex items-center justify-between gap-2 border-b border-[#dce3ec] bg-white px-3 py-3 sm:px-4 lg:pr-48">
<div className="flex min-w-0 flex-1 items-center gap-2 sm:gap-3"> <div className="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
<button <button
type="button" type="button"
@@ -1776,38 +1776,38 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
<Button <Button
size="sm" size="sm"
variant="secondary" variant="secondary"
className="h-9 w-9 rounded-xl p-0 sm:h-auto sm:w-auto sm:px-3" className="h-9 w-9 shrink-0 rounded-xl p-0"
aria-label="Поиск по сообщениям" aria-label="Поиск по сообщениям"
onClick={() => { onClick={() => {
setChatToolsTab('search'); setChatToolsTab('search');
setChatToolsOpen(true); setChatToolsOpen(true);
}} }}
> >
<Search className="h-4 w-4" /> <Search className="h-4 w-4 shrink-0" />
</Button> </Button>
<Button <Button
size="sm" size="sm"
variant="secondary" variant="secondary"
className="hidden h-9 w-9 rounded-xl p-0 sm:flex sm:h-auto sm:w-auto sm:px-3" className="hidden h-9 w-9 shrink-0 rounded-xl p-0 sm:inline-flex"
aria-label="Перейти к дате" aria-label="Перейти к дате"
onClick={() => { onClick={() => {
setChatToolsTab('calendar'); setChatToolsTab('calendar');
setChatToolsOpen(true); setChatToolsOpen(true);
}} }}
> >
<CalendarDays className="h-4 w-4" /> <CalendarDays className="h-4 w-4 shrink-0" />
</Button> </Button>
<Button <Button
size="sm" size="sm"
variant="secondary" variant="secondary"
className="rounded-xl" className="h-9 w-9 shrink-0 rounded-xl p-0"
aria-label="Медиа чата" aria-label="Медиа чата"
onClick={() => { onClick={() => {
setChatToolsTab('media'); setChatToolsTab('media');
setChatToolsOpen(true); setChatToolsOpen(true);
}} }}
> >
<ImageIcon className="h-4 w-4" /> <ImageIcon className="h-4 w-4 shrink-0" />
</Button> </Button>
</> </>
) : null} ) : null}

View File

@@ -0,0 +1,15 @@
'use client';
import { Loader2 } from 'lucide-react';
export function AppBootstrapScreen() {
return (
<div className="fixed inset-0 z-[120] flex min-h-screen flex-col items-center justify-center bg-white px-6">
<Loader2 className="h-10 w-10 animate-spin text-[#3390ec]" aria-hidden />
<p className="mt-5 text-base font-medium text-[#101828]">Подключение к серверу</p>
<p className="mt-2 max-w-sm text-center text-sm text-[#667085]">
Подготавливаем ваш профиль и сервисы. Это займёт несколько секунд.
</p>
</div>
);
}

View File

@@ -22,12 +22,14 @@ import {
PinVerificationResponse, PinVerificationResponse,
PublicUser, PublicUser,
refreshAuthSession, refreshAuthSession,
resetApiGatewayWarmup,
resetPinRequiredNotification, resetPinRequiredNotification,
setPinRequiredHandler, setPinRequiredHandler,
syncFedcmSession syncFedcmSession
} from '@/lib/api'; } from '@/lib/api';
import { useToast } from './toast-provider'; import { useToast } from './toast-provider';
import { PinLockModal } from './pin-lock-modal'; import { PinLockModal } from './pin-lock-modal';
import { AppBootstrapScreen } from './app-bootstrap-screen';
import { EMBEDDED_AUTH_MESSAGE, type EmbeddedAuthPayload } from '@/lib/embedded-auth-bridge'; import { EMBEDDED_AUTH_MESSAGE, type EmbeddedAuthPayload } from '@/lib/embedded-auth-bridge';
interface AuthContextValue { interface AuthContextValue {
@@ -112,6 +114,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [pinError, setPinError] = React.useState<string | null>(null); const [pinError, setPinError] = React.useState<string | null>(null);
const isPinLockedRef = React.useRef(false); const isPinLockedRef = React.useRef(false);
const refreshInFlightRef = React.useRef<Promise<void> | null>(null); const refreshInFlightRef = React.useRef<Promise<void> | null>(null);
const initialBootstrapDoneRef = React.useRef(false);
React.useEffect(() => { React.useEffect(() => {
isPinLockedRef.current = isPinLocked; isPinLockedRef.current = isPinLocked;
@@ -160,8 +163,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
cacheUserProfile(auth.user); cacheUserProfile(auth.user);
setHasStoredSession(true); setHasStoredSession(true);
clearPinLock(); clearPinLock();
initialBootstrapDoneRef.current = false;
resetApiGatewayWarmup();
setIsLoading(true); setIsLoading(true);
void ensureApiGatewayReady().finally(() => setIsLoading(false)); void ensureApiGatewayReady().finally(() => {
initialBootstrapDoneRef.current = true;
setIsLoading(false);
});
if (auth.pinVerified !== false) { if (auth.pinVerified !== false) {
void syncFedcmSession(auth.accessToken); void syncFedcmSession(auth.accessToken);
} }
@@ -174,6 +182,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
window.localStorage.removeItem(AUTH_REFRESH_KEY); window.localStorage.removeItem(AUTH_REFRESH_KEY);
window.localStorage.removeItem(AUTH_SESSION_KEY); window.localStorage.removeItem(AUTH_SESSION_KEY);
window.localStorage.removeItem(AUTH_USER_CACHE_KEY); window.localStorage.removeItem(AUTH_USER_CACHE_KEY);
resetApiGatewayWarmup();
initialBootstrapDoneRef.current = false;
setToken(null); setToken(null);
setUser(null); setUser(null);
setHasStoredSession(false); setHasStoredSession(false);
@@ -192,10 +202,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setHasStoredSession(Boolean(refreshToken)); setHasStoredSession(Boolean(refreshToken));
if (!currentToken && !refreshToken) { if (!currentToken && !refreshToken) {
initialBootstrapDoneRef.current = true;
setIsLoading(false); setIsLoading(false);
return; return;
} }
const isInitialBootstrap = !initialBootstrapDoneRef.current;
if (isInitialBootstrap) {
setIsLoading(true);
resetApiGatewayWarmup();
}
try { try {
if (currentToken) { if (currentToken) {
setToken(currentToken); setToken(currentToken);
@@ -259,14 +276,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const hasSession = Boolean( const hasSession = Boolean(
window.localStorage.getItem(AUTH_TOKEN_KEY) || window.localStorage.getItem(AUTH_REFRESH_KEY) window.localStorage.getItem(AUTH_TOKEN_KEY) || window.localStorage.getItem(AUTH_REFRESH_KEY)
); );
if (hasSession) { if (hasSession && isInitialBootstrap) {
try { try {
await ensureApiGatewayReady(); await ensureApiGatewayReady();
} catch { } catch {
// Продолжаем даже если gateway не ответил в срок прогрева. // Продолжаем даже если gateway не ответил в срок прогрева.
} }
} }
if (isInitialBootstrap) {
initialBootstrapDoneRef.current = true;
setIsLoading(false); setIsLoading(false);
}
refreshInFlightRef.current = null; refreshInFlightRef.current = null;
} }
})(); })();
@@ -519,6 +539,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
[login] [login]
); );
const isBootstrapping =
isLoading && !isPinLocked && Boolean(hasStoredSession || token || user);
return ( return (
<AuthContext.Provider <AuthContext.Provider
value={{ value={{
@@ -542,7 +565,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
logout logout
}} }}
> >
{children} {isBootstrapping ? <AppBootstrapScreen /> : children}
<PinLockModal open={isPinLocked} isSubmitting={pinSubmitting} error={pinError} onSubmit={handlePinUnlock} /> <PinLockModal open={isPinLocked} isSubmitting={pinSubmitting} error={pinError} onSubmit={handlePinUnlock} />
</AuthContext.Provider> </AuthContext.Provider>
); );

View File

@@ -505,6 +505,8 @@ export async function refreshAuthSession(): Promise<RefreshSessionResponse> {
throw new ApiError('Refresh token недоступен на сервере', 401, 'NO_REFRESH'); throw new ApiError('Refresh token недоступен на сервере', 401, 'NO_REFRESH');
} }
await ensureApiGatewayReady();
const refreshToken = window.localStorage.getItem(AUTH_REFRESH_KEY); const refreshToken = window.localStorage.getItem(AUTH_REFRESH_KEY);
const sessionId = window.localStorage.getItem(AUTH_SESSION_KEY); const sessionId = window.localStorage.getItem(AUTH_SESSION_KEY);
if (!refreshToken) { if (!refreshToken) {
@@ -632,8 +634,9 @@ export function buildSystemSettingPayload(setting: Pick<SystemSetting, 'key' | '
const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]); const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]);
const GATEWAY_RETRY_ATTEMPTS = 6; const GATEWAY_RETRY_ATTEMPTS = 6;
const GATEWAY_WARMUP_MAX_ATTEMPTS = 24; const GATEWAY_WARMUP_MAX_ATTEMPTS = 40;
const GATEWAY_WARMUP_DELAY_MS = 300; const GATEWAY_WARMUP_DELAY_MS = 300;
const GATEWAY_READY_CONSECUTIVE_OK = 2;
function sleep(ms: number): Promise<void> { function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
@@ -651,9 +654,9 @@ export function markApiGatewayReady() {
gatewayReadyResolved = true; gatewayReadyResolved = true;
} }
async function probeGatewayHealth(): Promise<boolean> { async function probeGatewayReady(): Promise<boolean> {
try { try {
const response = await fetch(`${getApiUrl()}/health`, { const response = await fetch(`${getApiUrl()}/health/ready`, {
method: 'GET', method: 'GET',
cache: 'no-store' cache: 'no-store'
}); });
@@ -663,18 +666,24 @@ async function probeGatewayHealth(): Promise<boolean> {
} }
} }
/** Ждёт готовности api-gateway (кратковременные 502 после перезапуска nginx/upstream). */ /** Ждёт готовности api-gateway и sso-core (readiness по gRPC, не liveness /health). */
export async function ensureApiGatewayReady(force = false): Promise<void> { export async function ensureApiGatewayReady(force = false): Promise<void> {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
if (gatewayReadyResolved && !force) return; if (gatewayReadyResolved && !force) return;
if (gatewayReadyPromise && !force) return gatewayReadyPromise; if (gatewayReadyPromise && !force) return gatewayReadyPromise;
gatewayReadyPromise = (async () => { gatewayReadyPromise = (async () => {
let consecutiveOk = 0;
for (let attempt = 0; attempt < GATEWAY_WARMUP_MAX_ATTEMPTS; attempt++) { for (let attempt = 0; attempt < GATEWAY_WARMUP_MAX_ATTEMPTS; attempt++) {
if (await probeGatewayHealth()) { if (await probeGatewayReady()) {
consecutiveOk += 1;
if (consecutiveOk >= GATEWAY_READY_CONSECUTIVE_OK) {
gatewayReadyResolved = true; gatewayReadyResolved = true;
return; return;
} }
} else {
consecutiveOk = 0;
}
await sleep(GATEWAY_WARMUP_DELAY_MS); await sleep(GATEWAY_WARMUP_DELAY_MS);
} }
gatewayReadyResolved = true; gatewayReadyResolved = true;