Files
property-match/api/livia/research/refresh.ts
T
Benjamin SutterandClaude Opus 5 f53f3be1e8 fix(livia): Dateiendungen in den Importen der serverlosen Funktion
Erster Produktionslauf schlug mit HTTP 500 fehl, lokal lief derselbe Code
grün. Ursache: Vercel kompiliert jede Datei einzeln statt sie zu bündeln, und
weil das Projekt `"type": "module"` ist, verlangt Nodes ESM-Lader explizite
Dateiendungen in relativen Importen. Vite löst im Entwicklungsbetrieb
grosszügiger auf und verdeckt das.

Das ist genau die Klasse Fehler, die kein Typecheck und kein Test findet — nur
ein echter Aufruf gegen die deployte Funktion.

Geprüft gegen Produktion: HTTP 200, 20 Einträge aus 3/3 Quellen in 24
Sekunden, Beurteilung läuft, Originalquellen verlinkt, keine Laufzeitfehler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 17:41:48 +02:00

108 lines
3.4 KiB
TypeScript

/**
* 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.js'
import type { ResearchItem } from '../../_lib/researchSources.js'
import { analyzeResearchItems } from '../../_lib/researchAnalysis.js'
import type { ResearchRefreshResult, ResearchSourceResult } from '../../../src/domain/researchLead.js'
interface Quelle {
id: string
name: string
url: string
laden: () => Promise<ResearchItem[]>
}
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<void> {
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.',
}))
}
}