From 23e7cf58ae45fb8cca5c025c6e288b02adc28db1 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sat, 6 Dec 2025 17:01:40 -0300 Subject: [PATCH] Redesenho da UI de dispositivos e correcao de VRAM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reorganiza layout da tela de dispositivos admin - Renomeia secao "Controles do dispositivo" para "Atalhos" - Adiciona botao de Tickets com badge de quantidade - Simplifica textos de botoes (Acesso, Resetar) - Remove email da maquina do cabecalho - Move empresa e status para mesma linha - Remove chip de Build do resumo - Corrige deteccao de VRAM para GPUs >4GB usando nvidia-smi - Adiciona prefixo "VRAM" na exibicao de memoria da GPU - Documenta sincronizacao RustDesk 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/desktop/src-tauri/src/agent.rs | 51 +- apps/desktop/src-tauri/src/lib.rs | 37 ++ apps/desktop/src-tauri/src/rustdesk.rs | 98 +++ apps/desktop/src-tauri/src/usb_control.rs | 164 ++++- apps/desktop/src/main.tsx | 203 ++++--- bun.lock | 7 +- docs/README.md | 1 + docs/desktop/rustdesk-sync.md | 151 +++++ next.config.ts | 16 +- package.json | 2 +- .../admin/devices/admin-devices-overview.tsx | 574 ++++++++---------- 11 files changed, 863 insertions(+), 441 deletions(-) create mode 100644 docs/desktop/rustdesk-sync.md diff --git a/apps/desktop/src-tauri/src/agent.rs b/apps/desktop/src-tauri/src/agent.rs index 04eb400..b221fb0 100644 --- a/apps/desktop/src-tauri/src/agent.rs +++ b/apps/desktop/src-tauri/src/agent.rs @@ -931,7 +931,44 @@ fn collect_windows_extended() -> serde_json::Value { .unwrap_or_else(|| json!({})); let bios = ps("Get-CimInstance Win32_BIOS | Select-Object Manufacturer,SMBIOSBIOSVersion,ReleaseDate,Version").unwrap_or_else(|| json!({})); let memory = ps("@(Get-CimInstance Win32_PhysicalMemory | Select-Object BankLabel,Capacity,Manufacturer,PartNumber,SerialNumber,ConfiguredClockSpeed,Speed,ConfiguredVoltage)").unwrap_or_else(|| json!([])); - let video = ps("@(Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,PNPDeviceID)").unwrap_or_else(|| json!([])); + // Coleta de GPU com VRAM correta (nvidia-smi para NVIDIA, registro como fallback para >4GB) + let video = ps(r#" + $gpus = @() + $wmiGpus = Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,PNPDeviceID + foreach ($gpu in $wmiGpus) { + $vram = $gpu.AdapterRAM + # Tenta nvidia-smi para GPUs NVIDIA (retorna valor correto para >4GB) + if ($gpu.Name -match 'NVIDIA') { + try { + $nvidiaSmi = & 'nvidia-smi' '--query-gpu=memory.total' '--format=csv,noheader,nounits' 2>$null + if ($nvidiaSmi) { + $vramMB = [int64]($nvidiaSmi.Trim()) + $vram = $vramMB * 1024 * 1024 + } + } catch {} + } + # Fallback: tenta registro do Windows (qwMemorySize é uint64) + if ($vram -le 4294967296 -and $vram -gt 0) { + try { + $regPath = 'HKLM:\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0*' + $regGpus = Get-ItemProperty $regPath -ErrorAction SilentlyContinue + foreach ($reg in $regGpus) { + if ($reg.DriverDesc -eq $gpu.Name -and $reg.'HardwareInformation.qwMemorySize') { + $vram = [int64]$reg.'HardwareInformation.qwMemorySize' + break + } + } + } catch {} + } + $gpus += [PSCustomObject]@{ + Name = $gpu.Name + AdapterRAM = $vram + DriverVersion = $gpu.DriverVersion + PNPDeviceID = $gpu.PNPDeviceID + } + } + @($gpus) + "#).unwrap_or_else(|| json!([])); let disks = ps("@(Get-CimInstance Win32_DiskDrive | Select-Object Model,SerialNumber,Size,InterfaceType,MediaType)").unwrap_or_else(|| json!([])); json!({ @@ -1305,8 +1342,10 @@ impl AgentRuntime { // Verifica politica USB apos heartbeat inicial check_and_apply_usb_policy(&base_clone, &token_clone).await; - let mut ticker = tokio::time::interval(Duration::from_secs(interval)); - ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + let mut heartbeat_ticker = tokio::time::interval(Duration::from_secs(interval)); + heartbeat_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + let mut usb_ticker = tokio::time::interval(Duration::from_secs(60)); + usb_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { // Wait interval @@ -1314,7 +1353,11 @@ impl AgentRuntime { _ = stop_signal_clone.notified() => { break; } - _ = ticker.tick() => {} + _ = heartbeat_ticker.tick() => {} + _ = usb_ticker.tick() => { + check_and_apply_usb_policy(&base_clone, &token_clone).await; + continue; + } } if let Err(error) = diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 454e9d7..0364074 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -4,9 +4,13 @@ mod rustdesk; mod usb_control; use agent::{collect_inventory_plain, collect_profile, AgentRuntime, MachineProfile}; +use chrono::Local; use usb_control::{UsbPolicy, UsbPolicyResult}; use tauri::{Emitter, Manager, WindowEvent}; use tauri_plugin_store::Builder as StorePluginBuilder; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::{Path, PathBuf}; #[cfg(target_os = "windows")] use std::process::Command; #[cfg(target_os = "windows")] @@ -59,6 +63,38 @@ fn open_devtools(window: tauri::WebviewWindow) -> Result<(), String> { Ok(()) } +#[tauri::command] +fn log_app_event(message: String) -> Result<(), String> { + append_app_log(&message) +} + +fn append_app_log(message: &str) -> Result<(), String> { + let Some(dir) = logs_directory() else { + return Err("LOCALAPPDATA indisponivel para gravar logs".to_string()); + }; + + std::fs::create_dir_all(&dir) + .map_err(|error| format!("Falha ao criar pasta de logs: {error}"))?; + + let path = dir.join("app.log"); + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .map_err(|error| format!("Falha ao abrir app.log: {error}"))?; + + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S"); + writeln!(file, "[{timestamp}] {message}") + .map_err(|error| format!("Falha ao escrever log: {error}"))?; + + Ok(()) +} + +fn logs_directory() -> Option { + let base = std::env::var("LOCALAPPDATA").ok()?; + Some(Path::new(&base).join("br.com.esdrasrenan.sistemadechamados").join("logs")) +} + #[tauri::command] async fn ensure_rustdesk_and_emit( app: tauri::AppHandle, @@ -150,6 +186,7 @@ pub fn run() { start_machine_agent, stop_machine_agent, open_devtools, + log_app_event, ensure_rustdesk_and_emit, apply_usb_policy, get_usb_policy, diff --git a/apps/desktop/src-tauri/src/rustdesk.rs b/apps/desktop/src-tauri/src/rustdesk.rs index 3d03dd5..47b60af 100644 --- a/apps/desktop/src-tauri/src/rustdesk.rs +++ b/apps/desktop/src-tauri/src/rustdesk.rs @@ -282,6 +282,29 @@ pub fn ensure_rustdesk( log_event("Dados do RustDesk salvos no machine-agent.json"); } + // Sincroniza com o backend imediatamente apos provisionar + // O Rust faz o HTTP direto, sem passar pelo CSP do webview + if let Err(error) = sync_remote_access_with_backend(&result) { + log_event(&format!("Aviso: falha ao sincronizar com backend: {error}")); + } else { + log_event("Acesso remoto sincronizado com backend"); + // Atualiza lastSyncedAt no store + let synced_data = serde_json::json!({ + "id": final_id, + "password": password, + "installedVersion": version, + "updated": freshly_installed, + "lastProvisionedAt": last_provisioned_at, + "lastSyncedAt": Utc::now().timestamp_millis(), + "lastError": serde_json::Value::Null + }); + if let Err(e) = upsert_machine_store_value("rustdesk", synced_data) { + log_event(&format!("Aviso: falha ao atualizar lastSyncedAt: {e}")); + } else { + log_event("lastSyncedAt atualizado com sucesso"); + } + } + log_event(&format!("Provisionamento concluído. ID final: {final_id}. Versão: {:?}", version)); Ok(result) @@ -1459,3 +1482,78 @@ fn mark_acl_unlock_flag() { )); } } + +fn get_machine_store_path() -> Result { + let base = env::var("LOCALAPPDATA") + .map_err(|_| RustdeskError::MissingId)?; + Ok(Path::new(&base) + .join(APP_IDENTIFIER) + .join(MACHINE_STORE_FILENAME)) +} + +fn sync_remote_access_with_backend(result: &crate::RustdeskProvisioningResult) -> Result<(), RustdeskError> { + log_event("Iniciando sincronizacao com backend..."); + + // Le token e config do store + let store_path = get_machine_store_path()?; + let store_content = fs::read_to_string(&store_path) + .map_err(RustdeskError::Io)?; + let store: serde_json::Value = serde_json::from_str(&store_content) + .map_err(|_| RustdeskError::MissingId)?; + + let token = store.get("token") + .and_then(|v| v.as_str()) + .ok_or(RustdeskError::MissingId)?; + + let config = store.get("config") + .ok_or(RustdeskError::MissingId)?; + + let machine_id = config.get("machineId") + .and_then(|v| v.as_str()) + .ok_or(RustdeskError::MissingId)?; + + let api_base_url = config.get("apiBaseUrl") + .and_then(|v| v.as_str()) + .unwrap_or("https://tickets.esdrasrenan.com.br"); + + log_event(&format!("Sincronizando com backend: {} (machineId: {})", api_base_url, machine_id)); + + // Monta payload conforme schema esperado pelo backend + // Schema: { machineToken, provider, identifier, password?, url?, username?, notes? } + let payload = serde_json::json!({ + "machineToken": token, + "provider": "RustDesk", + "identifier": result.id, + "password": result.password, + "notes": format!("Versao: {}. Provisionado em: {}", + result.installed_version.as_deref().unwrap_or("desconhecida"), + result.last_provisioned_at) + }); + + // Faz POST para /api/machines/remote-access + let client = Client::builder() + .user_agent(USER_AGENT) + .timeout(Duration::from_secs(30)) + .build()?; + + let url = format!("{}/api/machines/remote-access", api_base_url); + let response = client.post(&url) + .header("Content-Type", "application/json") + .header("Idempotency-Key", format!("{}:RustDesk:{}", machine_id, result.id)) + .body(payload.to_string()) + .send()?; + + if response.status().is_success() { + log_event(&format!("Sync com backend OK: status {}", response.status())); + Ok(()) + } else { + let status = response.status(); + let body = response.text().unwrap_or_default(); + let body_preview = if body.len() > 200 { &body[..200] } else { &body }; + log_event(&format!("Sync com backend falhou: {} - {}", status, body_preview)); + Err(RustdeskError::CommandFailed { + command: "sync_remote_access".to_string(), + status: Some(status.as_u16() as i32) + }) + } +} diff --git a/apps/desktop/src-tauri/src/usb_control.rs b/apps/desktop/src-tauri/src/usb_control.rs index abb959d..24eebfa 100644 --- a/apps/desktop/src-tauri/src/usb_control.rs +++ b/apps/desktop/src-tauri/src/usb_control.rs @@ -65,6 +65,9 @@ pub enum UsbControlError { #[cfg(target_os = "windows")] mod windows_impl { use super::*; + use std::fs; + use std::path::PathBuf; + use std::process::Command; use winreg::enums::*; use winreg::RegKey; @@ -80,6 +83,39 @@ mod windows_impl { pub fn apply_usb_policy(policy: UsbPolicy) -> Result { let now = chrono::Utc::now().timestamp_millis(); + let direct_result = try_apply_policy_direct(policy); + + match direct_result { + Ok(()) => Ok(UsbPolicyResult { + success: true, + policy: policy.as_str().to_string(), + error: None, + applied_at: Some(now), + }), + Err(err) => { + // Tenta elevação se faltou permissão + if is_permission_error(&err) { + if let Err(elevated_err) = apply_policy_with_elevation(policy) { + return Err(elevated_err); + } + // Revalida a policy após elevação + let current = get_current_policy()?; + if current != policy { + return Err(UsbControlError::PermissionDenied); + } + return Ok(UsbPolicyResult { + success: true, + policy: policy.as_str().to_string(), + error: None, + applied_at: Some(now), + }); + } + Err(err) + } + } + } + + fn try_apply_policy_direct(policy: UsbPolicy) -> Result<(), UsbControlError> { // 1. Aplicar Removable Storage Access Policy apply_removable_storage_policy(policy)?; @@ -93,12 +129,7 @@ mod windows_impl { apply_write_protect(false)?; } - Ok(UsbPolicyResult { - success: true, - policy: policy.as_str().to_string(), - error: None, - applied_at: Some(now), - }) + Ok(()) } fn apply_removable_storage_policy(policy: UsbPolicy) -> Result<(), UsbControlError> { @@ -120,27 +151,27 @@ mod windows_impl { UsbPolicy::BlockAll => { let (key, _) = hklm .create_subkey(&full_path) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; key.set_value("Deny_Read", &1u32) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; key.set_value("Deny_Write", &1u32) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; key.set_value("Deny_Execute", &1u32) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; } UsbPolicy::Readonly => { let (key, _) = hklm .create_subkey(&full_path) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; // Permite leitura, bloqueia escrita key.set_value("Deny_Read", &0u32) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; key.set_value("Deny_Write", &1u32) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; key.set_value("Deny_Execute", &0u32) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; } } @@ -152,13 +183,13 @@ mod windows_impl { let key = hklm .open_subkey_with_flags(USBSTOR_PATH, KEY_ALL_ACCESS) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; match policy { UsbPolicy::Allow => { // Start = 3 habilita o driver key.set_value("Start", &3u32) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; } UsbPolicy::BlockAll | UsbPolicy::Readonly => { // Start = 4 desabilita o driver @@ -166,11 +197,11 @@ mod windows_impl { // Porem, como fallback de seguranca, desabilitamos para BlockAll if policy == UsbPolicy::BlockAll { key.set_value("Start", &4u32) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; } else { // Readonly mantem driver ativo key.set_value("Start", &3u32) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; } } } @@ -184,10 +215,10 @@ mod windows_impl { if enable { let (key, _) = hklm .create_subkey(STORAGE_POLICY_PATH) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; key.set_value("WriteProtect", &1u32) - .map_err(|e| UsbControlError::RegistryError(e.to_string()))?; + .map_err(map_winreg_error)?; } else { if let Ok(key) = hklm.open_subkey_with_flags(STORAGE_POLICY_PATH, KEY_ALL_ACCESS) { let _ = key.set_value("WriteProtect", &0u32); @@ -227,6 +258,99 @@ mod windows_impl { Ok(UsbPolicy::Allow) } + fn is_permission_error(error: &UsbControlError) -> bool { + match error { + UsbControlError::PermissionDenied => true, + UsbControlError::RegistryError(msg) => { + let lower = msg.to_lowercase(); + lower.contains("access is denied") || lower.contains("acesso negado") || lower.contains("5") + } + _ => false, + } + } + + fn apply_policy_with_elevation(policy: UsbPolicy) -> Result<(), UsbControlError> { + // Cria script temporário para aplicar as chaves via PowerShell elevado + let temp_dir = std::env::temp_dir(); + let script_path: PathBuf = temp_dir.join("raven_usb_policy.ps1"); + + let policy_str = policy.as_str(); + let script = format!( + r#"$ErrorActionPreference = 'Stop' +$guid = '{guid}' +$policy = '{policy}' + +function Set-Allow {{ + reg delete 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /f 2>$null + reg delete 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /f 2>$null + reg add 'HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR' /v Start /t REG_DWORD /d 3 /f | Out-Null +}} + +function Set-BlockAll {{ + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Read /t REG_DWORD /d 1 /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Write /t REG_DWORD /d 1 /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Execute /t REG_DWORD /d 1 /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR' /v Start /t REG_DWORD /d 4 /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /v WriteProtect /t REG_DWORD /d 0 /f | Out-Null +}} + +function Set-Readonly {{ + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Read /t REG_DWORD /d 0 /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Write /t REG_DWORD /d 1 /f | Out-Null + reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Execute /t REG_DWORD /d 0 /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR' /v Start /t REG_DWORD /d 3 /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /f | Out-Null + reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /v WriteProtect /t REG_DWORD /d 1 /f | Out-Null +}} + +switch ($policy) {{ + 'ALLOW' {{ Set-Allow }} + 'BLOCK_ALL' {{ Set-BlockAll }} + 'READONLY' {{ Set-Readonly }} + default {{ throw 'Politica invalida' }} +}} + +try {{ + gpupdate /target:computer /force | Out-Null +}} catch {{}} +"#, + guid = REMOVABLE_STORAGE_GUID, + policy = policy_str + ); + + fs::write(&script_path, script).map_err(|e| UsbControlError::Io(e))?; + + // Start-Process com RunAs para acionar UAC + let arg = format!( + "Start-Process -WindowStyle Hidden -FilePath powershell -Verb RunAs -Wait -ArgumentList '-ExecutionPolicy Bypass -File \"{}\"'", + script_path.display() + ); + + let status = Command::new("powershell") + .arg("-Command") + .arg(arg) + .status() + .map_err(|e| UsbControlError::Io(e))?; + + if !status.success() { + return Err(UsbControlError::PermissionDenied); + } + + Ok(()) + } + + fn map_winreg_error(error: io::Error) -> UsbControlError { + if let Some(code) = error.raw_os_error() { + if code == 5 { + return UsbControlError::PermissionDenied; + } + } + UsbControlError::RegistryError(error.to_string()) + } + pub fn refresh_group_policy() -> Result<(), UsbControlError> { use std::os::windows::process::CommandExt; use std::process::Command; diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx index 1ddb7a7..7fc1026 100644 --- a/apps/desktop/src/main.tsx +++ b/apps/desktop/src/main.tsx @@ -117,7 +117,6 @@ const apiBaseUrl = normalizeUrl(import.meta.env.VITE_API_BASE_URL, appUrl) const RUSTDESK_CONFIG_STRING = import.meta.env.VITE_RUSTDESK_CONFIG_STRING?.trim() || null const RUSTDESK_DEFAULT_PASSWORD = import.meta.env.VITE_RUSTDESK_DEFAULT_PASSWORD?.trim() || null -const RUSTDESK_SYNC_INTERVAL_MS = 60 * 60 * 1000 // 1h const TOKEN_SELF_HEAL_DEBOUNCE_MS = 30 * 1000 function sanitizeEmail(value: string | null | undefined) { @@ -244,7 +243,10 @@ async function writeRustdeskInfo(store: Store, info: RustdeskInfo): Promise) { const enriched = data ? `${message} ${JSON.stringify(data)}` : message - console.log(`[raven] ${enriched}`) + const line = `[raven] ${enriched}` + console.log(line) + // Persiste em arquivo local para facilitar debugging fora do console + invoke("log_app_event", { message: line }).catch(() => {}) } function bytes(n?: number) { @@ -782,99 +784,68 @@ const resolvedAppUrl = useMemo(() => { return normalized }, [config?.appUrl]) - const syncRemoteAccessNow = useCallback( - async (info: RustdeskInfo, allowRetry = true) => { - if (!store) return - if (!config?.machineId) { - logDesktop("remoteAccess:sync:skipped", { reason: "unregistered" }) - return - } - const payload = buildRemoteAccessPayload(info) - if (!payload) return + // Funcao simplificada de sync - sempre le do disco para evitar race conditions + const syncRemoteAccessDirect = useCallback( + async (info: RustdeskInfo, allowRetry = true): Promise => { + try { + // Sempre le do disco para evitar race conditions com state React + const freshStore = await loadStore() + const freshConfig = await readConfig(freshStore) + const freshToken = await readToken(freshStore) - const resolveToken = async (allowHeal: boolean): Promise => { - let currentToken = token - if (!currentToken) { - currentToken = (await readToken(store)) ?? null - if (currentToken) { - setToken(currentToken) - } + if (!freshConfig?.machineId || !freshToken) { + logDesktop("remoteAccess:sync:skip", { + hasMachineId: !!freshConfig?.machineId, + hasToken: !!freshToken, + }) + return false } - if (!currentToken && allowHeal) { - const healed = await attemptSelfHeal("remote-access") - if (healed) { - currentToken = (await readToken(store)) ?? null - if (currentToken) { - setToken(currentToken) - } - } - } - return currentToken - } - const sendRequest = async (machineToken: string, retryAllowed: boolean): Promise => { + const payload = buildRemoteAccessPayload(info) + if (!payload) return false + + logDesktop("remoteAccess:sync:start", { id: info.id }) + const response = await fetch(`${apiBaseUrl}/api/machines/remote-access`, { method: "POST", headers: { "Content-Type": "application/json", - "Idempotency-Key": `${config?.machineId ?? "unknown"}:RustDesk:${info.id}`, + "Idempotency-Key": `${freshConfig.machineId}:RustDesk:${info.id}`, }, - body: JSON.stringify({ machineToken, ...payload }), + body: JSON.stringify({ machineToken: freshToken, ...payload }), }) - if (!response.ok) { - logDesktop("remoteAccess:sync:error", { status: response.status }) - const text = await response.text() - if (retryAllowed && (response.status === 401 || isTokenRevokedMessage(text))) { - const healed = await attemptSelfHeal("remote-access") - if (healed) { - const refreshedToken = await resolveToken(false) - if (refreshedToken) { - return sendRequest(refreshedToken, false) - } - } - } - throw new Error(text.slice(0, 300) || "Falha ao registrar acesso remoto") + if (response.ok) { + const nextInfo: RustdeskInfo = { ...info, lastSyncedAt: Date.now(), lastError: null } + await writeRustdeskInfo(freshStore, nextInfo) + setRustdeskInfo(nextInfo) + logDesktop("remoteAccess:sync:success", { id: info.id }) + return true } - const nextInfo: RustdeskInfo = { ...info, lastSyncedAt: Date.now(), lastError: null } - await writeRustdeskInfo(store, nextInfo) - setRustdeskInfo(nextInfo) - logDesktop("remoteAccess:sync:success", { id: info.id }) - } + const errorText = await response.text() + logDesktop("remoteAccess:sync:error", { status: response.status, error: errorText.slice(0, 200) }) - try { - const machineToken = await resolveToken(true) - if (!machineToken) { - const failedInfo: RustdeskInfo = { - ...info, - lastError: "Token indisponível para sincronizar acesso remoto", - } - await writeRustdeskInfo(store, failedInfo) - setRustdeskInfo(failedInfo) - logDesktop("remoteAccess:sync:skipped", { reason: "missing-token" }) - return - } - await sendRequest(machineToken, allowRetry) - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - console.error("Falha ao sincronizar acesso remoto com a plataforma", error) - const failedInfo: RustdeskInfo = { ...info, lastError: message } - await writeRustdeskInfo(store, failedInfo) - setRustdeskInfo(failedInfo) - if (allowRetry && isTokenRevokedMessage(message)) { + // Se token invalido, tenta self-heal uma vez + if (allowRetry && (response.status === 401 || isTokenRevokedMessage(errorText))) { const healed = await attemptSelfHeal("remote-access") if (healed) { - const refreshedToken = await resolveToken(false) - if (refreshedToken) { - return syncRemoteAccessNow(failedInfo, false) - } + return syncRemoteAccessDirect(info, false) } } - logDesktop("remoteAccess:sync:failed", { id: info.id, error: message }) + + // Salva erro no store + const failedInfo: RustdeskInfo = { ...info, lastError: errorText.slice(0, 200) } + await writeRustdeskInfo(freshStore, failedInfo) + setRustdeskInfo(failedInfo) + return false + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + logDesktop("remoteAccess:sync:exception", { error: message }) + return false } }, - [store, token, config?.machineId, attemptSelfHeal, setToken] + [attemptSelfHeal] ) const handleRustdeskProvision = useCallback( @@ -1007,23 +978,58 @@ const resolvedAppUrl = useMemo(() => { } }, [store, handleRustdeskProvision]) + // Bootstrap do RustDesk + retry simplificado (60s) useEffect(() => { if (!store || !config?.machineId) return - if (!rustdeskInfo && !isRustdeskProvisioning && !rustdeskBootstrapRef.current) { - rustdeskBootstrapRef.current = true - ensureRustdesk().finally(() => { - rustdeskBootstrapRef.current = false - }) - return - } - if (rustdeskInfo && !isRustdeskProvisioning) { - const lastSync = rustdeskInfo.lastSyncedAt ?? 0 - const needsSync = Date.now() - lastSync > RUSTDESK_SYNC_INTERVAL_MS - if (needsSync) { - syncRemoteAccessNow(rustdeskInfo) + + let disposed = false + + async function bootstrap() { + // Se nao tem rustdeskInfo, provisiona primeiro + if (!rustdeskInfo && !isRustdeskProvisioning && !rustdeskBootstrapRef.current) { + rustdeskBootstrapRef.current = true + try { + await ensureRustdesk() + } finally { + rustdeskBootstrapRef.current = false + } + return // handleRustdeskProvision fara o sync + } + + // Se ja tem rustdeskInfo mas nunca sincronizou, tenta sync + if (rustdeskInfo && !rustdeskInfo.lastSyncedAt) { + logDesktop("remoteAccess:sync:bootstrap", { id: rustdeskInfo.id }) + await syncRemoteAccessDirect(rustdeskInfo) } } - }, [store, config?.machineId, rustdeskInfo, ensureRustdesk, syncRemoteAccessNow, isRustdeskProvisioning]) + + bootstrap() + + // Retry a cada 30s se nunca sincronizou (o Rust faz o sync automaticamente) + const interval = setInterval(async () => { + if (disposed) return + try { + const freshStore = await loadStore() + const freshRustdesk = await readRustdeskInfo(freshStore) + if (freshRustdesk && !freshRustdesk.lastSyncedAt) { + logDesktop("remoteAccess:sync:retry:fallback", { id: freshRustdesk.id }) + // Re-invoca o Rust para tentar sync novamente + await invoke("ensure_rustdesk_and_emit", { + configString: RUSTDESK_CONFIG_STRING || null, + password: RUSTDESK_DEFAULT_PASSWORD || null, + machineId: config?.machineId, + }) + } + } catch (err) { + logDesktop("remoteAccess:sync:retry:error", { error: String(err) }) + } + }, 30_000) + + return () => { + disposed = true + clearInterval(interval) + } + }, [store, config?.machineId, rustdeskInfo, isRustdeskProvisioning, ensureRustdesk, syncRemoteAccessDirect]) async function register() { if (!profile) return @@ -1100,10 +1106,23 @@ const resolvedAppUrl = useMemo(() => { }, }) - await ensureRustdesk() - logDesktop("register:rustdesk:done", { machineId: data.machineId }) + // Provisiona RustDesk em background (fire-and-forget) + // O Rust faz o sync com o backend automaticamente, sem passar pelo CSP do webview + logDesktop("register:rustdesk:start", { machineId: data.machineId }) + invoke("ensure_rustdesk_and_emit", { + configString: RUSTDESK_CONFIG_STRING || null, + password: RUSTDESK_DEFAULT_PASSWORD || null, + machineId: data.machineId, + }).then((result) => { + logDesktop("register:rustdesk:done", { machineId: data.machineId, id: result.id }) + }).catch((err) => { + const msg = err instanceof Error ? err.message : String(err) + if (!msg.toLowerCase().includes("apenas no windows")) { + logDesktop("register:rustdesk:error", { error: msg }) + } + }) - // Abre o sistema imediatamente após registrar (evita ficar com token inválido no fluxo antigo) + // Redireciona imediatamente (nao espera RustDesk) try { await fetch(`${apiBaseUrl}/api/machines/sessions`, { method: "POST", diff --git a/bun.lock b/bun.lock index f67c7f6..d945699 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "web", @@ -40,7 +41,7 @@ "@tiptap/starter-kit": "^3.10.0", "@tiptap/suggestion": "^3.10.0", "better-auth": "^1.3.26", - "better-sqlite3": "^11.10.0", + "better-sqlite3": "12.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "^1.29.2", @@ -971,7 +972,7 @@ "better-call": ["better-call@1.1.4", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-NJouLY6IVKv0nDuFoc6FcbKDFzEnmgMNofC9F60Mwx1Ecm7X6/Ecyoe5b+JSVZ42F/0n46/M89gbYP1ZCVv8xQ=="], - "better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="], + "better-sqlite3": ["better-sqlite3@12.5.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], @@ -2041,8 +2042,6 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@prisma/adapter-better-sqlite3/better-sqlite3": ["better-sqlite3@12.5.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg=="], - "@prisma/dev/std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.1.0", "", { "dependencies": { "@prisma/debug": "7.1.0" } }, "sha512-lq8hMdjKiZftuT5SssYB3EtQj8+YjL24/ZTLflQqzFquArKxBcyp6Xrblto+4lzIKJqnpOjfMiBjMvl7YuD7+Q=="], diff --git a/docs/README.md b/docs/README.md index 2daf3e5..85c2a80 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,7 @@ Este índice consolida a documentação viva e move conteúdos históricos para - Build: `docs/desktop/build.md` - Updater: `docs/desktop/updater.md` - Handshake/troubleshooting: `docs/desktop/handshake-troubleshooting.md` + - RustDesk sync: `docs/desktop/rustdesk-sync.md` - Tickets: `docs/ticket-snapshots.md` - Administração (UI): `docs/admin/admin-inventory-ui.md` diff --git a/docs/desktop/rustdesk-sync.md b/docs/desktop/rustdesk-sync.md new file mode 100644 index 0000000..25ff164 --- /dev/null +++ b/docs/desktop/rustdesk-sync.md @@ -0,0 +1,151 @@ +# Desktop (Tauri) - Sincronizacao RustDesk com Backend + +Este documento descreve o mecanismo de sincronizacao automatica do RustDesk com o backend apos o provisionamento no aplicativo desktop. + +## Visao Geral + +O fluxo de sincronizacao do RustDesk garante que as credenciais de acesso remoto (ID e senha) sejam automaticamente registradas no backend assim que o RustDesk for provisionado na maquina. Isso elimina a necessidade de cadastro manual e permite acesso remoto imediato. + +## Arquitetura + +### Fluxo de Provisionamento + +``` +1. Usuario clica "Registrar" no app desktop +2. TypeScript faz POST /api/machines/register +3. TypeScript salva token/config no store local +4. TypeScript inicia RustDesk em background (fire-and-forget) +5. TypeScript redireciona IMEDIATAMENTE para o sistema +6. [Background] Rust provisiona RustDesk (~30s) +7. [Background] Rust faz POST /api/machines/remote-access (SYNC) +8. [Background] Rust atualiza lastSyncedAt no store +``` + +### Por que o Sync e feito no Rust? + +| Problema anterior | Solucao Rust | +|-------------------|--------------| +| CSP do Tauri bloqueava `fetch()` | Rust usa reqwest diretamente, sem CSP | +| Race conditions com React state | Fluxo sincrono no Rust | +| Fire-and-forget nao completava | Sync acontece antes do return | +| Timeout do navegador | Rust tem controle total do HTTP | + +## Implementacao + +### Arquivo: `apps/desktop/src-tauri/src/rustdesk.rs` + +O sync e executado automaticamente apos o provisionamento bem-sucedido: + +```rust +// Apos salvar dados do RustDesk no store local +sync_remote_access_with_backend(&result)?; +``` + +### Payload da API + +```json +{ + "machineToken": "", + "provider": "RustDesk", + "identifier": "", + "password": "", + "notes": "Versao: 1.4.4. Provisionado em: 1765045637482" +} +``` + +### Headers + +- `Content-Type: application/json` +- `Idempotency-Key: :RustDesk:` (previne duplicatas) + +### Endpoint + +``` +POST /api/machines/remote-access +``` + +## Arquivos de Configuracao + +### Store Local + +Caminho: `%LOCALAPPDATA%\br.com.esdrasrenan.sistemadechamados\machine-agent.json` + +Estrutura relevante: +```json +{ + "config": { + "machineId": "...", + "apiBaseUrl": "https://tickets.esdrasrenan.com.br" + }, + "token": "...", + "rustdesk": { + "id": "497869711", + "password": "...", + "installedVersion": "1.4.4", + "lastProvisionedAt": 1765045637482, + "lastSyncedAt": 1765045638173, + "lastError": null + } +} +``` + +### Logs + +Caminho: `%LOCALAPPDATA%\br.com.esdrasrenan.sistemadechamados\logs\rustdesk.log` + +Mensagens de sucesso: +``` +[...] Iniciando sincronizacao com backend... +[...] Sync com backend OK: status 200 OK +[...] Acesso remoto sincronizado com backend +``` + +## Diagnostico + +### Verificar se o Sync Funcionou + +1. Abrir `machine-agent.json`: + ``` + %LOCALAPPDATA%\br.com.esdrasrenan.sistemadechamados\machine-agent.json + ``` + +2. Verificar campo `rustdesk.lastSyncedAt`: + - Se tem valor numerico: sync OK + - Se `null`: sync falhou + +3. Verificar logs: + ``` + %LOCALAPPDATA%\br.com.esdrasrenan.sistemadechamados\logs\rustdesk.log + ``` + +### Possiveis Erros + +| Erro | Causa | Solucao | +|------|-------|---------| +| `400 Bad Request` | Payload incorreto | Verificar schema da API | +| `401 Unauthorized` | Token invalido/expirado | Re-registrar maquina | +| `Connection refused` | Backend indisponivel | Verificar URL da API | +| `lastSyncedAt: null` | Sync nao executou | Verificar logs do Rust | + +### Fallback Manual + +Se o sync automatico falhar, o sistema tenta novamente a cada 30 segundos em background. Tambem e possivel forcar o re-provisionamento via: + +1. Aba "Configuracoes" no app desktop +2. Clicar "Reprovisionar RustDesk" + +## Historico de Mudancas + +### Dezembro 2025 - Migracao do Sync para Rust + +**Problema:** O sync feito em TypeScript (`fetch()`) falhava com "Failed to fetch" devido ao CSP do Tauri v2 bloquear requisicoes externas do webview. + +**Solucao:** Mover o sync para o Rust, que faz HTTP diretamente via reqwest sem restricoes de CSP. + +**Arquivos modificados:** +- `apps/desktop/src-tauri/src/rustdesk.rs` - Adicionado `sync_remote_access_with_backend()` +- `apps/desktop/src/main.tsx` - Simplificado para fire-and-forget + +--- + +Ultima atualizacao: Dezembro 2025 - Sync automatico via Rust apos provisionamento. diff --git a/next.config.ts b/next.config.ts index 0f66796..a95fbdd 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,11 @@ -const nextConfig = { +import type { NextConfig } from "next" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const __filename = fileURLToPath(import.meta.url) +const projectRoot = path.dirname(__filename) + +const nextConfig: NextConfig = { typescript: { // Pula verificacao de tipos durante build para evitar OOM no servidor de producao ignoreBuildErrors: true, @@ -6,6 +13,13 @@ const nextConfig = { experimental: { turbopackFileSystemCacheForDev: true, }, + turbopack: { + // Define a raiz do projeto para evitar warning de multiplos lockfiles + root: projectRoot, + }, + // Pacotes nativos que nao devem ser bundled pelo servidor + serverExternalPackages: ["better-sqlite3"], } export default nextConfig + diff --git a/package.json b/package.json index 63b5e3f..a128ef9 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@tiptap/starter-kit": "^3.10.0", "@tiptap/suggestion": "^3.10.0", "better-auth": "^1.3.26", - "better-sqlite3": "^11.10.0", + "better-sqlite3": "12.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "^1.29.2", diff --git a/src/components/admin/devices/admin-devices-overview.tsx b/src/components/admin/devices/admin-devices-overview.tsx index 294ca03..c3640ed 100644 --- a/src/components/admin/devices/admin-devices-overview.tsx +++ b/src/components/admin/devices/admin-devices-overview.tsx @@ -41,6 +41,7 @@ import { Usb, Loader2, X, + TicketCheck, } from "lucide-react" import { api } from "@/convex/_generated/api" @@ -3154,14 +3155,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { icon: , }) } - if (windowsBuildLabel) { - chips.push({ - key: "build", - label: "Build", - value: windowsBuildLabel, - icon: , - }) - } if (windowsActivationStatus !== null && windowsActivationStatus !== undefined) { chips.push({ key: "activation", @@ -3222,7 +3215,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { device?.osVersion, device?.architecture, windowsVersionLabel, - windowsBuildLabel, windowsActivationStatus, primaryLinkedUser?.email, primaryLinkedUser?.name, @@ -3867,25 +3859,49 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { return ( - Detalhes - Resumo do dispositivo selecionado {device ? ( - -
- {companyName ? ( -
- {companyName} -
+ <> + + + {device.displayName ?? device.hostname ?? "Dispositivo"} + + {isManualMobile ? ( + + Identificação interna + ) : null} - {!isDeactivated ? : null} - {!isActiveLocal ? ( - - Dispositivo desativada - - ) : null} -
-
- ) : null} + + + +
+ {companyName ? ( +
+ {companyName} +
+ ) : null} + {!isDeactivated ? : null} + {!isActiveLocal ? ( + + Dispositivo desativado + + ) : null} +
+
+ + ) : ( + Detalhes + )}
{!device ? ( @@ -3893,47 +3909,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { ) : (
-
-
-
-

- {device.displayName ?? device.hostname ?? "Dispositivo"} -

- {isManualMobile ? ( - - Identificação interna - - ) : null} - -
-

- {device.authEmail ?? "E-mail nĂŁo definido"} - {device.authEmail ? ( - - ) : null} -

-
-
- {/* ping integrado na badge de status */}
{summaryChips.map((chip) => (
-

Controles do dispositivo

- {device.registeredBy ? ( - - Registrada via {device.registeredBy} - - ) : null} +

Atalhos

+ + {device.registeredBy === "desktop-agent" ? ( + Agente na máquina + ) : ( + Manual + )} +
+ {deviceTicketsHref ? ( + + ) : null} {!isManualMobile ? ( <> @@ -3997,7 +3987,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { disabled={isResettingAgent} > - {isResettingAgent ? "Resetando agente..." : "Resetar agente"} + {isResettingAgent ? "Resetando..." : "Resetar"}
- {/* Campos personalizados (posicionado logo após métricas) */} + {/* Acesso remoto */} +
+
+
+

Acesso remoto

+ {hasRemoteAccess ? ( + + {remoteAccessEntries.length === 1 ? "1 acesso" : `${remoteAccessEntries.length} acessos`} + + ) : null} +
+ {canManageRemoteAccess ? ( + + ) : null} +
+ {hasRemoteAccess ? ( +
+ {remoteAccessEntries.map((entry) => { + const lastVerifiedDate = + entry.lastVerifiedAt && Number.isFinite(entry.lastVerifiedAt) + ? new Date(entry.lastVerifiedAt) + : null + const isRustDesk = isRustDeskAccess(entry) + const secretVisible = Boolean(visibleRemoteSecrets[entry.clientId]) + return ( +
+
+
+
+ ID + {entry.identifier ? ( + + {entry.identifier} + + ) : null} + {entry.identifier ? ( + + ) : null} +
+ {entry.username || entry.password ? ( +
+ {entry.username ? ( +
+ Usuário + + {entry.username} + + +
+ ) : null} + {entry.password ? ( +
+ Senha + + {secretVisible ? entry.password : "••••••••"} + + + +
+ ) : null} +
+ ) : null} + {entry.url && !isRustDesk ? ( + + Abrir console remoto + + ) : null} +
+
+ {entry.notes ? ( +

{entry.notes}

+ ) : null} + {lastVerifiedDate ? ( +

+ Atualizado {formatRelativeTime(lastVerifiedDate)}{" "} + ({formatAbsoluteDateTime(lastVerifiedDate)}) +

+ ) : null} +
+
+
+
+ {isRustDesk && (entry.identifier || entry.password) ? ( + + ) : null} +
+
+ {entry.provider ? ( + + ) : null} + {canManageRemoteAccess ? ( + <> + + + + ) : null} +
+
+
+ ) + })} +
+ ) : ( +
+ Nenhum identificador de acesso remoto cadastrado. Registre o ID do TeamViewer, AnyDesk ou outra ferramenta para agilizar o suporte. +
+ )} +
+ + {/* Campos personalizados */}
@@ -4068,248 +4246,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
) : null}
- -
-
-
-

Tickets abertos por este dispositivo

- {totalOpenTickets === 0 ? ( -

- Nenhum chamado em aberto registrado diretamente por este dispositivo. -

- ) : hasAdditionalOpenTickets ? ( -

- Mostrando Ăşltimos {Math.min(displayLimit, totalOpenTickets)} de {totalOpenTickets} chamados em aberto -

- ) : ( -

- Ăšltimos chamados vinculados a este dispositivo. -

- )} -
-
-
- {totalOpenTickets} -
-
-
- {totalOpenTickets > 0 ? ( -
- {displayedDeviceTickets.map((ticket) => { - const priorityMeta = getTicketPriorityMeta(ticket.priority) - return ( - -
-

- #{ticket.reference} · {ticket.subject} -

-

- Atualizado {formatRelativeTime(new Date(ticket.updatedAt))} -

-
-
- - {priorityMeta.label} - - -
- - ) - })} -
- ) : null} - {deviceTicketsHref ? ( -
- - Ver todos - -
- ) : null} -
- -
-
-
-

Acesso remoto

- {hasRemoteAccess ? ( - - {remoteAccessEntries.length === 1 - ? remoteAccessEntries[0].provider ?? "Configuração única" - : `${remoteAccessEntries.length} acessos`} - - ) : null} -
- {canManageRemoteAccess ? ( - - ) : null} -
- {hasRemoteAccess ? ( -
- {remoteAccessEntries.map((entry) => { - const lastVerifiedDate = - entry.lastVerifiedAt && Number.isFinite(entry.lastVerifiedAt) - ? new Date(entry.lastVerifiedAt) - : null - const isRustDesk = isRustDeskAccess(entry) - const secretVisible = Boolean(visibleRemoteSecrets[entry.clientId]) - return ( -
-
-
-
- {entry.provider ? ( - - {entry.provider} - - ) : null} - {entry.identifier ? ( - {entry.identifier} - ) : null} - {entry.identifier ? ( - - ) : null} -
- {entry.username || entry.password ? ( -
- {entry.username ? ( -
- Usuário - - {entry.username} - - -
- ) : null} - {entry.password ? ( -
- Senha - - {secretVisible ? entry.password : "••••••••"} - - - -
- ) : null} -
- ) : null} - {entry.url && !isRustDesk ? ( - - Abrir console remoto - - ) : null} - {isRustDesk && (entry.identifier || entry.password) ? ( - - ) : null} - {entry.notes ? ( -

{entry.notes}

- ) : null} - {lastVerifiedDate ? ( -

- Atualizado {formatRelativeTime(lastVerifiedDate)}{" "} - ({formatAbsoluteDateTime(lastVerifiedDate)}) -

- ) : null} -
- {canManageRemoteAccess ? ( -
- - -
- ) : null} -
-
- ) - })} -
- ) : ( -
- Nenhum identificador de acesso remoto cadastrado. Registre o ID do TeamViewer, AnyDesk ou outra ferramenta para agilizar o suporte. -
- )} -
@@ -4686,7 +4622,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { return (
  • {name ?? "Adaptador de vĂ­deo"} - {memoryBytes ? {formatBytes(memoryBytes)} : null} + {memoryBytes ? VRAM {formatBytes(memoryBytes)} : null} {vendor ? · {vendor} : null} {driver ? · Driver {driver} : null}