feat: cron automático de thumbnails + lista de pacientes em tabela + CRUD
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:
VPS 4 Deploy Agent
2026-05-31 06:44:45 +02:00
parent 003e568318
commit 67accb56aa
7 changed files with 687 additions and 6 deletions
+68 -6
View File
@@ -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('═══════════════════════════════════════════════════════');