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();
|
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, () => {
|
server.listen(PORT, () => {
|
||||||
console.log('═══════════════════════════════════════════════════════');
|
console.log('═══════════════════════════════════════════════════════');
|
||||||
console.log('🚀 DENTAL IMAGE SERVER STARTED');
|
console.log('🚀 DENTAL IMAGE SERVER STARTED');
|
||||||
|
|||||||
Reference in New Issue
Block a user