From 6ee3ffe0a5470d2ec03fb046eb00072e7cd5ed59 Mon Sep 17 00:00:00 2001 From: lendry Date: Tue, 30 Jun 2026 19:47:26 +0300 Subject: [PATCH] fix and update --- apps/frontend/app/icon.svg | 4 +++ apps/frontend/app/layout.tsx | 11 +++++--- .../documents/document-photo-gallery.tsx | 3 ++- .../notifications/realtime-provider.tsx | 26 +++++++++++++++---- apps/frontend/lib/resilient-media-fetch.ts | 22 +++++++++++++++- install.sh | 2 +- 6 files changed, 57 insertions(+), 11 deletions(-) create mode 100644 apps/frontend/app/icon.svg diff --git a/apps/frontend/app/icon.svg b/apps/frontend/app/icon.svg new file mode 100644 index 0000000..1e194cc --- /dev/null +++ b/apps/frontend/app/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/frontend/app/layout.tsx b/apps/frontend/app/layout.tsx index 56bd834..c02ba29 100644 --- a/apps/frontend/app/layout.tsx +++ b/apps/frontend/app/layout.tsx @@ -4,13 +4,18 @@ import './globals.css'; export const metadata: Metadata = { title: 'MVK ID', - description: 'Единый аккаунт для сервисов Lendry' + description: 'Единый аккаунт для сервисов Lendry', + icons: { + icon: '/icon.svg', + shortcut: '/icon.svg', + apple: '/icon.svg' + } }; export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( - - + + {children} diff --git a/apps/frontend/components/documents/document-photo-gallery.tsx b/apps/frontend/components/documents/document-photo-gallery.tsx index e89628b..015fffa 100644 --- a/apps/frontend/components/documents/document-photo-gallery.tsx +++ b/apps/frontend/components/documents/document-photo-gallery.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { Loader2, Trash2 } from 'lucide-react'; import { useAuth } from '@/components/id/auth-provider'; import { fetchDocumentPhotoUrl } from '@/lib/api'; +import { resolveBrowserMediaUrl } from '@/lib/resilient-media-fetch'; import { DocumentFullscreenViewer, DocumentPhotoThumbnail } from './document-photo-viewer'; interface DocumentPhotoGalleryProps { @@ -33,7 +34,7 @@ export function DocumentPhotoGallery({ userId, token, storageKeys, readOnly, onR storageKeys.map(async (storageKey) => { try { const response = await fetchDocumentPhotoUrl(userId, storageKey, token); - return { storageKey, accessUrl: response.accessUrl }; + return { storageKey, accessUrl: resolveBrowserMediaUrl(response.accessUrl) }; } catch { return { storageKey, accessUrl: '' }; } diff --git a/apps/frontend/components/notifications/realtime-provider.tsx b/apps/frontend/components/notifications/realtime-provider.tsx index 7c15f6c..354d0e2 100644 --- a/apps/frontend/components/notifications/realtime-provider.tsx +++ b/apps/frontend/components/notifications/realtime-provider.tsx @@ -5,6 +5,9 @@ import { useAuth } from '@/components/id/auth-provider'; import { ensureApiGatewayReady, fetchUnreadNotificationCount, getWsUrl, type ChatMessage } from '@/lib/api'; import { dispatchAvatarUpdated, dispatchChatRoomAvatarUpdated } from '@/lib/avatar-events'; +const WS_RECONNECT_BASE_MS = 2_000; +const WS_RECONNECT_MAX_MS = 30_000; + export interface RealtimeEvent { userId?: string; type: string; @@ -37,6 +40,7 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) { const listenersRef = React.useRef(new Set()); const socketRef = React.useRef(null); const reconnectTimer = React.useRef | null>(null); + const reconnectAttemptRef = React.useRef(0); const subscribe = React.useCallback((listener: Listener) => { listenersRef.current.add(listener); @@ -76,24 +80,35 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) { let cancelled = false; + function scheduleReconnect() { + if (cancelled) return; + if (reconnectTimer.current) clearTimeout(reconnectTimer.current); + const attempt = reconnectAttemptRef.current; + const delay = Math.min(WS_RECONNECT_MAX_MS, WS_RECONNECT_BASE_MS * 2 ** Math.min(attempt, 4)); + reconnectAttemptRef.current = attempt + 1; + reconnectTimer.current = setTimeout(connect, delay); + } + async function connect() { if (cancelled) return; try { await ensureApiGatewayReady(); } catch { - // Пробуем подключиться даже если health не ответил в срок. + scheduleReconnect(); + return; } if (cancelled) return; const url = `${getWsUrl()}?token=${encodeURIComponent(token!)}`; const socket = new WebSocket(url); socketRef.current = socket; - socket.onopen = () => setConnected(true); + socket.onopen = () => { + reconnectAttemptRef.current = 0; + setConnected(true); + }; socket.onclose = () => { setConnected(false); - if (!cancelled) { - reconnectTimer.current = setTimeout(connect, 3000); - } + scheduleReconnect(); }; socket.onerror = () => socket.close(); socket.onmessage = (event) => { @@ -136,6 +151,7 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) { return () => { cancelled = true; if (reconnectTimer.current) clearTimeout(reconnectTimer.current); + reconnectAttemptRef.current = 0; socketRef.current?.close(); socketRef.current = null; setConnected(false); diff --git a/apps/frontend/lib/resilient-media-fetch.ts b/apps/frontend/lib/resilient-media-fetch.ts index ca21f2c..1bef974 100644 --- a/apps/frontend/lib/resilient-media-fetch.ts +++ b/apps/frontend/lib/resilient-media-fetch.ts @@ -3,6 +3,8 @@ import { ensureApiGatewayReady } from '@/lib/api'; const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]); const MEDIA_FETCH_MAX_ATTEMPTS = 8; const MEDIA_SLOW_LOAD_WARN_MS = 8_000; +const MEDIA_STREAM_PATH = '/media/stream/'; +const SAME_ORIGIN_API_PREFIX = '/idp-api'; function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -14,6 +16,23 @@ function warnSlowMediaLoad(label: string, startedAt: number, error: unknown) { console.warn(`[media] ${label}: не удалось загрузить за ${Math.round((Date.now() - startedAt) / 1000)} с (${detail})`); } +export function resolveBrowserMediaUrl(accessUrl: string): string { + if (typeof window === 'undefined') return accessUrl; + + try { + const url = new URL(accessUrl, window.location.origin); + const streamIndex = url.pathname.indexOf(MEDIA_STREAM_PATH); + if (streamIndex === -1) { + return accessUrl; + } + + const streamPath = url.pathname.slice(streamIndex); + return `${window.location.origin}${SAME_ORIGIN_API_PREFIX}${streamPath}${url.search}`; + } catch { + return accessUrl; + } +} + export async function fetchMediaBlobWithRetry( accessUrl: string, token: string, @@ -21,12 +40,13 @@ export async function fetchMediaBlobWithRetry( ): Promise { const startedAt = Date.now(); const label = options?.label ?? 'изображение'; + const mediaUrl = resolveBrowserMediaUrl(accessUrl); let lastError: unknown; for (let attempt = 0; attempt < MEDIA_FETCH_MAX_ATTEMPTS; attempt += 1) { try { await ensureApiGatewayReady(); - const response = await fetch(accessUrl, { + const response = await fetch(mediaUrl, { headers: { Authorization: `Bearer ${token}` }, cache: 'no-store' }); diff --git a/install.sh b/install.sh index a9b4a45..d01843b 100644 --- a/install.sh +++ b/install.sh @@ -2177,7 +2177,7 @@ render_proxy_idp_ws() { local up up="$(nginx_upstream ws)" if [[ "$NGINX_MODE" == "docker" ]]; then - printf ' set $idp_up_ws %s;\n rewrite ^/idp-api/ws/?(.*)$ /ws/$1 break;\n proxy_pass $idp_up_ws;' "$up" + printf ' set $idp_up_ws %s;\n rewrite ^/idp-api/ws$ /ws break;\n rewrite ^/idp-api/ws/(.*)$ /ws/$1 break;\n proxy_pass $idp_up_ws;' "$up" return 0 fi printf ' proxy_pass %s/ws;' "$up"