Initial
EK-DOS-WEB bauen und ausrollen / frontend (push) Failing after 22s
EK-DOS-WEB bauen und ausrollen / backend (push) Failing after 27s
EK-DOS-WEB bauen und ausrollen / deploy (push) Skipped

This commit is contained in:
Kyle Müller
2026-09-06 13:25:44 +02:00
commit 7d82e807fc
71 changed files with 10601 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
# EK-DOS-WEB betreiben
Zielsystem: Debian/Ubuntu mit nginx und php8.5-fpm. Postgres und Redis laufen
bereits für n8n und werden mitbenutzt (eigene Datenbank, eigene Redis-Nummer).
## 1. Pakete
```bash
sudo apt update
sudo apt install -y nginx php8.5-fpm php8.5-cli php8.5-pgsql php8.5-redis \
php8.5-mbstring php8.5-curl php8.5-intl php8.5-xml
```
`php8.5-intl` ist optional, verbessert aber die Erkennung der Kundenrücksprache
in Ticketberichten (Unicode-Normalisierung). Ohne die Erweiterung greift ein
Rückfall ohne Normalisierung.
## 2. Datenbank und Redis
```bash
sudo -u postgres createuser ekdos --pwprompt
sudo -u postgres createdb ekdos --owner=ekdos
```
Redis braucht nichts weiter -- EK-DOS legt seine Schlüssel unter dem Präfix
`ekdos:` ab und nutzt eine eigene Datenbanknummer (`REDIS_DB`).
> Redis hält Sitzungen **und** den n8n-Cache. Ist Redis weg, ist niemand mehr
> angemeldet und jede Ansicht fragt wieder direkt bei n8n an. Die Anwendung
> läuft weiter, aber langsamer und mit erneuter Anmeldung.
## 3. Verzeichnisse
```bash
sudo mkdir -p /var/www/ekdos/{releases,backend}
sudo useradd --system --home /var/www/ekdos --shell /usr/sbin/nologin ekdos
sudo chown -R ekdos:ekdos /var/www/ekdos
sudo mkdir -p /var/log/php && sudo chown ekdos:ekdos /var/log/php
```
## 4. Backend einrichten
```bash
cd /var/www/ekdos/backend
sudo -u ekdos cp .env.example .env
sudo -u ekdos editor .env
sudo -u ekdos composer install --no-dev --optimize-autoloader
sudo -u ekdos php bin/ekdos migrate
sudo -u ekdos php bin/ekdos user:create # erster Administrator
sudo -u ekdos php bin/ekdos check
```
`.env` gehört dem Benutzer `ekdos` und sollte `chmod 600` sein. Sie wird beim
Ausrollen ausdrücklich nicht überschrieben.
### n8n-Adressen
| Einstellung | Wert | warum |
| ------------------- | --------------------------------- | ----------------------------------------------------------- |
| `N8N_PUBLIC_BASE` | `https://n8n.elektro-krueger.eu` | Der Dienst auf Port 5678 ist nur über den CNAME erreichbar |
| `N8N_INTERNAL_HOST` | `10.0.11.131` | Jeder weitere n8n-Dienst auf einem anderen Port |
Alle heutigen `ek-dos-web`-Webhooks laufen über den 5678-Dienst und damit über
`N8N_PUBLIC_BASE`. Kommt später ein zweiter n8n-Dienst auf eigenem Port dazu,
wird er mit `Endpoints::internal(<port>, '<pfad>')` angesprochen und geht über
die RFC1918-Adresse -- er ist nach aussen nicht veröffentlicht.
## 5. php-fpm
```bash
sudo cp deploy/php-fpm/ekdos.pool.conf /etc/php/8.5/fpm/pool.d/ekdos.conf
sudo php-fpm8.5 -t
sudo systemctl restart php8.5-fpm
ls -l /run/php/ekdos.sock # muss www-data:www-data 0660 gehören
```
Der Pool setzt `opcache.validate_timestamps = 0`. Neuer Quelltext wird deshalb
**erst nach einem `systemctl reload php8.5-fpm`** wirksam -- die Ausrollstrecke
macht das selbst.
## 6. nginx
```bash
sudo cp deploy/nginx/ekdos.conf /etc/nginx/sites-available/ekdos.conf
sudo ln -sf /etc/nginx/sites-available/ekdos.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
Kurzfassung: `fastcgi_pass unix:/run/php/ekdos.sock` für `/api/`, alles andere
`try_files $uri $uri/ /index.html`. Kein Anwendungsserver, kein Reverse Proxy
auf einen Node-Prozess.
Läuft der Zugriff nur intern über HTTP, kann `SESSION_COOKIE_SECURE=false`
gesetzt und der 443-Block entfernt werden. Sobald TLS anliegt: wieder auf `true`.
## 7. Ausrollen
Die Gitea-Strecke (`.gitea/workflows/build.yml`) macht das bei jedem Push auf
`main`. Nötig sind:
- ein SSH-Schlüssel als Gitea-Secret `DEPLOY_SSH_KEY`
- ein Konto `deploy` auf dem Server, das nach `/var/www/ekdos` schreiben darf
- `sudo systemctl reload php8.5-fpm` ohne Passwort für dieses Konto
```
deploy ALL=(root) NOPASSWD: /bin/systemctl reload php8.5-fpm
```
Jede Veröffentlichung landet unter `releases/<sha>`; sichtbar wird sie erst
durch den Symlink-Tausch von `current`. Die letzten fünf bleiben liegen, ein
Rückschritt ist damit ein `ln -sfn`.
### Von Hand
```bash
cd frontend && pnpm install --frozen-lockfile && pnpm build
rsync -az --delete frontend/dist/ server:/var/www/ekdos/releases/manuell/
ssh server 'cd /var/www/ekdos && ln -sfn releases/manuell current && sudo systemctl reload php8.5-fpm'
```
## Prüfen, wenn etwas klemmt
```bash
php backend/bin/ekdos check # Postgres, Redis, n8n
curl -sS https://<host>/api/health # ohne Anmeldung erreichbar
tail -f /var/log/php/ekdos-error.log # Ausnahmen aus dem Backend
tail -f /var/log/nginx/ekdos.error.log
```
| Symptom | meistens |
| ------------------------------------------- | --------------------------------------------------------------------- |
| 502 auf `/api/` | Socket fehlt oder falsche Rechte; `systemctl status php8.5-fpm` |
| Anmeldung wirkt, danach sofort abgemeldet | Redis nicht erreichbar, oder `SESSION_COOKIE_SECURE=true` ohne TLS |
| Listen bleiben leer, Meldung „nicht erreichbar" | `N8N_PUBLIC_BASE` falsch, oder der Webhook ist in n8n nicht aktiv |
| Angezeigte Daten sind veraltet | Rückfall-Cache greift, weil n8n gerade nicht antwortet |
| 503 „noch nicht eingerichtet" | `N8N_INVOICE_SYNC_SECRET` bzw. `N8N_CUSTOMER_KEY` fehlt in `.env` |
| Quelltextänderung wirkt nicht | Opcache; `systemctl reload php8.5-fpm` |
## Alle Benutzer aussperren
```bash
redis-cli --scan --pattern 'ekdos:sess:*' | xargs -r redis-cli del
```
Danach muss sich jeder neu anmelden. Einzelne Konten trifft es gezielt über die
Benutzerverwaltung (Deaktivieren beendet die offenen Sitzungen sofort).
+109
View File
@@ -0,0 +1,109 @@
# n8n-Schnittstelle
Alle Fachdaten von EK-DOS-WEB liegen in n8n. Das Backend hält davon nichts vor
ausser einem kurzlebigen Cache.
## Adressen
Der n8n-Hauptdienst auf **Port 5678** ist ausschliesslich über
`https://n8n.elektro-krueger.eu` erreichbar. Jeder weitere n8n-Dienst auf einem
anderen Port ist nicht veröffentlicht und wird über **10.0.11.131** angesprochen.
In `backend/src/N8n/Endpoints.php`:
```php
$url->webhook('/offene-tickets'); // https://n8n.elektro-krueger.eu/webhook/ek-dos-web/offene-tickets
$url->internal(5679, '/webhook/irgendwas'); // http://10.0.11.131:5679/webhook/irgendwas
```
Neue Adressen gehören in diese Klasse, nicht verstreut in die Controller.
## Endpunkte
Alle unter `/webhook/ek-dos-web`. Die Spalte *Cache* nennt Gruppe sowie frische
und Rückfall-Haltbarkeit in Sekunden.
| Pfad | Verwendung | Cache |
| ----------------------------------- | -------------------------------- | ------------------------ |
| `/offene-tickets` | Ticketliste | `tickets` 15 / 600 |
| `/offene-tickets/kundenruecksprache`| Ergebnis speichern | schreibend |
| `/tickets/digitale-akte` | PDF | – |
| `/tickets/servicebericht` | PDF | – |
| `/angebote` | Angebotsliste | `offers` 30 / 600 |
| `/angebote/versendet` `/beauftragt` `/zuruecksetzen` `/loeschen` | Aktionen | schreibend |
| `/kundenstamm` | Kundenliste, Anlage, Änderung | `customers` 120 / 1800 |
| `/kundenstamm/digitale-akte` | PDF | – |
| `/rechnungen` | Rechnungen eines Kunden | `invoices` 30 / 600 |
| `/rechnungen/alle` | Gesamtübersicht (geschützt) | `invoices` 30 / 600 |
| `/rechnungen/zuordnen` `/sync` | Zuordnung, Abgleich (geschützt) | schreibend |
| `/rechnungen/pdf` | PDF | – |
| `/rechnungen-anfertigen` | Aufgabenliste | `invoices-create` 30/600 |
| `/rechnungen-anfertigen/erledigt` | Aufgabe abschließen | schreibend |
| `/rechnungen-pruefen` | Prüfliste | `invoices-review` 30/600 |
| `/rechnungen-versenden` | Versandliste und -bestätigung | `invoices-send` 30/600 |
| `/digitale-akte` | PDF als base64 im JSON | |
| `/interne-aufgaben` (+ `/erledigt` `/bearbeiten` `/loeschen`) | Aufgaben | `tasks` 20 / 600 |
| `/online-kaeufe` (+ `/erledigt` `/loeschen`) | Online-Käufe | `purchases` 30 / 600 |
| `/stundennachweise` | Monatsreport | `hours` 300 / 3600 |
## Geteilte Geheimnisse
| Kopfzeile | aus | wofür |
| -------------------------- | ------------------------- | ------------------------------------------------------------ |
| `x-ekdos-invoice-sync` | `N8N_INVOICE_SYNC_SECRET` | Rechnungsübersicht, Zuordnung, Abgleich, Kundenanlage, Angebot zurücksetzen |
| `X-EK-DOS-Customer-Key` | `N8N_CUSTOMER_KEY` | Lesen und Ändern im Kundenstamm |
| `x-ekdos-webhook-secret` | `N8N_REFRESH_SECRET` | n8n meldet EK-DOS eine Ticketänderung |
Fehlt eines davon, antworten die betroffenen Routen mit **503** und einer
Meldung, die genau das sagt -- sie schlagen nicht stumm fehl.
> Der Kundenschlüssel stand in der Next.js-Fassung als Literal im Quelltext
> (`app/api/customers/route.ts`). Er liegt jetzt in `.env`. **Er sollte in n8n
> gewechselt werden**, weil der alte Wert in der Versionsgeschichte steht.
## n8n meldet eine Ticketänderung
Der Workflow *Datenübergabe an EK-DOS-WEB* darf weiter auf seinen Endpunkt
zeigen, nur ohne Port:
```
POST https://schulung.elektro-krueger.local/api/tickets/refresh
x-ekdos-webhook-secret: <N8N_REFRESH_SECRET>
```
Anders als früher ist das kein Platzhalter mehr. In der Next.js-Fassung
antwortete die Route nur `{ok:true}` und tat sonst nichts; die Oberfläche sah
neue Tickets rein zufällig beim nächsten 20-Sekunden-Takt. Jetzt **verwirft der
Aufruf den Ticket-Cache**, sodass die nächste Abfrage die neuen Daten garantiert
sieht.
Ohne gesetztes `N8N_REFRESH_SECRET` verlangt die Route eine angemeldete Sitzung
und weist den Aufruf aus n8n mit 401 ab.
## Feldabbildung Tickets
Die einzige Stelle, an der das Backend n8n-Felder umbenennt
(`backend/src/Relay/TicketsController.php`):
| n8n | Oberfläche |
| -------------------------------- | ------------------------------ |
| `ticketnummer` / `id` | `id` |
| `kunde` / `customer` | `customer` |
| `liegenschaft` / `property` | `property` |
| `letzter_servicebericht_status` | `reportStatus` |
| `fortsetzung` | `continuation` |
| `entscheidung_code` | `decisionCode` |
| `kundenruecksprache_ergebnis` | `consultationResult` |
| `kundenruecksprache_am` / `_von` | `consultationUpdatedAt` / `By` |
`requiresCustomerConsultation` wird abgeleitet: entweder
`entscheidung_code === "TEILERLEDIGUNG_RUECKSPRACHE_KUNDE"`, oder der
Fortsetzungstext enthält „nimmt mit Kunden für das weitere Vorgehen Kontakt auf".
Zeilen ohne Ticketnummer werden verworfen.
## Bekannte Kopplung
Interne Aufgaben kennen in n8n genau zwei Empfänger: `sascha` und `svenja`. Ein
drittes EK-DOS-Konto kann sich anmelden und alle Ansichten nutzen, aber noch
keine Aufgaben zugewiesen bekommen. Dafür müsste der n8n-Workflow *Interne
Aufgaben* einen freien Empfängerschlüssel annehmen.