diff --git a/components/CameraScanner.js b/components/CameraScanner.js index 8a21d50..f0faa1b 100644 --- a/components/CameraScanner.js +++ b/components/CameraScanner.js @@ -361,7 +361,8 @@ export default function CameraScanner({ onCardScanned, onError }) { } if (!response.ok) { - throw new Error(`Scan identify failed: ${response.status}`); + const errBody = await response.json().catch(() => ({})); + throw new Error(errBody.error || `Scan identify failed: ${response.status}`); } const result = await response.json(); diff --git a/lib/card-catalog-match.js b/lib/card-catalog-match.js index 0447b34..d8a74f3 100644 --- a/lib/card-catalog-match.js +++ b/lib/card-catalog-match.js @@ -212,18 +212,23 @@ export async function logScanAttempt({ resultKind, latencyMs, }) { - await sql` - INSERT INTO scan_attempts ( - user_id, ocr_text, ocr_confidence, layer, - matched_card_id, result_kind, latency_ms - ) VALUES ( - ${userId}, - ${ocrText || null}, - ${ocrConfidence ?? null}, - ${layer}, - ${matchedCardId}, - ${resultKind}, - ${latencyMs ?? null} - ) - `; + try { + await sql` + INSERT INTO scan_attempts ( + user_id, ocr_text, ocr_confidence, layer, + matched_card_id, result_kind, latency_ms + ) VALUES ( + ${userId}, + ${ocrText || null}, + ${ocrConfidence ?? null}, + ${layer}, + ${matchedCardId}, + ${resultKind}, + ${latencyMs ?? null} + ) + `; + } catch (error) { + // Telemetry must not block identification (e.g. migration not yet applied). + console.error('[logScanAttempt]', error.message); + } } diff --git a/lib/scan-gemini.js b/lib/scan-gemini.js index 70ddafe..a95f4b6 100644 --- a/lib/scan-gemini.js +++ b/lib/scan-gemini.js @@ -1,5 +1,6 @@ +const DEFAULT_VISION_MODEL = process.env.GEMINI_VISION_MODEL || 'gemini-2.5-flash'; const GEMINI_MODEL = - 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'; + `https://generativelanguage.googleapis.com/v1beta/models/${DEFAULT_VISION_MODEL}:generateContent`; const CARD_PROMPT = `You are a specialized trading card recognition system. Analyze this image and determine if it contains a trading card (Magic: The Gathering, Pokemon, Yu-Gi-Oh, Lorcana, etc.). diff --git a/pages/api/scan/identify.js b/pages/api/scan/identify.js index 391c261..ff24e3b 100644 --- a/pages/api/scan/identify.js +++ b/pages/api/scan/identify.js @@ -3,6 +3,51 @@ import { checkScanRateLimit } from '../../../lib/rate-limit.js'; import { analyzeCardImage } from '../../../lib/scan-gemini.js'; import { matchCardInCatalog, logScanAttempt } from '../../../lib/card-catalog-match.js'; +function scanErrorResponse(error) { + const msg = error?.message || ''; + + if (msg.includes('GEMINI_AI_API_KEY is not configured')) { + return { + status: 503, + body: { + error: 'Card scanning is not configured on this server (missing GEMINI_AI_API_KEY).', + }, + }; + } + + if (msg.includes('Gemini API error: 429')) { + return { + status: 502, + body: { + error: 'Vision service quota exceeded. Check Gemini API billing or try again later.', + }, + }; + } + + if (msg.includes('Gemini API error: 403') || msg.includes('PERMISSION_DENIED')) { + return { + status: 502, + body: { + error: 'Vision service access denied. Regenerate the Gemini API key in Google AI Studio.', + }, + }; + } + + if (msg.includes('scan_attempts') || msg.includes('card_submissions')) { + return { + status: 503, + body: { + error: 'Scan database tables are missing. Run npm run migrate up on the deployment database.', + }, + }; + } + + return { + status: 500, + body: { error: 'Internal server error' }, + }; +} + function formatCardResponse(card, ocrResult) { return { id: card.id, @@ -177,6 +222,7 @@ export default async function handler(req, res) { }); } catch (error) { console.error('[POST /api/scan/identify]', error); - return res.status(500).json({ error: 'Internal server error' }); + const { status, body } = scanErrorResponse(error); + return res.status(status).json(body); } }