139 lines
6.3 KiB
Markdown
139 lines
6.3 KiB
Markdown
# 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*.
|