diff --git a/client-monitor.js b/client-monitor.js index 541ca59..dd975b7 100644 --- a/client-monitor.js +++ b/client-monitor.js @@ -5,22 +5,27 @@ const path = require('path'); const readline = require('readline'); const http = require('http'); const { exec } = require('child_process'); -const sharp = require('sharp'); -// Função para comprimir imagem PNG/JPEG de alta resolução para JPEG otimizado -async function comprimirImagem(buffer) { - return await sharp(buffer) - .resize({ - width: 1600, - withoutEnlargement: true, - fit: 'inside' - }) - .jpeg({ - quality: 75, - mozjpeg: true, - progressive: true - }) - .toBuffer(); +// Função para gerar o thumbnail usando a CPU local do cliente (via IPC com o index.js do Electron) +async function gerarThumbnail(buffer) { + return new Promise((resolve) => { + if (!process.send) return resolve(null); // Fallback se não for fork + + const id = Date.now().toString() + Math.random().toString(); + const timeout = setTimeout(() => resolve(null), 5000); + + const listener = (message) => { + if (message && message.event === 'resize-result' && message.id === id) { + clearTimeout(timeout); + process.removeListener('message', listener); + if (message.success) resolve(Buffer.from(message.thumbBase64, 'base64')); + else resolve(null); + } + }; + + process.on('message', listener); + process.send({ event: 'resize-image', id, base64: buffer.toString('base64') }); + }); } // Resolve dynamic folder in AppData or default path passed by Electron @@ -1084,6 +1089,7 @@ function identifyClient() { socket.emit('client-identify', { type: 'windows', // MANDATÓRIO: Identifica o notebook na Área de Sincronização name: config.clientName || 'Notebook Consultório Principal', // Nome amigável que aparecerá no painel + clinicName: config.clinicName || '', // Nome da clínica (preenchido pelo servidor via machine_token auth) version: '1.1.0', system: 'windows', path: config.monitorPath, @@ -1291,13 +1297,19 @@ function sendImageToServer(filePath, isBacklog, bypassCache = false) { let fileBuffer = fs.readFileSync(filePath); const originalSize = fileBuffer.length; const isImage = /\.(png|jpg|jpeg)$/i.test(fileName); + let thumbBuffer = null; if (isImage) { try { - fileBuffer = await comprimirImagem(fileBuffer); - addLog(`⚡ Imagem comprimida com sucesso: ${fileName} (${Math.round(originalSize / 1024)} KB -> ${Math.round(fileBuffer.length / 1024)} KB)`); - } catch (compressErr) { - addLog(`⚠️ Falha na compressão da imagem ${fileName}, enviando original: ${compressErr.message}`); + addLog(`⚡ Gerando miniatura localmente para: ${fileName}...`); + thumbBuffer = await gerarThumbnail(fileBuffer); + if (thumbBuffer) { + addLog(`✅ Miniatura gerada com sucesso (${Math.round(thumbBuffer.length / 1024)} KB)`); + } else { + addLog(`⚠️ Não foi possível gerar miniatura, o servidor precisará usar o original.`); + } + } catch (e) { + addLog(`⚠️ Erro ao gerar miniatura: ${e.message}`); } } @@ -1312,58 +1324,110 @@ function sendImageToServer(filePath, isBacklog, bypassCache = false) { doctor = localInfo.doctor; remark = localInfo.remark; photographTime = localInfo.photographTime; - addLog(`🔍 Dados do paciente encontrados localmente: "${patientName}", Dentista: "${doctor}", Obs: "${remark}", Data: "${photographTime}"`); + addLog(`🔍 Dados do paciente: "${patientName}", Dentista: "${doctor}"`); } else { - addLog(`🔍 Nenhum registro no banco local para o arquivo: ${fileName}`); try { const stat = fs.statSync(filePath); const fileDate = stat.mtime || stat.birthtime || new Date(); const pad = (num) => String(num).padStart(2, '0'); photographTime = `${fileDate.getFullYear()}-${pad(fileDate.getMonth() + 1)}-${pad(fileDate.getDate())} ${pad(fileDate.getHours())}:${pad(fileDate.getMinutes())}:${pad(fileDate.getSeconds())}`; - addLog(`ℹ️ Usando data de modificação do arquivo como fallback: "${photographTime}"`); } catch (e) { - addLog(`⚠️ Erro ao ler metadados do arquivo: ${e.message}`); + addLog(`⚠️ Erro ao ler metadados: ${e.message}`); } } - addLog(`📤 Enviando: ${fileName} (${Math.round(fileBuffer.length / 1024)} KB) ${isBacklog ? '[Backlog]' : '[Novo]'}`); + const metadata = { + fileName: fileName, + guid: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + fileSize: fileBuffer.length + }; + + const patientData = { + name: patientName, + doctor: doctor, + remark: remark, + photographTime: photographTime + }; - // Definir timeout de 45 segundos para segurança - let didTimeout = false; - const timeoutId = setTimeout(() => { - didTimeout = true; - reject(new Error(`Timeout de 45 segundos aguardando resposta do servidor para: ${fileName}`)); - }, 45000); + addLog(`📤 Solicitando URLs temporárias para envio de: ${fileName} (${Math.round(fileBuffer.length / 1024)} KB)`); - socket.emit('send-image', { - imageBase64: fileBuffer.toString('base64'), - isBacklog: isBacklog, - metadata: { - fileName: fileName, - guid: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - fileSize: fileBuffer.length - }, - patientData: { - name: patientName, - doctor: doctor, - remark: remark, - photographTime: photographTime - } - }, (response) => { - if (didTimeout) return; - clearTimeout(timeoutId); - - if (response && response.success) { - addLog(`✅ Confirmação do servidor recebida: ${response.message || 'sucesso'}`); - if (response.originalFilename) { - uploadedCache.add(response.originalFilename); - saveUploadCache(); + // Pedir Presigned URLs + socket.emit('request-presigned-urls', { metadata, patientData }, async (urlResponse) => { + if (urlResponse && urlResponse.success) { + try { + addLog(`🌐 Fazendo Direct Upload pro Wasabi (Bypass VPS)...`); + // Fazer PUT da original + await fetch(urlResponse.originalUrl, { + method: 'PUT', + body: fileBuffer, + headers: { 'Content-Type': 'image/png' } + }); + + // Fazer PUT do thumb se existir + if (thumbBuffer && urlResponse.thumbnailUrl) { + await fetch(urlResponse.thumbnailUrl, { + method: 'PUT', + body: thumbBuffer, + headers: { 'Content-Type': 'image/jpeg' } + }); + } + + addLog(`✅ Upload direto concluído! Notificando servidor...`); + + // Notificar o servidor que o upload terminou + socket.emit('image-upload-direct', { + metadata, + patientData, + filename: urlResponse.filename, + thumbFilename: thumbBuffer ? urlResponse.thumbFilename : null + }, (response) => { + if (response && response.success) { + uploadedCache.add(response.originalFilename); + saveUploadCache(); + resolve(); + } else { + reject(new Error(response ? response.error : 'Erro na notificação de direct-upload')); + } + }); + + } catch (uploadErr) { + addLog(`❌ Falha no Direct Upload: ${uploadErr.message}. Tentando via VPS...`); + fallbackUpload(); } - resolve(); } else { - reject(new Error(response ? response.error : 'Erro desconhecido do servidor')); + addLog(`ℹ️ Wasabi não disponível no servidor. Usando envio padrão via VPS.`); + fallbackUpload(); } }); + + function fallbackUpload() { + let didTimeout = false; + const timeoutId = setTimeout(() => { + didTimeout = true; + reject(new Error(`Timeout de 45 segundos aguardando resposta do servidor para: ${fileName}`)); + }, 45000); + + socket.emit('send-image', { + imageBase64: fileBuffer.toString('base64'), + isBacklog: isBacklog, + metadata: metadata, + patientData: patientData + }, (response) => { + if (didTimeout) return; + clearTimeout(timeoutId); + + if (response && response.success) { + addLog(`✅ Confirmação do servidor recebida: ${response.message || 'sucesso'}`); + if (response.originalFilename) { + uploadedCache.add(response.originalFilename); + saveUploadCache(); + } + resolve(); + } else { + reject(new Error(response ? response.error : 'Erro desconhecido do servidor')); + } + }); + } } catch (error) { addLog(`❌ Erro ao ler/enviar arquivo: ${error.message}`); diff --git a/deploy-client.sh b/deploy-client.sh index d060ee1..3ed10b5 100755 --- a/deploy-client.sh +++ b/deploy-client.sh @@ -19,10 +19,9 @@ log() { log "🚀 Iniciando build do client desktop..." # 1. Atualizar repositório local -log "📦 Sincronizando repositório Git..." -cd "$CLIENT_PATH" -git fetch origin main -git reset --hard origin/main +log "📦 Mantendo alterações locais (git reset ignorado)..." +# git fetch origin main +# git reset --hard origin/main # 2. Instalar dependências log "📦 Instalando dependências (npm install)..." diff --git a/index.js b/index.js index cc30a27..4fcb909 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, Tray, Menu, ipcMain, shell, dialog } = require('electron'); +const { app, BrowserWindow, Tray, Menu, ipcMain, shell, dialog, nativeImage } = require('electron'); const path = require('path'); const fs = require('fs'); const { fork } = require('child_process'); @@ -152,6 +152,14 @@ function startMonitorProcess() { if (configWindow && !configWindow.isDestroyed()) { configWindow.webContents.send('new-log', message.log); } + } else if (message && message.event === 'resize-image') { + try { + const img = nativeImage.createFromBuffer(Buffer.from(message.base64, 'base64')); + const thumb = img.resize({ width: 300, quality: "good" }).toJPEG(80); + monitorProcess.send({ event: 'resize-result', id: message.id, success: true, thumbBase64: thumb.toString('base64') }); + } catch (e) { + monitorProcess.send({ event: 'resize-result', id: message.id, success: false, error: e.message }); + } } }); @@ -210,7 +218,12 @@ function updateTrayMenu() { // Se houver um download em andamento, mostra a barra no menu if (downloadProgress !== null) { if (downloadProgress === "Pronto") { - template.push({ label: '📦 Atualização pronta para instalar (reinicie o app)', enabled: false }); + template.push({ + label: '📦 Instalar Nova Versão (Reiniciar)', + click: () => { + autoUpdater.quitAndInstall(); + } + }); } else { template.push({ label: `⬇️ Baixando atualização: ${downloadProgress}%`, enabled: false }); } diff --git a/package.json b/package.json index 711b0f1..4086b49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rx.scoreodonto.com-client", - "version": "1.2.4", + "version": "1.2.7", "author": "ScoreOdonto", "description": "Cliente Windows para envio de imagens dentais da ScoreOdonto", "main": "index.js", @@ -11,7 +11,6 @@ "dependencies": { "chokidar": "^3.5.3", "electron-updater": "^6.3.0", - "sharp": "^0.34.5", "socket.io-client": "^4.7.5" }, "build": { @@ -47,4 +46,4 @@ "electron": "^42.2.0", "electron-builder": "^26.8.1" } -} \ No newline at end of file +}