fix and update
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
15
apps/frontend/components/id/app-bootstrap-screen.tsx
Normal file
15
apps/frontend/components/id/app-bootstrap-screen.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 не ответил в срок прогрева.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
if (isInitialBootstrap) {
|
||||||
|
initialBootstrapDoneRef.current = true;
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,17 +666,23 @@ 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()) {
|
||||||
gatewayReadyResolved = true;
|
consecutiveOk += 1;
|
||||||
return;
|
if (consecutiveOk >= GATEWAY_READY_CONSECUTIVE_OK) {
|
||||||
|
gatewayReadyResolved = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
consecutiveOk = 0;
|
||||||
}
|
}
|
||||||
await sleep(GATEWAY_WARMUP_DELAY_MS);
|
await sleep(GATEWAY_WARMUP_DELAY_MS);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user