docs: echte Umlaute in der Dokumentation
893 Stellen in Fliesstext, Tabellen und Mermaid-Beschriftungen. Dazu die sichtbaren ss-Fälle (ausschließlich, groß, außen, schließt, heißt, draußen, weiße, anstoßen, Straße). Nicht angetastet, damit die Doku weiter beschreibt, was wirklich in der Datenbank steht: - Codeblöcke und Code-Spans - dort stehen Bezeichner, SQL, Enum-Werte und Dateinamen: `mail_pool`, `'uebernommen'`, `extract.js` - in Mermaid nur der Text in Anführungszeichen; Knotennamen, Pfeile und Werte wie source = 'Mail' bleiben, wie sie sind - `empfaenger` im ER-Diagramm von crm_mail_contact_candidates: die Aufzählung dort nennt die Datenbankwerte, nicht die Anzeige Die Menü-Skizze in der Benutzeranleitung zeigt jetzt die aufgeklappten Kontakte mit "Aus Mails" samt Zähler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8b7ce448a6
commit
7973dc07b1
+68
-68
@@ -1,6 +1,6 @@
|
||||
# 09 Quellcode-Referenz
|
||||
|
||||
Was in welcher Datei steckt und welche Zusagen sie einhaelt. Gedacht zum
|
||||
Was in welcher Datei steckt und welche Zusagen sie einhält. Gedacht zum
|
||||
Nachschlagen, nicht zum Durchlesen.
|
||||
|
||||
## Backend
|
||||
@@ -11,16 +11,16 @@ Baut die Express-App. Die **Reihenfolge** ist bedeutsam:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
J["express.json({ limit: '6mb' })<br/>Avatare kommen als Data-URL"] --> C["cors (nur fuer den Dev-Server)"]
|
||||
J["express.json({ limit: '6mb' })<br/>Avatare kommen als Data-URL"] --> C["cors (nur für den Dev-Server)"]
|
||||
C --> H["/api/health"]
|
||||
H --> R["/api/auth, /users, /leads,<br/>/connections, /events, /opportunities"]
|
||||
R --> S["/api/scanner (requireAuth + Router)"]
|
||||
S --> F["/api → 404 JSON<br/>faengt alles Unbekannte"]
|
||||
F --> E["Fehlerbehandlung:<br/>400 kaputtes JSON, 413 zu gross, 500 Rest"]
|
||||
S --> F["/api → 404 JSON<br/>fängt alles Unbekannte"]
|
||||
F --> E["Fehlerbehandlung:<br/>400 kaputtes JSON, 413 zu groß, 500 Rest"]
|
||||
```
|
||||
|
||||
Beim Start: `initDatabase()` (CRM-Schema, Admin-Seed) → `initScannerDatabase()`
|
||||
(darf fehlschlagen, dann laeuft das CRM ohne Scanner) → Scheduler, sofern
|
||||
(darf fehlschlagen, dann läuft das CRM ohne Scanner) → Scheduler, sofern
|
||||
`SCANNER_SCHEDULER !== 'false'`.
|
||||
|
||||
### `server/src/db.js`
|
||||
@@ -28,11 +28,11 @@ Beim Start: `initDatabase()` (CRM-Schema, Admin-Seed) → `initScannerDatabase()
|
||||
| Export | Zweck |
|
||||
| --- | --- |
|
||||
| `pool` | mysql2-Pool, `connectionLimit: 8`, `dateStrings: true` |
|
||||
| `initDatabase()` | fuehrt `schema.sql` Statement fuer Statement aus, dann `ensureAdminUser()` |
|
||||
| `initDatabase()` | führt `schema.sql` Statement für Statement aus, dann `ensureAdminUser()` |
|
||||
|
||||
`dateStrings: true` ist eine bewusste Entscheidung: Termine kommen als
|
||||
`YYYY-MM-DD HH:MM:SS` **ohne** Zeitzonenumrechnung an. Der Kalender rechnet mit
|
||||
Wandzeit; ein `Date`-Objekt aus dem Treiber wuerde beim Serialisieren nach UTC
|
||||
Wandzeit; ein `Date`-Objekt aus dem Treiber würde beim Serialisieren nach UTC
|
||||
verschieben.
|
||||
|
||||
`ensureAdminUser()` legt nur an, wenn `crm_users` **leer** ist. Ohne
|
||||
@@ -44,19 +44,19 @@ im Quelltext steht kein Passwort.
|
||||
| Export | Zweck |
|
||||
| --- | --- |
|
||||
| `signToken(user)` | JWT mit `sub` und `role`, Laufzeit aus `JWT_EXPIRES_IN` (12 h) |
|
||||
| `requireAuth` | Token pruefen **und** Benutzer frisch aus der DB laden |
|
||||
| `requireAuth` | Token prüfen **und** Benutzer frisch aus der DB laden |
|
||||
| `requireAdmin` | 403, wenn `req.user.role !== 'admin'` |
|
||||
|
||||
Dass `requireAuth` bei jeder Anfrage die Datenbank liest, ist Absicht: ein
|
||||
deaktivierter oder geloeschter Benutzer ist sofort draussen und nicht erst, wenn
|
||||
sein Token ablaeuft. `secret()` wirft, wenn `JWT_SECRET` fehlt oder kuerzer als 16
|
||||
deaktivierter oder gelöschter Benutzer ist sofort draußen und nicht erst, wenn
|
||||
sein Token abläuft. `secret()` wirft, wenn `JWT_SECRET` fehlt oder kürzer als 16
|
||||
Zeichen ist - lieber ein harter Startfehler als eine schwache Signatur.
|
||||
|
||||
### `server/src/userFields.js`
|
||||
|
||||
`normalizeColor` und `normalizeAvatar`, benutzt von `routes/auth.js` und
|
||||
`routes/users.js`. Farben nur als `#rrggbb`, Avatare nur als Bild-Data-URL oder
|
||||
`http(s)`-Adresse, jeweils laengenbegrenzt.
|
||||
`http(s)`-Adresse, jeweils längenbegrenzt.
|
||||
|
||||
### Die CRM-Router
|
||||
|
||||
@@ -65,33 +65,33 @@ Alle unter `router.use(requireAuth)`.
|
||||
| Router | Endpunkte | Rechte |
|
||||
| --- | --- | --- |
|
||||
| `auth.js` | `POST /login`, `GET /me`, `PATCH /me`, `POST /change-password` | `/login` offen, Rest angemeldet |
|
||||
| `users.js` | `GET /directory`; danach `requireAdmin`: `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | `/directory` fuer alle, Rest Admin |
|
||||
| `users.js` | `GET /directory`; danach `requireAdmin`: `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | `/directory` für alle, Rest Admin |
|
||||
| `leads.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id`; Vorbereich: `GET /mail-pool/sources`, `POST /mail-pool/promote`, `POST /:id/promote` | jeder Angemeldete |
|
||||
| `connections.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | jeder Angemeldete |
|
||||
| `events.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | anlegen jeder; aendern/loeschen nur Ersteller oder Admin |
|
||||
| `opportunities.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | aendern/loeschen nur Ersteller, Zustaendiger oder Admin |
|
||||
| `events.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | anlegen jeder; ändern/löschen nur Ersteller oder Admin |
|
||||
| `opportunities.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | ändern/löschen nur Ersteller, Zuständiger oder Admin |
|
||||
| `mail.js` | `GET /status`, `/messages`, `/messages/:id`, `/candidates`; `POST /messages/:id/links`, `/candidates/:id/adopt`, `/candidates/:id/reject`; `DELETE /links/:id`; `requireAdmin`: `GET/POST /settings`, `POST /test`, `POST /sync` | lesen und zuordnen jeder; einrichten Admin |
|
||||
|
||||
Wiederkehrende Muster in diesen Routern:
|
||||
|
||||
* `emptyToNull()` - leere Zeichenketten werden `NULL`, damit „nicht gesetzt" und
|
||||
„leer" in der Datenbank nicht zwei Zustaende sind.
|
||||
* `normalizeDate()` prueft echte Kalendertage; der 31. Februar wird abgelehnt und
|
||||
„leer" in der Datenbank nicht zwei Zustände sind.
|
||||
* `normalizeDate()` prüft echte Kalendertage; der 31. Februar wird abgelehnt und
|
||||
nicht stillschweigend verschoben.
|
||||
* `normalizeNumber()` akzeptiert Komma als Dezimaltrennzeichen und begrenzt nach
|
||||
oben - ein Tagessatz von 10 Millionen ist ein Tippfehler.
|
||||
* `parseTags()` liest die als Text gespeicherten JSON-Arrays defensiv: kaputter
|
||||
Inhalt ergibt eine leere Liste, keinen Absturz.
|
||||
* `GET /login` antwortet bei unbekanntem Benutzer und falschem Passwort
|
||||
**identisch**, damit sich ueber die API keine Benutzernamen aufzaehlen lassen.
|
||||
**identisch**, damit sich über die API keine Benutzernamen aufzählen lassen.
|
||||
* Der letzte aktive Administrator kann nicht degradiert, deaktiviert oder
|
||||
geloescht werden.
|
||||
gelöscht werden.
|
||||
|
||||
### `server/src/settings.js`
|
||||
|
||||
`getSetting`, `getSettings`, `setSetting` auf `crm_settings`, dazu `asBool` und
|
||||
`asInt`. Alle Werte liegen als Text in der Datenbank. `getSettings(defaults)`
|
||||
nimmt die Vorgaben gleichzeitig als Positivliste der erlaubten Schluessel - ein
|
||||
nimmt die Vorgaben gleichzeitig als Positivliste der erlaubten Schlüssel - ein
|
||||
unbekannter Name kommt so nie in die Datenbank.
|
||||
|
||||
Bewusst getrennt vom Setting-Modell des Scanners (andere Datenbank) und bewusst
|
||||
@@ -108,36 +108,36 @@ Postfachanbindung, fachlich in [11 Postfachanbindung](11-postfach.md).
|
||||
| `html.js` | `htmlToText()` (Zeilen bleiben erhalten, Linkziele werden hinter den Linktext gesetzt), `sanitizeHtml()` |
|
||||
| `extract.js` | `extractCandidates()`, `parsePersonName()`, `parseSignature()`, `AUTO_ADOPT_MIN_CONFIDENCE` |
|
||||
| `store.js` | Zustandszeile, `upsertMessage()`, `insertCandidates()`, `linkMessage()`, `findLeadByEmail()` je Bestand |
|
||||
| `contacts.js` | `adoptCandidate()`: Eintrag in „Aus Mails" (`target: 'pool'`) oder richtiger Kontakt (`'contacts'`), sonst ergaenzen; `markCandidate()` |
|
||||
| `contacts.js` | `adoptCandidate()`: Eintrag in „Aus Mails" (`target: 'pool'`) oder richtiger Kontakt (`'contacts'`), sonst ergänzen; `markCandidate()` |
|
||||
| `sync.js` | ein Abgleichslauf, Delta-Seiten, Einzelsperre, Fehlerbehandlung |
|
||||
| `scheduler.js` | Minutentakt, Faelligkeit aus `last_run_at`, kein Zeitfenster |
|
||||
| `scheduler.js` | Minutentakt, Fälligkeit aus `last_run_at`, kein Zeitfenster |
|
||||
|
||||
Eigenheiten, die man kennen muss:
|
||||
|
||||
* `graphRequest()` nimmt Pfade **und** vollstaendige URLs - Delta-Links kommen
|
||||
absolut von Graph zurueck.
|
||||
* Ein `401` verwirft das Token genau einmal und wiederholt; so ueberlebt ein Lauf
|
||||
ein serverseitig zurueckgezogenes Token.
|
||||
* `upsertMessage()` laesst die Textspalten unberuehrt, wenn `bodyText` im
|
||||
uebergebenen Objekt `undefined` ist. Ein reines Metadaten-Update darf den schon
|
||||
geholten Mailtext nicht loeschen.
|
||||
* `graphRequest()` nimmt Pfade **und** vollständige URLs - Delta-Links kommen
|
||||
absolut von Graph zurück.
|
||||
* Ein `401` verwirft das Token genau einmal und wiederholt; so überlebt ein Lauf
|
||||
ein serverseitig zurückgezogenes Token.
|
||||
* `upsertMessage()` lässt die Textspalten unberührt, wenn `bodyText` im
|
||||
übergebenen Objekt `undefined` ist. Ein reines Metadaten-Update darf den schon
|
||||
geholten Mailtext nicht löschen.
|
||||
* `extractCandidates()` verlangt im Fliesstext eine Anrede vor dem Namen.
|
||||
Ohne diese Bedingung liest der Musterabgleich jede Folge grossgeschriebener
|
||||
Woerter als Namen.
|
||||
Ohne diese Bedingung liest der Musterabgleich jede Folge großgeschriebener
|
||||
Wörter als Namen.
|
||||
* Der Lauf ist **einfach gesperrt** (`running` in `sync.js`) - ein zweiter Aufruf
|
||||
kommt sofort mit `skipped` zurueck, statt parallel zu laufen.
|
||||
kommt sofort mit `skipped` zurück, statt parallel zu laufen.
|
||||
|
||||
### `server/src/routes/scanner.js`
|
||||
|
||||
Die portierte Scanner-API unter `/api/scanner`, vollstaendig in
|
||||
Die portierte Scanner-API unter `/api/scanner`, vollständig in
|
||||
[03 Projekt Scanner](03-scanner.md#endpunkte) tabelliert. Eigenheiten:
|
||||
|
||||
* `multer` schreibt CV-PDFs nach `UPLOAD_DIR`, Grenze 20 MB.
|
||||
* `resolveCvPath()` findet eine Datei auch dann, wenn in der Datenbank noch ein
|
||||
relativer Pfad aus der alten App steht - es zaehlt der Dateiname.
|
||||
* `GET /agents` laesst `cvText` weg (mehrere Hundert KB pro Agent).
|
||||
relativer Pfad aus der alten App steht - es zählt der Dateiname.
|
||||
* `GET /agents` lässt `cvText` weg (mehrere Hundert KB pro Agent).
|
||||
* `SECRET_KEYS` (`openai_api_key`, `smtp_pass`) werden als `********`
|
||||
ausgeliefert; kommt dieser Wert oder ein leeres Feld zurueck, bleibt der
|
||||
ausgeliefert; kommt dieser Wert oder ein leeres Feld zurück, bleibt der
|
||||
gespeicherte Wert stehen.
|
||||
* `normalizeCookie()` biegt Exporte von Cookie-Editor auf das zurecht, was
|
||||
Playwright akzeptiert (`sameSite`, `expirationDate` → `expires`).
|
||||
@@ -147,27 +147,27 @@ Die portierte Scanner-API unter `/api/scanner`, vollstaendig in
|
||||
| Datei | Inhalt |
|
||||
| --- | --- |
|
||||
| `db/config.js` | Sequelize-Instanz, eigene Datenbank, `underscored: true` |
|
||||
| `db/ensureDatabase.js` | `CREATE DATABASE IF NOT EXISTS`, Name gegen Zeichenklasse geprueft |
|
||||
| `db/ensureDatabase.js` | `CREATE DATABASE IF NOT EXISTS`, Name gegen Zeichenklasse geprüft |
|
||||
| `models/index.js` | `initScannerDatabase()`: verbinden, `sync()`, Portale und Standardeinstellungen seeden |
|
||||
| `models/Agent.js` | `cvText`, `cvFilePath`, `keywords`, `excludeKeywords`, `isActive` |
|
||||
| `models/Project.js` | Unique auf `(portal, portal_project_id)`, `updatedAt: false` |
|
||||
| `models/Match.js` | Unique auf `(agent_id, project_id)`, `pros`/`cons` als JSON-Text mit Getter/Setter |
|
||||
| `models/Portal.js` | Zugangsdaten, `sessionState`, `connectionStatus` |
|
||||
| `models/Setting.js` | Schluessel-Wert mit `getVal`/`setVal` |
|
||||
| `services/scraper.js` | Playwright je Portal; unveraendert aus der alten App |
|
||||
| `models/Setting.js` | Schlüssel-Wert mit `getVal`/`setVal` |
|
||||
| `services/scraper.js` | Playwright je Portal; unverändert aus der alten App |
|
||||
| `services/matchingEngine.js` | OpenAI `gpt-4o-mini`, `response_format: json_object`, `temperature: 0.1` |
|
||||
| `services/pipeline.js` | ein Lauf: scrapen, speichern, bewerten, benachrichtigen |
|
||||
| `services/scheduler.js` | node-cron jede Minute, Zeitfenster, Sperrfrist, `last_scan_at` |
|
||||
| `services/cleanup.js` | Projekte aelter als 7 Tage loeschen (Treffer kaskadieren) |
|
||||
| `services/cleanup.js` | Projekte älter als 7 Tage löschen (Treffer kaskadieren) |
|
||||
| `services/pdfParser.js` | `pdf-parse`, doppelte Leerzeilen raus |
|
||||
| `utils/crypto.js` | AES ueber `crypto-js`; `decryptWith()` fuer fremde Schluessel |
|
||||
| `utils/crypto.js` | AES über `crypto-js`; `decryptWith()` für fremde Schlüssel |
|
||||
| `uploadDir.js` | `UPLOAD_DIR`, `ensureUploadDir()`, `resolveCvPath()` |
|
||||
| `importLegacy.js` | einmalige Uebernahme aus der alten Datenbank |
|
||||
| `importLegacy.js` | einmalige Übernahme aus der alten Datenbank |
|
||||
|
||||
Zusagen, auf die sich der Rest verlaesst:
|
||||
Zusagen, auf die sich der Rest verlässt:
|
||||
|
||||
* `runMatchingPipeline()` laeuft **nie zweimal gleichzeitig** und gibt `false`
|
||||
zurueck, wenn schon einer laeuft. `isPipelineRunning()` fragt den Zustand ab.
|
||||
* `runMatchingPipeline()` läuft **nie zweimal gleichzeitig** und gibt `false`
|
||||
zurück, wenn schon einer läuft. `isPipelineRunning()` fragt den Zustand ab.
|
||||
* Der Zeitpunkt landet im `finally` in `settings.last_scan_at` - auch nach einem
|
||||
Abbruch, damit sich ein gescheiterter Lauf nicht sofort wiederholt.
|
||||
* `cleanup.js` benutzt `sequelize.col('created_at')` statt des Attributnamens:
|
||||
@@ -181,10 +181,10 @@ Zusagen, auf die sich der Rest verlaesst:
|
||||
| --- | --- |
|
||||
| `apiRequest<T>(path, options)` | zentraler Fetch: Token, JSON-Header, 401-Behandlung, Fehlermeldung aus dem Body |
|
||||
| `getToken` / `setToken` / `clearToken` | Token im `localStorage` (`perinco_crm_token`) |
|
||||
| `setUnauthorizedHandler` | wird bei 401 gerufen; `AuthProvider` haengt sich hier ein |
|
||||
| `setUnauthorizedHandler` | wird bei 401 gerufen; `AuthProvider` hängt sich hier ein |
|
||||
| `ApiError` | Fehler mit `status` |
|
||||
| `authApi`, `usersApi`, `leadsApi`, `opportunitiesApi`, `eventsApi`, `connectionsApi` | typisierte Endpunkte |
|
||||
| `leadsApi.mailPoolSources` / `.promote` / `.promoteMany` | Vorbereich „Aus Mails": Herkunft lesen, einzeln bzw. gesammelt uebernehmen |
|
||||
| `leadsApi.mailPoolSources` / `.promote` / `.promoteMany` | Vorbereich „Aus Mails": Herkunft lesen, einzeln bzw. gesammelt übernehmen |
|
||||
|
||||
Zwei Feinheiten: Der `Content-Type` wird **nicht** gesetzt, wenn der Body
|
||||
`FormData` ist. Und ein Netzwerkfehler wird zu `ApiError(status 0)` mit
|
||||
@@ -198,8 +198,8 @@ Typen (`ScannerAgent`, `ScannerPortal`, `ScannerProject`, `ScannerMatch`,
|
||||
|
||||
Die Feldnamen sind absichtlich die der Scanner-Datenbank (camelCase aus
|
||||
Sequelize), inklusive der Grossbuchstaben `Agent` und `Project` an den Joins -
|
||||
damit bleibt der Vergleich mit der alten App moeglich. Nur `ScannerProject`
|
||||
traegt beide Schreibweisen fuer `created_at`, weil das Modell die Spalte selbst
|
||||
damit bleibt der Vergleich mit der alten App möglich. Nur `ScannerProject`
|
||||
trägt beide Schreibweisen für `created_at`, weil das Modell die Spalte selbst
|
||||
umbenennt.
|
||||
|
||||
### `src/lib/mailApi.ts`
|
||||
@@ -208,28 +208,28 @@ Typen (`MailMessageSummary`, `MailMessageDetail`, `MailCandidate`, `MailLink`,
|
||||
`MailStatus`, `MailSyncResult`, `MailConnectionTest`, `MailSettingsValues`), das
|
||||
Objekt `mailApi`, `MAIL_SETTINGS_DEFAULTS` und die Datumshelfer.
|
||||
|
||||
`parseMailDate()` haengt ein `Z` an den MariaDB-Zeitstempel, bevor daraus ein
|
||||
`parseMailDate()` hängt ein `Z` an den MariaDB-Zeitstempel, bevor daraus ein
|
||||
`Date` wird. Ohne das liest der Browser den Wert als Ortszeit, und die Anzeige
|
||||
waere je Zeitzone um Stunden verschoben - die Mailzeiten stehen als UTC in der
|
||||
wäre je Zeitzone um Stunden verschoben - die Mailzeiten stehen als UTC in der
|
||||
Datenbank, anders als die Kalendertermine. `formatMailDate()` zeigt heute nur die
|
||||
Uhrzeit, dieses Jahr Tag und Monat, sonst mit Jahr.
|
||||
|
||||
### Kontexte
|
||||
|
||||
| Datei | Haelt | Eigenheit |
|
||||
| Datei | Hält | Eigenheit |
|
||||
| --- | --- | --- |
|
||||
| `auth.tsx` | Benutzer, `isAdmin`, `login`, `logout`, `updateUser` | prueft beim Start das gespeicherte Token gegen `/auth/me` |
|
||||
| `theme.tsx` | hell/dunkel/System | Wahl in `localStorage`, Doppelklick loest sie wieder |
|
||||
| `navigation.tsx` | `currentView`, `goTo(view, { tab, leadId })` | erspart es, Callbacks durch vier Ebenen zu reichen; `leadId` oeffnet den Kontakt direkt |
|
||||
| `whisper.tsx` | Modellstatus, Worker, Fortschritt, `transcribe` | ein Worker fuer die ganze App |
|
||||
| `auth.tsx` | Benutzer, `isAdmin`, `login`, `logout`, `updateUser` | prüft beim Start das gespeicherte Token gegen `/auth/me` |
|
||||
| `theme.tsx` | hell/dunkel/System | Wahl in `localStorage`, Doppelklick löst sie wieder |
|
||||
| `navigation.tsx` | `currentView`, `goTo(view, { tab, leadId })` | erspart es, Callbacks durch vier Ebenen zu reichen; `leadId` öffnet den Kontakt direkt |
|
||||
| `whisper.tsx` | Modellstatus, Worker, Fortschritt, `transcribe` | ein Worker für die ganze App |
|
||||
|
||||
### Whisper
|
||||
|
||||
| Datei | Inhalt |
|
||||
| --- | --- |
|
||||
| `whisper.tsx` | Konstanten (`WHISPER_MODEL`, `WHISPER_DTYPE`, `WHISPER_LANGUAGE`), Provider, Fristen, Haenger-Erkennung |
|
||||
| `whisper.worker.ts` | `WorkerRequest`/`WorkerResponse`, laedt und erkennt |
|
||||
| `whisperModels.ts` | `fetchModelSize()` holt die echte Groesse von Hugging Face |
|
||||
| `whisper.tsx` | Konstanten (`WHISPER_MODEL`, `WHISPER_DTYPE`, `WHISPER_LANGUAGE`), Provider, Fristen, Hänger-Erkennung |
|
||||
| `whisper.worker.ts` | `WorkerRequest`/`WorkerResponse`, lädt und erkennt |
|
||||
| `whisperModels.ts` | `fetchModelSize()` holt die echte Größe von Hugging Face |
|
||||
| `audio.ts` | `toWhisperAudio()` - Blob zu 16-kHz-Mono-PCM |
|
||||
| `dictation.tsx` | `useRecorder()` - Mikrofon, Dauer, Abbruch |
|
||||
|
||||
@@ -242,12 +242,12 @@ Warum genau diese Konstanten festgelegt sind, steht in
|
||||
| --- | --- |
|
||||
| `components/leads/leadUi.tsx` | `inputClass`, `labelClass`, `StatusBadge`, `Avatar`, Statusfarben |
|
||||
| `components/leads/mailPool.ts` | `findMailPoolMatch()`, `diffFields()`, `isConflict()`, `MAIL_POOL_FIELDS` - Dublettenabgleich des Vorbereichs |
|
||||
| `components/leads/MailPoolLeads.tsx` | Ansicht „Aus Mails": Goldmarkierung, Abweichungen, Uebernehmen, Sammeluebernahme |
|
||||
| `components/leads/MailPoolMergeDialog.tsx` | Feldauswahl beim Ergaenzen eines vorhandenen Kontakts |
|
||||
| `components/leads/MailPoolLeads.tsx` | Ansicht „Aus Mails": Goldmarkierung, Abweichungen, Übernehmen, Sammelübernahme |
|
||||
| `components/leads/MailPoolMergeDialog.tsx` | Feldauswahl beim Ergänzen eines vorhandenen Kontakts |
|
||||
| `components/scanner/scannerUi.tsx` | `cardClass`, Buttonklassen, `MatchBadge`, `MatchStatusBadge`, `PortalStatusPill`, `formatDateTime`, `projectTimestamp` |
|
||||
| `components/mail/mailUi.tsx` | `Badge`, `ConfidenceBar`, Herkunfts- und Statusbeschriftungen, `formatAddresses`, `avatarColor` |
|
||||
| `components/mail/MailBody.tsx` | Mailinhalt im `<iframe sandbox="">`, externe Bilder per CSP gesperrt |
|
||||
| `components/ui/Modal.tsx` | `Modal` (Escape schliesst) und `ConfirmDialog` |
|
||||
| `components/ui/Modal.tsx` | `Modal` (Escape schließt) und `ConfirmDialog` |
|
||||
| `components/Toast.tsx` | `useToast().addToast(text, kind)` mit `success`, `error` oder `info` |
|
||||
| `components/ui/DictationField.tsx` | Mikrofon-Knopf, Hinweisbox, Textfeld mit Diktat |
|
||||
| `lib/imageCompress.ts` | `compressImage()` auf 256 px, WebP → JPEG → PNG |
|
||||
@@ -263,7 +263,7 @@ nicht auseinanderlaufen:
|
||||
| `MAIL_LEAD_SOURCE`, `LeadOriginFilter` | Herkunft „Mail" in `Lead.source` und ihr Filter |
|
||||
| `USER_COLORS`, `userColor()` | Farbe gesetzt, sonst stabil aus der Benutzer-Id |
|
||||
| `opportunityVolume()`, `weightedVolume()`, `formatEuro()` | Tagessatz x Personentage, gewichtet, formatiert |
|
||||
| `OPPORTUNITY_STAGES`, `OPEN_STAGES`, `LEAD_STATUSES`, `CONNECTION_TYPES` | die Aufzaehlungen der Datenbank |
|
||||
| `OPPORTUNITY_STAGES`, `OPEN_STAGES`, `LEAD_STATUSES`, `CONNECTION_TYPES` | die Aufzählungen der Datenbank |
|
||||
| `eventColor()`, `leadFullName()`, `leadInitials()`, `contactFullName()` | Anzeigelogik |
|
||||
|
||||
## Datenfluss im Frontend
|
||||
@@ -278,16 +278,16 @@ flowchart TD
|
||||
F -->|"still, ohne Ladeanzeige"| C
|
||||
|
||||
G["Scanner-Ansichten"] -->|"eigener 30-s-Takt"| H["/api/scanner"]
|
||||
I["Mailansicht"] -->|"blaettert, sucht, filtert"| J["/api/mail"]
|
||||
I["Mailansicht"] -->|"blättert, sucht, filtert"| J["/api/mail"]
|
||||
D --> I
|
||||
I -->|"nach Uebernahme eines Vorschlags"| F
|
||||
I -->|"nach Übernahme eines Vorschlags"| F
|
||||
```
|
||||
|
||||
Die Mailansicht laedt ihre Mails selbst, bekommt Kontakte und Opportunities aber
|
||||
Die Mailansicht lädt ihre Mails selbst, bekommt Kontakte und Opportunities aber
|
||||
als Props - der Zuordnungsdialog braucht beide Listen zum Vorschlagen. Nach einer
|
||||
Kontaktuebernahme ruft sie `reload()`, damit der neue Kontakt in der Kontaktliste
|
||||
Kontaktübernahme ruft sie `reload()`, damit der neue Kontakt in der Kontaktliste
|
||||
der App auftaucht.
|
||||
|
||||
Die fuenf CRM-Endpunkte laufen **parallel**, nicht nacheinander - sonst addieren
|
||||
sich fuenf Rundreisen beim Anmelden. Die Scanner-Ansichten haengen bewusst nicht
|
||||
Die fünf CRM-Endpunkte laufen **parallel**, nicht nacheinander - sonst addieren
|
||||
sich fünf Rundreisen beim Anmelden. Die Scanner-Ansichten hängen bewusst nicht
|
||||
an `loadData`: andere Datenbank, andere Auffrischung.
|
||||
|
||||
Reference in New Issue
Block a user