import { Router } from 'express'; import { sql } from 'drizzle-orm'; import multer from 'multer'; import { db } from '../db/client'; import { rabbits } from '../db/schema'; import { isPedigreeScanEnabled, PedigreeScanError, scanPedigreeImage } from '../lib/pedigreeScan'; const router = Router(); const MEDIA_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp'] as const); type MediaType = 'image/jpeg' | 'image/png' | 'image/webp'; // In memory: the image is forwarded to the API and discarded, never written to // disk. The client downsizes to ~2400px JPEG first, so real uploads are well // under the limit. const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 }, }).single('image'); // Lets the client decide whether to show the "Scan a pedigree" button. router.get('/', (req, res) => { res.json({ enabled: isPedigreeScanEnabled() }); }); // Confident herd match for a scanned sire/dam (FEATURE_OCR_Pedigree_Scan.md // Q6): an exact ear number wins; otherwise exactly one exact-name match. // Anything looser stays a manual entry the user can link by hand. function findHerdMatch(name: string | null, earNumber: string | null) { const columns = { id: rabbits.id, name: rabbits.name }; if (earNumber) { const byEar = db .select(columns) .from(rabbits) .where(sql`upper(${rabbits.earNumber}) = ${earNumber.toUpperCase()}`) .limit(2) .all(); if (byEar.length === 1) return byEar[0]; } if (name) { const byName = db .select(columns) .from(rabbits) .where(sql`lower(${rabbits.name}) = ${name.toLowerCase()}`) .limit(2) .all(); if (byName.length === 1) return byName[0]; } return null; } router.post('/', (req, res, next) => { if (!isPedigreeScanEnabled()) { res.status(503).json({ error: 'Pedigree scanning isn’t set up on this server.' }); return; } upload(req, res, async (uploadErr) => { if (uploadErr) { res.status(400).json({ error: uploadErr.message }); return; } const file = req.file; if (!file || !MEDIA_TYPES.has(file.mimetype as MediaType)) { res.status(400).json({ error: 'Upload a JPEG, PNG, or WebP photo of the pedigree.' }); return; } try { const result = await scanPedigreeImage(file.buffer, file.mimetype as MediaType); const matches = { sire: findHerdMatch(result.ancestors.sire.name, result.ancestors.sire.ear_number), dam: findHerdMatch(result.ancestors.dam.name, result.ancestors.dam.ear_number), }; res.json({ ...result, matches }); } catch (err) { if (err instanceof PedigreeScanError) { res.status(err.status).json({ error: err.message }); return; } next(err); } }); }); export default router;