feat(livia): Runde 10 Prompt 2 — eine Leadliste, dynamische Quellen, zeitliche Relevanz

Livia arbeitet nicht mehr neben dem Leadbestand, sondern in ihn hinein.

- Systemzugänge tragen Titel, Beschreibung und Adresse; die Liste zeigt je
  Eintrag nur noch Aktiv/Inaktiv und Lesen/Schreiben — kein Berechtigungsbalken,
  kein Auge-Icon, keine Lese-/Schreibgruppen
- Die Quellenregistry ist dynamisch: Der Lauf liest genau die aktiven
  Lesezugänge mit Adresse aus Livias Personalblatt. Für die drei gepflegten
  Quellen greift weiterhin ihr eigener Leser, für alles andere ein allgemeiner —
  eine Übersichtsseite, bis zu zehn Unterseiten, kein Spider. Freie Adressen
  werden auf http/https geprüft und gegen interne Netze gesperrt
- Manuell abgelegte PDF- und Word-Dokumente fliessen in denselben Lauf. Text
  wird beim Ablegen nativ extrahiert (ZIP + Flate über DecompressionStream,
  ohne neue Abhängigkeit), Datei und Text liegen in IndexedDB
- Neuer harter Filter «Zeitliche Relevanz»: das Modell beurteilt die
  Ereigniszeit der Veränderung, nicht das Publikationsdatum. «unknown» fällt
  bewusst nicht durch — ein erfundenes Datum wäre die schlechtere Antwort
- Quellentypen und Beobachtungsraum sind Chips mit Freitext; jeder Wert lässt
  sich einzeln entfernen, auch die vorgegebenen
- Info-Knopf bei «Zeitliche Relevanz» und «Mindestrelevanz» erklärt, wie der
  Wert entsteht — ohne erfundene Prozentgewichte
- Gefundene Leads gehen in den zentralen Signalbestand statt in eine zweite
  Ergebnisliste; die Herkunft steht am Signal (origin: RESEARCH_RUN)
- Der Lauf erzeugt einen echten Protokolleintrag mit den Zahlen des Laufs
- Lead-Detail: zeitliche Einordnung mit Fundstelle, semantisches Suchprofil bei
  Chancen mit «Matching starten», und bei Risiko ausdrücklich «Kein internes
  Objekt eindeutig zugeordnet» statt einer beliebigen Mock-Immobilie
- Aufgabenliste nach §11 bereinigt, «Unsichere Signale in Review Queue legen»
  von Nora übernommen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Benjamin SutterandClaude Opus 5 committed 2026-09-12 23:42:59 +02:00
1 parent 1c4b3f7dd8
commit 30daa77937
33 files changed
+2275 -301

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { researchDocumentService } from '../services/researchDocumentService'
import { STALE_SIGNALS } from '../lib/constants'
import { useToastStore } from '../stores/toastStore'
const KEY = ['researchDocuments']
/** Die manuell abgelegten Newsquellen (Runde 10, §2.6). */
export function useResearchDocuments() {
return useQuery({
queryKey: KEY,
queryFn: () => researchDocumentService.getAll(),
staleTime: STALE_SIGNALS,
select: (res) => res.data,
})
}
export function useUploadResearchDocument() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (file: File) => researchDocumentService.upload(file),
onSuccess: (res) => {
void queryClient.invalidateQueries({ queryKey: KEY })
// Ein Dokument ohne brauchbaren Text wird abgelegt, trägt aber nichts
// bei — das muss beim Ablegen gesagt werden und nicht erst, wenn der
// Lauf keine Leads daraus findet.
if (res.data.extractionNote) {
useToastStore.getState().showToast(res.data.extractionNote, 'warning')
} else {
useToastStore.getState().showToast(`«${res.data.name}» abgelegt.`, 'success')
}
},
onError: (err: Error) => {
useToastStore.getState().showToast(err.message, 'error')
},
})
}
export function useRemoveResearchDocument() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: string) => researchDocumentService.remove(id),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: KEY })
},
onError: () => {
useToastStore.getState().showToast('Das Dokument konnte nicht entfernt werden.', 'error')
},
})
}