/** * Livia Research-PoC — der eine serverseitige Endpoint (§20). * * POST /api/livia/research/refresh * * Ruft die drei Quellen ab, normalisiert, beurteilt einmal per LLM und gibt * die relevanten Leads zurück. Kein Scheduler, keine Queue, keine Datenbank — * der Lauf beginnt und endet mit dieser Anfrage. * * Läuft als serverlose Funktion auf derselben Vercel-Instanz, die schon die * Anwendung ausliefert. Das ist der Grund, warum für den PoC gar keine neue * Infrastruktur nötig war: der Hoster kann bereits Serverfunktionen, es hat * bisher nur niemand eine gebraucht. */ import type { IncomingMessage, ServerResponse } from 'node:http' import { SOURCES, fetchZhkResearch, fetchGreaterZurichResearch, fetchZefixResearch, } from '../../_lib/researchSources' import type { ResearchItem } from '../../_lib/researchSources' import { analyzeResearchItems } from '../../_lib/researchAnalysis' import type { ResearchRefreshResult, ResearchSourceResult } from '../../../src/domain/researchLead' interface Quelle { id: string name: string url: string laden: () => Promise } const QUELLEN: Quelle[] = [ { ...SOURCES.ZHK, laden: fetchZhkResearch }, { ...SOURCES.GZA, laden: fetchGreaterZurichResearch }, { ...SOURCES.ZEFIX, laden: fetchZefixResearch }, ] export default async function handler(req: IncomingMessage, res: ServerResponse): Promise { res.setHeader('Content-Type', 'application/json; charset=utf-8') // Ein Live-Lauf soll nie aus einem Zwischenspeicher beantwortet werden. res.setHeader('Cache-Control', 'no-store') if (req.method !== 'POST') { res.statusCode = 405 res.end(JSON.stringify({ error: 'Nur POST' })) return } // Alle drei parallel: eine langsame Quelle soll die beiden anderen nicht aufhalten. const ergebnisse = await Promise.all( QUELLEN.map(async (q): Promise<{ meta: ResearchSourceResult; items: ResearchItem[] }> => { try { const items = await q.laden() return { meta: { id: q.id, name: q.name, url: q.url, ok: true, itemCount: items.length }, items, } } catch (err) { // Eine kaputte Quelle darf die Demo nicht zerstören (§21). return { meta: { id: q.id, name: q.name, url: q.url, ok: false, itemCount: 0, error: err instanceof Error ? err.message : 'Unbekannter Fehler', }, items: [], } } }), ) const sources = ergebnisse.map(e => e.meta) const items = ergebnisse.flatMap(e => e.items) const basis: ResearchRefreshResult = { sources, analyzed: items.length, discarded: 0, watchlist: 0, leads: [], refreshedAt: new Date().toISOString(), } if (items.length === 0) { res.statusCode = 200 res.end(JSON.stringify({ ...basis, analysisError: 'Keine der drei Quellen lieferte Einträge.', })) return } try { const { leads, discarded, watchlist } = await analyzeResearchItems(items) res.statusCode = 200 res.end(JSON.stringify({ ...basis, leads, discarded, watchlist })) } catch (err) { // Gelesen wurde trotzdem — das sagen wir offen, statt eine leere Liste als // «nichts gefunden» auszugeben. res.statusCode = 200 res.end(JSON.stringify({ ...basis, analysisError: err instanceof Error ? err.message : 'Die Beurteilung schlug fehl.', })) } }