Anmeldung gilt 7 Tage statt 12 Stunden

JWT-Laufzeit (JWT_EXPIRES_IN) standardmaessig auf 7d gesetzt, damit
Benutzer nicht mehr taeglich neu anmelden muessen. Doku (Betrieb,
Benutzeranleitung, Glossar) und .env.example angepasst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 22:10:46 +02:00
co-authored by Claude Opus 5.5
parent cbac02a96a
commit 4adf48f336
5 changed files with 439 additions and 439 deletions
+159 -159
View File
@@ -1,159 +1,159 @@
# 07 Betrieb # 07 Betrieb
## Deployment ## Deployment
Gebaut wird **lokal**, auf den Server gehen nur `dist/` plus die Backend-Quellen. Gebaut wird **lokal**, auf den Server gehen nur `dist/` plus die Backend-Quellen.
```powershell ```powershell
$env:ICON_CRM_SSH_PASSWORD = '…' $env:ICON_CRM_SSH_PASSWORD = '…'
powershell -File deploy/deploy.ps1 powershell -File deploy/deploy.ps1
powershell -File deploy/deploy.ps1 -SkipBuild # ohne neuen Build powershell -File deploy/deploy.ps1 -SkipBuild # ohne neuen Build
powershell -File deploy/deploy.ps1 -Deps # bei neuen Backend-Paketen powershell -File deploy/deploy.ps1 -Deps # bei neuen Backend-Paketen
``` ```
Das Skript (`deploy/deploy.ps1`) macht in dieser Reihenfolge: Das Skript (`deploy/deploy.ps1`) macht in dieser Reihenfolge:
```mermaid ```mermaid
flowchart LR flowchart LR
B["npm run build"] --> U1["pscp dist/"] B["npm run build"] --> U1["pscp dist/"]
U1 --> U2["pscp server/src + package.json"] U1 --> U2["pscp server/src + package.json"]
U2 --> R["systemctl restart icon-crm-backend"] U2 --> R["systemctl restart icon-crm-backend"]
R --> C["curl /api/health"] R --> C["curl /api/health"]
``` ```
Schlägt der Build fehl, wird **nichts** hochgeladen – sonst stünde auf dem Schlägt der Build fehl, wird **nichts** hochgeladen – sonst stünde auf dem
Server ein halber Stand. Server ein halber Stand.
> **Falle aus der Praxis:** Wer `index.html` von Hand ändert und dann mit > **Falle aus der Praxis:** Wer `index.html` von Hand ändert und dann mit
> `-SkipBuild` hochlädt, spielt das alte `dist/` aus. Nach jeder Änderung an > `-SkipBuild` hochlädt, spielt das alte `dist/` aus. Nach jeder Änderung an
> `src/` oder `index.html` gilt: erst bauen, dann hochladen. > `src/` oder `index.html` gilt: erst bauen, dann hochladen.
Alte Build-Dateien werden beim Hochladen **nicht** entfernt – `dist/assets/` Alte Build-Dateien werden beim Hochladen **nicht** entfernt – `dist/assets/`
wächst mit jeder Auslieferung. Das ist Absicht: ein Browser mit offenem Tab holt wächst mit jeder Auslieferung. Das ist Absicht: ein Browser mit offenem Tab holt
sonst ein Bruchstück, das es nicht mehr gibt. Gelegentlich aufräumen schadet sonst ein Bruchstück, das es nicht mehr gibt. Gelegentlich aufräumen schadet
trotzdem nicht. trotzdem nicht.
## Dienste auf dem Server ## Dienste auf dem Server
| Was | Befehl | | Was | Befehl |
| --- | --- | | --- | --- |
| Zustand | `systemctl status icon-crm-backend` | | Zustand | `systemctl status icon-crm-backend` |
| Neustart | `systemctl restart icon-crm-backend` | | Neustart | `systemctl restart icon-crm-backend` |
| Log | `journalctl -u icon-crm-backend -n 100 --no-pager` | | Log | `journalctl -u icon-crm-backend -n 100 --no-pager` |
| Gesundheit | `curl -s http://127.0.0.1:5007/api/health` → `{"ok":true,"db":"up"}` | | Gesundheit | `curl -s http://127.0.0.1:5007/api/health` → `{"ok":true,"db":"up"}` |
| Apache | `systemctl reload apache2`, Logs unter `/var/log/apache2/icon_crm_*.log` | | Apache | `systemctl reload apache2`, Logs unter `/var/log/apache2/icon_crm_*.log` |
Die systemd-Unit begrenzt den Speicher auf 1 GB (`MemoryMax=1024M`). Das ist Die systemd-Unit begrenzt den Speicher auf 1 GB (`MemoryMax=1024M`). Das ist
kein Sparzwang, sondern ein Sicherheitsnetz: ein Leck im Backend soll nicht kein Sparzwang, sondern ein Sicherheitsnetz: ein Leck im Backend soll nicht
Apache und MariaDB mitreißen. Apache und MariaDB mitreißen.
## Umgebungsvariablen ## Umgebungsvariablen
`server/.env`, chmod 600, gehört **nicht** ins Repository. Vorlage: `server/.env`, chmod 600, gehört **nicht** ins Repository. Vorlage:
`server/.env.example`. `server/.env.example`.
| Variable | Bedeutung | | Variable | Bedeutung |
| --- | --- | | --- | --- |
| `PORT`, `HOST` | `5007` auf `127.0.0.1` – von außen nur über Apache | | `PORT`, `HOST` | `5007` auf `127.0.0.1` – von außen nur über Apache |
| `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` | MariaDB, Datenbank `icon_CRM` | | `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` | MariaDB, Datenbank `icon_CRM` |
| `JWT_SECRET`, `JWT_EXPIRES_IN` | Sitzungen, Vorgabe 12 h | | `JWT_SECRET`, `JWT_EXPIRES_IN` | Sitzungen, Vorgabe 7 Tage (`7d`) |
| `ADMIN_USERNAME`, `ADMIN_NAME`, `ADMIN_EMAIL`, `ADMIN_PASSWORD` | erster Administrator, **nur solange `icon_users` leer ist** | | `ADMIN_USERNAME`, `ADMIN_NAME`, `ADMIN_EMAIL`, `ADMIN_PASSWORD` | erster Administrator, **nur solange `icon_users` leer ist** |
| `CORS_ORIGINS` | erlaubte Herkunft für den lokalen Dev-Server | | `CORS_ORIGINS` | erlaubte Herkunft für den lokalen Dev-Server |
| `UPLOAD_DIR` | Ablage der Lebensläufe | | `UPLOAD_DIR` | Ablage der Lebensläufe |
| `OPENAI_API_KEY`, `OPENAI_MODEL` | KI; fehlt der Schlüssel, laufen die übrigen Teile weiter | | `OPENAI_API_KEY`, `OPENAI_MODEL` | KI; fehlt der Schlüssel, laufen die übrigen Teile weiter |
| `PERINCO_API_URL`, `PERINCO_API_TOKEN` | ICON liest perinco | | `PERINCO_API_URL`, `PERINCO_API_TOKEN` | ICON liest perinco |
| `PERINCO_INBOUND_TOKEN` | perinco liest ICON | | `PERINCO_INBOUND_TOKEN` | perinco liest ICON |
Ohne `ADMIN_PASSWORD` erzeugt das Backend eines und schreibt es **einmalig** ins Ohne `ADMIN_PASSWORD` erzeugt das Backend eines und schreibt es **einmalig** ins
Log. Danach ist das Passwort nur noch über die Nutzerverwaltung bzw. *Mein Log. Danach ist das Passwort nur noch über die Nutzerverwaltung bzw. *Mein
Konto* änderbar. Konto* änderbar.
## Erstaufsetzung (einmalig, bereits erledigt) ## Erstaufsetzung (einmalig, bereits erledigt)
```bash ```bash
apt-get install -y apache2 mariadb-server certbot python3-certbot-apache nodejs apt-get install -y apache2 mariadb-server certbot python3-certbot-apache nodejs
a2enmod proxy proxy_http rewrite headers expires a2enmod proxy proxy_http rewrite headers expires
mariadb -e "CREATE DATABASE icon_CRM CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mariadb -e "CREATE DATABASE icon_CRM CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mariadb -e "CREATE USER 'iconcrm'@'localhost' IDENTIFIED BY '…'; mariadb -e "CREATE USER 'iconcrm'@'localhost' IDENTIFIED BY '…';
GRANT ALL PRIVILEGES ON icon_CRM.* TO 'iconcrm'@'localhost';" GRANT ALL PRIVILEGES ON icon_CRM.* TO 'iconcrm'@'localhost';"
cd /var/www/icon_CRM/server && npm install --omit=dev cd /var/www/icon_CRM/server && npm install --omit=dev
cp deploy/icon-crm-backend.service /etc/systemd/system/ cp deploy/icon-crm-backend.service /etc/systemd/system/
systemctl daemon-reload && systemctl enable --now icon-crm-backend systemctl daemon-reload && systemctl enable --now icon-crm-backend
cp deploy/icrm.icon-experts.de.conf /etc/apache2/sites-available/ cp deploy/icrm.icon-experts.de.conf /etc/apache2/sites-available/
a2ensite icrm.icon-experts.de && systemctl reload apache2 a2ensite icrm.icon-experts.de && systemctl reload apache2
certbot --apache -d icrm.icon-experts.de certbot --apache -d icrm.icon-experts.de
``` ```
Das Schema legt das Backend beim Start selbst an (`schema.sql` + Das Schema legt das Backend beim Start selbst an (`schema.sql` +
Migrationen in `db.js`). Die Firewall (`ufw`) lässt 22, 80 und 443 durch; das Migrationen in `db.js`). Die Firewall (`ufw`) lässt 22, 80 und 443 durch; das
Zertifikat erneuert certbot selbst. Zertifikat erneuert certbot selbst.
## Sicherung ## Sicherung
Der Bestand steht vollständig in MariaDB; hochgeladene Lebensläufe liegen unter Der Bestand steht vollständig in MariaDB; hochgeladene Lebensläufe liegen unter
`/var/www/icon_CRM/uploads`. Beides gehört in die Sicherung: `/var/www/icon_CRM/uploads`. Beides gehört in die Sicherung:
```bash ```bash
mariadb-dump --single-transaction icon_CRM | gzip > icon_CRM-$(date +%F).sql.gz mariadb-dump --single-transaction icon_CRM | gzip > icon_CRM-$(date +%F).sql.gz
tar czf uploads-$(date +%F).tar.gz -C /var/www/icon_CRM uploads tar czf uploads-$(date +%F).tar.gz -C /var/www/icon_CRM uploads
``` ```
## Der alte Name crm.icon-experts.de ## Der alte Name crm.icon-experts.de
Das erste ICON CRM lief unter **crm.icon-experts.de** auf `212.227.135.41` Das erste ICON CRM lief unter **crm.icon-experts.de** auf `212.227.135.41`
(`/var/www/ICON`, Supabase als Datenbank). Seit dem 14.09.2026 zeigt der Name (`/var/www/ICON`, Supabase als Datenbank). Seit dem 14.09.2026 zeigt der Name
auf diesen Server und wird vom Vhost `icrm.icon-experts.de` als `ServerAlias` auf diesen Server und wird vom Vhost `icrm.icon-experts.de` als `ServerAlias`
mitbedient – dasselbe Zertifikat deckt beide Namen ab. Verschickte Links und mitbedient – dasselbe Zertifikat deckt beide Namen ab. Verschickte Links und
Lesezeichen laufen damit weiter, es gibt keinen zweiten Stand zu pflegen. Lesezeichen laufen damit weiter, es gibt keinen zweiten Stand zu pflegen.
Der alte Aufbau ist zurückgebaut: Vhosts (`icon`, `icon-le-ssl`) abgeschaltet, Der alte Aufbau ist zurückgebaut: Vhosts (`icon`, `icon-le-ssl`) abgeschaltet,
`/var/www/ICON` gelöscht, das dortige Zertifikat für `crm.icon-experts.de` `/var/www/ICON` gelöscht, das dortige Zertifikat für `crm.icon-experts.de`
entfernt (sonst hätte die Erneuerung ab sofort täglich Fehler gemeldet). entfernt (sonst hätte die Erneuerung ab sofort täglich Fehler gemeldet).
Gesichert ist er als `icon-crm-alt-2026-09-14.zip` – Quellcode, Build, `.env` Gesichert ist er als `icon-crm-alt-2026-09-14.zip` – Quellcode, Build, `.env`
und beide Vhost-Dateien, ohne `node_modules`. Die Datei liegt **auf beiden und beide Vhost-Dateien, ohne `node_modules`. Die Datei liegt **auf beiden
Servern** unter `/root/backups/`, Prüfsumme Servern** unter `/root/backups/`, Prüfsumme
`252f7fa3226c2df7c4020a0234c3095c`. `252f7fa3226c2df7c4020a0234c3095c`.
> Die Supabase-Instanz `icondb.perinco.de` läuft weiter. Sie war die Datenbank > Die Supabase-Instanz `icondb.perinco.de` läuft weiter. Sie war die Datenbank
> des alten CRM und ist die Quelle der Migration (`server/src/migrate.js`) – > des alten CRM und ist die Quelle der Migration (`server/src/migrate.js`) –
> abzuschalten wäre eine eigene Entscheidung, nicht Teil dieses Rückbaus. > abzuschalten wäre eine eigene Entscheidung, nicht Teil dieses Rückbaus.
## Störungssuche ## Störungssuche
| Symptom | Erster Griff | | Symptom | Erster Griff |
| --- | --- | | --- | --- |
| Weiße Seite nach dem Rollout | `index.html` darf nicht aus dem Cache kommen – der Vhost setzt dafür `no-cache`; im Zweifel Strg+F5 | | Weiße Seite nach dem Rollout | `index.html` darf nicht aus dem Cache kommen – der Vhost setzt dafür `no-cache`; im Zweifel Strg+F5 |
| 502 auf `/api` | Backend aus: `systemctl status icon-crm-backend`, dann `journalctl -u …` | | 502 auf `/api` | Backend aus: `systemctl status icon-crm-backend`, dann `journalctl -u …` |
| `{"db":"down"}` | MariaDB aus oder Zugangsdaten falsch | | `{"db":"down"}` | MariaDB aus oder Zugangsdaten falsch |
| Anmeldung schlägt fehl | `icon_users` prüfen: existiert der Benutzer, ist `is_active = 1`? | | Anmeldung schlägt fehl | `icon_users` prüfen: existiert der Benutzer, ist `is_active = 1`? |
| Zertifikatswarnung | `certbot certificates`; deckt das Zertifikat **alle** Namen ab, auch `www`? | | Zertifikatswarnung | `certbot certificates`; deckt das Zertifikat **alle** Namen ab, auch `www`? |
| Website zeigt alte Ausschreibungen | `curl -s https://icon-experts.de/api/public/tenders | head` – kommt das durch den Proxy? | | Website zeigt alte Ausschreibungen | `curl -s https://icon-experts.de/api/public/tenders | head` – kommt das durch den Proxy? |
| perinco-Kontakte fehlen | `GET /api/perinco/status`; Token beidseitig gesetzt? | | perinco-Kontakte fehlen | `GET /api/perinco/status`; Token beidseitig gesetzt? |
| Diktat lädt nicht | siehe [04 Diktat](04-diktat.md) – das ist eine Sache des Browsers, nicht des Servers | | Diktat lädt nicht | siehe [04 Diktat](04-diktat.md) – das ist eine Sache des Browsers, nicht des Servers |
### Wenn Daten zu fehlen scheinen ### Wenn Daten zu fehlen scheinen
Erst messen, dann handeln. Die drei Griffe, die in der Praxis geholfen haben: Erst messen, dann handeln. Die drei Griffe, die in der Praxis geholfen haben:
```bash ```bash
mysql icon_CRM -e "SELECT COUNT(*) FROM icon_contacts" # steht es in der Datenbank? mysql icon_CRM -e "SELECT COUNT(*) FROM icon_contacts" # steht es in der Datenbank?
curl -s .../api/contacts -H "Authorization: Bearer …" | wc -c # liefert die API es aus? curl -s .../api/contacts -H "Authorization: Bearer …" | wc -c # liefert die API es aus?
tail -n 50 /var/log/apache2/icon_crm_access.log # kam es beim Browser an? tail -n 50 /var/log/apache2/icon_crm_access.log # kam es beim Browser an?
``` ```
Stimmen alle drei, liegt es an der Ansicht – meist an einem aktiven Filter. Stimmen alle drei, liegt es an der Ansicht – meist an einem aktiven Filter.
### DNS-Umstellungen ### DNS-Umstellungen
Nach einem Wechsel des A-Records antworten fremde Resolver unterschiedlich Nach einem Wechsel des A-Records antworten fremde Resolver unterschiedlich
lange mit der alten Adresse. Zum Prüfen die **autoritativen** Nameserver und lange mit der alten Adresse. Zum Prüfen die **autoritativen** Nameserver und
mindestens einen öffentlichen Resolver befragen, nicht nur den eigenen: mindestens einen öffentlichen Resolver befragen, nicht nur den eigenen:
```bash ```bash
dig +short icon-experts.de @1.1.1.1 dig +short icon-experts.de @1.1.1.1
dig +short icon-experts.de @8.8.8.8 dig +short icon-experts.de @8.8.8.8
``` ```
Ein lokaler Resolver (PiHole, Router) kann den alten Wert deutlich länger Ein lokaler Resolver (PiHole, Router) kann den alten Wert deutlich länger
halten – das sieht dann aus wie ein Serverfehler und ist keiner. halten – das sieht dann aus wie ein Serverfehler und ist keiner.
+224 -224
View File
@@ -1,224 +1,224 @@
# 08 Anleitung für Benutzer # 08 Anleitung für Benutzer
Diese Anleitung setzt nichts voraus. Adresse: <https://icrm.icon-experts.de>. Diese Anleitung setzt nichts voraus. Adresse: <https://icrm.icon-experts.de>.
## Anmelden ## Anmelden
Benutzername oder E-Mail und Passwort. Die Anmeldung gilt 12 Stunden; danach Benutzername oder E-Mail und Passwort. Die Anmeldung gilt 7 Tage; danach
fragt das CRM erneut. Passwort ändern: *Einstellungen → Mein Konto*. fragt das CRM erneut. Passwort ändern: *Einstellungen → Mein Konto*.
Rechts oben stehen zwei Umschalter: die **Farbwelt** (ICON, Half-Life, Pixel Rechts oben stehen zwei Umschalter: die **Farbwelt** (ICON, Half-Life, Pixel
Art) und **hell/dunkel**. Beides hängt am Konto und gilt auf jedem Gerät. Ein Art) und **hell/dunkel**. Beides hängt am Konto und gilt auf jedem Gerät. Ein
Doppelklick auf den Hell/Dunkel-Knopf stellt „wie das System" wieder her. Doppelklick auf den Hell/Dunkel-Knopf stellt „wie das System" wieder her.
## Suchen ## Suchen
Oben in der Kopfleiste, oder mit `Strg+K`. Rechts im Feld zwei Symbole: Oben in der Kopfleiste, oder mit `Strg+K`. Rechts im Feld zwei Symbole:
* **Lupe** – die normale Suche. Läuft beim Tippen mit, durchsucht Kontakte, * **Lupe** – die normale Suche. Läuft beim Tippen mit, durchsucht Kontakte,
Unternehmen, Ausschreibungen, Projekte, Leads und die Historie. Ein Klick auf Unternehmen, Ausschreibungen, Projekte, Leads und die Historie. Ein Klick auf
einen Treffer öffnet dessen Karte. einen Treffer öffnet dessen Karte.
* **Funke** – die erweiterte Suche. Hier steht eine ganze Frage: * **Funke** – die erweiterte Suche. Hier steht eine ganze Frage:
*„Wer arbeitet alles bei der KfW?"*, *„Wer hat den höchsten Satz?"*. Antwort *„Wer arbeitet alles bei der KfW?"*, *„Wer hat den höchsten Satz?"*. Antwort
auf Enter, nicht beim Tippen. Die genannten Datensätze stehen darunter als auf Enter, nicht beim Tippen. Die genannten Datensätze stehen darunter als
anklickbare Schaltflächen. anklickbare Schaltflächen.
## Dashboard ## Dashboard
Zahlen zum Bestand und die letzten Tätigkeiten. Von hier führt jeder Eintrag in Zahlen zum Bestand und die letzten Tätigkeiten. Von hier führt jeder Eintrag in
die zugehörige Karte. die zugehörige Karte.
## Leads ## Leads
Die Pipeline als Kanban über acht Spalten, von **Backlog** bis **Absage**. Die Pipeline als Kanban über acht Spalten, von **Backlog** bis **Absage**.
Kacheln lassen sich mit der Maus in eine andere Spalte ziehen; der Wechsel landet Kacheln lassen sich mit der Maus in eine andere Spalte ziehen; der Wechsel landet
in der Historie. in der Historie.
Auf jeder Kachel: Auf jeder Kachel:
| Zeichen | Bedeutung | | Zeichen | Bedeutung |
| --- | --- | | --- | --- |
| Aktenschrank | archivieren – landet unter *Leads → Archiviert* und lässt sich von dort zurückholen | | Aktenschrank | archivieren – landet unter *Leads → Archiviert* und lässt sich von dort zurückholen |
| roter Mülleimer | löschen, mit Rückfrage | | roter Mülleimer | löschen, mit Rückfrage |
| Balken mit Prozentzahl | Stand der ToDo-Liste | | Balken mit Prozentzahl | Stand der ToDo-Liste |
Die Karte eines Leads hat drei Reiter: Die Karte eines Leads hat drei Reiter:
1. **Stammdaten** – Titel, Unternehmen, Ansprechpartner, Kontaktstand, 1. **Stammdaten** – Titel, Unternehmen, Ansprechpartner, Kontaktstand,
Zuständiger, Notizen und die Erinnerung. Zuständiger, Notizen und die Erinnerung.
2. **ToDos** – Checkliste mit Fälligkeitsdatum. Der Fortschritt steht als 2. **ToDos** – Checkliste mit Fälligkeitsdatum. Der Fortschritt steht als
Prozentwert auf der Kachel. Prozentwert auf der Kachel.
3. **Historie** – wer wann was geändert hat. 3. **Historie** – wer wann was geändert hat.
### Erinnerungen ### Erinnerungen
Zwei Arten, beide als Pop-up, das sich unabhängig von der geöffneten Ansicht Zwei Arten, beide als Pop-up, das sich unabhängig von der geöffneten Ansicht
meldet: meldet:
* **gesetzt** – „in 2 Tagen anrufen". Eingegeben werden **Tage**, nicht ein * **gesetzt** – „in 2 Tagen anrufen". Eingegeben werden **Tage**, nicht ein
Datum; gerechnet wird auf dem Server. Quittieren heißt *Erledigt* oder *in n Datum; gerechnet wird auf dem Server. Quittieren heißt *Erledigt* oder *in n
Tagen erneut*. Tagen erneut*.
* **automatisch** – wird ein Lead 30 Tage nicht angefasst, fragt das CRM, ob er * **automatisch** – wird ein Lead 30 Tage nicht angefasst, fragt das CRM, ob er
archiviert werden soll. Bleibt er stehen, läuft die Frist von vorn. archiviert werden soll. Bleibt er stehen, läuft die Frist von vorn.
## Kontakte ## Kontakte
Drei Ansichten, oben rechts umschaltbar: **Kacheln**, **Liste**, **Netzwerk**. Drei Ansichten, oben rechts umschaltbar: **Kacheln**, **Liste**, **Netzwerk**.
* In der **Liste** sortiert ein Klick auf eine Spaltenüberschrift; ein zweiter * In der **Liste** sortiert ein Klick auf eine Spaltenüberschrift; ein zweiter
Klick dreht die Richtung um. Leere Felder stehen immer am Ende. Klick dreht die Richtung um. Leere Felder stehen immer am Ende.
* Der Schalter **perinco** blendet zusätzlich die Kontakte des perinco CRM ein – * Der Schalter **perinco** blendet zusätzlich die Kontakte des perinco CRM ein –
grün umrandet, nur zum Lesen. grün umrandet, nur zum Lesen.
* Filter (Art, Status, Unternehmen …) sind sichtbar ausgezeichnet. Steht die * Filter (Art, Status, Unternehmen …) sind sichtbar ausgezeichnet. Steht die
Liste leer da, sagt der Text welche Filter aktiv sind; daneben liegt *Filter Liste leer da, sagt der Text welche Filter aktiv sind; daneben liegt *Filter
zurücksetzen*. zurücksetzen*.
Die Karte eines Kontakts hat Stammdaten, Werdegang, Gespräche und Historie. Die Karte eines Kontakts hat Stammdaten, Werdegang, Gespräche und Historie.
Im Reiter **Werdegang & Interviews** lässt sich jede Station bearbeiten: Im Reiter **Werdegang & Interviews** lässt sich jede Station bearbeiten:
*Station hinzufügen*, der Stift ändert eine bestehende, der Mülleimer entfernt *Station hinzufügen*, der Stift ändert eine bestehende, der Mülleimer entfernt
sie. Das Unternehmen ist ein Textfeld mit Vorschlagsliste – ein Name, den es sie. Das Unternehmen ist ein Textfeld mit Vorschlagsliste – ein Name, den es
noch nicht gibt, legt das Unternehmen an (wie im CV-Import). noch nicht gibt, legt das Unternehmen an (wie im CV-Import).
Der Haken **aktuelle Station** hängt mit dem Arbeitgeber im Reiter Stammdaten Der Haken **aktuelle Station** hängt mit dem Arbeitgeber im Reiter Stammdaten
zusammen: es kann immer nur eine Station laufen. Wird eine dazu erklärt, bekommt zusammen: es kann immer nur eine Station laufen. Wird eine dazu erklärt, bekommt
die bisherige ein Enddatum, und der Arbeitgeber des Kontakts wechselt mit. Wird die bisherige ein Enddatum, und der Arbeitgeber des Kontakts wechselt mit. Wird
die laufende Station gelöscht, hat der Kontakt danach keinen Arbeitgeber mehr – die laufende Station gelöscht, hat der Kontakt danach keinen Arbeitgeber mehr –
die Rückfrage sagt das. die Rückfrage sagt das.
| Feld | Hinweis | | Feld | Hinweis |
| --- | --- | | --- | --- |
| Art | Experte, Ansprechpartner, Lead – die Liste wird in den Stammdaten gepflegt | | Art | Experte, Ansprechpartner, Lead – die Liste wird in den Stammdaten gepflegt |
| Status | Interessiert, Interview, Gebucht, Inaktiv | | Status | Interessiert, Interview, Gebucht, Inaktiv |
| Stundensatz vor Ort / remote | in **Euro je Stunde**; leer heißt „nicht hinterlegt", nicht „null Euro" | | Stundensatz vor Ort / remote | in **Euro je Stunde**; leer heißt „nicht hinterlegt", nicht „null Euro" |
| Foto | hochladen oder mit *Von LinkedIn* aus dem öffentlichen Profil holen | | Foto | hochladen oder mit *Von LinkedIn* aus dem öffentlichen Profil holen |
| CV | Link oder die hochgeladene Datei aus dem CV-Import | | CV | Link oder die hochgeladene Datei aus dem CV-Import |
### CV-Import ### CV-Import
*Kontakte → CV-Import.* Lebensläufe hineinziehen (PDF, DOCX, Text; bis 20 auf *Kontakte → CV-Import.* Lebensläufe hineinziehen (PDF, DOCX, Text; bis 20 auf
einmal). Danach drei Ordner: einmal). Danach drei Ordner:
1. **Eingang** – je Datei entscheiden: *Ja, auslesen* oder *Nein*. 1. **Eingang** – je Datei entscheiden: *Ja, auslesen* oder *Nein*.
2. **Screening** – der Vorschlag der KI steht im Formular. Alles ist änderbar; 2. **Screening** – der Vorschlag der KI steht im Formular. Alles ist änderbar;
was hier korrigiert wird, gewinnt. was hier korrigiert wird, gewinnt.
3. **Auswahl** – bestätigt, Kontakt angelegt, Lebenslauf hängt daran. 3. **Auswahl** – bestätigt, Kontakt angelegt, Lebenslauf hängt daran.
Trägt der Lebenslauf ein **Bewerbungsfoto**, steht es oben im Formular, mit Trägt der Lebenslauf ein **Bewerbungsfoto**, steht es oben im Formular, mit
gesetztem Haken. Es wird auf 400 px verkleinert (höchstens 512 kB) und beim gesetztem Haken. Es wird auf 400 px verkleinert (höchstens 512 kB) und beim
Übernehmen als Foto des Kontakts gespeichert. Hat der Automat statt des Übernehmen als Foto des Kontakts gespeichert. Hat der Automat statt des
Portraits das Firmenlogo erwischt, Haken entfernen – ein anderes Bild lässt sich Portraits das Firmenlogo erwischt, Haken entfernen – ein anderes Bild lässt sich
jederzeit in der Kontaktkarte setzen. jederzeit in der Kontaktkarte setzen.
Es wird **nichts automatisch angelegt**. Frühere Stationen werden als beendete Es wird **nichts automatisch angelegt**. Frühere Stationen werden als beendete
Anstellungen übernommen – der Kontakt erscheint dadurch beim alten Arbeitgeber Anstellungen übernommen – der Kontakt erscheint dadurch beim alten Arbeitgeber
sofort unter „Ehemalige". sofort unter „Ehemalige".
## Unternehmen ## Unternehmen
Ebenfalls Kacheln, Liste und Netzwerk, dazu der Reiter **Group** (Konzern). Ebenfalls Kacheln, Liste und Netzwerk, dazu der Reiter **Group** (Konzern).
Ein Unternehmen mit gesetztem Haken *Kunde* erscheint zusätzlich in der Ein Unternehmen mit gesetztem Haken *Kunde* erscheint zusätzlich in der
Kundenansicht; der Zustand *aktiv* oder *alumni* unterscheidet laufende von Kundenansicht; der Zustand *aktiv* oder *alumni* unterscheidet laufende von
früheren Kunden. früheren Kunden.
Fünf Sterne bewerten das Unternehmen, `Freelancer` sagt, ob dort überhaupt Fünf Sterne bewerten das Unternehmen, `Freelancer` sagt, ob dort überhaupt
freiberuflich gearbeitet wird. freiberuflich gearbeitet wird.
### Netzwerkansicht und Stammbaum ### Netzwerkansicht und Stammbaum
Rechtecke sind Unternehmen, Kreise sind Kontakte. Ziehen verschiebt, das Mausrad Rechtecke sind Unternehmen, Kreise sind Kontakte. Ziehen verschiebt, das Mausrad
zoomt. Ein Klick auf ein Unternehmen mit Kontakten öffnet dessen **Stammbaum**: zoomt. Ein Klick auf ein Unternehmen mit Kontakten öffnet dessen **Stammbaum**:
| Ast | Bedeutung | | Ast | Bedeutung |
| --- | --- | | --- | --- |
| golden | Berater (Art „Experte") | | golden | Berater (Art „Experte") |
| hell | Ansprechpartner und übrige | | hell | Ansprechpartner und übrige |
| grau, gestrichelt | Ehemalige – darunter steht, wo die Person heute ist | | grau, gestrichelt | Ehemalige – darunter steht, wo die Person heute ist |
In den Kontakten lassen sich zusätzlich Verbindungen anlegen: *Verbindung In den Kontakten lassen sich zusätzlich Verbindungen anlegen: *Verbindung
erstellen*, dann zwei Kontakte anklicken; Esc bricht ab. erstellen*, dann zwei Kontakte anklicken; Esc bricht ab.
## Ausschreibungen ## Ausschreibungen
Eine Tabelle; ein Klick klappt die Zeile auf. Links die zugewiesenen Experten Eine Tabelle; ein Klick klappt die Zeile auf. Links die zugewiesenen Experten
und der Kundenkontakt, rechts der **Ausschreibungstext**. und der Kundenkontakt, rechts der **Ausschreibungstext**.
> **Der Schalter „Online" entscheidet, ob die Ausschreibung auf der ICON Website > **Der Schalter „Online" entscheidet, ob die Ausschreibung auf der ICON Website
> in der Projektbörse steht.** Er sitzt deshalb direkt in der Liste. > in der Projektbörse steht.** Er sitzt deshalb direkt in der Liste.
Der Text wird automatisch gespeichert, 800 ms nach dem letzten Tastendruck – es Der Text wird automatisch gespeichert, 800 ms nach dem letzten Tastendruck – es
gibt keinen Speichern-Knopf. Über dem Feld liegen die Knöpfe der Schreibhilfe gibt keinen Speichern-Knopf. Über dem Feld liegen die Knöpfe der Schreibhilfe
(*Text erzeugen*, *Verbessern*, *Kürzen*, *Stichpunkte*, *Anforderungsprofil*) (*Text erzeugen*, *Verbessern*, *Kürzen*, *Stichpunkte*, *Anforderungsprofil*)
und der Diktat-Knopf. *Zurück* holt den Stand vor dem letzten KI-Eingriff. und der Diktat-Knopf. *Zurück* holt den Stand vor dem letzten KI-Eingriff.
Zwei Untermenüs: Zwei Untermenüs:
* **Live!** – dieselben Ausschreibungen als Kanban über die Phasen, mit Ziehen * **Live!** – dieselben Ausschreibungen als Kanban über die Phasen, mit Ziehen
bearbeitbar. bearbeitbar.
* **Archiv** – archivierte Ausschreibungen; von dort zurückholbar. * **Archiv** – archivierte Ausschreibungen; von dort zurückholbar.
## Laufende Projekte ## Laufende Projekte
Titel, Kunde, Experte, Zeitraum, Status, Tagessatz und Notizen. Die Notizen Titel, Kunde, Experte, Zeitraum, Status, Tagessatz und Notizen. Die Notizen
sind ein WYSIWYG-Feld – im übernommenen Bestand steht dort ein Kopfblock aus dem sind ein WYSIWYG-Feld – im übernommenen Bestand steht dort ein Kopfblock aus dem
alten CRM und darunter der Ausschreibungstext. alten CRM und darunter der Ausschreibungstext.
### Marge ### Marge
Jede Projektkachel weist die Marge aus, und in der Projektkarte steht die Jede Projektkachel weist die Marge aus, und in der Projektkarte steht die
Rechnung aufgeschlüsselt: Rechnung aufgeschlüsselt:
| Zeile | Woher | | Zeile | Woher |
| --- | --- | | --- | --- |
| Tagessatz Kunde | Feld **Tagessatz** am Projekt | | Tagessatz Kunde | Feld **Tagessatz** am Projekt |
| Stundensatz Berater | Kontaktkarte des Experten | | Stundensatz Berater | Kontaktkarte des Experten |
| Kosten (8 h) | Stundensatz × 8 | | Kosten (8 h) | Stundensatz × 8 |
| Marge | Tagessatz − Kosten, dazu der Prozentsatz vom Tagessatz | | Marge | Tagessatz − Kosten, dazu der Prozentsatz vom Tagessatz |
**Ein Tag sind 8 Stunden.** Hat der Berater einen Remote- *und* einen **Ein Tag sind 8 Stunden.** Hat der Berater einen Remote- *und* einen
Vor-Ort-Satz, wird mit dem **Remote-Satz** gerechnet; fehlt der, mit dem Vor-Ort-Satz, wird mit dem **Remote-Satz** gerechnet; fehlt der, mit dem
Vor-Ort-Satz. Welcher benutzt wurde, steht daneben. Vor-Ort-Satz. Welcher benutzt wurde, steht daneben.
Die Farbe ist eine schnelle Ampel: grün ab 25 %, gelb darüber bis 0, rot bei Die Farbe ist eine schnelle Ampel: grün ab 25 %, gelb darüber bis 0, rot bei
negativer Marge. negativer Marge.
Fehlt der Tagessatz oder hat der Berater keinen Stundensatz, steht statt einer Fehlt der Tagessatz oder hat der Berater keinen Stundensatz, steht statt einer
Zahl der Grund dort – eine leere Zeile ließe offen, ob die Marge null oder Zahl der Grund dort – eine leere Zeile ließe offen, ob die Marge null oder
unbekannt ist. Der Tagessatz wird am Projekt gepflegt, die Stundensätze beim unbekannt ist. Der Tagessatz wird am Projekt gepflegt, die Stundensätze beim
Kontakt (*Kontakt → Stundensatz vor Ort / remote*). Kontakt (*Kontakt → Stundensatz vor Ort / remote*).
## Historie ## Historie
Alles, was im CRM getan wurde, mit Zeitpunkt, Person und – bei Änderungen – den Alles, was im CRM getan wurde, mit Zeitpunkt, Person und – bei Änderungen – den
betroffenen Feldern samt Vorher/Nachher. Filterbar nach Art des Datensatzes. betroffenen Feldern samt Vorher/Nachher. Filterbar nach Art des Datensatzes.
**@-Erwähnungen:** In jedem Notizfeld öffnet `@` eine Vorschlagsliste über **@-Erwähnungen:** In jedem Notizfeld öffnet `@` eine Vorschlagsliste über
Kontakte, Unternehmen und Benutzer. Im Lesezustand ist jede Erwähnung anklickbar Kontakte, Unternehmen und Benutzer. Im Lesezustand ist jede Erwähnung anklickbar
und führt auf das Profil; im Bearbeiten-Zustand steht dort die rohe Form und führt auf das Profil; im Bearbeiten-Zustand steht dort die rohe Form
`@[Name](contact:12)` – anders ließe sie sich nicht ändern oder löschen. `@[Name](contact:12)` – anders ließe sie sich nicht ändern oder löschen.
## Diktat ## Diktat
*Einstellungen → Diktat*, einmal je Gerät: **Sprachmodell laden** (rund 300 MB, *Einstellungen → Diktat*, einmal je Gerät: **Sprachmodell laden** (rund 300 MB,
bleibt danach im Browser). Anschließend steht in jedem längeren Textfeld ein bleibt danach im Browser). Anschließend steht in jedem längeren Textfeld ein
Mikrofon-Knopf. Mikrofon-Knopf.
Aufnahme starten, sprechen, Knopf erneut drücken – der erkannte Text wird Aufnahme starten, sprechen, Knopf erneut drücken – der erkannte Text wird
**angehängt**, nie ersetzt. Das Kreuz daneben verwirft die Aufnahme. **angehängt**, nie ersetzt. Das Kreuz daneben verwirft die Aufnahme.
Die Erkennung läuft auf dem eigenen Rechner. Es wird nichts hochgeladen und Die Erkennung läuft auf dem eigenen Rechner. Es wird nichts hochgeladen und
nichts gespeichert. Details: [04 Diktat](04-diktat.md). nichts gespeichert. Details: [04 Diktat](04-diktat.md).
## Einstellungen ## Einstellungen
| Reiter | Wer | Inhalt | | Reiter | Wer | Inhalt |
| --- | --- | --- | | --- | --- | --- |
| Nutzerverwaltung | Administratoren | Benutzer anlegen, Rolle, aktiv/inaktiv | | Nutzerverwaltung | Administratoren | Benutzer anlegen, Rolle, aktiv/inaktiv |
| Stammdaten | Administratoren | Auswahllisten: Branchen, Arten der Kontakte | | Stammdaten | Administratoren | Auswahllisten: Branchen, Arten der Kontakte |
| KI | Administratoren | OpenAI-Schlüssel, Modell (Auswahlliste der für den Schlüssel verfügbaren Modelle), Verbindungstest | | KI | Administratoren | OpenAI-Schlüssel, Modell (Auswahlliste der für den Schlüssel verfügbaren Modelle), Verbindungstest |
| Mein Konto | alle | Foto, Farbe, Passwort, Design | | Mein Konto | alle | Foto, Farbe, Passwort, Design |
| Diktat | alle | Spracherkennung einrichten | | Diktat | alle | Spracherkennung einrichten |
Beim Pflegen der Stammdaten gelten zwei Regeln: **Umbenennen zieht den Bestand Beim Pflegen der Stammdaten gelten zwei Regeln: **Umbenennen zieht den Bestand
mit**, und **Entfernen geht nur, solange ein Wert nirgends benutzt wird** – mit**, und **Entfernen geht nur, solange ein Wert nirgends benutzt wird** –
hinter jedem Eintrag steht, wie oft er vorkommt. hinter jedem Eintrag steht, wie oft er vorkommt.
+1 -1
View File
@@ -60,7 +60,7 @@ angemeldeten Benutzer vorbelegt.
**Diktat / Whisper** – Spracherkennung, die vollständig im Browser läuft. Siehe **Diktat / Whisper** – Spracherkennung, die vollständig im Browser läuft. Siehe
[04 Diktat](04-diktat.md). [04 Diktat](04-diktat.md).
**JWT** – das Token der Anmeldung, 12 Stunden gültig, im `localStorage`. Wird **JWT** – das Token der Anmeldung, 7 Tage gültig, im `localStorage`. Wird
bei jeder Anfrage zusätzlich gegen die Datenbank geprüft. bei jeder Anfrage zusätzlich gegen die Datenbank geprüft.
**MentionField** – das Textfeld mit zwei Zuständen (lesen / bearbeiten) für **MentionField** – das Textfeld mit zwei Zuständen (lesen / bearbeiten) für
+54 -54
View File
@@ -1,54 +1,54 @@
# ICON CRM Backend # ICON CRM Backend
# Als .env kopieren und Werte setzen. .env gehoert nicht ins Repository. # Als .env kopieren und Werte setzen. .env gehoert nicht ins Repository.
PORT=5007 PORT=5007
# Nur lokal binden - von aussen erreichbar ist die Anwendung ausschliesslich # Nur lokal binden - von aussen erreichbar ist die Anwendung ausschliesslich
# ueber den Apache-Reverse-Proxy auf icrm.icon-experts.de. # ueber den Apache-Reverse-Proxy auf icrm.icon-experts.de.
HOST=127.0.0.1 HOST=127.0.0.1
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3306 DB_PORT=3306
DB_USER=iconcrm DB_USER=iconcrm
DB_PASSWORD=changeme DB_PASSWORD=changeme
DB_NAME=icon_CRM DB_NAME=icon_CRM
# Signiert die JWT-Sitzungen. Langer Zufallswert, z. B.: # Signiert die JWT-Sitzungen. Langer Zufallswert, z. B.:
# node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" # node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
JWT_SECRET=changeme-langer-zufallswert JWT_SECRET=changeme-langer-zufallswert
JWT_EXPIRES_IN=12h JWT_EXPIRES_IN=7d
# Wird beim ersten Start angelegt, solange noch kein Benutzer existiert. # Wird beim ersten Start angelegt, solange noch kein Benutzer existiert.
# Ohne ADMIN_PASSWORD erzeugt das Backend eines und schreibt es einmalig ins Log. # Ohne ADMIN_PASSWORD erzeugt das Backend eines und schreibt es einmalig ins Log.
ADMIN_USERNAME=Admin ADMIN_USERNAME=Admin
ADMIN_NAME=Administrator ADMIN_NAME=Administrator
ADMIN_EMAIL=admin@icon-experts.de ADMIN_EMAIL=admin@icon-experts.de
ADMIN_PASSWORD= ADMIN_PASSWORD=
# Erlaubte Herkunft fuer den lokalen Vite-Dev-Server. # Erlaubte Herkunft fuer den lokalen Vite-Dev-Server.
CORS_ORIGINS=http://localhost:3101,http://127.0.0.1:3101 CORS_ORIGINS=http://localhost:3101,http://127.0.0.1:3101
# Ablage der hochgeladenen Lebenslaeufe. Leer = <server>/../uploads. # Ablage der hochgeladenen Lebenslaeufe. Leer = <server>/../uploads.
UPLOAD_DIR=/var/www/icon_CRM/uploads UPLOAD_DIR=/var/www/icon_CRM/uploads
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# KI (CV-Auslesung und Schreibhilfe fuer Ausschreibungen) # KI (CV-Auslesung und Schreibhilfe fuer Ausschreibungen)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Der Schluessel bleibt auf dem Server - der Browser spricht nie direkt mit # Der Schluessel bleibt auf dem Server - der Browser spricht nie direkt mit
# OpenAI. Fehlt er, laeuft das CRM normal weiter und blendet die KI-Knoepfe aus. # OpenAI. Fehlt er, laeuft das CRM normal weiter und blendet die KI-Knoepfe aus.
# #
# Hinweis zum Modell: gpt-5.6-luna akzeptiert KEIN eigenes "temperature"; # Hinweis zum Modell: gpt-5.6-luna akzeptiert KEIN eigenes "temperature";
# ai.js schickt deshalb nur den Standardwert. # ai.js schickt deshalb nur den Standardwert.
OPENAI_API_KEY= OPENAI_API_KEY=
OPENAI_MODEL=gpt-5.6-luna OPENAI_MODEL=gpt-5.6-luna
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Lesender Zugriff auf die Kontakte des perinco CRM # Lesender Zugriff auf die Kontakte des perinco CRM
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Das ICON CRM zeigt die perinco-Kontakte zusaetzlich an - gekennzeichnet und # Das ICON CRM zeigt die perinco-Kontakte zusaetzlich an - gekennzeichnet und
# ausschliesslich lesend. Gespeichert wird nichts. # ausschliesslich lesend. Gespeichert wird nichts.
# #
# Der Token steht drueben in server/.env als ICON_API_TOKEN und muss # Der Token steht drueben in server/.env als ICON_API_TOKEN und muss
# uebereinstimmen. Fehlt einer der beiden Werte, bleibt der Schalter in der # uebereinstimmen. Fehlt einer der beiden Werte, bleibt der Schalter in der
# Oberflaeche aus und das CRM laeuft normal weiter. # Oberflaeche aus und das CRM laeuft normal weiter.
PERINCO_API_URL=https://pcrm.perinco.de PERINCO_API_URL=https://pcrm.perinco.de
PERINCO_API_TOKEN= PERINCO_API_TOKEN=
+1 -1
View File
@@ -11,7 +11,7 @@ function secret() {
export function signToken(user) { export function signToken(user) {
return jwt.sign({ sub: user.id, role: user.role }, secret(), { return jwt.sign({ sub: user.id, role: user.role }, secret(), {
expiresIn: process.env.JWT_EXPIRES_IN || '12h', expiresIn: process.env.JWT_EXPIRES_IN || '7d',
}); });
} }