From 016305ebf0c38d5adc55442701009a7c123f1e6c Mon Sep 17 00:00:00 2001 From: VPS 4 Deploy Agent Date: Fri, 12 Jun 2026 18:14:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(rx):=20healthcheck=20de=20reconcilia=C3=A7?= =?UTF-8?q?=C3=A3o=20banco<->Wasabi=20por=20tenant/cl=C3=ADnica=20(T3.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/admin/rx-health (admin): contagens por (tenant_id, clinica_id) + último resultado do cron de storage; ?clinica_id=&checkLimit=N faz HEAD-check bounded de objetos ausentes no Wasabi. Read-only, aditivo. Co-Authored-By: Claude Opus 4.8 --- dental-server/server.js | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/dental-server/server.js b/dental-server/server.js index 9748907..c4076df 100644 --- a/dental-server/server.js +++ b/dental-server/server.js @@ -1623,6 +1623,53 @@ async function start() { fn(); }); + // GET /api/admin/rx-health — reconciliação banco↔Wasabi por tenant/clínica (read-only). + // sem params: contagens por (tenant_id, clinica_id) + último resultado do cron de storage. + // ?clinica_id=&checkLimit=N: HEAD-check das N imagens recentes da clínica → ausentes. + app.get('/api/admin/rx-health', authenticateToken, requireAdmin, async (req, res) => { + try { + const byClinic = await db.all( + `SELECT tenant_id, clinica_id, + MAX(clinic_name) AS clinic_name, + COUNT(*)::int AS image_count, + COUNT(DISTINCT patient_name)::int AS patient_count, + MAX(created_at) AS last_image_at + FROM images + WHERE enabled = 1 + GROUP BY tenant_id, clinica_id + ORDER BY clinic_name` + ); + + let missingCheck = null; + const clinicaId = req.query.clinica_id; + const checkLimit = Math.min(500, parseInt(req.query.checkLimit) || 0); + if (clinicaId && checkLimit > 0) { + const rows = await db.all( + `SELECT id, filename, thumb_filename, clinic_name, client_name, patient_name + FROM images WHERE enabled = 1 AND clinica_id = $1 ORDER BY created_at DESC LIMIT $2`, + [clinicaId, checkLimit] + ); + let checked = 0, missing = 0; const missingIds = []; + for (const row of rows) { + checked++; + const ok = await storage.fileExists(row.thumb_filename || row.filename, row.clinic_name, row.client_name, row.patient_name); + if (!ok) { missing++; if (missingIds.length < 50) missingIds.push(row.id); } + } + missingCheck = { clinica_id: clinicaId, checked, missing, missingIds }; + } + + res.json({ + by_clinic: byClinic, + storage_verify_cron: cronState.verifyStorage?.lastResult || null, + storage_verify_last_run: cronState.verifyStorage?.lastRun || null, + missing_check: missingCheck, + generated_at: new Date().toISOString(), + }); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + server.listen(PORT, () => { console.log('═══════════════════════════════════════════════════════'); console.log('🚀 DENTAL IMAGE SERVER STARTED');