From e716e0e131f49b3eb7789f1db3e1a233ef17007e Mon Sep 17 00:00:00 2001 From: VPS 4 Deploy Agent Date: Sun, 31 May 2026 06:52:39 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20cron=20enabled=3D1=20+=20plan-b=20reenvi?= =?UTF-8?q?o=20autom=C3=A1tico=20de=20imagens=20ausentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fix: - server.js e routes/images.js: 'enabled = true' → 'enabled = 1' (PostgreSQL rejeita smallint = boolean — causava 'operator does not exist') Plan B — imagens ausentes no Wasabi: - server.js: pendingReupload (Map clientName → Set) quando getImageBufferWithFallback retorna null no endpoint /thumbnail, scheduleReupload() registra o original_filename e emite 'reupload-files' imediatamente se o cliente estiver online, ou na próxima conexão. - server.js: client-identify agora drena a fila pendente do cliente ao reconectar, antes de emitir clients-list-updated. - original_filename adicionado ao SELECT do endpoint /thumbnail. Co-Authored-By: Claude Sonnet 4.6 --- dental-server/routes/images.js | 2 +- dental-server/server.js | 49 ++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/dental-server/routes/images.js b/dental-server/routes/images.js index e265fe9..a9ea7d0 100644 --- a/dental-server/routes/images.js +++ b/dental-server/routes/images.js @@ -571,7 +571,7 @@ router.post('/backfill-thumbs', async (req, res) => { const rows = await db.all( `SELECT id, filename, clinic_name, client_name, patient_name FROM images - WHERE thumb_filename IS NULL AND enabled = true + WHERE thumb_filename IS NULL AND enabled = 1 ORDER BY created_at DESC` ); res.json({ queued: rows.length, message: 'Backfill iniciado em background' }); diff --git a/dental-server/server.js b/dental-server/server.js index 1e3974d..c722e7a 100644 --- a/dental-server/server.js +++ b/dental-server/server.js @@ -522,6 +522,34 @@ app.use(checkInstallationMiddleware); // ROTAS PÚBLICAS (SEM AUTENTICAÇÃO) // ================================================================ +// ================================================================ +// PLAN B — Fila de reenvio por cliente +// Quando nem thumb nem original existem no Wasabi, o servidor +// registra o original_filename na fila do cliente que enviou a imagem. +// Na próxima conexão (ou imediatamente se já estiver online) o servidor +// emite 'reupload-files' para que o cliente reenvie os arquivos ausentes. +// ================================================================ +const pendingReupload = new Map(); // clientName → Set + +function scheduleReupload(image) { + if (!image.original_filename || !image.client_name) return; + const key = image.client_name; + if (!pendingReupload.has(key)) pendingReupload.set(key, new Set()); + pendingReupload.get(key).add(image.original_filename); + + // Se o cliente já estiver online, notifica imediatamente + const clientInfo = connectedClients.find(c => c.name === key); + if (clientInfo) { + const clientSocket = io.sockets.sockets.get(clientInfo.socketId); + if (clientSocket) { + const files = [...pendingReupload.get(key)]; + clientSocket.emit('reupload-files', { files }); + pendingReupload.delete(key); + console.log(`📤 [plan-b] ${files.length} arquivo(s) solicitado(s) para reenvio → ${key}`); + } + } +} + // Middleware que pula autenticação para rotas públicas de imagens const publicImageRoutes = express.Router(); @@ -549,7 +577,7 @@ async function getImageBufferWithFallback(filename, clinicName, clientName, pati publicImageRoutes.get('/:id/thumbnail', async (req, res) => { try { const image = await db.get( - 'SELECT id, thumb_filename, filename, clinic_name, client_name, patient_name FROM images WHERE id = $1', + 'SELECT id, thumb_filename, filename, original_filename, clinic_name, client_name, patient_name FROM images WHERE id = $1', [req.params.id] ); if (!image) return res.status(404).json({ error: 'Imagem não encontrada' }); @@ -574,7 +602,9 @@ publicImageRoutes.get('/:id/thumbnail', async (req, res) => { // 2. Sem thumbnail — puxa a original (com fallback legado), gera a thumb na hora const originalBuf = await getImageBufferWithFallback(image.filename, image.clinic_name, image.client_name, image.patient_name); if (!originalBuf) { - return res.status(404).json({ error: 'Imagem original não encontrada no storage' }); + // Plan B: solicita ao cliente que reenvie o arquivo original ausente + scheduleReupload(image); + return res.status(404).json({ error: 'Imagem original não encontrada no storage. Reenvio solicitado ao cliente.' }); } const thumbBuf = await imageProcessor.getThumbnail(originalBuf, 500, 500); @@ -863,7 +893,7 @@ io.on('connection', (socket) => { connectedClients.push(clientInfo); socket.isIdentified = true; console.log('✅ Cliente adicionado à lista. Total conectados:', connectedClients.length); - + // Confirmar para o cliente socket.emit('server-identified', { success: true, @@ -871,7 +901,16 @@ io.on('connection', (socket) => { clientInfo: clientInfo }); console.log('✅ Confirmação enviada para cliente:', socket.id); - + + // Plan B: enviar fila de reenvio pendente para este cliente + const pending = pendingReupload.get(clientName); + if (pending && pending.size > 0) { + const files = [...pending]; + socket.emit('reupload-files', { files }); + pendingReupload.delete(clientName); + console.log(`📤 [plan-b] ${files.length} arquivo(s) pendente(s) enviados para ${clientName} ao reconectar`); + } + // Atualizar lista para todos io.emit('clients-list-updated', connectedClients); }); @@ -1313,7 +1352,7 @@ async function start() { const { runThumbBackfill } = require('./routes/images'); const missing = await db.all( `SELECT id, filename, clinic_name, client_name, patient_name FROM images - WHERE thumb_filename IS NULL AND enabled = true ORDER BY created_at DESC` + WHERE thumb_filename IS NULL AND enabled = 1 ORDER BY created_at DESC` ); if (missing.length > 0) { console.log(`[cron:thumbs] ${missing.length} imagem(ns) sem thumbnail — processando...`);