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