6.3 KiB
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.
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.
[{ "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 = 1undstatus <> '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.
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.