diff --git a/dental-server/server.js b/dental-server/server.js index c722e7a..4c4cdea 100644 --- a/dental-server/server.js +++ b/dental-server/server.js @@ -1326,22 +1326,27 @@ async function start() { redis.initRedis(); // ================================================================ - // CRON — Geração automática de thumbnails ausentes + // CRON — Tarefas automáticas em background // ================================================================ - const THUMB_CRON_INTERVAL_MS = 10 * 60 * 1000; // 10 minutos + const THUMB_CRON_INTERVAL_MS = 10 * 60 * 1000; // 10 minutos + const VERIFY_CRON_INTERVAL_MS = 30 * 60 * 1000; // 30 minutos const cronState = { thumbBackfill: { name: 'Geração de Thumbnails', - description: 'Gera e envia ao Wasabi as miniaturas de RX que ainda não possuem thumbnail.', + description: 'Gera e envia ao Wasabi as miniaturas de RX que ainda não possuem thumbnail (thumb_filename IS NULL).', intervalMs: THUMB_CRON_INTERVAL_MS, - running: false, - lastRun: null, - nextRun: null, - lastResult: null, - } + running: false, lastRun: null, nextRun: null, lastResult: null, + }, + verifyStorage: { + name: 'Verificação de Integridade', + description: 'Verifica se os arquivos de imagem existem no Wasabi; para os ausentes aciona Plan B (reenvio pelo cliente Windows).', + intervalMs: VERIFY_CRON_INTERVAL_MS, + running: false, lastRun: null, nextRun: null, lastResult: null, + }, }; + // ── Cron 1: thumb backfill (images sem thumb_filename) ────────── async function runThumbCron() { if (cronState.thumbBackfill.running) return; cronState.thumbBackfill.running = true; @@ -1370,13 +1375,60 @@ async function start() { } } - // Rodar na inicialização após 5s e depois a cada intervalo + // ── Cron 2: verificar se arquivos existem no storage ──────────── + // Percorre imagens com thumb_filename preenchido e verifica se o + // arquivo (original ou thumb) está realmente no Wasabi. Para os + // ausentes, chama scheduleReupload() para acionar o Plan B. + async function runVerifyStorageCron() { + if (cronState.verifyStorage.running) return; + cronState.verifyStorage.running = true; + cronState.verifyStorage.lastRun = new Date().toISOString(); + cronState.verifyStorage.nextRun = new Date(Date.now() + VERIFY_CRON_INTERVAL_MS).toISOString(); + console.log('[cron:verify] Iniciando verificação de integridade do storage...'); + let checked = 0, missing = 0, requested = 0; + try { + // Limita a 200 por execução para não sobrecarregar + const rows = await db.all( + `SELECT id, filename, thumb_filename, original_filename, clinic_name, client_name, patient_name + FROM images WHERE enabled = 1 ORDER BY created_at DESC LIMIT 200` + ); + for (const row of rows) { + checked++; + const buf = await getImageBufferWithFallback( + row.thumb_filename || row.filename, + row.clinic_name, row.client_name, row.patient_name + ); + if (!buf) { + missing++; + scheduleReupload(row); + requested++; + console.log(`[cron:verify] ⚠️ Arquivo ausente id=${row.id} (${row.original_filename}) → Plan B acionado`); + } + } + cronState.verifyStorage.lastResult = { checked, missing, requested, status: 'ok' }; + console.log(`[cron:verify] Concluído: ${checked} verificadas, ${missing} ausentes, ${requested} reenvios solicitados.`); + } catch (e) { + console.error('[cron:verify] Erro:', e.message); + cronState.verifyStorage.lastResult = { checked, missing, requested, status: 'error', error: e.message }; + } finally { + cronState.verifyStorage.running = false; + } + } + + // Rodar na inicialização e depois em intervalos setTimeout(() => { cronState.thumbBackfill.nextRun = new Date(Date.now() + THUMB_CRON_INTERVAL_MS).toISOString(); runThumbCron(); }, 5000); setInterval(runThumbCron, THUMB_CRON_INTERVAL_MS); + // Verificação de storage: 1ª vez após 15s (depois do boot estabilizar) + setTimeout(() => { + cronState.verifyStorage.nextRun = new Date(Date.now() + VERIFY_CRON_INTERVAL_MS).toISOString(); + runVerifyStorageCron(); + }, 15000); + setInterval(runVerifyStorageCron, VERIFY_CRON_INTERVAL_MS); + // GET /api/cron/status — estado dos cron jobs (admin) app.get('/api/cron/status', authenticateToken, (req, res) => { res.json( @@ -1395,12 +1447,11 @@ async function start() { // POST /api/cron/run/:key — dispara um cron job manualmente (admin) app.post('/api/cron/run/:key', authenticateToken, async (req, res) => { - if (req.params.key === 'thumbBackfill') { - res.json({ success: true, message: 'Cron iniciado em background.' }); - runThumbCron(); - } else { - res.status(404).json({ error: 'Cron job não encontrado.' }); - } + const runners = { thumbBackfill: runThumbCron, verifyStorage: runVerifyStorageCron }; + const fn = runners[req.params.key]; + if (!fn) return res.status(404).json({ error: 'Cron job não encontrado.' }); + res.json({ success: true, message: 'Cron iniciado em background.' }); + fn(); }); server.listen(PORT, () => {