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>
81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
/**
|
|
* Property On — die abgelegten Dokumente liegen in IndexedDB (Runde 10, §2.6).
|
|
*
|
|
* Keine Mockup-Variante und kein zweiter Backend-Dienst: Die Anwendung läuft
|
|
* im Browser, und der Browser hat eine Ablage, die auch Dateien verträgt. Ein
|
|
* Server nur für die Dateihaltung wäre Infrastruktur für einen einzigen Zweck.
|
|
*
|
|
* Aufgeteilt in zwei Speicher: die Metadaten samt extrahiertem Text in `meta`,
|
|
* die Originaldatei in `blobs`. So kann die Liste aufgebaut werden, ohne dass
|
|
* zwanzig Megabyte PDF durch den Speicher wandern — gebraucht wird die Datei
|
|
* erst, wenn jemand sie herunterlädt.
|
|
*/
|
|
|
|
import type { IResearchDocumentProvider } from './IResearchDocumentProvider'
|
|
import type { ResearchDocument } from '../domain/researchDocument'
|
|
|
|
const DB_NAME = 'property-on'
|
|
const DB_VERSION = 1
|
|
const STORE_META = 'research-document-meta'
|
|
const STORE_BLOBS = 'research-document-blobs'
|
|
|
|
function openDb(): Promise<IDBDatabase> {
|
|
return new Promise((resolve, reject) => {
|
|
const req = indexedDB.open(DB_NAME, DB_VERSION)
|
|
req.onupgradeneeded = () => {
|
|
const db = req.result
|
|
if (!db.objectStoreNames.contains(STORE_META)) db.createObjectStore(STORE_META, { keyPath: 'id' })
|
|
if (!db.objectStoreNames.contains(STORE_BLOBS)) db.createObjectStore(STORE_BLOBS)
|
|
}
|
|
req.onsuccess = () => resolve(req.result)
|
|
req.onerror = () => reject(req.error ?? new Error('IndexedDB liess sich nicht öffnen.'))
|
|
})
|
|
}
|
|
|
|
/** Eine Transaktion als Versprechen — IndexedDB spricht sonst nur in Ereignissen. */
|
|
function alsVersprechen<T>(
|
|
req: IDBRequest<T>,
|
|
tx: IDBTransaction,
|
|
): Promise<T> {
|
|
return new Promise((resolve, reject) => {
|
|
req.onsuccess = () => resolve(req.result)
|
|
req.onerror = () => reject(req.error ?? new Error('Zugriff auf die Dokumentenablage fehlgeschlagen.'))
|
|
tx.onabort = () => reject(tx.error ?? new Error('Die Dokumentenablage brach die Transaktion ab.'))
|
|
})
|
|
}
|
|
|
|
export const IndexedDbResearchDocumentProvider: IResearchDocumentProvider = {
|
|
async getAll() {
|
|
const db = await openDb()
|
|
const tx = db.transaction(STORE_META, 'readonly')
|
|
const alle = await alsVersprechen(tx.objectStore(STORE_META).getAll() as IDBRequest<ResearchDocument[]>, tx)
|
|
db.close()
|
|
// Neuestes zuerst — wer eben etwas abgelegt hat, sucht es oben.
|
|
return alle.sort((a, b) => b.uploadedAt.localeCompare(a.uploadedAt))
|
|
},
|
|
|
|
async add(doc, blob) {
|
|
const db = await openDb()
|
|
const tx = db.transaction([STORE_META, STORE_BLOBS], 'readwrite')
|
|
tx.objectStore(STORE_META).put(doc)
|
|
await alsVersprechen(tx.objectStore(STORE_BLOBS).put(blob, doc.id), tx)
|
|
db.close()
|
|
},
|
|
|
|
async remove(id) {
|
|
const db = await openDb()
|
|
const tx = db.transaction([STORE_META, STORE_BLOBS], 'readwrite')
|
|
tx.objectStore(STORE_META).delete(id)
|
|
await alsVersprechen(tx.objectStore(STORE_BLOBS).delete(id), tx)
|
|
db.close()
|
|
},
|
|
|
|
async getBlob(id) {
|
|
const db = await openDb()
|
|
const tx = db.transaction(STORE_BLOBS, 'readonly')
|
|
const blob = await alsVersprechen(tx.objectStore(STORE_BLOBS).get(id) as IDBRequest<Blob | undefined>, tx)
|
|
db.close()
|
|
return blob ?? null
|
|
},
|
|
}
|