Dokumentation: docs/06-schnittstellen.md
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# 06 Schnittstellen
|
||||
|
||||
Zwei fremde Systeme hängen am CRM: die **ICON Website** liest die Projektbörse,
|
||||
und das **perinco CRM** tauscht Kontakte in beide Richtungen aus.
|
||||
|
||||
## ICON Website
|
||||
|
||||
Seit dem 14.09.2026 liegt die Website auf demselben Server wie das CRM
|
||||
(`/var/www/icon_website/dist`, Vhost `icon-experts.de` + `www`). Vorher lief sie
|
||||
auf `212.227.135.41` – derselben Maschine wie perinco CRM, n8n, Docker und
|
||||
Supabase.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
V(("Besucher")) --> W["Apache<br/>icon-experts.de"]
|
||||
W -->|"/"| S["Website (dist)"]
|
||||
W -->|"/api/public<br/>ProxyPass"| E["Express :5007"]
|
||||
E --> DB[("icon_CRM")]
|
||||
```
|
||||
|
||||
Der Vhost reicht **`/api/public` an das CRM-Backend durch**. Das ist kein
|
||||
Schönheitsfehler, sondern der Grund, warum die Content-Security-Policy der Seite
|
||||
weiter mit `connect-src 'self'` auskommt: der Aufruf ist gleicher Herkunft.
|
||||
Zeigte die Seite direkt auf `icrm.icon-experts.de`, müsste dieser Host in der CSP
|
||||
stehen und der Browser bräuchte einen CORS-Vorabruf.
|
||||
|
||||
### Der öffentliche Endpunkt
|
||||
|
||||
`GET /api/public/tenders` – ohne Anmeldung, nur Lesen, neueste zuerst. Dazu
|
||||
`GET /api/public/tenders/:id` für einen direkten Link. Beide antworten mit
|
||||
`Access-Control-Allow-Origin: *` und `Cache-Control: public, max-age=60`.
|
||||
|
||||
```json
|
||||
[{ "id": 8, "title": "…", "status": "Aktiv", "notes": "<h2>…</h2>",
|
||||
"created_at": "…", "updated_at": "…" }]
|
||||
```
|
||||
|
||||
**Nicht** ausgeliefert werden Kunde, zugewiesene Experten, Quelle, Fristen und
|
||||
Phase. Das ist der eigentliche Gewinn gegenüber dem vorherigen Weg: die Website
|
||||
las mit dem Supabase-Anon-Key direkt in der Tabelle und durfte `select('*')` –
|
||||
der Schlüssel lag dafür im Browser-Bundle.
|
||||
|
||||
> **Zwei Bedingungen, nicht eine:** `online = 1` **und** `status <> 'Archiviert'`.
|
||||
> Im Altbestand steht eine archivierte Ausschreibung, deren Schalter noch an ist –
|
||||
> über Supabase war sie unsichtbar, über diesen Endpunkt wäre sie mit der
|
||||
> Kategorie „Archiviert" auf der Website aufgetaucht.
|
||||
|
||||
Auf Seiten der Website hängt das an `src/lib/iconCrm.js`; `src/pages/Experts.jsx`
|
||||
ruft nur noch `fetchTenders()` auf. `@supabase/supabase-js` wird dort seither
|
||||
nirgends mehr benutzt.
|
||||
|
||||
**Der Schalter `online` in der Liste der Ausschreibungen entscheidet also
|
||||
unmittelbar, was auf der Website steht** – einen Klick entfernt, ohne Umweg.
|
||||
|
||||
## perinco CRM, in beide Richtungen
|
||||
|
||||
*Kontakte → Häkchen „perinco".* Die Kontakte des perinco CRM werden zusätzlich
|
||||
angezeigt – **grün umrandet**, rein lesend, und **nichts davon wird
|
||||
gespeichert**. Ein nächtlicher Abgleich in die eigene Datenbank wäre der falsche
|
||||
Weg: dann stünde derselbe Mensch in zwei Systemen, und irgendwann wäre zu
|
||||
klären, welcher Eintrag gilt.
|
||||
|
||||
Dasselbe gibt es spiegelbildlich drüben: im perinco CRM erscheinen die
|
||||
ICON-Kontakte hinter dem Häkchen „ICON", **gold** umrandet.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
I["ICON CRM<br/>icrm.icon-experts.de"] -->|"GET /api/integrations/contacts<br/>Bearer-Token"| P["perinco CRM<br/>pcrm.perinco.de"]
|
||||
P -->|"GET /api/integrations/contacts<br/>Bearer-Token"| I
|
||||
```
|
||||
|
||||
| Richtung | Wer fragt | Wer antwortet | Token |
|
||||
| --- | --- | --- | --- |
|
||||
| ICON liest perinco | `server/src/routes/perinco.js` | drüben `server/src/routes/integrations.js` | hier `PERINCO_API_TOKEN` (+ `PERINCO_API_URL`), drüben `ICON_API_TOKEN` |
|
||||
| perinco liest ICON | drüben `server/src/routes/iconCrm.js` | hier `server/src/routes/integrations.js` | hier `PERINCO_INBOUND_TOKEN`, drüben der gleiche Wert |
|
||||
|
||||
Mitgeschickt wird der Token als `Authorization: Bearer …`.
|
||||
|
||||
Beide Router arbeiten mit **festem Token statt JWT** – der Aufrufer ist ein
|
||||
Backend, kein angemeldeter Mensch – und verglichen wird mit
|
||||
`crypto.timingSafeEqual`. Beide kennen ausschließlich `GET`; einen Schreibweg
|
||||
gibt es nicht, auch später nicht.
|
||||
|
||||
Fehlt einer der Werte, bleibt das Häkchen unsichtbar und beide Systeme laufen
|
||||
normal weiter. Ein Knopf, der nur eine Fehlermeldung erzeugt, ist keiner.
|
||||
|
||||
### Wer weitergegeben wird
|
||||
|
||||
**Ein Häkchen am einzelnen Datensatz entscheidet.** Im perinco CRM trägt jeder
|
||||
Kontakt das Feld *„Im ICON CRM anzeigen"* (`crm_leads.icon`); nur wer es gesetzt
|
||||
hat, taucht hier auf. Beim Anlegen der Spalte wurden alle vorhandenen Kontakte
|
||||
auf gesetzt gestellt – sie waren bis dahin sichtbar, und ein neues Feld darf
|
||||
nichts verschwinden lassen. **Neue Kontakte starten ungesetzt.**
|
||||
|
||||
Kontakte im Vorbereich „Aus Mails" (`mail_pool = 1`) bleiben außen vor:
|
||||
unvollständig und teils Dubletten.
|
||||
|
||||
### Feldabbildung
|
||||
|
||||
Abgebildet wird auf die **vorhandenen** Felder; neue wurden ausdrücklich keine
|
||||
angelegt:
|
||||
|
||||
| perinco `crm_leads` | ICON |
|
||||
| --- | --- |
|
||||
| `first_name`, `last_name`, `position` | gleichnamig |
|
||||
| `company` | wird als Unternehmensname angezeigt |
|
||||
| `email`, `phone` bzw. `mobile` | `email`, `phone` |
|
||||
| `linkedin`, `avatar`, `tags` | gleichnamig |
|
||||
| `kind` (Recruiter/Partner/Endkunde) | `kind` – **unverändert**, nicht auf die ICON-Arten umgebogen |
|
||||
| `source` | `lead_source` |
|
||||
| `notes` | `bio` |
|
||||
|
||||
Bleiben drüben, weil ICON bei Kontakten kein Gegenstück hat: Anrede, Website,
|
||||
Anschrift. Ebenso Zuständiger und Ersteller – das sind Benutzer *jenes* Systems,
|
||||
deren Kennungen hier nichts bedeuten.
|
||||
|
||||
Die Kennung bekommt ein Präfix (`perinco-31`), damit sie nie mit einer
|
||||
ICON-Kennung verwechselt wird. Sie ist eine Zeichenkette und gehört nicht in die
|
||||
Verweise der erweiterten Suche.
|
||||
|
||||
### Wo sie auftauchen
|
||||
|
||||
* **Kontaktliste und Kacheln** – grün umrandet, mit Herkunftsmerkmal
|
||||
* **Netzwerkansicht** – grün umrandete Kreise, über eine fein gestrichelte grüne
|
||||
Kante an ihr Unternehmen gehängt. Die Verbindung läuft über den
|
||||
**Firmennamen**, denn drüben steht dort nur Text und keine Kennung; groß/klein
|
||||
wird ignoriert, damit „KfW" und „KFW" zusammenfinden. Passt kein Name, steht
|
||||
der Kontakt für sich – das ist ehrlicher als eine geratene Zuordnung.
|
||||
* **Erweiterte Suche** – sie zählen mit, werden aber klar als perinco-Kontakte
|
||||
benannt.
|
||||
|
||||
Verbinden lassen sie sich nicht, und ihre Seitenleiste bietet keine Karte an –
|
||||
gepflegt werden sie drüben.
|
||||
|
||||
> **Erfahrung aus dem Betrieb:** Ein zusätzlicher Filter in der Kontaktliste
|
||||
> kann die Liste still leeren und sieht dann aus wie Datenverlust. Deshalb sind
|
||||
> aktive Filter sichtbar ausgezeichnet, der Leerzustand benennt sie, und daneben
|
||||
> steht *Filter zurücksetzen*.
|
||||
Reference in New Issue
Block a user