feat(client): envia clinicName no identify e migra upload para Presigned URLs

- client-monitor.js: adiciona clinicName no client-identify (fallback para auth legada)
- Direct Upload via Presigned URLs (bypass VPS) como caminho principal
- Fallback automático para envio via VPS se Wasabi não disponível
- Thumbnail gerado localmente via IPC com index.js (remove dependência sharp)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-05-31 02:06:19 +02:00
parent 32f27ebe56
commit 2a26f72460
4 changed files with 139 additions and 64 deletions
+119 -55
View File
@@ -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}`);