fix(cron): adiciona cron verifyStorage + corrige dispatch manual
continuous-integration/webhook Deploy concluído (VPS4)

- Novo cron 'verifyStorage' (30min): percorre as 200 imagens mais
  recentes, tenta buscar no storage com fallback legado; para cada
  arquivo ausente aciona scheduleReupload() (Plan B) imediatamente.
  Primeira execução 15s após o boot para cobrir os IDs 404 sem esperar
  o usuário abrir o card.

- cronState agora tem dois jobs; /api/cron/status retorna ambos.
- POST /api/cron/run/:key usa mapa de funções; suporta 'thumbBackfill'
  e 'verifyStorage' (botão "Executar agora" na tela de Configurações).

- lastResult do verifyStorage inclui: checked, missing, requested.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-05-31 07:10:28 +02:00
parent b044aa0f3a
commit aa147a3551
+64 -13
View File
@@ -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 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') {
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.' });
runThumbCron();
} else {
res.status(404).json({ error: 'Cron job não encontrado.' });
}
fn();
});
server.listen(PORT, () => {