Files
ICON-CRM/docs/06-schnittstellen.md
T

6.3 KiB
Raw Blame History

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 = 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.

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.