# 07 Betrieb ## Deployment Gebaut wird **lokal**, auf den Server gehen nur `dist/` plus die Backend-Quellen. ```powershell $env:ICON_CRM_SSH_PASSWORD = '…' powershell -File deploy/deploy.ps1 powershell -File deploy/deploy.ps1 -SkipBuild # ohne neuen Build powershell -File deploy/deploy.ps1 -Deps # bei neuen Backend-Paketen ``` Das Skript (`deploy/deploy.ps1`) macht in dieser Reihenfolge: ```mermaid flowchart LR B["npm run build"] --> U1["pscp dist/"] U1 --> U2["pscp server/src + package.json"] U2 --> R["systemctl restart icon-crm-backend"] R --> C["curl /api/health"] ``` Schlägt der Build fehl, wird **nichts** hochgeladen – sonst stünde auf dem Server ein halber Stand. > **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 > `src/` oder `index.html` gilt: erst bauen, dann hochladen. Alte Build-Dateien werden beim Hochladen **nicht** entfernt – `dist/assets/` 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 trotzdem nicht. ## Dienste auf dem Server | Was | Befehl | | --- | --- | | Zustand | `systemctl status icon-crm-backend` | | Neustart | `systemctl restart icon-crm-backend` | | 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"}` | | 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 kein Sparzwang, sondern ein Sicherheitsnetz: ein Leck im Backend soll nicht Apache und MariaDB mitreißen. ## Umgebungsvariablen `server/.env`, chmod 600, gehört **nicht** ins Repository. Vorlage: `server/.env.example`. | Variable | Bedeutung | | --- | --- | | `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` | | `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** | | `CORS_ORIGINS` | erlaubte Herkunft für den lokalen Dev-Server | | `UPLOAD_DIR` | Ablage der Lebensläufe | | `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_INBOUND_TOKEN` | perinco liest ICON | Ohne `ADMIN_PASSWORD` erzeugt das Backend eines und schreibt es **einmalig** ins Log. Danach ist das Passwort nur noch über die Nutzerverwaltung bzw. *Mein Konto* änderbar. ## Erstaufsetzung (einmalig, bereits erledigt) ```bash apt-get install -y apache2 mariadb-server certbot python3-certbot-apache nodejs a2enmod proxy proxy_http rewrite headers expires mariadb -e "CREATE DATABASE icon_CRM CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mariadb -e "CREATE USER 'iconcrm'@'localhost' IDENTIFIED BY '…'; GRANT ALL PRIVILEGES ON icon_CRM.* TO 'iconcrm'@'localhost';" cd /var/www/icon_CRM/server && npm install --omit=dev cp deploy/icon-crm-backend.service /etc/systemd/system/ systemctl daemon-reload && systemctl enable --now icon-crm-backend cp deploy/icrm.icon-experts.de.conf /etc/apache2/sites-available/ a2ensite icrm.icon-experts.de && systemctl reload apache2 certbot --apache -d icrm.icon-experts.de ``` 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 Zertifikat erneuert certbot selbst. ## Sicherung Der Bestand steht vollständig in MariaDB; hochgeladene Lebensläufe liegen unter `/var/www/icon_CRM/uploads`. Beides gehört in die Sicherung: ```bash 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 ``` ## Der alte Name crm.icon-experts.de 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 auf diesen Server und wird vom Vhost `icrm.icon-experts.de` als `ServerAlias` mitbedient – dasselbe Zertifikat deckt beide Namen ab. Verschickte Links und Lesezeichen laufen damit weiter, es gibt keinen zweiten Stand zu pflegen. 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` 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` und beide Vhost-Dateien, ohne `node_modules`. Die Datei liegt **auf beiden Servern** unter `/root/backups/`, Prüfsumme `252f7fa3226c2df7c4020a0234c3095c`. > 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`) – > abzuschalten wäre eine eigene Entscheidung, nicht Teil dieses Rückbaus. ## Störungssuche | 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 | | 502 auf `/api` | Backend aus: `systemctl status icon-crm-backend`, dann `journalctl -u …` | | `{"db":"down"}` | MariaDB aus oder Zugangsdaten falsch | | 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`? | | 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? | | Diktat lädt nicht | siehe [04 Diktat](04-diktat.md) – das ist eine Sache des Browsers, nicht des Servers | ### Wenn Daten zu fehlen scheinen Erst messen, dann handeln. Die drei Griffe, die in der Praxis geholfen haben: ```bash 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? 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. ### DNS-Umstellungen Nach einem Wechsel des A-Records antworten fremde Resolver unterschiedlich lange mit der alten Adresse. Zum Prüfen die **autoritativen** Nameserver und mindestens einen öffentlichen Resolver befragen, nicht nur den eigenen: ```bash dig +short icon-experts.de @1.1.1.1 dig +short icon-experts.de @8.8.8.8 ``` Ein lokaler Resolver (PiHole, Router) kann den alten Wert deutlich länger halten – das sieht dann aus wie ein Serverfehler und ist keiner.