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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user