feat: Adicionado timeout assíncrono para ACK do Cliente Windows
continuous-integration/webhook Deploy concluído (VPS4)

This commit is contained in:
VPS 4 Deploy Agent
2026-06-02 20:55:21 +02:00
parent 34ef3efe1f
commit 5d2ba1a178
9 changed files with 95 additions and 33 deletions
@@ -1 +1 @@
2.1.63 2.1.65
@@ -14,8 +14,14 @@ export default function CreatePatientModal({ isOpen, onClose, onCreated }) {
e.preventDefault(); e.preventDefault();
setSaving(true); setSaving(true);
try { try {
await api.post('/images/patients', form); const response = await api.post('/images/patients', form);
if (response.data?.warning) {
showToast(response.data.warning, 'warning');
} else if (response.data?.message) {
showToast(response.data.message, 'success');
} else {
showToast('Paciente criado com sucesso!', 'success'); showToast('Paciente criado com sucesso!', 'success');
}
setForm({ firstName: '', lastName: '', doctor: '', birthday: '', gender: '0', phone: '', remark: '' }); setForm({ firstName: '', lastName: '', doctor: '', birthday: '', gender: '0', phone: '', remark: '' });
onClose(); onClose();
onCreated?.(); onCreated?.();
@@ -25,8 +25,14 @@ export default function EditPatientModal({ isOpen, onClose, patient, onSaved })
if (!form.newName.trim()) return showToast('Nome é obrigatório.', 'error'); if (!form.newName.trim()) return showToast('Nome é obrigatório.', 'error');
setSaving(true); setSaving(true);
try { try {
await api.put(`/images/patients/${encodeURIComponent(patient.patient_name)}`, form); const response = await api.put(`/images/patients/${encodeURIComponent(patient.patient_name)}`, form);
if (response.data?.warning) {
showToast(response.data.warning, 'warning');
} else if (response.data?.message) {
showToast(response.data.message, 'success');
} else {
showToast('Paciente atualizado com sucesso!', 'success'); showToast('Paciente atualizado com sucesso!', 'success');
}
onSaved?.(); onSaved?.();
onClose(); onClose();
} catch (err) { } catch (err) {
@@ -253,8 +253,14 @@ export default function DashboardPage() {
if (!confirm(`⚠️ Excluir permanentemente todas as imagens de "${patientName}"?\n\nEsta ação não pode ser desfeita.`)) return; if (!confirm(`⚠️ Excluir permanentemente todas as imagens de "${patientName}"?\n\nEsta ação não pode ser desfeita.`)) return;
showToast('Excluindo imagens do paciente...', 'info'); showToast('Excluindo imagens do paciente...', 'info');
try { try {
await api.delete(`/images/by-patient?name=${encodeURIComponent(patientName)}`); const response = await api.delete(`/images/by-patient?name=${encodeURIComponent(patientName)}`);
if (response.data?.warning) {
showToast(response.data.warning, 'warning');
} else if (response.data?.message) {
showToast(response.data.message, 'success');
} else {
showToast('✅ Todas as imagens foram excluídas!', 'success'); showToast('✅ Todas as imagens foram excluídas!', 'success');
}
loadPatients(); loadPatients();
loadClientsList(); loadClientsList();
} catch (e) { showToast(e.response?.data?.error || 'Falha ao excluir.', 'error'); } } catch (e) { showToast(e.response?.data?.error || 'Falha ao excluir.', 'error'); }
@@ -83,8 +83,14 @@ export default function PatientsPage() {
const deletePatient = async (patientName) => { const deletePatient = async (patientName) => {
if (!confirm(`⚠️ Excluir permanentemente todas as imagens de "${patientName}"?\n\nEsta ação não pode ser desfeita.`)) return; if (!confirm(`⚠️ Excluir permanentemente todas as imagens de "${patientName}"?\n\nEsta ação não pode ser desfeita.`)) return;
try { try {
await api.delete(`/images/by-patient?name=${encodeURIComponent(patientName)}`); const response = await api.delete(`/images/by-patient?name=${encodeURIComponent(patientName)}`);
if (response.data?.warning) {
showToast(response.data.warning, 'warning');
} else if (response.data?.message) {
showToast(response.data.message, 'success');
} else {
showToast('Paciente excluído com sucesso!', 'success'); showToast('Paciente excluído com sucesso!', 'success');
}
loadPatients(true); loadPatients(true);
} catch (e) { } catch (e) {
showToast(e.response?.data?.error || 'Falha ao excluir.', 'error'); showToast(e.response?.data?.error || 'Falha ao excluir.', 'error');
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "dental-image-server", "name": "dental-image-server",
"version": "2.0.5", "version": "2.1.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dental-image-server", "name": "dental-image-server",
"version": "2.0.5", "version": "2.1.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1055.0", "@aws-sdk/client-s3": "^3.1055.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "dental-image-server", "name": "dental-image-server",
"version": "2.1.3", "version": "2.1.4",
"description": "Servidor Socket.IO para receber e processar imagens dentais com interface web", "description": "Servidor Socket.IO para receber e processar imagens dentais com interface web",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+40 -15
View File
@@ -2,10 +2,44 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../database'); const db = require('../database');
const imageProcessor = require('../image-processor'); const imageProcessor = require('../image-processor');
const crypto = require('crypto');
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const storage = require('../storage'); const storage = require('../storage');
async function emitAndAwaitAck(io, action, payload) {
if (!io) return { success: true, warning: 'Socket.IO não inicializado no servidor.' };
const eventId = crypto.randomUUID();
payload.eventId = eventId;
payload.action = action;
const ackPromise = new Promise((resolve) => {
if (global.pendingAcks) {
global.pendingAcks.set(eventId, resolve);
setTimeout(() => {
if (global.pendingAcks.has(eventId)) {
global.pendingAcks.delete(eventId);
resolve(null); // Timeout
}
}, 4000);
} else {
resolve(null);
}
});
io.emit('remote-crud-patient', payload);
const result = await ackPromise;
if (result && result.success) {
return { success: true, message: `Sincronizado com PC: ${result.pcName || 'Desconhecido'}`, ackInfo: result };
} else if (result && !result.success) {
return { success: true, warning: `Erro no app Windows (${result.pcName || 'Desconhecido'}): ${result.error || 'Erro local'}` };
} else {
return { success: true, warning: 'Salvo na nuvem, porém o App Windows não respondeu. Verifique se o PC está ligado e conectado.' };
}
}
// ================================================================ // ================================================================
// GET /api/images/unique-clients - Buscar lista leve de clínicas // GET /api/images/unique-clients - Buscar lista leve de clínicas
// ================================================================ // ================================================================
@@ -217,7 +251,6 @@ router.post('/patients', async (req, res) => {
const fullName = `${firstName || ''} ${lastName || ''}`.trim(); const fullName = `${firstName || ''} ${lastName || ''}`.trim();
if (!fullName) return res.status(400).json({ error: 'Nome obrigatório' }); if (!fullName) return res.status(400).json({ error: 'Nome obrigatório' });
const crypto = require('crypto');
const dummyGuid = crypto.randomUUID(); const dummyGuid = crypto.randomUUID();
await db.run( await db.run(
@@ -235,14 +268,11 @@ router.post('/patients', async (req, res) => {
); );
const io = req.app.get('io') || global.io; const io = req.app.get('io') || global.io;
if (io) { const syncResult = await emitAndAwaitAck(io, 'create', {
io.emit('remote-crud-patient', {
action: 'create',
patient: { id: dummyGuid, firstName, lastName, doctor, birthday, gender, phone, remark, fullName } patient: { id: dummyGuid, firstName, lastName, doctor, birthday, gender, phone, remark, fullName }
}); });
}
res.json({ success: true, message: 'Paciente criado com sucesso' }); res.json(syncResult);
} catch (error) { } catch (error) {
console.error('Erro ao criar paciente:', error); console.error('Erro ao criar paciente:', error);
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
@@ -286,15 +316,12 @@ router.put('/patients/:name', async (req, res) => {
); );
const io = req.app.get('io') || global.io; const io = req.app.get('io') || global.io;
if (io) { const syncResult = await emitAndAwaitAck(io, 'update', {
io.emit('remote-crud-patient', {
action: 'update',
oldName: oldName, oldName: oldName,
patient: { newName: newName?.trim(), doctor, remark } patient: { newName: newName?.trim(), doctor, remark }
}); });
}
res.json({ success: true, updated: result.changes }); res.json({ ...syncResult, updated: result.changes });
} catch (error) { } catch (error) {
console.error('Erro ao editar paciente:', error); console.error('Erro ao editar paciente:', error);
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
@@ -318,11 +345,9 @@ router.delete('/by-patient', async (req, res) => {
await db.run('DELETE FROM images WHERE patient_name = $1', [patientName]); await db.run('DELETE FROM images WHERE patient_name = $1', [patientName]);
const io = req.app.get('io') || global.io; const io = req.app.get('io') || global.io;
if (io) { const syncResult = await emitAndAwaitAck(io, 'delete', { patientName });
io.emit('remote-crud-patient', { action: 'delete', patientName });
}
res.json({ success: true, count: images.length }); res.json({ ...syncResult, count: images.length });
} catch (error) { } catch (error) {
console.error('Erro ao excluir imagens do paciente:', error); console.error('Erro ao excluir imagens do paciente:', error);
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
+13
View File
@@ -42,6 +42,10 @@ const io = socketIo(server, {
maxHttpBufferSize: 5e7 // 50MB para suportar raios-x grandes maxHttpBufferSize: 5e7 // 50MB para suportar raios-x grandes
}); });
app.set('io', io);
global.io = io;
global.pendingAcks = new Map();
const connectedClients = []; const connectedClients = [];
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@@ -851,6 +855,15 @@ io.on('connection', (socket) => {
} }
}, 1000); }, 1000);
// Ouvinte para receber confirmações do CRUD remoto
socket.on('remote-crud-ack', (data) => {
if (data && data.eventId && global.pendingAcks && global.pendingAcks.has(data.eventId)) {
const resolve = global.pendingAcks.get(data.eventId);
resolve(data);
global.pendingAcks.delete(data.eventId);
}
});
// Identificação de clientes // Identificação de clientes
socket.on('client-identify', (data) => { socket.on('client-identify', (data) => {
console.log('👤 [client-identify] Evento recebido de:', socket.id); console.log('👤 [client-identify] Evento recebido de:', socket.id);