fix and update

This commit is contained in:
lendry
2026-06-30 23:43:49 +03:00
parent 4b2ade9354
commit deb213bd77
6 changed files with 60 additions and 29 deletions

View File

@@ -76,9 +76,10 @@ function Row({
export default function DataPage() { export default function DataPage() {
const router = useRouter(); const router = useRouter();
const { user, token, refreshProfile } = useAuth(); const { user, token, applyUserPatch } = useAuth();
const { isReady, isPinLocked } = useRequireAuth(); const { isReady, isPinLocked } = useRequireAuth();
const { showToast } = useToast(); const { showToast } = useToast();
const userId = user?.id;
const [displayName, setDisplayName] = useState(''); const [displayName, setDisplayName] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [backupEmail, setBackupEmail] = useState(''); const [backupEmail, setBackupEmail] = useState('');
@@ -98,15 +99,15 @@ export default function DataPage() {
const [cancellingDeletion, setCancellingDeletion] = useState(false); const [cancellingDeletion, setCancellingDeletion] = useState(false);
const loadDocuments = useCallback(async () => { const loadDocuments = useCallback(async () => {
if (!user || !token || isPinLocked) return; if (!userId || !token || isPinLocked) return;
try { try {
const response = await apiFetch<{ documents?: UserDocument[] }>(`/documents/users/${user.id}`, {}, token); const response = await apiFetch<{ documents?: UserDocument[] }>(`/documents/users/${userId}`, {}, token);
setDocuments(response.documents ?? []); setDocuments(response.documents ?? []);
} catch (error) { } catch (error) {
const message = getApiErrorMessage(error, 'Не удалось загрузить документы'); const message = getApiErrorMessage(error, 'Не удалось загрузить документы');
if (message) showToast(message); if (message) showToast(message);
} }
}, [isPinLocked, showToast, token, user]); }, [isPinLocked, showToast, token, userId]);
useEffect(() => { useEffect(() => {
if (!user) return; if (!user) return;
@@ -124,31 +125,31 @@ export default function DataPage() {
}, [user]); }, [user]);
useEffect(() => { useEffect(() => {
if (!isReady || !user || !token || isPinLocked) return; if (!isReady || !userId || !token || isPinLocked) return;
void apiFetch<UserProfileResponse>(`/profile/users/${user.id}`, {}, token) void apiFetch<UserProfileResponse>(`/profile/users/${userId}`, {}, token)
.then((profile) => { .then((profile) => {
if (profile.birthDate) setBirthDate(profile.birthDate); if (profile.birthDate) setBirthDate(profile.birthDate);
}) })
.catch(() => undefined); .catch(() => undefined);
}, [isPinLocked, isReady, token, user]); }, [isPinLocked, isReady, token, userId]);
useEffect(() => { useEffect(() => {
if (isReady && user && !isPinLocked) void loadDocuments(); if (isReady && userId && !isPinLocked) void loadDocuments();
}, [isPinLocked, isReady, loadDocuments, user]); }, [isPinLocked, isReady, loadDocuments, userId]);
const loadDeletionStatus = useCallback(async () => { const loadDeletionStatus = useCallback(async () => {
if (!user || !token || isPinLocked) return; if (!userId || !token || isPinLocked) return;
try { try {
const status = await fetchAccountDeletionStatus(user.id, token); const status = await fetchAccountDeletionStatus(userId, token);
setDeletionStatus(status); setDeletionStatus(status);
} catch { } catch {
setDeletionStatus(null); setDeletionStatus(null);
} }
}, [isPinLocked, token, user]); }, [isPinLocked, token, userId]);
useEffect(() => { useEffect(() => {
if (isReady && user && !isPinLocked) void loadDeletionStatus(); if (isReady && userId && !isPinLocked) void loadDeletionStatus();
}, [isPinLocked, isReady, loadDeletionStatus, user]); }, [isPinLocked, isReady, loadDeletionStatus, userId]);
const documentsByType = useMemo(() => indexDocumentsByType(documents), [documents]); const documentsByType = useMemo(() => indexDocumentsByType(documents), [documents]);
@@ -186,7 +187,13 @@ export default function DataPage() {
}, token); }, token);
} }
await refreshProfile(); applyUserPatch({
displayName: trimmedName || user.displayName,
email: contacts.email ?? user.email,
phone: contacts.phone ?? user.phone,
backupEmail: contacts.backupEmail ?? user.backupEmail,
backupPhone: contacts.backupPhone ?? user.backupPhone
});
showToast('Профиль обновлён'); showToast('Профиль обновлён');
} catch (error) { } catch (error) {
const message = getApiErrorMessage(error, 'Не удалось обновить профиль'); const message = getApiErrorMessage(error, 'Не удалось обновить профиль');
@@ -244,7 +251,9 @@ export default function DataPage() {
verificationIcon={user.verificationIcon} verificationIcon={user.verificationIcon}
className="h-14 w-14" className="h-14 w-14"
badgeSize="sm" badgeSize="sm"
onUpdated={refreshProfile} onUpdated={async () => {
applyUserPatch({ hasAvatar: true });
}}
/> />
) : ( ) : (
<AvatarDisplay userId="" displayName="" hasAvatar={false} token={null} className="h-14 w-14" /> <AvatarDisplay userId="" displayName="" hasAvatar={false} token={null} className="h-14 w-14" />

View File

@@ -40,7 +40,7 @@ export function FamilySidebarPanel() {
} }
fetchChatRooms(group.id, token) fetchChatRooms(group.id, token)
.then((response) => setRooms(response.rooms ?? [])) .then((response) => setRooms(response.rooms ?? []))
.catch(() => setRooms([])); .catch(() => undefined);
}, [group?.id, token]); }, [group?.id, token]);
const roomByPeerUserId = useMemo(() => { const roomByPeerUserId = useMemo(() => {

View File

@@ -56,6 +56,7 @@ interface AuthContextValue {
verifyTotpLogin: (totpChallengeToken: string, code: string) => Promise<AuthTokens>; verifyTotpLogin: (totpChallengeToken: string, code: string) => Promise<AuthTokens>;
completePin: (sessionId: string, pin: string) => Promise<void>; completePin: (sessionId: string, pin: string) => Promise<void>;
applyLoginAuth: (auth: AuthTokens) => Promise<AuthTokens>; applyLoginAuth: (auth: AuthTokens) => Promise<AuthTokens>;
applyUserPatch: (patch: Partial<PublicUser>) => void;
register: (data: { displayName: string; login: string; password: string }) => Promise<void>; register: (data: { displayName: string; login: string; password: string }) => Promise<void>;
refreshProfile: () => Promise<void>; refreshProfile: () => Promise<void>;
logout: () => void; logout: () => void;
@@ -273,6 +274,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
router.push('/auth/login'); router.push('/auth/login');
}, [clearPinLock, router]); }, [clearPinLock, router]);
const applyUserPatch = React.useCallback((patch: Partial<PublicUser>) => {
setUser((current) => {
if (!current) return current;
const next = { ...current, ...patch };
cacheUserProfile(next);
return next;
});
}, []);
const refreshProfile = React.useCallback(async () => { const refreshProfile = React.useCallback(async () => {
if (refreshInFlightRef.current) { if (refreshInFlightRef.current) {
return refreshInFlightRef.current; return refreshInFlightRef.current;
@@ -691,6 +701,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
verifyTotpLogin, verifyTotpLogin,
completePin, completePin,
applyLoginAuth, applyLoginAuth,
applyUserPatch,
register, register,
refreshProfile, refreshProfile,
logout logout

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useFamilyOverlay } from '@/components/family/family-overlay-provider'; import { useFamilyOverlay } from '@/components/family/family-overlay-provider';
import { useAuth } from '@/components/id/auth-provider'; import { useAuth } from '@/components/id/auth-provider';
import { import {
@@ -15,23 +15,26 @@ import {
export function useSelectedFamily(enabled = true) { export function useSelectedFamily(enabled = true) {
const { user, token, isPinLocked, isLoading, isContentReady } = useAuth(); const { user, token, isPinLocked, isLoading, isContentReady } = useAuth();
const { selectedGroupId, setSelectedGroupId } = useFamilyOverlay(); const { selectedGroupId, setSelectedGroupId } = useFamilyOverlay();
const userId = user?.id;
const [groups, setGroups] = useState<FamilyGroup[]>([]); const [groups, setGroups] = useState<FamilyGroup[]>([]);
const [group, setGroup] = useState<FamilyGroup | null>(null); const [group, setGroup] = useState<FamilyGroup | null>(null);
const [presenceMembers, setPresenceMembers] = useState<FamilyPresenceMember[]>([]); const [presenceMembers, setPresenceMembers] = useState<FamilyPresenceMember[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const hasLoadedRef = useRef(false);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const accessToken = getAccessToken() ?? token?.trim() ?? null; const accessToken = getAccessToken() ?? token?.trim() ?? null;
if (!enabled || !user || !accessToken || isPinLocked || isLoading || !isContentReady) { if (!enabled || !userId || !accessToken || isPinLocked || isLoading || !isContentReady) {
setGroups([]); setGroups([]);
setGroup(null); setGroup(null);
setPresenceMembers([]); setPresenceMembers([]);
hasLoadedRef.current = false;
setLoading(false); setLoading(false);
return; return;
} }
try { try {
const groupsResponse = await fetchFamilyGroups(user.id, accessToken); const groupsResponse = await fetchFamilyGroups(userId, accessToken);
const list = groupsResponse.groups ?? []; const list = groupsResponse.groups ?? [];
setGroups(list); setGroups(list);
@@ -39,6 +42,7 @@ export function useSelectedFamily(enabled = true) {
setGroup(null); setGroup(null);
setPresenceMembers([]); setPresenceMembers([]);
if (selectedGroupId) setSelectedGroupId(null); if (selectedGroupId) setSelectedGroupId(null);
hasLoadedRef.current = true;
return; return;
} }
@@ -55,19 +59,26 @@ export function useSelectedFamily(enabled = true) {
]); ]);
setGroup(groupResponse); setGroup(groupResponse);
setPresenceMembers(presenceResponse.members ?? []); setPresenceMembers(presenceResponse.members ?? []);
hasLoadedRef.current = true;
} catch { } catch {
setGroups([]); // Фоновая ошибка не должна сбрасывать уже показанную семью в сайдбаре.
setGroup(null);
setPresenceMembers([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [enabled, isContentReady, isLoading, isPinLocked, selectedGroupId, setSelectedGroupId, token, user]); }, [enabled, isContentReady, isLoading, isPinLocked, selectedGroupId, setSelectedGroupId, token, userId]);
useEffect(() => { useEffect(() => {
setLoading(true); if (!enabled || !userId || !token || isPinLocked || isLoading || !isContentReady) {
setGroups([]);
setGroup(null);
setPresenceMembers([]);
hasLoadedRef.current = false;
setLoading(false);
return;
}
setLoading(!hasLoadedRef.current);
void refresh(); void refresh();
}, [refresh]); }, [enabled, isContentReady, isLoading, isPinLocked, refresh, token, userId]);
useEffect(() => { useEffect(() => {
const accessToken = getAccessToken() ?? token?.trim() ?? null; const accessToken = getAccessToken() ?? token?.trim() ?? null;

View File

@@ -677,7 +677,7 @@ 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 = 4; const GATEWAY_RETRY_ATTEMPTS = 2;
const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800; const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800;
const GATEWAY_STABLE_MAX_ROUNDS = 8; const GATEWAY_STABLE_MAX_ROUNDS = 8;
const API_MAX_CONCURRENT = 6; const API_MAX_CONCURRENT = 6;
@@ -827,7 +827,7 @@ export async function ensureApiGatewayReady(force = false): Promise<void> {
} }
function gatewayRetryDelay(attempt: number): number { function gatewayRetryDelay(attempt: number): number {
return 800 + attempt * 700; return 600 + attempt * 500;
} }
/** Повтор при кратковременном 502/503/504 без сброса всего UI в bootstrap. */ /** Повтор при кратковременном 502/503/504 без сброса всего UI в bootstrap. */

View File

@@ -1,5 +1,5 @@
const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]); const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]);
const MEDIA_FETCH_MAX_ATTEMPTS = 8; const MEDIA_FETCH_MAX_ATTEMPTS = 3;
const MEDIA_SLOW_LOAD_WARN_MS = 8_000; const MEDIA_SLOW_LOAD_WARN_MS = 8_000;
const MEDIA_STREAM_PATH = '/media/stream/'; const MEDIA_STREAM_PATH = '/media/stream/';
const SAME_ORIGIN_API_PREFIX = '/idp-api'; const SAME_ORIGIN_API_PREFIX = '/idp-api';