From 19cc1ac9f64e806286e563907f25d7031145968f Mon Sep 17 00:00:00 2001 From: Sascha Date: Mon, 14 Sep 2026 15:34:14 +0200 Subject: [PATCH] Dokumentation: docs/06-schnittstellen.md --- docs/06-schnittstellen.md | 138 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 docs/06-schnittstellen.md diff --git a/docs/06-schnittstellen.md b/docs/06-schnittstellen.md new file mode 100644 index 0000000..62c5377 --- /dev/null +++ b/docs/06-schnittstellen.md @@ -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
icon-experts.de"] + W -->|"/"| S["Website (dist)"] + W -->|"/api/public
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": "

…

", + "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
icrm.icon-experts.de"] -->|"GET /api/integrations/contacts
Bearer-Token"| P["perinco CRM
pcrm.perinco.de"] + P -->|"GET /api/integrations/contacts
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*.