feat(rx): healthcheck de reconciliação banco<->Wasabi por tenant/clínica (T3.2)

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 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-06-12 18:14:43 +02:00
parent b815218c30
commit 016305ebf0
+47
View File
@@ -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=<c>&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');