fix: cron enabled=1 + plan-b reenvio automático de imagens ausentes
continuous-integration/webhook Deploy concluído (VPS4)

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<original_filename>)
  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 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-05-31 06:52:39 +02:00
parent 75ce7c1f36
commit e716e0e131
2 changed files with 45 additions and 6 deletions
+1 -1
View File
@@ -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' });
+44 -5
View File
@@ -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<original_filename>
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...`);