From fb97d9bec8b45cf83fc080d02e1f698702d7f0b2 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Wed, 10 Dec 2025 23:28:31 -0300 Subject: [PATCH] fix: corrige multiplos problemas de chat e infra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stack.yml: reduz replicas web para 1 (SQLite nao suporta escrita concorrente) - chat.rs: janela de chat ja abre minimizada para evitar marcar mensagens como lidas prematuramente - rustdesk.rs: preserva ID existente do RustDesk ao reprovisionar (evita criar novo ID a cada reinstalacao do Raven) - ChatWidget.tsx: remove isMinimized das dependencias do useEffect para evitar memory leak de resubscriptions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/desktop/src-tauri/src/chat.rs | 22 ++++++-- apps/desktop/src-tauri/src/rustdesk.rs | 69 +++++++++++++++++++------- apps/desktop/src/chat/ChatWidget.tsx | 19 +++---- stack.yml | 4 +- 4 files changed, 80 insertions(+), 34 deletions(-) diff --git a/apps/desktop/src-tauri/src/chat.rs b/apps/desktop/src-tauri/src/chat.rs index 0f13787..92d4b4e 100644 --- a/apps/desktop/src-tauri/src/chat.rs +++ b/apps/desktop/src-tauri/src/chat.rs @@ -675,14 +675,16 @@ async fn process_chat_update( ); // Mostrar janela de chat minimizada (menos intrusivo que abrir completo) + // A janela ja abre minimizada por padrao (start_minimized=true) if let Some(session) = current_sessions.first() { let label = format!("chat-{}", session.ticket_id); if let Some(window) = app.get_webview_window(&label) { + // Janela ja existe - apenas mostrar e garantir que esta minimizada let _ = window.show(); let _ = set_chat_minimized(app, &session.ticket_id, true); } else { + // Criar nova janela ja minimizada (sem necessidade de chamar set_chat_minimized depois) let _ = open_chat_window(app, &session.ticket_id, session.ticket_ref); - let _ = set_chat_minimized(app, &session.ticket_id, true); } } @@ -707,6 +709,11 @@ async fn process_chat_update( // ============================================================================ fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str, ticket_ref: u64) -> Result<(), String> { + open_chat_window_with_state(app, ticket_id, ticket_ref, true) // Por padrao abre minimizada +} + +/// Abre janela de chat com estado inicial de minimizacao configuravel +fn open_chat_window_with_state(app: &tauri::AppHandle, ticket_id: &str, ticket_ref: u64, start_minimized: bool) -> Result<(), String> { let label = format!("chat-{}", ticket_id); // Verificar se ja existe @@ -716,6 +723,13 @@ fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str, ticket_ref return Ok(()); } + // Dimensoes baseadas no estado inicial + let (width, height) = if start_minimized { + (240.0, 52.0) // Tamanho minimizado (chip com badge) + } else { + (380.0, 520.0) // Tamanho expandido + }; + // Obter tamanho da tela para posicionar no canto inferior direito let monitors = app.available_monitors().map_err(|e| e.to_string())?; let primary = monitors.into_iter().next(); @@ -723,8 +737,6 @@ fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str, ticket_ref let (x, y) = if let Some(monitor) = primary { let size = monitor.size(); let scale = monitor.scale_factor(); - let width = 380.0; - let height = 520.0; let margin = 20.0; let taskbar_height = 50.0; ( @@ -744,7 +756,7 @@ fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str, ticket_ref WebviewUrl::App(url_path.into()), ) .title("Chat de Suporte") - .inner_size(380.0, 520.0) + .inner_size(width, height) // Abre ja no tamanho correto .min_inner_size(240.0, 52.0) // Tamanho minimo para modo minimizado com badge .position(x, y) .decorations(false) // Sem decoracoes nativas - usa header customizado @@ -757,7 +769,7 @@ fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str, ticket_ref .build() .map_err(|e| e.to_string())?; - crate::log_info!("Janela de chat aberta: {}", label); + crate::log_info!("Janela de chat aberta (minimizada={}): {}", start_minimized, label); Ok(()) } diff --git a/apps/desktop/src-tauri/src/rustdesk.rs b/apps/desktop/src-tauri/src/rustdesk.rs index 47b60af..ef4a81f 100644 --- a/apps/desktop/src-tauri/src/rustdesk.rs +++ b/apps/desktop/src-tauri/src/rustdesk.rs @@ -77,6 +77,27 @@ struct ReleaseResponse { assets: Vec, } +/// Auxiliar para definir ID customizado baseado no machine_id +fn define_custom_id_from_machine(exe_path: &Path, machine_id: Option<&str>) -> Option { + if let Some(value) = machine_id.and_then(|raw| { + let trimmed = raw.trim(); + if trimmed.is_empty() { None } else { Some(trimmed) } + }) { + match set_custom_id(exe_path, value) { + Ok(custom) => { + log_event(&format!("ID determinístico definido: {custom}")); + Some(custom) + } + Err(error) => { + log_event(&format!("Falha ao definir ID determinístico: {error}")); + None + } + } + } else { + None + } +} + pub fn ensure_rustdesk( config_string: Option<&str>, password_override: Option<&str>, @@ -91,6 +112,13 @@ pub fn ensure_rustdesk( )); } + // IMPORTANTE: Ler o ID existente ANTES de qualquer limpeza + // Isso preserva o ID quando o Raven é reinstalado mas o RustDesk permanece + let preserved_remote_id = read_remote_id_from_profiles(); + if let Some(ref id) = preserved_remote_id { + log_event(&format!("ID existente preservado antes da limpeza: {}", id)); + } + let exe_path = detect_executable_path(); let (installed_version, freshly_installed) = ensure_installed(&exe_path)?; log_event(if freshly_installed { @@ -106,11 +134,17 @@ pub fn ensure_rustdesk( )), } - match purge_existing_rustdesk_profiles() { - Ok(_) => log_event("Configurações antigas do RustDesk limpas antes da reaplicação"), - Err(error) => log_event(&format!( - "Aviso: não foi possível limpar completamente os perfis existentes do RustDesk ({error})" - )), + // So limpa perfis se for instalacao fresca (RustDesk nao existia) + // Se ja existia, preservamos o ID para manter consistencia + if freshly_installed { + match purge_existing_rustdesk_profiles() { + Ok(_) => log_event("Configurações antigas do RustDesk limpas (instalação fresca)"), + Err(error) => log_event(&format!( + "Aviso: não foi possível limpar completamente os perfis existentes do RustDesk ({error})" + )), + } + } else { + log_event("Mantendo perfis existentes do RustDesk (preservando ID)"); } if let Some(value) = config_string.and_then(|raw| { @@ -169,22 +203,19 @@ pub fn ensure_rustdesk( } } - let custom_id = if let Some(value) = machine_id.and_then(|raw| { - let trimmed = raw.trim(); - if trimmed.is_empty() { None } else { Some(trimmed) } - }) { - match set_custom_id(&exe_path, value) { - Ok(custom) => { - log_event(&format!("ID determinístico definido: {custom}")); - Some(custom) - } - Err(error) => { - log_event(&format!("Falha ao definir ID determinístico: {error}")); - None - } + // Se ja existe um ID preservado E o RustDesk nao foi recem-instalado, usa o ID existente + // Isso garante que reinstalar o Raven nao muda o ID do RustDesk + let custom_id = if let Some(ref existing_id) = preserved_remote_id { + if !freshly_installed { + log_event(&format!("Reutilizando ID existente do RustDesk: {}", existing_id)); + Some(existing_id.clone()) + } else { + // Instalacao fresca - define novo ID baseado no machine_id + define_custom_id_from_machine(&exe_path, machine_id) } } else { - None + // Sem ID preservado - define novo ID baseado no machine_id + define_custom_id_from_machine(&exe_path, machine_id) }; if let Err(error) = ensure_service_running(&exe_path) { diff --git a/apps/desktop/src/chat/ChatWidget.tsx b/apps/desktop/src/chat/ChatWidget.tsx index 01bf472..eda9f95 100644 --- a/apps/desktop/src/chat/ChatWidget.tsx +++ b/apps/desktop/src/chat/ChatWidget.tsx @@ -80,7 +80,13 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) { hadSessionRef.current = hasSession }, [hasSession, ticketId]) - // Inicializacao via Convex (WS) + // Ref para acessar isMinimized dentro do callback sem causar resubscription + const isMinimizedRef = useRef(isMinimized) + useEffect(() => { + isMinimizedRef.current = isMinimized + }, [isMinimized]) + + // Inicializacao via Convex (WS) - NAO depende de isMinimized para evitar resubscriptions useEffect(() => { setIsLoading(true) setMessages([]) @@ -107,13 +113,8 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) { const first = payload.messages[0] setTicketInfo((prevInfo) => prevInfo ?? { ref: 0, subject: "", agentName: first.authorName ?? "Suporte" }) } - // Marca como lidas se a janela estiver expandida e houver nao lidas - if (backendUnreadCount > 0 && !isMinimized) { - const unreadIds = payload.messages.filter(m => !m.isFromMachine).map(m => m.id as string) - if (unreadIds.length > 0) { - markMachineMessagesRead(ticketId, unreadIds).catch(err => console.error("mark read falhou", err)) - } - } + // NAO marca como lidas aqui - deixa o useEffect de expansao fazer isso + // Isso evita marcar como lidas antes do usuario expandir o chat }, (err) => { setIsLoading(false) @@ -127,7 +128,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) { messagesSubRef.current?.() messagesSubRef.current = null } - }, [ticketId, isMinimized]) + }, [ticketId]) // Removido isMinimized - evita memory leak de resubscriptions // Sincroniza estado de minimizado com o tamanho da janela (Tauri pode alterar por fora) useEffect(() => { diff --git a/stack.yml b/stack.yml index be9b30e..7c2dd28 100644 --- a/stack.yml +++ b/stack.yml @@ -39,7 +39,9 @@ services: RELEASE_SHA: "${RELEASE_SHA:-dev}" deploy: mode: replicated - replicas: 2 + # IMPORTANTE: SQLite nao suporta multiplas conexoes de escrita simultaneas. + # Manter sempre 1 replica para evitar "attempt to write a readonly database". + replicas: 1 update_config: parallelism: 1 # start-first evita downtime: sobe o novo task antes de parar o anterior