const { run, all } = require('./postgres'); /** * Anonymes Scan-Logging für aggregierte Auswertungen (Content, Katalogpflege). * * Bewusst OHNE user_id, Bildreferenz oder Idempotency-Key: Die Tabelle ist von * Anfang an nicht personenbeziehbar, damit später nichts nachträglich * anonymisiert werden muss. Wer wann gescannt hat, steht weiterhin in * billing_accounts bzw. PostHog — hier geht es nur um das Was. * * Schreibfehler dürfen einen Scan nie scheitern lassen, siehe recordScanEvent. */ const ensureScanEventSchema = async (db) => { await run( db, `CREATE TABLE IF NOT EXISTS scan_events ( id BIGSERIAL PRIMARY KEY, scan_type TEXT NOT NULL, species TEXT, confidence DOUBLE PRECISION, diagnosis TEXT, health_status TEXT, locale TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, ); await run(db, 'CREATE INDEX IF NOT EXISTS idx_scan_events_created_at ON scan_events (created_at DESC)'); await run(db, 'CREATE INDEX IF NOT EXISTS idx_scan_events_species ON scan_events (LOWER(species))'); await run(db, 'CREATE INDEX IF NOT EXISTS idx_scan_events_scan_type ON scan_events (scan_type)'); }; const trimOrNull = (value) => { if (typeof value !== 'string') return null; const trimmed = value.trim(); return trimmed ? trimmed.slice(0, 200) : null; }; const toNumberOrNull = (value) => { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; }; /** * Best effort. Ein fehlgeschlagenes Log darf den Scan des Nutzers nicht * beeinflussen — deshalb wird hier geloggt und geschluckt, nicht geworfen. */ const recordScanEvent = async (db, event) => { try { await run( db, `INSERT INTO scan_events (scan_type, species, confidence, diagnosis, health_status, locale) VALUES ($1, $2, $3, $4, $5, $6)`, [ trimOrNull(event?.scanType) || 'unknown', trimOrNull(event?.species), toNumberOrNull(event?.confidence), trimOrNull(event?.diagnosis), trimOrNull(event?.healthStatus), trimOrNull(event?.locale), ], ); } catch (error) { console.warn('scan_events write failed (ignored):', error.message); } }; /** * Aggregat für spätere Content-Auswertungen. Erst ab nennenswerter Menge * aussagekräftig — die Mindestmenge bewusst im Aufrufer prüfen, nicht hier. */ const getScanEventStats = async (db, { since = null, limit = 100 } = {}) => { const params = []; let whereClause = ''; if (since) { params.push(since); whereClause = 'WHERE created_at >= $1'; } params.push(limit); const topSpecies = await all( db, `SELECT species, COUNT(*)::int AS scans, ROUND(AVG(confidence)::numeric, 3) AS avg_confidence FROM scan_events ${whereClause}${whereClause ? ' AND' : 'WHERE'} species IS NOT NULL GROUP BY species ORDER BY scans DESC LIMIT $${params.length}`, params, ); const totals = await all( db, `SELECT scan_type, COUNT(*)::int AS scans FROM scan_events ${whereClause} GROUP BY scan_type`, since ? [since] : [], ); return { topSpecies, totals }; }; module.exports = { ensureScanEventSchema, getScanEventStats, recordScanEvent, };