# Telefonliste

Zentrale interne Telefonliste fuer IKK Kliniken — Live-Suche, Abteilungs-/Standort-Filter, klickbare Telefon- und Mail-Links, optional Active-Directory-Anbindung.

## Zweck

Ersetzt Excel-Listen und veraltete SharePoint-Seiten. Fuer alle internen Nutzer:
- Telefonnummer suchen und anrufen (Klick auf `tel:`-Link)
- Mail schreiben (Klick auf `mailto:`)
- Sammelnummern, Bereitschaftsdienste, externe Dienstleister pflegen
- Optional: Stammdaten aus dem AD automatisch ziehen

## Architektur

```
Browser (Single-Page SPA, Dark Cyan)
  ├── Live-Suche ab 3 Zeichen (Debounce 150 ms)
  ├── Filter: Abteilung, Standort
  ├── Bereiche-Ansicht (alle Kontakte gruppiert nach Abteilung, aufklappbar)
  ├── Lokale Einträge (manuell pflegbar)
  └── Admin: AD-Konfiguration, Statistik, Update

FastAPI (main.py, Port 8870)
  ├── Public: /api/kontakte/suche, /api/kontakte, /api/kontakte/bereiche, /api/kontakte/{id}, /api/abteilungen, /api/standorte
  ├── JWT-Auth fuer Schreibzugriff (ADMIN / REDAKTEUR)
  ├── AD-Konfig: /api/ad/konfiguration (PUT/GET), /api/ad/sync
  └── Auto-Update: /api/update/check, /api/update/install

SQLite (telefonliste.db, aiosqlite, WAL)
  ├── benutzer (3 Rollen)
  ├── kontakt (quelle=AD|LOKAL, ad_object_guid, stamm + ov_* Overrides)
  └── ad_konfiguration (single-row, aktivierbar per UI)
```

## Overlay-Schema

Ein Kontakt kann aus dem **AD** stammen (Stammdaten werden beim Sync ueberschrieben) oder **lokal** angelegt sein (frei editierbar). Fuer AD-Kontakte koennen lokale Overrides (`ov_mail`, `ov_telefon`, ...) gesetzt werden — diese ueberschreiben den AD-Wert in der Anzeige, bleiben aber beim Sync erhalten.

| Anzeige-Feld | Logik |
|---|---|
| E-Mail | `COALESCE(ov_mail, mail)` |
| Telefon | `COALESCE(ov_telefon, telefon)` |
| Mobil | `COALESCE(ov_mobil, mobil)` |
| Abteilung | `COALESCE(ov_abteilung, abteilung)` |
| Raum / Standort / Notiz | nur lokal |

AD-Nutzer werden beim Loeschen im AD nicht entfernt, sondern auf `ad_aktiv=0` gesetzt (ausgegraut im UI).

## Befehle

```bash
python main.py                  # Startet auf localhost:8870, öffnet Browser
pip install -r requirements.txt # Abhängigkeiten
python deploy_vps.py            # Deploy auf VPS (systemd, Nginx, DNS, SSL)
python deploy_downloads.py      # Download-Seite auf VPS deployen
```

## Installation

**Windows-Einzeiler:**
```powershell
irm https://downloads.c3po42.de/telefonliste/install.ps1 | iex
```

**Linux-Einzeiler:**
```bash
curl -fsSL https://downloads.c3po42.de/telefonliste/install.sh | bash
```

## Konfiguration

Umgebungsvariablen:

| Variable | Default | Zweck |
|---|---|---|
| `PORT` | 8870 | HTTP-Port |
| `DB_PATH` | `./telefonliste.db` | SQLite-Pfad |
| `JWT_SECRET` | dev-secret | Token-Signierung (produktiv setzen!) |
| `VPS` | – | bei `1` kein Browser-Autostart |

Die **AD-Konfiguration** wird im Admin-UI gesetzt (nicht via `.env`). So ist sie auf jedem Host sofort editierbar.

## Demo-Daten

Beim ersten Start werden ca. 70 Beispielkontakte aus `demo_kontakte.json` angelegt (IKK-typische Abteilungen: Radiologie, Chirurgie, Anaesthesie, Innere, Paediatrie, Labor, Physio, Stationen, Intensiv, Notaufnahme, Bereitschaften, IT, Verwaltung, Haustechnik, Kueche, Empfang, Seelsorge, Medizintechnik, Dialyse, Apotheke, etc.).

## Zugriffsmodell

Ab Version 1.4.0 ist die Telefonliste **public-by-default**: jeder unangemeldete Aufruf landet direkt in der Suche (Viewer-Modus, nur lesen). Eine Anmeldung ist nur noetig, um lokale Eintraege zu pflegen oder die AD-Konfig zu aendern.

- Public-Endpoints (kein Token): `/api/kontakte/suche`, `/api/kontakte`, `/api/kontakte/bereiche`, `/api/kontakte/{id}`, `/api/abteilungen`, `/api/standorte`
- Protected-Endpoints (Token Pflicht): alle POST/PUT/DELETE auf Kontakte, `/api/auth/logout`, `/api/ad/*`, `/api/admin/*`

### Login-API

`POST /api/auth/login` mit JSON `{"username": "...", "password": "..."}` liefert `{"access_token": "...", "user": {...}}`. Das Token gehoert als `Authorization: Bearer <token>` in alle geschuetzten Aufrufe. `POST /api/auth/logout` invalidiert das Token serverseitig (Token-Blacklist, in-memory). Rate-Limit: 5 Versuche pro IP+Username pro Minute → danach HTTP 429 mit `Retry-After: 60`.

## Initial-Login

Beim allerersten Start generiert die Telefonliste fuer `admin` und `redakteur` **zufaellige Passwoerter** und schreibt sie nach `initial_credentials.txt` neben der DB (Rechte 600). Diese Datei muss nach erstmaliger Anmeldung geloescht werden, und das Passwort sollte per Admin-UI geaendert werden.

Ein `viewer`-Benutzer entfaellt — unangemeldet = Viewer. Die Rolle `VIEWER` bleibt in der DB (CHECK-Constraint) fuer spaetere Erweiterungen.

## AD-Anbindung

Der AD-Sync ist ab Version 1.1.0 produktiv. `ldap3` verbindet sich via LDAPS gegen den DC, liest `objectCategory=person` aus der Base-OU und upserted per stabiler `objectGUID`.

### Konfiguration (Admin-UI)

| Feld | Beispiel |
|---|---|
| LDAP-Server | `dc01.ikk.local` |
| Port | `636` (LDAPS) |
| TLS | an |
| Bind-DN | `CN=Telefonliste,OU=ServiceAccounts,DC=ikk,DC=local` |
| Bind-Passwort | Service-Account-PW |
| Base-OU | `OU=Mitarbeiter,DC=ikk,DC=local` |
| Sync-Intervall | `30` Minuten |

### Ablauf pro Sync

1. Paged Search (500 Eintraege/Seite) ueber `(&(objectCategory=person)(objectClass=user)(!(objectClass=computer)))`
2. Upsert auf `ad_object_guid` — neue Benutzer werden angelegt (mit `standort`/`raum` aus `physicalDeliveryOfficeName` falls leer), bekannte aktualisiert
3. AD-Stammdaten landen in `ad_*` Spalten — lokale Overrides in `ov_*` bleiben erhalten und ueberschreiben in der Anzeige
4. Soft-Delete: Benutzer, die in der Base-OU nicht mehr auftauchen oder via `userAccountControl` Bit `0x0002` deaktiviert sind, bekommen `ad_aktiv=0` (ausgegraut im UI, nicht geloescht)
5. Ergebnis wird in `letzter_sync` / `letzter_status` / `letzter_fehler` geschrieben und per SSE ans Frontend gepusht

### Endpunkte

- `PUT /api/ad/konfiguration` — Konfiguration speichern (weckt den Scheduler sofort auf)
- `POST /api/ad/test` — Bind-Test ohne Sync, liefert gefundene Benutzer-Anzahl
- `POST /api/ad/sync` — Einmaligen Sync ausloesen (synchron, liefert `angelegt/aktualisiert/gemerged/deaktiviert/gesamt`)
- `GET /api/ad/konfiguration` — Aktuelle Konfig + letzter Status
- `GET /api/admin/duplikate` — Kandidatenpaare (LOKAL, AD) fuer manuelles Zusammenfuehren
- `POST /api/admin/duplikate/merge` — Zusammenfuehren zweier Kontakte

### Scheduler

Beim Start legt `lifespan()` einen Background-Task `_ad_scheduler()` an. Dieser wartet auf `_sync_wakeup` (gesetzt nach jedem Config-Save) oder das konfigurierte Intervall, und ruft dann `run_ad_sync()` auf. Bei Deaktivierung schlaeft der Scheduler in 60s-Zyklen, bis jemand wieder aktiviert.

### Duplikate-Handling (LOKAL vs AD)

Zwei Mechanismen verhindern, dass derselbe Mitarbeiter doppelt in der Liste steht:

1. **Auto-Merge beim Sync (Mail-Match).** Beim AD-Sync wird vor jedem INSERT geprueft, ob bereits ein `quelle='LOKAL'`-Kontakt mit identischer Mail existiert. Wenn ja, wird der lokale Eintrag auf `quelle='AD'` gehoben, die `ad_object_guid` gesetzt und alle lokalen Werte, die vom AD-Wert abweichen, als `ov_*` Overrides gesichert (sofern noch nicht gesetzt). Vorteil: Notizen, Raum, Standort bleiben erhalten, es entsteht kein Doppler.
2. **Manuelle Dubletten-Ansicht.** `GET /api/admin/duplikate` findet Paare nach drei Kriterien (Score 100/70/60): gleiche Mail, gleicher Vor+Nachname, gleicher Anzeigename. Die Admin-UI zeigt beide Seiten nebeneinander, Admin entscheidet pro Paar: "Ignorieren" (nur ausblenden) oder "Zusammenfuehren" — bei Merge wird derselbe Override-Algorithmus wie beim Auto-Merge angewandt und der lokale Eintrag geloescht.

Der AD-Eintrag bleibt immer bestehen (wegen stabiler `objectGUID`); gemerged wird in Richtung AD.

## Nutzungsstatistik

Ab Version 1.3.0 wird pro Tag gezaehlt, wie oft die Telefonliste benutzt wird — ohne personenbezogene Daten. Tabelle `nutzung_tag(datum, typ, anzahl)`, Upsert per `ON CONFLICT`.

Gezaehlt werden zwei Ereignisse:

- `suche` — `GET /api/kontakte/suche` mit `q >= 3 Zeichen` **oder** gesetztem Filter (Abteilung/Standort). Das reine Initial-Laden der Liste zaehlt nicht.
- `detail` — `GET /api/kontakte/{id}` (Klick auf einen Treffer).

Der Endpoint `GET /api/admin/nutzung?tage=30` liefert eine lueckenlose Reihe (fehlende Tage = 0), Summe, Durchschnitt pro Tag und die heutigen Werte. Im Admin-Tab wird das als gestapeltes SVG-Balkendiagramm angezeigt.

## Auto-Update

Das Tool prueft beim Start gegen `https://downloads.c3po42.de/telefonliste/version.json` und blendet bei neuer Version einen Update-Banner ein. Installation via Klick (laedt `update.zip`, entpackt, startet systemd-Service neu).

## Ports / URL

- Lokal: `http://localhost:8870`
- Produktion: `https://telefon.c3po42.de`
- Downloads: `https://downloads.c3po42.de/telefonliste/`

## Konventionen

- Single-file Backend (`main.py`), single-file Frontend (`static/index.html`)
- aiosqlite mit WAL + Foreign Keys
- Echte Umlaute (aeoeue) in UI, ae/oe/ue in Code
- `VERSION` in `main.py` vor jedem Deploy hochzaehlen
- Auto-Update-Endpoints liegen **vor** dem StaticFiles-Mount
