From 12891da889d1d8f2544b4a411d58d0b1a6db2c9d Mon Sep 17 00:00:00 2001 From: VPS 4 Deploy Agent Date: Sat, 30 May 2026 18:48:25 +0200 Subject: [PATCH] fix(thumbnail): serve image bytes directly + on-demand thumb generation with Wasabi backfill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O endpoint /api/images/:id/thumbnail retornava JSON {url}, mas o React aponta direto para ele, causando placeholders cinza (sem erro no devtools). Agora serve image/jpeg diretamente. Quando thumb_filename é null, puxa a original, gera a thumb na hora, serve imediatamente e faz backfill no Wasabi + atualiza o banco. --- .../dental-client/public/version.txt | 2 +- dental-server/server.js | 56 +++++++++++++------ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/dental-server/dental-client/public/version.txt b/dental-server/dental-client/public/version.txt index 1b5105d..67da954 100644 --- a/dental-server/dental-client/public/version.txt +++ b/dental-server/dental-client/public/version.txt @@ -1 +1 @@ -2.1.14 +2.1.15 diff --git a/dental-server/server.js b/dental-server/server.js index a3e4414..4c65205 100644 --- a/dental-server/server.js +++ b/dental-server/server.js @@ -524,32 +524,56 @@ app.use(checkInstallationMiddleware); // Middleware que pula autenticação para rotas públicas de imagens const publicImageRoutes = express.Router(); + +// Serve os BYTES da thumbnail diretamente (image/jpeg), para uso direto em . +// Se a imagem ainda não tem thumbnail, gera na hora a partir da original, +// serve imediatamente e faz backfill (upload pro Wasabi + atualiza banco) em background. publicImageRoutes.get('/:id/thumbnail', async (req, res) => { try { - const image = await db.get('SELECT thumb_filename, filename, client_name, patient_name FROM images WHERE id = $1', [req.params.id]); + const image = await db.get( + 'SELECT id, thumb_filename, filename, client_name, patient_name FROM images WHERE id = $1', + [req.params.id] + ); if (!image) return res.status(404).json({ error: 'Imagem não encontrada' }); - const fileToCheck = image.thumb_filename || image.filename; - let url; + res.setHeader('Cache-Control', 'public, max-age=86400'); - const fsSync = require('fs'); - const path = require('path'); - const localPath = path.join(process.env.UPLOAD_DIR || '/app/uploads', fileToCheck); - - if (fsSync.existsSync(localPath)) { - url = `/uploads/${fileToCheck}`; - } else { + // 1. Já existe thumbnail — serve direto (local ou Wasabi) + if (image.thumb_filename) { try { - const wasabiUrl = await storage.getImageUrl(fileToCheck, image.client_name, image.patient_name, false); - url = wasabiUrl || `/uploads/${fileToCheck}`; + const buf = await storage.getImageBuffer(image.thumb_filename, image.client_name, image.patient_name); + if (buf) { + res.setHeader('Content-Type', 'image/jpeg'); + return res.end(buf); + } } catch (e) { - console.error('Erro ao gerar URL Wasabi para thumbnail:', e); - url = `/uploads/${fileToCheck}`; + console.error(`⚠️ [thumbnail] Falha ao ler thumb existente (id ${image.id}):`, e.message); + // cai para regeneração abaixo } } - res.json({ url }); + + // 2. Sem thumbnail — puxa a original, gera a thumb na hora + const originalBuf = await storage.getImageBuffer(image.filename, image.client_name, image.patient_name); + if (!originalBuf) { + return res.status(404).json({ error: 'Imagem original não encontrada' }); + } + + const thumbBuf = await imageProcessor.getThumbnail(originalBuf, 500, 500); + + // Serve imediatamente para não travar a UI + res.setHeader('Content-Type', 'image/jpeg'); + res.end(thumbBuf); + + // Backfill em background: salva a thumb no Wasabi e atualiza o banco + const thumbFilename = `thumb_${image.filename.replace(/\.[^.]+$/, '')}.jpg`; + storage.saveImage(thumbFilename, thumbBuf, image.client_name, image.patient_name, false) + .then(() => db.run('UPDATE images SET thumb_filename = $1 WHERE id = $2', [thumbFilename, image.id])) + .then(() => console.log(`✅ [thumb-backfill] Thumb gerada/salva: ${thumbFilename} (id ${image.id})`)) + .catch(err => console.error(`⚠️ [thumb-backfill] Falha id ${image.id}:`, err.message)); + } catch (e) { - res.status(500).json({ error: e.message }); + console.error(`❌ [thumbnail] Erro id ${req.params.id}:`, e.message); + if (!res.headersSent) res.status(500).json({ error: e.message }); } });