diff --git a/docs/01-architektur.md b/docs/01-architektur.md
index d9a71aa..3f4493f 100644
--- a/docs/01-architektur.md
+++ b/docs/01-architektur.md
@@ -1,132 +1,132 @@
-# 01 Architektur
-
-## Bausteine
-
-Drei Teile; keiner spricht direkt mit einem anderen außer über HTTP bzw. SQL:
-
-| Teil | Technik | Wo |
-| --- | --- | --- |
-| Frontend | React 19, TypeScript, Tailwind 4, Vite 6 | statisches Build in `/var/www/icon_CRM/dist` |
-| Backend | Express 4, mysql2, JWT, bcrypt | `/var/www/icon_CRM/server`, `127.0.0.1:5007` |
-| Datenbank | MariaDB | `icon_CRM` |
-
-Der Browser spricht **nie** direkt mit MariaDB – deshalb überhaupt ein Backend.
-Es bindet ausschließlich auf `127.0.0.1` und ist von außen nur über den
-Apache-Reverse-Proxy erreichbar.
-
-Das ersetzt den vorherigen Aufbau: dort lag der Supabase-Anon-Key im
-Browser-Bundle und hatte vollen Lesezugriff auf alle Tabellen, `icon_users`
-samt Passwort-Hashes eingeschlossen.
-
-## Topologie
-
-```mermaid
-flowchart LR
- U(("Browser")) -->|"HTTPS"| A["Apache :443
icrm.icon-experts.de"]
- A -->|"/"| D["dist/
SPA, FallbackResource"]
- A -->|"/api"| E["Express :5007"]
- E --> C[("icon_CRM")]
-
- W(("Besucher")) -->|"HTTPS"| B["Apache :443
icon-experts.de"]
- B -->|"/"| WD["icon_website/dist"]
- B -->|"/api/public"| E
-```
-
-Beide Vhosts liegen auf demselben Rechner. Die Website reicht **nur**
-`/api/public` durch – Anmeldung, Kontakte und Leads sind von dort aus
-unerreichbar. Der Umweg über den eigenen Vhost ist Absicht: so bleibt der
-Aufruf gleicher Herkunft und die Content-Security-Policy der Website kommt
-weiter mit `connect-src 'self'` aus.
-
-## Ausgehende Verbindungen
-
-Das Backend spricht von sich aus mit drei fremden Diensten. Alle sind optional –
-fehlen die Zugangsdaten oder ist das Ziel nicht erreichbar, läuft die Anwendung
-ohne den jeweiligen Teil weiter.
-
-| Ziel | Wofür | Ausgelöst von |
-| --- | --- | --- |
-| `api.openai.com` | CV-Auslesung, Schreibhilfe, erweiterte Suche | Benutzer, jeweils auf Klick |
-| `pcrm.perinco.de` | Kontakte des perinco CRM lesen | Schalter „perinco" in den Kontakten |
-| `linkedin.com`, `media.licdn.com` | Profilfoto zu einem Kontakt holen | Knopf im Kontaktformular |
-
-Das Diktat taucht in dieser Liste **nicht** auf: es lädt sein Modell direkt aus
-dem Browser vom Hugging-Face-CDN, das Backend ist daran nicht beteiligt (siehe
-[04 Diktat](04-diktat.md)).
-
-## Anfrageweg mit Anmeldung
-
-```mermaid
-sequenceDiagram
- actor B as Browser
- participant A as Apache
- participant E as Express
- participant M as MariaDB
-
- B->>A: POST /api/auth/login
- A->>E: weiterleiten
- E->>M: SELECT icon_users WHERE username OR email
- M-->>E: Zeile mit Hash
- E->>E: bcrypt.compare (SHA-256 des Altbestands wird dabei umgeschrieben)
- E-->>B: { token (JWT, 12 h), user }
- Note over B: Token im localStorage
-
- B->>A: GET /api/contacts
Authorization: Bearer ...
- A->>E: weiterleiten
- E->>E: requireAuth: JWT pruefen
- E->>M: SELECT icon_users WHERE id (noch aktiv?)
- E->>M: SELECT contacts JOIN companies
- M-->>E: Zeilen
- E-->>B: JSON
-```
-
-Bei `401` löscht das Frontend das Token und springt auf den Anmeldeschirm
-(`setUnauthorizedHandler` in `src/lib/api.ts`). Das Token wird bei jeder Anfrage
-neu gegen die Datenbank geprüft – ein deaktivierter Benutzer ist sofort draußen,
-ohne auf den Ablauf des Tokens zu warten.
-
-**Passwörter aus dem Altbestand:** im alten CRM lagen sie als ungesalzenes
-SHA-256. Diese Hashes sind an ihrer Form erkennbar (64 Hexziffern) und werden
-beim ersten erfolgreichen Login auf bcrypt umgeschrieben. Neu vergeben wird
-ausschließlich bcrypt.
-
-## Rollen
-
-```mermaid
-flowchart TD
- L["Anmeldung"] --> R{"Rolle"}
- R -->|admin| A["Dashboard, Leads, Kontakte, Unternehmen, Ausschreibungen,
Laufende Projekte, Historie
Einstellungen: Nutzerverwaltung, Stammdaten, Mein Konto, Diktat"]
- R -->|user| U["Dashboard, Leads, Kontakte, Unternehmen, Ausschreibungen,
Laufende Projekte, Historie
Einstellungen: Mein Konto, Diktat"]
-```
-
-Die Prüfung sitzt **doppelt**: das Menü blendet aus, was nicht erlaubt ist, und
-das Backend weist zusätzlich mit `403` ab (`requireAdmin`). Ausnahme ist
-`GET /api/users/directory` – die Benutzerliste brauchen alle Rollen für
-Erwähnungen, Zuständigkeiten und die Farben in der Historie. Passwort-Hashes
-liefert der Endpunkt nie.
-
-Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator
-degradiert, deaktiviert oder gelöscht wird.
-
-Der Reiter *Diktat* ist bewusst für jede Rolle sichtbar: jeder richtet die
-Spracherkennung auf seinem eigenen Gerät ein, mit seinem eigenen Download.
-
-## Belegte Ports
-
-| Port | Dienst |
-| --- | --- |
-| 5007 | ICON CRM Backend (nur `127.0.0.1`) |
-| 3101 | Vite-Dev-Server (nur lokal beim Entwickeln) |
-| 80/443 | Apache: `icrm.icon-experts.de`, `icon-experts.de`, `www.icon-experts.de` |
-
-## Was wo liegt
-
-```
-/var/www/icon_CRM/dist SPA (Build)
-/var/www/icon_CRM/server Backend samt .env (chmod 600)
-/var/www/icon_CRM/uploads hochgeladene Lebenslaeufe
-/var/www/icon_website/dist ICON Website
-/etc/systemd/system/icon-crm-backend.service
-/etc/apache2/sites-available/icrm.icon-experts.de*.conf
-/etc/apache2/sites-available/icon-experts.de*.conf
-```
+# 01 Architektur
+
+## Bausteine
+
+Drei Teile; keiner spricht direkt mit einem anderen außer über HTTP bzw. SQL:
+
+| Teil | Technik | Wo |
+| --- | --- | --- |
+| Frontend | React 19, TypeScript, Tailwind 4, Vite 6 | statisches Build in `/var/www/icon_CRM/dist` |
+| Backend | Express 4, mysql2, JWT, bcrypt | `/var/www/icon_CRM/server`, `127.0.0.1:5007` |
+| Datenbank | MariaDB | `icon_CRM` |
+
+Der Browser spricht **nie** direkt mit MariaDB – deshalb überhaupt ein Backend.
+Es bindet ausschließlich auf `127.0.0.1` und ist von außen nur über den
+Apache-Reverse-Proxy erreichbar.
+
+Das ersetzt den vorherigen Aufbau: dort lag der Supabase-Anon-Key im
+Browser-Bundle und hatte vollen Lesezugriff auf alle Tabellen, `icon_users`
+samt Passwort-Hashes eingeschlossen.
+
+## Topologie
+
+```mermaid
+flowchart LR
+ U(("Browser")) -->|"HTTPS"| A["Apache :443
icrm.icon-experts.de"]
+ A -->|"/"| D["dist/
SPA, FallbackResource"]
+ A -->|"/api"| E["Express :5007"]
+ E --> C[("icon_CRM")]
+
+ W(("Besucher")) -->|"HTTPS"| B["Apache :443
icon-experts.de"]
+ B -->|"/"| WD["icon_website/dist"]
+ B -->|"/api/public"| E
+```
+
+Beide Vhosts liegen auf demselben Rechner. Die Website reicht **nur**
+`/api/public` durch – Anmeldung, Kontakte und Leads sind von dort aus
+unerreichbar. Der Umweg über den eigenen Vhost ist Absicht: so bleibt der
+Aufruf gleicher Herkunft und die Content-Security-Policy der Website kommt
+weiter mit `connect-src 'self'` aus.
+
+## Ausgehende Verbindungen
+
+Das Backend spricht von sich aus mit drei fremden Diensten. Alle sind optional –
+fehlen die Zugangsdaten oder ist das Ziel nicht erreichbar, läuft die Anwendung
+ohne den jeweiligen Teil weiter.
+
+| Ziel | Wofür | Ausgelöst von |
+| --- | --- | --- |
+| `api.openai.com` | CV-Auslesung, Schreibhilfe, erweiterte Suche | Benutzer, jeweils auf Klick |
+| `pcrm.perinco.de` | Kontakte des perinco CRM lesen | Schalter „perinco" in den Kontakten |
+| `linkedin.com`, `media.licdn.com` | Profilfoto zu einem Kontakt holen | Knopf im Kontaktformular |
+
+Das Diktat taucht in dieser Liste **nicht** auf: es lädt sein Modell direkt aus
+dem Browser vom Hugging-Face-CDN, das Backend ist daran nicht beteiligt (siehe
+[04 Diktat](04-diktat.md)).
+
+## Anfrageweg mit Anmeldung
+
+```mermaid
+sequenceDiagram
+ actor B as Browser
+ participant A as Apache
+ participant E as Express
+ participant M as MariaDB
+
+ B->>A: POST /api/auth/login
+ A->>E: weiterleiten
+ E->>M: SELECT icon_users WHERE username OR email
+ M-->>E: Zeile mit Hash
+ E->>E: bcrypt.compare (SHA-256 des Altbestands wird dabei umgeschrieben)
+ E-->>B: { token (JWT, 12 h), user }
+ Note over B: Token im localStorage
+
+ B->>A: GET /api/contacts
Authorization: Bearer ...
+ A->>E: weiterleiten
+ E->>E: requireAuth: JWT pruefen
+ E->>M: SELECT icon_users WHERE id (noch aktiv?)
+ E->>M: SELECT contacts JOIN companies
+ M-->>E: Zeilen
+ E-->>B: JSON
+```
+
+Bei `401` löscht das Frontend das Token und springt auf den Anmeldeschirm
+(`setUnauthorizedHandler` in `src/lib/api.ts`). Das Token wird bei jeder Anfrage
+neu gegen die Datenbank geprüft – ein deaktivierter Benutzer ist sofort draußen,
+ohne auf den Ablauf des Tokens zu warten.
+
+**Passwörter aus dem Altbestand:** im alten CRM lagen sie als ungesalzenes
+SHA-256. Diese Hashes sind an ihrer Form erkennbar (64 Hexziffern) und werden
+beim ersten erfolgreichen Login auf bcrypt umgeschrieben. Neu vergeben wird
+ausschließlich bcrypt.
+
+## Rollen
+
+```mermaid
+flowchart TD
+ L["Anmeldung"] --> R{"Rolle"}
+ R -->|admin| A["Dashboard, Leads, Kontakte, Unternehmen, Ausschreibungen,
Laufende Projekte, Historie
Einstellungen: Nutzerverwaltung, Stammdaten, KI, Mein Konto, Diktat"]
+ R -->|user| U["Dashboard, Leads, Kontakte, Unternehmen, Ausschreibungen,
Laufende Projekte, Historie
Einstellungen: Mein Konto, Diktat"]
+```
+
+Die Prüfung sitzt **doppelt**: das Menü blendet aus, was nicht erlaubt ist, und
+das Backend weist zusätzlich mit `403` ab (`requireAdmin`). Ausnahme ist
+`GET /api/users/directory` – die Benutzerliste brauchen alle Rollen für
+Erwähnungen, Zuständigkeiten und die Farben in der Historie. Passwort-Hashes
+liefert der Endpunkt nie.
+
+Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator
+degradiert, deaktiviert oder gelöscht wird.
+
+Der Reiter *Diktat* ist bewusst für jede Rolle sichtbar: jeder richtet die
+Spracherkennung auf seinem eigenen Gerät ein, mit seinem eigenen Download.
+
+## Belegte Ports
+
+| Port | Dienst |
+| --- | --- |
+| 5007 | ICON CRM Backend (nur `127.0.0.1`) |
+| 3101 | Vite-Dev-Server (nur lokal beim Entwickeln) |
+| 80/443 | Apache: `icrm.icon-experts.de`, `icon-experts.de`, `www.icon-experts.de` |
+
+## Was wo liegt
+
+```
+/var/www/icon_CRM/dist SPA (Build)
+/var/www/icon_CRM/server Backend samt .env (chmod 600)
+/var/www/icon_CRM/uploads hochgeladene Lebenslaeufe
+/var/www/icon_website/dist ICON Website
+/etc/systemd/system/icon-crm-backend.service
+/etc/apache2/sites-available/icrm.icon-experts.de*.conf
+/etc/apache2/sites-available/icon-experts.de*.conf
+```