diff --git a/docs/05-ki.md b/docs/05-ki.md
index e7e07b6..7dfe0ef 100644
--- a/docs/05-ki.md
+++ b/docs/05-ki.md
@@ -1,135 +1,155 @@
-# 05 KI
-
-Modell: **`gpt-5.6-luna`** (OpenAI), angesprochen über `chat/completions`.
-Schlüssel und Modell stehen ausschließlich in `server/.env` (chmod 600) – der
-Browser spricht nie direkt mit OpenAI, jede Anfrage läuft über das Backend
-(`server/src/ai.js`).
-
-Fehlt der Schlüssel, läuft das CRM normal weiter und blendet die KI-Knöpfe aus.
-`GET /api/ai/status` sagt der Oberfläche, ob etwas eingerichtet ist.
-
-> **Fallstrick:** `gpt-5.6-luna` akzeptiert **kein eigenes `temperature`** und
-> weist jeden Wert außer dem Standard mit HTTP 400 ab. `ai.js` schickt deshalb
-> keines. `response_format: {type:'json_object'}` funktioniert dagegen.
-
-Drei Stellen nutzen das Modell:
-
-```mermaid
-flowchart LR
- CV["CV-Import
Lebenslauf → Kontaktvorschlag"] --> AI["server/src/ai.js"]
- TX["Schreibhilfe
Ausschreibungstext"] --> AI
- SU["Erweiterte Suche
Frage in Worten"] --> AI
- AI -->|"HTTPS, Schluessel bleibt am Server"| OAI["api.openai.com"]
-```
-
-## CV-Import
-
-*Kontakte → CV-Import.* Lebensläufe als PDF, DOCX oder Text, per Dateiauswahl
-oder Ziehen, auch mehrere auf einmal (bis 20, je 15 MB).
-
-```mermaid
-flowchart LR
- U["Hochladen"] --> E["Eingang"]
- E -->|"Mensch: Ja, auslesen"| S["Screening"]
- E -->|"Mensch: Nein"| V["Verworfen"]
- S -->|"Formular bestaetigt"| A["Auswahl
Kontakt angelegt"]
- S -->|"abgelehnt"| V
-```
-
-| Ordner | Bedeutung |
-| --- | --- |
-| Eingang | hochgeladen; ein Mensch entscheidet je Datei „Ja, auslesen" oder „Nein" |
-| Screening | ausgelesen; der Vorschlag steht im Formular und wird bestätigt |
-| Auswahl | übernommen – Kontakt angelegt, Lebenslauf hängt daran |
-
-Es wird bewusst **nichts automatisch angelegt**: ein falsch gelesener Name oder
-Stundensatz wäre in der Datenbank schwerer zu finden als im Formular. Was dort
-korrigiert wird, gewinnt gegen die KI.
-
-Übernommen werden Name, Position, Kontaktdaten, LinkedIn, aktueller Arbeitgeber,
-Schlagworte, eine Zusammenfassung und – nur wenn sie ausdrücklich im Text stehen
-– die **Stundensätze**. Steht im Lebenslauf ein Tagessatz, wird er *nicht*
-umgerechnet, sondern weggelassen.
-
-Frühere Stationen werden als **beendete** Anstellungen angelegt; dadurch
-erscheint der Kontakt beim jeweiligen Unternehmen sofort im Zweig „Verlassen".
-Unbekannte Unternehmen legt der Import mit an.
-
-Der Text wird vor der Anfrage lokal aus der Datei geholt
-(`server/src/cvText.js`) – PDF und DOCX ohne Fremddienst. Die Datei bleibt am
-Kontakt (`icon_contacts.cv_file`) und lässt sich aus dessen Karte herunterladen;
-der Download läuft über `fetch` mit Bearer-Kopf, ein normaler Link schickt ihn
-nicht mit.
-
-## Schreibhilfe für Ausschreibungen
-
-Das Feld *Ausschreibungstext* (`icon_tenders.notes`) ist ein WYSIWYG-Editor mit
-fünf Knöpfen: *Text erzeugen*, *Verbessern*, *Kürzen*, *Stichpunkte*,
-*Anforderungsprofil*. Darunter ein Feld für die Vorgabe („Interim PL SAP
-S/4HANA, Bank, 12 Monate, 80 % remote").
-
-Der vorherige Stand lässt sich mit *Zurück* zurückholen – die KI überschreibt
-nichts unwiderruflich. Gespeichert wird 800 ms nach dem letzten Tastendruck, es
-gibt keinen Speichern-Knopf.
-
-Der Diktat-Knopf steht in derselben Leiste und ist **unabhängig** von der KI: er
-rechnet im Browser und braucht keinen Schlüssel (siehe [04 Diktat](04-diktat.md)).
-
-## Erweiterte Suche
-
-Ein Feld, zwei Modi – umgeschaltet über die Symbole rechts im Feld:
-
-**Lupe – die normale Suche** (`Strg+K`). Über Kontakte, Unternehmen,
-Ausschreibungen, Projekte, Leads und die Historie; läuft beim Tippen mit, ein
-Treffer öffnet direkt die Karte. Bewusst `LIKE` statt Volltextindex: die
-Datenmengen sind klein, und ein `FULLTEXT`-Index hülfe bei Teilwortsuche
-(„informat" soll „Finanz Informatik" finden) nicht.
-
-**Funke – die erweiterte Suche.** Eine Frage in Worten, beantwortet vom Modell.
-Die genannten Datensätze kommen als anklickbare Chips zurück.
-
-```mermaid
-sequenceDiagram
- actor B as Benutzer
- participant E as Express
- participant M as MariaDB
- participant O as OpenAI
-
- B->>E: POST /api/search/ai { question }
- E->>M: Bestand lesen (Kontakte, Unternehmen, Ausschreibungen, Projekte, Leads)
- E->>E: zusammenfassen, ab 400 Zeilen je Liste kuerzen
- E->>O: Systemprompt + Bestand + Frage
- O-->>E: { answer, refs }
- E-->>B: Antwort + anklickbare Chips
-```
-
-Sie läuft **nicht** beim Tippen mit, sondern erst auf Enter – jede Anfrage
-kostet. Und sie erzeugt **kein SQL**: der Bestand wird mitgegeben. Ein Modell,
-das Abfragen auf die Datenbank schreiben darf, wäre eine Angriffsfläche; der
-Bestand ist klein genug, um ihn vollständig mitzuliefern. Wird gekürzt, sagt die
-Antwort das dazu.
-
-### Was im Bestand steht
-
-Je Kontakt: Name, Position, Art, Status, E-Mail, Telefon, Quelle, Schlagworte,
-freiberuflich, Kontaktstand, **`rate_onsite` und `rate_remote`**, LinkedIn,
-Unternehmen und dessen Branche.
-
-Zwei Regeln stehen ausdrücklich im Systemprompt, weil die Antworten sonst falsch
-werden:
-
-* Ein fehlender Satz ist **nicht** null Euro. Verglichen wird nur, wo ein Wert
- steht; die Antwort sagt dazu, bei wie vielen keiner hinterlegt ist.
-* Es sind **Stundensätze**, keine Tagessätze. Fragt jemand nach dem Tagessatz,
- antwortet die Suche mit dem Stundensatz und sagt das dazu.
-
-Beispiel:
-
-> **Frage:** Wer hat den höchsten Tagessatz?
-> **Antwort:** Oliver Schaible hat mit 165,00 € pro Stunde den höchsten
-> hinterlegten Satz vor Ort. Im CRM sind Stundensätze, keine Tagessätze,
-> gepflegt. Bei 8 Kontakten ist kein Satz hinterlegt.
-
-Die Kontakte des perinco CRM stehen dem Bestand ebenfalls zur Verfügung, solange
-die Schnittstelle eingerichtet ist – gelesen über denselben Zwischenspeicher wie
-die Ansicht (`fetchPerincoContacts()`), und ausdrücklich als fremd gekennzeichnet.
+# 05 KI
+
+Modell: **`gpt-5.6-luna`** (OpenAI), angesprochen über `chat/completions`. Der
+Browser spricht nie direkt mit OpenAI, jede Anfrage läuft über das Backend
+(`server/src/ai.js`). Schlüssel und Modell werden unter *Einstellungen → KI*
+gepflegt und liegen in `icon_settings`; `server/.env` (chmod 600) bleibt
+Startwert und Rückfall.
+
+Fehlt der Schlüssel, läuft das CRM normal weiter und blendet die KI-Knöpfe aus.
+`GET /api/ai/status` sagt der Oberfläche, ob etwas eingerichtet ist.
+
+## Schlüssel pflegen
+
+*Einstellungen → KI* (nur Administratoren). Der Schlüssel steht in
+`icon_settings` (`openai_api_key`), die `.env` bleibt Startwert und Rückfall –
+**die Datenbank gewinnt**. Grund: ein Schlüssel läuft ab oder wird gewechselt,
+und dann soll niemand dafür eine Datei auf dem Server bearbeiten und einen
+Dienst neu starten müssen.
+
+| Was | Verhalten |
+| --- | --- |
+| Anzeige | nur verkürzt (`sk-proj…a1b2`) – vollständig verlässt der Schlüssel den Server nie |
+| Eingabe | Passwortfeld, schreibt nur; ein bestehender Wert wird ersetzt |
+| Entfernen | löscht den Wert in der Datenbank; steht in der `.env` einer, gilt wieder dieser |
+| Modell | frei setzbar, Vorgabe `gpt-5.6-luna` |
+| Test | ein Miniaufruf gegen das Modell – sagt, ob Schlüssel, Modellname und Kontingent zusammenpassen |
+
+In der Historie landet, **dass** jemand den Schlüssel gewechselt hat, nie der
+Wert. Ein stiller Wechsel wäre später nicht mehr nachvollziehbar.
+
+> **Fallstrick:** `gpt-5.6-luna` akzeptiert **kein eigenes `temperature`** und
+> weist jeden Wert außer dem Standard mit HTTP 400 ab. `ai.js` schickt deshalb
+> keines. `response_format: {type:'json_object'}` funktioniert dagegen.
+
+Drei Stellen nutzen das Modell:
+
+```mermaid
+flowchart LR
+ CV["CV-Import
Lebenslauf → Kontaktvorschlag"] --> AI["server/src/ai.js"]
+ TX["Schreibhilfe
Ausschreibungstext"] --> AI
+ SU["Erweiterte Suche
Frage in Worten"] --> AI
+ AI -->|"HTTPS, Schluessel bleibt am Server"| OAI["api.openai.com"]
+```
+
+## CV-Import
+
+*Kontakte → CV-Import.* Lebensläufe als PDF, DOCX oder Text, per Dateiauswahl
+oder Ziehen, auch mehrere auf einmal (bis 20, je 15 MB).
+
+```mermaid
+flowchart LR
+ U["Hochladen"] --> E["Eingang"]
+ E -->|"Mensch: Ja, auslesen"| S["Screening"]
+ E -->|"Mensch: Nein"| V["Verworfen"]
+ S -->|"Formular bestaetigt"| A["Auswahl
Kontakt angelegt"]
+ S -->|"abgelehnt"| V
+```
+
+| Ordner | Bedeutung |
+| --- | --- |
+| Eingang | hochgeladen; ein Mensch entscheidet je Datei „Ja, auslesen" oder „Nein" |
+| Screening | ausgelesen; der Vorschlag steht im Formular und wird bestätigt |
+| Auswahl | übernommen – Kontakt angelegt, Lebenslauf hängt daran |
+
+Es wird bewusst **nichts automatisch angelegt**: ein falsch gelesener Name oder
+Stundensatz wäre in der Datenbank schwerer zu finden als im Formular. Was dort
+korrigiert wird, gewinnt gegen die KI.
+
+Übernommen werden Name, Position, Kontaktdaten, LinkedIn, aktueller Arbeitgeber,
+Schlagworte, eine Zusammenfassung und – nur wenn sie ausdrücklich im Text stehen
+– die **Stundensätze**. Steht im Lebenslauf ein Tagessatz, wird er *nicht*
+umgerechnet, sondern weggelassen.
+
+Frühere Stationen werden als **beendete** Anstellungen angelegt; dadurch
+erscheint der Kontakt beim jeweiligen Unternehmen sofort im Zweig „Verlassen".
+Unbekannte Unternehmen legt der Import mit an.
+
+Der Text wird vor der Anfrage lokal aus der Datei geholt
+(`server/src/cvText.js`) – PDF und DOCX ohne Fremddienst. Die Datei bleibt am
+Kontakt (`icon_contacts.cv_file`) und lässt sich aus dessen Karte herunterladen;
+der Download läuft über `fetch` mit Bearer-Kopf, ein normaler Link schickt ihn
+nicht mit.
+
+## Schreibhilfe für Ausschreibungen
+
+Das Feld *Ausschreibungstext* (`icon_tenders.notes`) ist ein WYSIWYG-Editor mit
+fünf Knöpfen: *Text erzeugen*, *Verbessern*, *Kürzen*, *Stichpunkte*,
+*Anforderungsprofil*. Darunter ein Feld für die Vorgabe („Interim PL SAP
+S/4HANA, Bank, 12 Monate, 80 % remote").
+
+Der vorherige Stand lässt sich mit *Zurück* zurückholen – die KI überschreibt
+nichts unwiderruflich. Gespeichert wird 800 ms nach dem letzten Tastendruck, es
+gibt keinen Speichern-Knopf.
+
+Der Diktat-Knopf steht in derselben Leiste und ist **unabhängig** von der KI: er
+rechnet im Browser und braucht keinen Schlüssel (siehe [04 Diktat](04-diktat.md)).
+
+## Erweiterte Suche
+
+Ein Feld, zwei Modi – umgeschaltet über die Symbole rechts im Feld:
+
+**Lupe – die normale Suche** (`Strg+K`). Über Kontakte, Unternehmen,
+Ausschreibungen, Projekte, Leads und die Historie; läuft beim Tippen mit, ein
+Treffer öffnet direkt die Karte. Bewusst `LIKE` statt Volltextindex: die
+Datenmengen sind klein, und ein `FULLTEXT`-Index hülfe bei Teilwortsuche
+(„informat" soll „Finanz Informatik" finden) nicht.
+
+**Funke – die erweiterte Suche.** Eine Frage in Worten, beantwortet vom Modell.
+Die genannten Datensätze kommen als anklickbare Chips zurück.
+
+```mermaid
+sequenceDiagram
+ actor B as Benutzer
+ participant E as Express
+ participant M as MariaDB
+ participant O as OpenAI
+
+ B->>E: POST /api/search/ai { question }
+ E->>M: Bestand lesen (Kontakte, Unternehmen, Ausschreibungen, Projekte, Leads)
+ E->>E: zusammenfassen, ab 400 Zeilen je Liste kuerzen
+ E->>O: Systemprompt + Bestand + Frage
+ O-->>E: { answer, refs }
+ E-->>B: Antwort + anklickbare Chips
+```
+
+Sie läuft **nicht** beim Tippen mit, sondern erst auf Enter – jede Anfrage
+kostet. Und sie erzeugt **kein SQL**: der Bestand wird mitgegeben. Ein Modell,
+das Abfragen auf die Datenbank schreiben darf, wäre eine Angriffsfläche; der
+Bestand ist klein genug, um ihn vollständig mitzuliefern. Wird gekürzt, sagt die
+Antwort das dazu.
+
+### Was im Bestand steht
+
+Je Kontakt: Name, Position, Art, Status, E-Mail, Telefon, Quelle, Schlagworte,
+freiberuflich, Kontaktstand, **`rate_onsite` und `rate_remote`**, LinkedIn,
+Unternehmen und dessen Branche.
+
+Zwei Regeln stehen ausdrücklich im Systemprompt, weil die Antworten sonst falsch
+werden:
+
+* Ein fehlender Satz ist **nicht** null Euro. Verglichen wird nur, wo ein Wert
+ steht; die Antwort sagt dazu, bei wie vielen keiner hinterlegt ist.
+* Es sind **Stundensätze**, keine Tagessätze. Fragt jemand nach dem Tagessatz,
+ antwortet die Suche mit dem Stundensatz und sagt das dazu.
+
+Beispiel:
+
+> **Frage:** Wer hat den höchsten Tagessatz?
+> **Antwort:** Oliver Schaible hat mit 165,00 € pro Stunde den höchsten
+> hinterlegten Satz vor Ort. Im CRM sind Stundensätze, keine Tagessätze,
+> gepflegt. Bei 8 Kontakten ist kein Satz hinterlegt.
+
+Die Kontakte des perinco CRM stehen dem Bestand ebenfalls zur Verfügung, solange
+die Schnittstelle eingerichtet ist – gelesen über denselben Zwischenspeicher wie
+die Ansicht (`fetchPerincoContacts()`), und ausdrücklich als fremd gekennzeichnet.