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:
Sascha Wolf
2026-08-11 09:45:58 +02:00
co-authored by Claude Opus 5
parent 8b7ce448a6
commit 7973dc07b1
13 changed files with 688 additions and 684 deletions
+49 -49
View File
@@ -1,27 +1,27 @@
# 08 Anleitung fuer Entwickler
# 08 Anleitung für Entwickler
Fuer jemanden, der das Projekt zum ersten Mal oeffnet. Was man braucht, wie man es
startet, wo man was aendert - und welche Fallen es gibt.
Für jemanden, der das Projekt zum ersten Mal öffnet. Was man braucht, wie man es
startet, wo man was ändert - und welche Fallen es gibt.
## In zwei Minuten zum Bild
* **Ein** Vite-Frontend (React 19, TypeScript, Tailwind 4), **ein** Express-Backend,
**zwei** MariaDB-Datenbanken.
* Kein Router im Frontend: `App.tsx` haelt die Ansicht in einem `useState`. Die
* Kein Router im Frontend: `App.tsx` hält die Ansicht in einem `useState`. Die
App liegt hinter einer Anmeldung, tiefe Links braucht sie nicht.
* Kein ORM im CRM-Teil - dort schreibt das Backend SQL mit `mysql2`. Der
Scanner-Teil benutzt Sequelize, weil er 1:1 aus einer anderen Anwendung
uebernommen wurde.
* Alles auf Deutsch: Bezeichner, Kommentare, Meldungen. Kommentare erklaeren das
übernommen wurde.
* Alles auf Deutsch: Bezeichner, Kommentare, Meldungen. Kommentare erklären das
**Warum**, nicht das Was.
## Voraussetzungen
| | Version |
| --- | --- |
| Node | 20 oder neuer (Server laeuft 20.20) |
| Node | 20 oder neuer (Server läuft 20.20) |
| MariaDB | 10.11 oder neuer, erreichbar mit Benutzer und Passwort |
| Browser | Chrome/Edge fuer WebGPU beim Diktat (sonst laeuft es auf der CPU) |
| Browser | Chrome/Edge für WebGPU beim Diktat (sonst läuft es auf der CPU) |
## Lokal starten
@@ -45,10 +45,10 @@ Was dabei von selbst passiert:
* Fehlt `ADMIN_PASSWORD`, wird beim ersten Start ein Zufallspasswort erzeugt und
**einmalig ins Log geschrieben**. Dort nachsehen, sonst kommt man nicht rein.
Ohne MariaDB startet das Backend nicht. Das Frontend laesst sich trotzdem
ansehen - siehe *Oberflaeche pruefen ohne Datenbank* weiter unten.
Ohne MariaDB startet das Backend nicht. Das Frontend lässt sich trotzdem
ansehen - siehe *Oberfläche prüfen ohne Datenbank* weiter unten.
## Pruefen
## Prüfen
```bash
npm run lint # = tsc --noEmit, das ist die Wahrheit
@@ -56,15 +56,15 @@ npm run build # muss ohne Fehler durchlaufen
cd server && npm test # Pruefungen der Postfachanbindung
```
Automatische Tests gibt es nur fuer **einen** Bereich: `server/test/mail.test.mjs`
prueft die Kontaktextraktion aus Mails und den Graph-Client. Beides laeuft
unbeaufsichtigt im Hintergrund und laesst sich von Hand kaum nachstellen - ein
Automatische Tests gibt es nur für **einen** Bereich: `server/test/mail.test.mjs`
prüft die Kontaktextraktion aus Mails und den Graph-Client. Beides läuft
unbeaufsichtigt im Hintergrund und lässt sich von Hand kaum nachstellen - ein
falsch erkannter Name landet still in der Kontaktliste, und ein 429 vom echten
Dienst kommt genau dann, wenn niemand hinsieht. Kein Test-Framework, reines Node,
keine Datenbank.
Fuer alles andere gilt: `tsc`, ein Build und ein Blick in die Oberflaeche - notfalls
mit dem Stub aus [Oberflaeche pruefen ohne Datenbank](#oberflaeche-pruefen-ohne-datenbank).
Für alles andere gilt: `tsc`, ein Build und ein Blick in die Oberfläche - notfalls
mit dem Stub aus [Oberfläche prüfen ohne Datenbank](#oberflaeche-pruefen-ohne-datenbank).
## Aufbau
@@ -86,7 +86,7 @@ docs/ diese Dokumentation
deploy/ Apache-Vhosts, systemd-Unit, Deployskript
```
## Wie eine Anfrage laeuft
## Wie eine Anfrage läuft
```mermaid
sequenceDiagram
@@ -109,25 +109,25 @@ sequenceDiagram
A-->>K: typisiertes Objekt
```
Jede Route liefert den **vollstaendigen, angereicherten Datensatz** zurueck (mit
Jede Route liefert den **vollständigen, angereicherten Datensatz** zurück (mit
`created_by_name`, `owner_name`, Ansprechpartnern, Teilnehmern). Das Frontend muss
danach nichts nachladen.
## Etwas hinzufuegen
## Etwas hinzufügen
### Neues Feld an einem Kontakt
1. `server/src/schema.sql`: Spalte mit `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`
ergaenzen - das Statement muss beim naechsten Start wiederholbar sein.
ergänzen - das Statement muss beim nächsten Start wiederholbar sein.
2. `server/src/routes/leads.js`: Feld in die Listen `TEXT_FIELDS` bzw. in
`INSERT`/`UPDATE` und in den `SELECT` aufnehmen.
3. `src/types.ts`: Feld ins `Lead`-Interface.
4. `src/components/leads/LeadModal.tsx`: Eingabefeld, `inputClass` benutzen.
5. Wenn es in Kachel oder Liste sichtbar sein soll: `LeadGrid.tsx`, `LeadList.tsx`.
### Neuer Menuepunkt
### Neuer Menüpunkt
1. `src/types.ts`: Wert in `ViewState` ergaenzen.
1. `src/types.ts`: Wert in `ViewState` ergänzen.
2. `src/components/Sidebar.tsx`: Eintrag in `CATEGORY_ITEMS` (Name + SVG-Pfad).
3. `src/App.tsx`: Komponente rendern und einen Untertitel setzen.
@@ -137,7 +137,7 @@ Kopfzeile. Wer eine abweichende Beschriftung braucht, setzt `label` am `MenuItem
### Neuer Reiter in den Einstellungen
1. `src/types.ts`: Wert in `SettingsTab`; nur fuer Admins zusaetzlich in
1. `src/types.ts`: Wert in `SettingsTab`; nur für Admins zusätzlich in
`ADMIN_SETTINGS_TABS`.
2. `src/components/settings/Settings.tsx`: Eintrag in `TABS` und Rendering.
@@ -147,24 +147,24 @@ Ein Sprung dorthin aus einer beliebigen Tiefe:
### Neuer Endpunkt
1. Router in `server/src/routes/` anlegen, `router.use(requireAuth)` nicht
vergessen, `requireAdmin` wo noetig.
2. In `server/src/server.js` einhaengen - **vor** dem `/api`-404-Fallback.
3. In `src/lib/api.ts` eine typisierte Funktion ergaenzen.
vergessen, `requireAdmin` wo nötig.
2. In `server/src/server.js` einhängen - **vor** dem `/api`-404-Fallback.
3. In `src/lib/api.ts` eine typisierte Funktion ergänzen.
### Neue Einstellung im CRM
Nur fuer Werte, die im Betrieb verstellbar sein sollen. **Zugangsdaten gehoeren
Nur für Werte, die im Betrieb verstellbar sein sollen. **Zugangsdaten gehören
nicht dazu** - die stehen in `server/.env` und werden beim Start gelesen, damit ein
Datenbank-Dump keine Geheimnisse enthaelt.
Datenbank-Dump keine Geheimnisse enthält.
1. Schluessel samt Vorgabe in das Vorgabenobjekt des Bereichs eintragen (Muster:
1. Schlüssel samt Vorgabe in das Vorgabenobjekt des Bereichs eintragen (Muster:
`MAIL_SETTING_DEFAULTS` in `server/src/mail/config.js`). Es ist gleichzeitig die
Positivliste des Speicher-Endpunkts.
2. Lesen mit `getSettings(defaults)` aus `server/src/settings.js`, schreiben mit
`setSetting()`. Zahlen und Schalter mit `asInt`/`asBool` wandeln.
3. Feld im passenden Einstellungsreiter ergaenzen.
3. Feld im passenden Einstellungsreiter ergänzen.
Die Tabelle `crm_settings` legt `schema.sql` an; ein neuer Schluessel braucht keine
Die Tabelle `crm_settings` legt `schema.sql` an; ein neuer Schlüssel braucht keine
Migration.
## Fallen
@@ -173,26 +173,26 @@ Diese Punkte haben real Zeit gekostet:
| Falle | Was passiert |
| --- | --- |
| **Reihenfolge in `server.js`** | Der 404-Fallback `app.use('/api', ...)` faengt alles, was danach registriert wird. Neue Router davor einhaengen. |
| **Reihenfolge in `server.js`** | Der 404-Fallback `app.use('/api', ...)` fängt alles, was danach registriert wird. Neue Router davor einhängen. |
| **`Content-Type` bei FormData** | `apiRequest` setzt JSON nur, wenn der Body **keine** `FormData` ist - sonst fehlt die multipart-Grenze und der Upload scheitert. |
| **Dateien in neuen Tabs** | Ein `<a href>` schickt kein Bearer-Token. Deshalb holt `openAgentCv()` das PDF per `fetch` und oeffnet einen Blob. |
| **Dateien in neuen Tabs** | Ein `<a href>` schickt kein Bearer-Token. Deshalb holt `openAgentCv()` das PDF per `fetch` und öffnet einen Blob. |
| **Leerzeichen in Pfaden** | Der alte Scanner liegt unter `/var/www/Projekt Scanner/…`. In der `.env` muss der Wert gequotet sein, sonst schneidet systemd ihn am Leerzeichen ab. |
| **`.env` ist gleichzeitig `EnvironmentFile`** | Also kein `export`, keine Shell-Ausdruecke, Werte mit Leerzeichen in Anfuehrungszeichen. |
| **Zeitstempel koennen nur vorziehen** | Ein „vorgealterter" `lastRunTime` verzoegert keinen Cronlauf. Wer verzoegern will, braucht eine eigene Frist. |
| **Sperrfristen bei jedem Start neu** | Frueher schob jedes Deployment den faelligen Scan um 30 Minuten - er fand nie statt. Deshalb liegt `last_scan_at` in der Datenbank. |
| **`.env` ist gleichzeitig `EnvironmentFile`** | Also kein `export`, keine Shell-Ausdrücke, Werte mit Leerzeichen in Anführungszeichen. |
| **Zeitstempel können nur vorziehen** | Ein „vorgealterter" `lastRunTime` verzögert keinen Cronlauf. Wer verzögern will, braucht eine eigene Frist. |
| **Sperrfristen bei jedem Start neu** | Früher schob jedes Deployment den fälligen Scan um 30 Minuten - er fand nie statt. Deshalb liegt `last_scan_at` in der Datenbank. |
| **`sequelize.sync({ alter: true })`** | Legt bei jedem Start Unique-Indizes neu an und sprengt irgendwann MySQLs Grenze von 64 Keys. Nur `sync()` benutzen. |
| **Kaskaden beim Loeschen** | Einen Suchagenten loeschen loescht alle seine Treffer mit. Inaktiv setzen ist meist gemeint. |
| **Doppelte Scanner-Laeufe** | Scheduler und *Jetzt scannen* teilen eine Sperre. Ohne sie zahlt man jede KI-Bewertung zweimal. |
| **`allow-same-origin` bei `srcDoc`** | Ein so eingebettetes Dokument erbt die Origin der Anwendung und kann `localStorage` lesen - also das Anmeldetoken. Fremdes HTML gehoert in `sandbox=""` ohne jedes Flag. Der Preis: die Inhaltshoehe ist nicht auslesbar, deshalb hat der Mailrahmen eine feste Hoehe. |
| **`/me` unter App-only** | Es gibt keinen Benutzer im Token, also auch kein `/me` - jeder Graph-Aufruf muss `/users/{postfach}/…` lauten. Genau daran ist das Altsystem gescheitert, still, weil die Antwort nicht geprueft wurde. |
| **Kaskaden beim Löschen** | Einen Suchagenten löschen löscht alle seine Treffer mit. Inaktiv setzen ist meist gemeint. |
| **Doppelte Scanner-Läufe** | Scheduler und *Jetzt scannen* teilen eine Sperre. Ohne sie zahlt man jede KI-Bewertung zweimal. |
| **`allow-same-origin` bei `srcDoc`** | Ein so eingebettetes Dokument erbt die Origin der Anwendung und kann `localStorage` lesen - also das Anmeldetoken. Fremdes HTML gehört in `sandbox=""` ohne jedes Flag. Der Preis: die Inhaltshöhe ist nicht auslesbar, deshalb hat der Mailrahmen eine feste Höhe. |
| **`/me` unter App-only** | Es gibt keinen Benutzer im Token, also auch kein `/me` - jeder Graph-Aufruf muss `/users/{postfach}/…` lauten. Genau daran ist das Altsystem gescheitert, still, weil die Antwort nicht geprüft wurde. |
| **UTC neben Wandzeit** | `crm_mail_messages.received_at` steht in UTC (so liefert Graph es), `crm_events.starts_at` in lokaler Wandzeit. Beim Lesen im Frontend `parseMailDate()` benutzen, sonst ist die Anzeige um Stunden verschoben. |
| **Aufzaehlungen in der Datenbank sind deutsch** | `crm_mail_links.origin` (`manuell`/`automatisch`), Vorschlagsstatus (`offen`, `uebernommen`, `verworfen`, `dublette`). Wer englische Werte schickt, bekommt einen Datenbankfehler statt einer Validierungsmeldung. |
| **Aufzählungen in der Datenbank sind deutsch** | `crm_mail_links.origin` (`manuell`/`automatisch`), Vorschlagsstatus (`offen`, `uebernommen`, `verworfen`, `dublette`). Wer englische Werte schickt, bekommt einen Datenbankfehler statt einer Validierungsmeldung. |
## Oberflaeche pruefen ohne Datenbank
## Oberfläche prüfen ohne Datenbank
Fuer Bildschirmfotos und Klickwege genuegt ein Stub: ein kleiner `node:http`-Server,
der `dist/` ausliefert und auf `/api/...` erfundene Daten zurueckgibt. Token vorab
setzen, dann ist die Anmeldung uebersprungen:
Für Bildschirmfotos und Klickwege genügt ein Stub: ein kleiner `node:http`-Server,
der `dist/` ausliefert und auf `/api/...` erfundene Daten zurückgibt. Token vorab
setzen, dann ist die Anmeldung übersprungen:
```js
await page.goto(BASE);
@@ -208,18 +208,18 @@ Browser-Download.
* **Deutsch** in Bezeichnern, Kommentaren, Meldungen. Fachbegriffe bleiben
englisch (`Match`, `Opportunity`, `Lead`).
* **Kommentare erklaeren das Warum.** Was der Code tut, steht im Code.
* **Kommentare erklären das Warum.** Was der Code tut, steht im Code.
* **Kein `dark:` im Markup.** Nur die Skalen `ink-*`, `brand-*`, `canvas`,
`surface` benutzen, den Rest macht `index.css`.
* **Gemeinsame Klassen** statt kopierter Utility-Ketten: `inputClass`,
`labelClass` aus `components/leads/leadUi.tsx`, dazu `cardClass`,
`primaryButtonClass` aus `components/scanner/scannerUi.tsx`.
* **Meldungen als Toast**, Loeschabfragen ueber `ConfirmDialog` - nie `alert()`
* **Meldungen als Toast**, Löschabfragen über `ConfirmDialog` - nie `alert()`
oder `confirm()`.
* **Rechte immer doppelt**: Oberflaeche blendet aus, Backend antwortet mit 403.
* **Rechte immer doppelt**: Oberfläche blendet aus, Backend antwortet mit 403.
## Deployment
Siehe [06 Betrieb](06-betrieb.md). Kurzform: lokal bauen, `deploy/deploy.ps1`
laedt `dist/` und `server/src` hoch und startet den Dienst neu. Auf dem Server
lädt `dist/` und `server/src` hoch und startet den Dienst neu. Auf dem Server
wird **nicht** gebaut - dort sind RAM und Swap knapp.