feat: cron automático de thumbnails + lista de pacientes em tabela + CRUD
continuous-integration/webhook Deploy concluído (VPS4)
continuous-integration/webhook Deploy concluído (VPS4)
Backend: - server.js: cron thumb-backfill a cada 10min com rastreio de estado (lastRun, nextRun, running, lastResult). GET /api/cron/status e POST /api/cron/run/:key para consulta e disparo manual via painel. - routes/images.js: PUT /api/images/patients/:name — edita patient_name, doctor e remark em todas as imagens do paciente de uma vez. Frontend: - PatientsPage.jsx (/patients): tabela com colunas Nome (+ miniatura), Dentista, Imagens, Última atualização, Observações, Ações. Suporta busca, paginação infinita, editar e excluir por linha. - SettingsPage.jsx (/settings): seção de Cron Jobs com status, resultado e botão "Executar agora"; seção de informações do servidor. - EditPatientModal.jsx: edita nome, dentista e observações do paciente. - Sidebar.jsx: add "Pacientes" (todos usuários) e "Configurações" (admin). - App.jsx: rotas /patients (protegida) e /settings (admin). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+68
-6
@@ -1286,9 +1286,29 @@ async function start() {
|
||||
// Inicializar cliente DragonflyDB/Redis
|
||||
redis.initRedis();
|
||||
|
||||
// Iniciar servidor
|
||||
// Backfill de thumbnails ausentes: roda em background após o boot
|
||||
setTimeout(async () => {
|
||||
// ================================================================
|
||||
// CRON — Geração automática de thumbnails ausentes
|
||||
// ================================================================
|
||||
const THUMB_CRON_INTERVAL_MS = 10 * 60 * 1000; // 10 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.',
|
||||
intervalMs: THUMB_CRON_INTERVAL_MS,
|
||||
running: false,
|
||||
lastRun: null,
|
||||
nextRun: null,
|
||||
lastResult: null,
|
||||
}
|
||||
};
|
||||
|
||||
async function runThumbCron() {
|
||||
if (cronState.thumbBackfill.running) return;
|
||||
cronState.thumbBackfill.running = true;
|
||||
cronState.thumbBackfill.lastRun = new Date().toISOString();
|
||||
cronState.thumbBackfill.nextRun = new Date(Date.now() + THUMB_CRON_INTERVAL_MS).toISOString();
|
||||
console.log('[cron:thumbs] Iniciando varredura de thumbnails ausentes...');
|
||||
try {
|
||||
const { runThumbBackfill } = require('./routes/images');
|
||||
const missing = await db.all(
|
||||
@@ -1296,11 +1316,53 @@ async function start() {
|
||||
WHERE thumb_filename IS NULL AND enabled = true ORDER BY created_at DESC`
|
||||
);
|
||||
if (missing.length > 0) {
|
||||
console.log(`[boot] Iniciando backfill de thumbnails para ${missing.length} imagem(ns) sem thumb...`);
|
||||
runThumbBackfill(missing);
|
||||
console.log(`[cron:thumbs] ${missing.length} imagem(ns) sem thumbnail — processando...`);
|
||||
await runThumbBackfill(missing);
|
||||
cronState.thumbBackfill.lastResult = { total: missing.length, status: 'ok' };
|
||||
} else {
|
||||
console.log('[cron:thumbs] Nenhuma imagem pendente.');
|
||||
cronState.thumbBackfill.lastResult = { total: 0, status: 'ok' };
|
||||
}
|
||||
} catch (e) { /* silencia erro de backfill para não afetar o boot */ }
|
||||
} catch (e) {
|
||||
console.error('[cron:thumbs] Erro:', e.message);
|
||||
cronState.thumbBackfill.lastResult = { total: 0, status: 'error', error: e.message };
|
||||
} finally {
|
||||
cronState.thumbBackfill.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Rodar na inicialização após 5s e depois a cada intervalo
|
||||
setTimeout(() => {
|
||||
cronState.thumbBackfill.nextRun = new Date(Date.now() + THUMB_CRON_INTERVAL_MS).toISOString();
|
||||
runThumbCron();
|
||||
}, 5000);
|
||||
setInterval(runThumbCron, THUMB_CRON_INTERVAL_MS);
|
||||
|
||||
// GET /api/cron/status — estado dos cron jobs (admin)
|
||||
app.get('/api/cron/status', authenticateToken, (req, res) => {
|
||||
res.json(
|
||||
Object.entries(cronState).map(([key, job]) => ({
|
||||
key,
|
||||
name: job.name,
|
||||
description: job.description,
|
||||
intervalMinutes: Math.round(job.intervalMs / 60000),
|
||||
running: job.running,
|
||||
lastRun: job.lastRun,
|
||||
nextRun: job.nextRun,
|
||||
lastResult: job.lastResult,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
// 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.' });
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
|
||||
Reference in New Issue
Block a user