fix and update

This commit is contained in:
lendry
2026-07-01 11:32:53 +03:00
parent 2c4b1fcc44
commit f8f25c8289
3 changed files with 37 additions and 16 deletions

View File

@@ -12,7 +12,7 @@ import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableContainer, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { AdminPermission, AdminRole, AdminUser, adminDisableUserTotp, apiFetch, fetchUserTotpStatus } from '@/lib/api';
import { AdminPermission, AdminRole, AdminUser, adminDisableUserTotp, apiFetch, fetchUserTotpStatus, getApiErrorMessage } from '@/lib/api';
import { getAdminLandingPath } from '@/lib/admin-access';
import { DEFAULT_VERIFICATION_ICON, VERIFICATION_ICON_OPTIONS } from '@/lib/verification-icons';
@@ -56,7 +56,8 @@ export default function AdminUsersPage() {
const response = await apiFetch<{ users: AdminUser[] }>(`/admin/users${search ? `?search=${encodeURIComponent(search)}` : ''}`, {}, token);
setUsers(response.users ?? []);
} catch (error) {
showToast(error instanceof Error ? error.message : 'Не удалось загрузить пользователей');
const message = getApiErrorMessage(error, 'Не удалось загрузить пользователей');
if (message) showToast(message);
} finally {
setLoading(false);
}

View File

@@ -16,6 +16,7 @@ import {
buildAuthDevicePayload,
IdentifyResponse,
getApiErrorMessage,
ensureApiGatewayReady,
invalidateApiGatewayReady,
isApiGatewayReady,
isGatewayUnavailableError,
@@ -25,6 +26,7 @@ import {
PinVerificationResponse,
PublicUser,
refreshAuthSession,
resetGatewayCircuit,
resetPinRequiredNotification,
scheduleFedcmSessionSync,
setPinRequiredHandler,
@@ -200,6 +202,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setIsApiReady(false);
try {
await ensureApiGatewayReady();
const session = await fetchAuthSession(accessToken);
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
initialBootstrapDoneRef.current = true;
@@ -321,6 +324,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
try {
for (let bootstrapAttempt = 0; bootstrapAttempt < 4; bootstrapAttempt += 1) {
if (isInitialBootstrap) {
try {
await ensureApiGatewayReady(bootstrapAttempt > 0);
} catch {
// цикл bootstrap повторит после паузы
}
}
try {
if (currentToken) {
setToken(currentToken);
@@ -375,6 +386,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
} catch (error) {
if (isGatewayUnavailableError(error) && isInitialBootstrap && bootstrapAttempt + 1 < 4) {
invalidateApiGatewayReady();
resetGatewayCircuit();
setBootstrapError(
'Не удалось подключиться к серверу API (502). Подождите несколько секунд и нажмите «Повторить».'
);
@@ -697,7 +709,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
isLoading,
isApiReady,
isSessionReady,
isContentReady: isSessionReady,
isContentReady: isSessionReady && isApiReady,
isPinLocked,
hasStoredSession,
login,
@@ -723,6 +735,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setBootstrapError(null);
initialBootstrapDoneRef.current = false;
invalidateApiGatewayReady();
resetGatewayCircuit();
setIsLoading(true);
setIsApiReady(false);
setIsSessionReady(false);

View File

@@ -479,7 +479,7 @@ function notifyPinRequired(sessionId: string) {
}
export async function fetchAuthSession(token?: string | null): Promise<AuthSessionResponse> {
return apiFetch<AuthSessionResponse>('/auth/session', {}, token);
return apiFetch<AuthSessionResponse>('/auth/session', {}, token, true, { critical: true });
}
export async function approveOAuthAuthorization(params: URLSearchParams, token: string) {
@@ -688,7 +688,7 @@ export function buildSystemSettingPayload(setting: Pick<SystemSetting, 'key' | '
}
const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]);
const GATEWAY_RETRY_ATTEMPTS = 2;
const GATEWAY_RETRY_ATTEMPTS = 3;
const GATEWAY_CRITICAL_RETRY_ATTEMPTS = 4;
export interface ApiFetchBehavior {
@@ -700,8 +700,8 @@ export interface ApiFetchBehavior {
const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800;
const GATEWAY_STABLE_MAX_ROUNDS = 8;
const API_MAX_CONCURRENT = 6;
const API_BURST_MAX_CONCURRENT = 2;
const API_BURST_WINDOW_MS = 5000;
const API_BURST_MAX_CONCURRENT = 4;
const API_BURST_WINDOW_MS = 8000;
const API_READY_EVENT = 'idp-api-ready';
const API_NOT_READY_EVENT = 'idp-api-not-ready';
export const AUTH_SESSION_INVALID_EVENT = 'idp-auth-session-invalid';
@@ -746,6 +746,8 @@ function shouldBypassGatewayCircuit(behavior?: ApiFetchBehavior): boolean {
function openGatewayCircuit(behavior?: ApiFetchBehavior): void {
if (shouldBypassGatewayCircuit(behavior)) return;
// До стабильного прогрева /health circuit не открываем — иначе каждая перезагрузка ловит ложный 502.
if (!gatewayReadyResolved) return;
gatewayReadyResolved = false;
gatewayReadyAt = 0;
stableGatewayPromise = null;
@@ -757,6 +759,7 @@ function openGatewayCircuit(behavior?: ApiFetchBehavior): void {
function assertGatewayCircuitClosed(behavior?: ApiFetchBehavior): void {
if (typeof window === 'undefined' || shouldBypassGatewayCircuit(behavior)) return;
if (!gatewayReadyAt && !gatewayCircuitOpenUntil) return;
if (Date.now() < gatewayCircuitOpenUntil) {
throw new ApiError('Сервер API временно недоступен. Запрос отложен, чтобы не перегружать консоль ошибками.', 503, 'GATEWAY_UNAVAILABLE');
}
@@ -876,7 +879,7 @@ export async function ensureApiGatewayReady(force = false): Promise<void> {
gatewayCircuitOpenUntil = 0;
}
const probes = force ? 2 : 1;
const probes = force ? 2 : 2;
gatewayReadyPromise = waitForStableGateway(probes).finally(() => {
gatewayReadyPromise = null;
});
@@ -900,9 +903,6 @@ async function fetchWithGatewayRetry(
try {
assertGatewayCircuitClosed(behavior);
const response = await fetch(url, init);
if (response.ok) {
markApiGatewayReady();
}
if (GATEWAY_RETRY_STATUS.has(response.status) && attempt + 1 < maxAttempts) {
await sleep(gatewayRetryDelay(attempt));
continue;
@@ -935,6 +935,14 @@ export async function apiFetch<T>(
allowRetry = true,
behavior: ApiFetchBehavior = {}
): Promise<T> {
if (!gatewayReadyResolved && !behavior.silent) {
try {
await ensureApiGatewayReady();
} catch {
// fetchWithGatewayRetry попробует сам после кратковременного 502
}
}
await acquireApiSlot();
const resolvedToken = resolveAccessToken(token);
@@ -1000,7 +1008,6 @@ export async function apiFetch<T>(
throw error;
}
markApiGatewayReady();
return response.json() as Promise<T>;
} finally {
releaseCurrentApiSlot();
@@ -1263,11 +1270,11 @@ export async function fetchFamilyGroups(userId: string, token?: string | null) {
if (!accessToken) {
throw new ApiError('Не авторизован', 401, 'NO_TOKEN');
}
return apiFetch<{ groups?: FamilyGroup[] }>(`/family/users/${userId}/groups`, {}, accessToken);
return apiFetch<{ groups?: FamilyGroup[] }>(`/family/users/${userId}/groups`, {}, accessToken, true, { silent: true });
}
export async function fetchFamilyGroup(groupId: string, token?: string | null) {
return apiFetch<FamilyGroup>(`/family/groups/${groupId}`, {}, token);
return apiFetch<FamilyGroup>(`/family/groups/${groupId}`, {}, token, true, { silent: true });
}
export async function updateFamilyGroup(groupId: string, name: string, token?: string | null) {
@@ -1325,7 +1332,7 @@ export async function fetchNotifications(token?: string | null, unreadOnly = fal
}
export async function fetchUnreadNotificationCount(token?: string | null) {
return apiFetch<{ count: number }>('/notifications/unread-count', {}, token);
return apiFetch<{ count: number }>('/notifications/unread-count', {}, token, true, { silent: true });
}
export async function markNotificationRead(notificationId: string, token?: string | null) {
@@ -1584,7 +1591,7 @@ export async function fetchFamilyPresence(groupId: string, token?: string | null
if (!accessToken) {
throw new ApiError('Не авторизован', 401, 'NO_TOKEN');
}
return apiFetch<FamilyPresence>(`/family/groups/${groupId}/presence`, {}, accessToken);
return apiFetch<FamilyPresence>(`/family/groups/${groupId}/presence`, {}, accessToken, true, { silent: true });
}
/** @deprecated Используйте getWsUrl() — URL может переключаться на same-origin /idp-api/ws в браузере */