From 940b2d47679b8a1d27d2fa9ceca0992274d0d56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20M=C3=BCller?= Date: Sun, 6 Sep 2026 22:41:04 +0200 Subject: [PATCH] Refactor deployment process and update documentation - Removed nginx configuration file as it is no longer needed. - Updated README.md to reflect changes in deployment structure. - Enhanced DEPLOYMENT.md with detailed directory structure and user permissions. - Added release.sh and rollback.sh scripts for managing deployments. - Improved N8N.md to clarify file access and document handling. - Adjusted health check and cache flushing procedures in deployment scripts. --- .gitea/workflows/build.yml | 262 ++++++++++++++++++++++-------------- README.md | 2 +- deploy/nginx/ekdos.conf | 97 -------------- deploy/release.sh | 126 ++++++++++++++++++ deploy/rollback.sh | 60 +++++++++ docs/DEPLOYMENT.md | 263 ++++++++++++++++++++++++++++--------- docs/N8N.md | 38 ++++++ 7 files changed, 589 insertions(+), 259 deletions(-) delete mode 100644 deploy/nginx/ekdos.conf create mode 100644 deploy/release.sh create mode 100644 deploy/rollback.sh diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 6098105..407a985 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -3,24 +3,69 @@ name: EK-DOS-WEB bauen und ausrollen on: push: branches: [main] + pull_request: + branches: [main] workflow_dispatch: +# Zwei gleichzeitige Ausrollvorgänge kämen sich beim Symlink-Tausch in die Quere. +# Ein neuer Lauf wartet, statt den laufenden abzubrechen. +concurrency: + group: ekdos-deploy + cancel-in-progress: false + env: DEPLOY_HOST: 10.0.11.131 DEPLOY_USER: deploy - DEPLOY_ROOT: /var/www/ekdos + DEPLOY_PATH: /var/www/ekdos jobs: # ────────────────────────────────────────────────────────────────────────── - # Die Oberfläche wird hier zu statischen Dateien kompiliert. Node existiert - # ausschliesslich in diesem Container; auf dem Zielserver läuft keines. + # Backend: nichts zu kompilieren, aber alles zu prüfen. Composer läuft hier + # NICHT -- die Abhängigkeiten werden auf dem Webserver aufgelöst, wo PHP- + # Version und Erweiterungen tatsächlich stimmen. Eine Plattformprüfung im + # Runner würde nur sich selbst prüfen. # ────────────────────────────────────────────────────────────────────────── - frontend: + backend-pruefen: runs-on: ubuntu-latest - container: node:22-bookworm-slim - defaults: - run: - working-directory: frontend + container: php:8.5-cli + steps: + - uses: actions/checkout@v4 + + - name: Syntax aller PHP-Dateien prüfen + working-directory: backend + run: | + set -eu + find src public bin -type f \( -name '*.php' -o -name 'ekdos' \) -print0 \ + | xargs -0 -n1 -P4 php -l + + - name: composer.json und composer.lock prüfen + working-directory: backend + run: | + set -eu + curl -sS https://getcomposer.org/installer \ + | php -- --install-dir=/usr/local/bin --filename=composer + composer validate --no-check-publish + # Ohne Sperrdatei löst der Webserver bei jedem Ausrollen neu auf und + # bekommt womöglich andere Fassungen als hier geprüft. + test -f composer.lock || { + echo "composer.lock fehlt. Einmal 'composer update' laufen lassen und einchecken." + exit 1 + } + + # ────────────────────────────────────────────────────────────────────────── + # Bauen und ausrollen in einem Auftrag. + # + # Bewusst ohne upload-/download-artifact: welche Fassung dieser Aktionen eine + # Gitea-Instanz beherrscht (v3 oder v4), hängt an deren Version und ist eine + # verlässliche Quelle für Fehlschläge beim ersten Lauf. Das Bündel bleibt + # einfach im Arbeitsverzeichnis liegen und geht von dort per rsync raus. + # + # Node existiert nur in diesem Container -- auf dem Webserver läuft keines. + # ────────────────────────────────────────────────────────────────────────── + bauen-und-ausrollen: + runs-on: ubuntu-latest + container: node:22-bookworm + needs: [backend-pruefen] steps: - uses: actions/checkout@v4 @@ -28,121 +73,144 @@ jobs: run: corepack enable && corepack prepare pnpm@10.16.1 --activate - name: Abhängigkeiten installieren + working-directory: frontend run: pnpm install --frozen-lockfile - # Typprüfung getrennt, damit ein Typfehler nicht als Bündelfehler erscheint. + # Getrennt von `build`, damit ein Typfehler als solcher im Protokoll steht + # und nicht als Bündelfehler. - name: Typen prüfen + working-directory: frontend run: pnpm typecheck - name: Bündel bauen + working-directory: frontend run: pnpm build - - uses: actions/upload-artifact@v3 - with: - name: frontend-dist - path: frontend/dist - retention-days: 7 - - # ────────────────────────────────────────────────────────────────────────── - # Das Backend wird nicht kompiliert, aber geprüft: Syntax aller Dateien und - # ein Abhängigkeitsbaum ohne Entwicklungspakete. - # ────────────────────────────────────────────────────────────────────────── - backend: - runs-on: ubuntu-latest - container: php:8.5-cli - defaults: - run: - working-directory: backend - steps: - - uses: actions/checkout@v4 - - - name: Composer und Erweiterungen bereitstellen + - name: Bündel prüfen + working-directory: frontend run: | - apt-get update && apt-get install -y --no-install-recommends git unzip libpq-dev - docker-php-ext-install pdo_pgsql - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + set -eu + test -f dist/index.html || { echo "dist/index.html fehlt -- der Bau hat nichts erzeugt."; exit 1; } + du -sh dist - - name: Abhängigkeiten installieren - run: composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-req=ext-redis + # ── Ab hier nur noch aus main ────────────────────────────────────────── - - name: Syntax prüfen - run: find src public bin -type f \( -name '*.php' -o -name 'ekdos' \) -print0 | xargs -0 -n1 php -l - - - uses: actions/upload-artifact@v3 - with: - name: backend-build - path: | - backend - !backend/.env - retention-days: 7 - - # ────────────────────────────────────────────────────────────────────────── - # Ausrollen. Jede Veröffentlichung landet in einem eigenen Verzeichnis; erst - # der Symlink-Tausch macht sie sichtbar. Ein Fehlschlag lässt die laufende - # Fassung unberührt. - # ────────────────────────────────────────────────────────────────────────── - deploy: - runs-on: ubuntu-latest - needs: [frontend, backend] - if: gitea.ref == 'refs/heads/main' - steps: - - uses: actions/download-artifact@v3 - with: - name: frontend-dist - path: dist - - - uses: actions/download-artifact@v3 - with: - name: backend-build - path: backend + - name: Werkzeuge zum Ausrollen installieren + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + set -eu + apt-get update + apt-get install -y --no-install-recommends rsync openssh-client - name: SSH einrichten + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }} run: | + set -eu + test -n "${DEPLOY_SSH_KEY:-}" || { echo "Secret DEPLOY_SSH_KEY fehlt."; exit 1; } + install -m 700 -d ~/.ssh - echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519 + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts + + # Bevorzugt der hinterlegte Fingerabdruck. Fehlt er, wird er erfragt -- + # bequem, aber ohne Schutz vor einem untergeschobenen Ziel. Für den + # Dauerbetrieb setzen: ssh-keyscan -t ed25519 10.0.11.131 + if [ -n "${DEPLOY_KNOWN_HOSTS:-}" ]; then + printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts + else + echo "Hinweis: DEPLOY_KNOWN_HOSTS ist nicht gesetzt, Fingerabdruck wird ungeprüft übernommen." + ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null + fi + chmod 600 ~/.ssh/known_hosts + + # Lieber hier scheitern als nach dem halben Hochladen. + - name: Ziel prüfen + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + set -eu + ssh "$DEPLOY_USER@$DEPLOY_HOST" " + set -eu + test -d '$DEPLOY_PATH/releases' || { echo '$DEPLOY_PATH/releases fehlt -- siehe docs/DEPLOYMENT.md'; exit 1; } + test -f '$DEPLOY_PATH/shared/.env' || { echo '$DEPLOY_PATH/shared/.env fehlt -- einmalig anlegen'; exit 1; } + command -v composer >/dev/null || { echo 'composer fehlt auf dem Webserver'; exit 1; } + command -v php >/dev/null || { echo 'php fehlt auf dem Webserver'; exit 1; } + # Prüft die sudo-Regel, ohne etwas auszuführen. + sudo -n -l /bin/systemctl reload php8.5-fpm >/dev/null 2>&1 \ + || echo 'Hinweis: sudo-Regel für den fpm-Neustart fehlt (siehe docs/DEPLOYMENT.md) -- release.sh wird daran scheitern.' + mkdir -p '$DEPLOY_PATH/releases/${{ github.sha }}' '$DEPLOY_PATH/deploy' + " - name: Oberfläche übertragen + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | - RELEASE="$DEPLOY_ROOT/releases/${{ gitea.sha }}" - ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$RELEASE'" - rsync -az --delete dist/ "$DEPLOY_USER@$DEPLOY_HOST:$RELEASE/" + set -eu + rsync -az --delete \ + frontend/dist/ \ + "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/releases/${{ github.sha }}/public/" - name: Backend übertragen + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | - # .env bleibt auf dem Server und wird nie überschrieben. + set -eu + # vendor/ entsteht auf dem Server; .env liegt in shared/ und wird dort + # verknüpft; var/ ist der Zwischenspeicher dieser Veröffentlichung. rsync -az --delete \ + --exclude 'vendor/' \ --exclude '.env' \ - --exclude 'var/cache' \ - backend/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_ROOT/backend/" + --exclude 'var/' \ + backend/ \ + "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/releases/${{ github.sha }}/backend/" - - name: Umschalten und neu laden + - name: Ausrollskripte übertragen + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | - ssh "$DEPLOY_USER@$DEPLOY_HOST" bash -euo pipefail <<'REMOTE' - cd /var/www/ekdos + set -eu + rsync -az --chmod=F755 \ + deploy/release.sh deploy/rollback.sh \ + "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/deploy/" - # Schema fortschreiben, bevor die neue Oberfläche sichtbar wird. - php backend/bin/ekdos migrate - - # Atomarer Tausch: ln -sfn auf ein temporäres Ziel, dann umbenennen. - ln -sfn "releases/${GITEA_SHA:-$(ls -1t releases | head -1)}" current.new - mv -Tf current.new current - - # Opcache hält den alten Quelltext, bis fpm neu lädt. - sudo systemctl reload php8.5-fpm - - # Zwischengespeicherte n8n-Antworten passen womöglich nicht mehr - # zum neuen Abbildungscode. - php backend/bin/ekdos cache:flush - - # Die letzten fünf Veröffentlichungen behalten. - ls -1t releases | tail -n +6 | xargs -r -I{} rm -rf "releases/{}" - REMOTE - env: - GITEA_SHA: ${{ gitea.sha }} - - - name: Gesundheitsprüfung + # Hier passiert das Eigentliche: composer install, Migration, atomarer + # Symlink-Tausch, php-fpm neu laden, n8n-Cache verwerfen, + # Gesundheitsprüfung. Das Skript liegt unter deploy/release.sh und lässt + # sich genauso von Hand aufrufen. + - name: Veröffentlichung aktivieren + id: aktivieren + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | + set -eu ssh "$DEPLOY_USER@$DEPLOY_HOST" \ - 'curl -fsS -o /dev/null -w "%{http_code}\n" http://127.0.0.1/api/health' + "$DEPLOY_PATH/deploy/release.sh '${{ github.sha }}'" + + # KEIN automatischer Rückschritt. + # + # release.sh tauscht den Symlink erst nach composer und Migration. Scheitert + # es davor, läuft die alte Fassung unverändert weiter -- ein Rückschritt + # würde dann auf die *vorletzte* schalten und aus einem folgenlosen + # Fehlschlag einen Ausfall machen. Deshalb hier nur berichten, was live ist. + - name: Bei Fehlschlag Lage berichten + if: failure() && steps.aktivieren.outcome == 'failure' + run: | + echo "Das Aktivieren ist fehlgeschlagen. Aktueller Stand auf dem Server:" + ssh "$DEPLOY_USER@$DEPLOY_HOST" " + echo -n ' aktiv: '; readlink '$DEPLOY_PATH/current' || echo '(keiner)' + echo -n ' vorher: '; cat '$DEPLOY_PATH/previous' 2>/dev/null || echo '(nichts vermerkt)' + curl -fsS --max-time 5 http://127.0.0.1/api/health && echo || echo ' /api/health antwortet NICHT' + " || true + echo + echo "Läuft die alte Fassung noch, ist nichts weiter zu tun -- Ursache beheben und erneut pushen." + echo "Sonst von Hand zurück: ssh $DEPLOY_USER@$DEPLOY_HOST $DEPLOY_PATH/deploy/rollback.sh" + exit 1 + + # /api/health ist im Port-80-Block von der HTTPS-Umleitung ausgenommen, + # sonst wäre die 301 für `curl -f` ein Erfolg und PHP nie erreicht. + - name: Ergebnis + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + set -eu + echo "Ausgerollt: ${{ github.sha }}" + ssh "$DEPLOY_USER@$DEPLOY_HOST" \ + "curl -fsS --max-time 5 -H 'Accept: application/json' http://127.0.0.1/api/health" + echo diff --git a/README.md b/README.md index 859f17b..446cc11 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ statischen Dateien kompiliert; ausgeliefert wird sie von nginx. | ------------------- | ----------------------------------------------------------------- | | `backend/` | PHP 8.5, Slim 4. BFF-Sitzung, Benutzerverwaltung, n8n-Weiterleitung | | `frontend/` | React 19 + Vite. Baut nach `frontend/dist/` | -| `deploy/nginx/` | nginx-Site | | `deploy/php-fpm/` | php-fpm-Pool | +| `deploy/*.sh` | Veröffentlichung aktivieren / zurückschalten | | `.gitea/workflows/` | Bau- und Ausrollstrecke | | `docs/` | Betrieb und n8n-Schnittstelle | | `legacy-nextjs/` | Die abgelöste Next.js-Fassung. Kann nach dem Umstieg weg | diff --git a/deploy/nginx/ekdos.conf b/deploy/nginx/ekdos.conf deleted file mode 100644 index 6db62f7..0000000 --- a/deploy/nginx/ekdos.conf +++ /dev/null @@ -1,97 +0,0 @@ -# EK-DOS-WEB -# -# nginx macht beides: es liefert das statisch gebaute React-Bündel aus und -# reicht ausschliesslich /api/ an php-fpm weiter. Auf dem Server läuft kein -# Node und kein Anwendungsserver -- nur nginx und php8.5-fpm. -# -# Ablage: /etc/nginx/sites-available/ekdos.conf -> sites-enabled/ -# Prüfen: nginx -t && systemctl reload nginx - -upstream ekdos_php { - # Unix-Socket statt TCP: kein Netzwerkstack, keine offene Portnummer. - server unix:/run/php/ekdos.sock; -} - -server { - listen 80; - listen [::]:80; - server_name schulung.elektro-krueger.local; - - # Hinter einem TLS-Terminator (Cloudflare, vorgelagertes nginx) diesen Block - # entfernen und stattdessen den 443-Block unten verwenden. - return 301 https://$host$request_uri; -} - -server { - listen 443 ssl; - listen [::]:443 ssl; - http2 on; - server_name schulung.elektro-krueger.local; - - ssl_certificate /etc/ssl/ekdos/fullchain.pem; - ssl_certificate_key /etc/ssl/ekdos/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - - # Das gebaute Bündel aus dem Gitea-Lauf. - root /var/www/ekdos/current; - index index.html; - - # Die Anwendung ist intern; sie gehört in keinen Suchindex. - add_header X-Robots-Tag "noindex, nofollow" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-Frame-Options "SAMEORIGIN" always; - add_header Referrer-Policy "same-origin" always; - - # Rechnungen und Serviceberichte sind mehrere Megabyte gross. - client_max_body_size 32m; - - # ── Statische Oberfläche ────────────────────────────────────────────── - # Vite vergibt Inhaltshashes, deshalb dürfen die Bündel dauerhaft im Cache - # liegen. Nur index.html darf das nicht, sonst sieht niemand ein Update. - location /assets/ { - expires 1y; - add_header Cache-Control "public, immutable"; - try_files $uri =404; - } - - location = /index.html { - add_header Cache-Control "no-store"; - } - - # Einseitenanwendung: jede unbekannte Adresse bekommt index.html. - location / { - try_files $uri $uri/ /index.html; - } - - # ── Backend ─────────────────────────────────────────────────────────── - location /api/ { - include fastcgi_params; - fastcgi_pass ekdos_php; - - # Einziger Einstiegspunkt. Es gibt keine weiteren .php-Dateien im Web. - fastcgi_param SCRIPT_FILENAME /var/www/ekdos/backend/public/index.php; - fastcgi_param SCRIPT_NAME /index.php; - fastcgi_param DOCUMENT_ROOT /var/www/ekdos/backend/public; - - # Der Rechnungsabgleich und grosse PDFs brauchen Luft. - fastcgi_read_timeout 60s; - fastcgi_buffering off; - - # Damit die Anmeldebremse die echte Adresse sieht. - fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for; - fastcgi_param HTTPS on; - } - - # PHP wird nirgendwo sonst ausgeführt. - location ~ \.php$ { - return 404; - } - - # Punktdateien bleiben unerreichbar (.env, .git und Ähnliches). - location ~ /\. { - deny all; - } - - access_log /var/log/nginx/ekdos.access.log; - error_log /var/log/nginx/ekdos.error.log warn; -} diff --git a/deploy/release.sh b/deploy/release.sh new file mode 100644 index 0000000..3663af2 --- /dev/null +++ b/deploy/release.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# +# Eine hochgeladene Veröffentlichung aktivieren. +# +# Läuft auf dem Webserver als Benutzer `deploy`. Die Gitea-Strecke ruft das Skript +# nach dem Hochladen auf; von Hand geht es genauso: +# +# /var/www/ekdos/deploy/release.sh 2f9c1ab +# +# Erwartete Verzeichnisse: +# +# $ROOT/releases//public das Vite-Bündel +# $ROOT/releases//backend das PHP-Backend (noch ohne vendor/) +# $ROOT/shared/.env die Konfiguration, überlebt jedes Ausrollen +# $ROOT/current -> releases/ der Symlink, den nginx liest +# +# Beide Hälften werden mit einem einzigen Symlink-Tausch gemeinsam sichtbar. +# Schlägt vorher etwas fehl, bricht das Skript ab und die laufende Fassung +# bleibt unberührt. + +set -euo pipefail + +ROOT="${EKDOS_ROOT:-/var/www/ekdos}" +KEEP="${EKDOS_KEEP:-5}" +FPM_SERVICE="${EKDOS_FPM:-php8.5-fpm}" +HEALTH_URL="${EKDOS_HEALTH_URL:-http://127.0.0.1/api/health}" + +RELEASE="${1:-}" +[ -n "$RELEASE" ] || { echo "Aufruf: release.sh " >&2; exit 2; } + +RELEASE_DIR="$ROOT/releases/$RELEASE" +BACKEND="$RELEASE_DIR/backend" + +step() { printf '\n\033[1m==> %s\033[0m\n' "$1"; } +fail() { printf '\033[31mFEHLER: %s\033[0m\n' "$1" >&2; exit 1; } + +# ── 1. Prüfen, was da hochgeladen wurde ────────────────────────────────────── +step "Veröffentlichung $RELEASE prüfen" +[ -d "$RELEASE_DIR" ] || fail "$RELEASE_DIR gibt es nicht." +[ -f "$RELEASE_DIR/public/index.html" ] || fail "Im Bündel fehlt public/index.html." +[ -f "$BACKEND/public/index.php" ] || fail "Im Backend fehlt public/index.php." +[ -f "$ROOT/shared/.env" ] || fail "$ROOT/shared/.env fehlt. Einmalig aus backend/.env.example anlegen." + +# ── 2. Gemeinsame Dateien einhängen ────────────────────────────────────────── +# Die .env gehört dem Server, nicht dem Repository, und wird nie mit ausgerollt. +step "Konfiguration verknüpfen" +ln -sfn "$ROOT/shared/.env" "$BACKEND/.env" + +# var/cache bleibt bewusst je Veröffentlichung eigen: dort liegt der von PHP-DI +# kompilierte Container. Geteilt über mehrere Veröffentlichungen hinweg würde er +# auf Klassen der alten Fassung zeigen. +mkdir -p "$BACKEND/var/cache" +chmod 2775 "$BACKEND/var" "$BACKEND/var/cache" + +# ── 3. Abhängigkeiten auf der Zielmaschine auflösen ────────────────────────── +# Bewusst hier und nicht im Gitea-Runner: nur hier stimmen PHP-Version und +# Erweiterungen (pdo_pgsql, redis), sodass die Plattformprüfung von Composer +# etwas Echtes prüft statt übersprungen zu werden. +step "Composer-Abhängigkeiten installieren" +( cd "$BACKEND" && composer install --no-dev --no-interaction --no-progress --prefer-dist --optimize-autoloader --classmap-authoritative ) + +# ── 4. Schema fortschreiben, bevor die neue Fassung sichtbar wird ──────────── +step "Datenbankschema fortschreiben" +php "$BACKEND/bin/ekdos" migrate + +# ── 5. Umschalten ──────────────────────────────────────────────────────────── +# ln -sfn legt bei einem vorhandenen Symlink sonst *im* Ziel an, deshalb erst +# einen neuen Namen anlegen und dann mit mv -T atomar darüberschieben. +step "Auf $RELEASE umschalten" +PREVIOUS="$(readlink "$ROOT/current" 2>/dev/null || echo '')" +ln -sfn "releases/$RELEASE" "$ROOT/current.new" +mv -Tf "$ROOT/current.new" "$ROOT/current" +[ -n "$PREVIOUS" ] && printf '%s\n' "$PREVIOUS" > "$ROOT/previous" || true + +# ── 6. php-fpm neu laden ───────────────────────────────────────────────────── +# Der Pool fährt mit opcache.validate_timestamps=0. Ohne Neuladen liefe weiter +# der alte Quelltext aus dem Opcache. +step "$FPM_SERVICE neu laden" +sudo systemctl reload "$FPM_SERVICE" + +# ── 7. Zwischenspeicher verwerfen ──────────────────────────────────────────── +# Zwischengespeicherte n8n-Antworten passen womöglich nicht mehr zur neuen +# Feldabbildung. Sitzungen bleiben unberührt -- niemand wird abgemeldet. +step "n8n-Cache verwerfen" +php "$BACKEND/bin/ekdos" cache:flush + +# ── 8. Nachsehen, ob es läuft ──────────────────────────────────────────────── +step "Gesundheitsprüfung" +for attempt in 1 2 3 4 5; do + if curl -fsS --max-time 5 "$HEALTH_URL" > /dev/null; then + echo " $HEALTH_URL antwortet." + break + fi + + [ "$attempt" -eq 5 ] && fail "$HEALTH_URL antwortet nicht. Zurück mit: $ROOT/deploy/rollback.sh" + echo " Versuch $attempt fehlgeschlagen, neuer Versuch …" + sleep 2 +done + +# ── 9. Aufräumen ───────────────────────────────────────────────────────────── +# Die aktuelle und die vorige Veröffentlichung bleiben in jedem Fall liegen. +step "Alte Veröffentlichungen aufräumen (die letzten $KEEP bleiben)" +KEEP_LIST="$(printf '%s\n%s\n' "$RELEASE" "$(basename "${PREVIOUS:-none}")")" + +# find statt ls: kein unexpandiertes */-Glob, das unter `pipefail` den ganzen +# Lauf nach einem bereits erfolgreichen Ausrollen scheitern liesse. +OBSOLETE="$(cd "$ROOT/releases" && find . -mindepth 1 -maxdepth 1 -type d -printf '%T@ %f\n' \ + | sort -rn | cut -d' ' -f2- | tail -n "+$((KEEP + 1))" || true)" + +if [ -n "$OBSOLETE" ]; then + printf '%s\n' "$OBSOLETE" | while read -r old; do + [ -n "$old" ] || continue + + # Die aktive und die vorige Veröffentlichung bleiben in jedem Fall liegen. + if printf '%s\n' "$KEEP_LIST" | grep -qxF "$old"; then + continue + fi + + echo " entferne $old" + rm -rf -- "$ROOT/releases/$old" + done +else + echo " nichts aufzuräumen" +fi + +step "$RELEASE ist aktiv." diff --git a/deploy/rollback.sh b/deploy/rollback.sh new file mode 100644 index 0000000..00e2d52 --- /dev/null +++ b/deploy/rollback.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# +# Auf die vorige Veröffentlichung zurückschalten. +# +# /var/www/ekdos/deploy/rollback.sh # eine zurück +# /var/www/ekdos/deploy/rollback.sh 2f9c1ab # auf eine bestimmte +# /var/www/ekdos/deploy/rollback.sh --list # zeigt, was vorrätig ist +# +# Es wird nur der Symlink getauscht und php-fpm neu geladen. Die alten +# Veröffentlichungen liegen vollständig da, ein Rückschritt dauert Sekunden. +# +# ACHTUNG: Datenbankmigrationen werden NICHT zurückgenommen. Hat die neue +# Fassung das Schema geändert, muss die alte damit umgehen können. Bei den +# bisherigen Migrationen (nur Tabellen anlegen) ist das der Fall. + +set -euo pipefail + +ROOT="${EKDOS_ROOT:-/var/www/ekdos}" +FPM_SERVICE="${EKDOS_FPM:-php8.5-fpm}" +HEALTH_URL="${EKDOS_HEALTH_URL:-http://127.0.0.1/api/health}" + +step() { printf '\n\033[1m==> %s\033[0m\n' "$1"; } +fail() { printf '\033[31mFEHLER: %s\033[0m\n' "$1" >&2; exit 1; } + +CURRENT="$(basename "$(readlink "$ROOT/current" 2>/dev/null || echo none)")" + +if [ "${1:-}" = "--list" ]; then + echo "Vorrätige Veröffentlichungen (neueste zuerst):" + ( cd "$ROOT/releases" && ls -1td -- */ | sed 's#/$##' ) | while read -r name; do + [ "$name" = "$CURRENT" ] && echo " * $name (aktiv)" || echo " $name" + done + exit 0 +fi + +TARGET="${1:-}" + +if [ -z "$TARGET" ]; then + # Ohne Argument: der Eintrag, den release.sh beim letzten Umschalten notiert hat. + [ -f "$ROOT/previous" ] || fail "Keine vorige Veröffentlichung vermerkt. Auswahl zeigen mit: rollback.sh --list" + TARGET="$(basename "$(cat "$ROOT/previous")")" +fi + +[ -d "$ROOT/releases/$TARGET" ] || fail "Die Veröffentlichung '$TARGET' gibt es nicht. Auswahl zeigen mit: rollback.sh --list" +[ "$TARGET" != "$CURRENT" ] || fail "'$TARGET' ist bereits aktiv." + +step "Zurück von $CURRENT auf $TARGET" +ln -sfn "releases/$TARGET" "$ROOT/current.new" +mv -Tf "$ROOT/current.new" "$ROOT/current" +printf '%s\n' "$CURRENT" > "$ROOT/previous" + +step "$FPM_SERVICE neu laden" +sudo systemctl reload "$FPM_SERVICE" + +step "n8n-Cache verwerfen" +php "$ROOT/current/backend/bin/ekdos" cache:flush + +step "Gesundheitsprüfung" +curl -fsS --max-time 5 "$HEALTH_URL" > /dev/null || fail "$HEALTH_URL antwortet nicht." + +step "$TARGET ist wieder aktiv." diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 64f04a3..87cdb16 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -3,14 +3,42 @@ 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). +## Verzeichnisse auf dem Webserver + +``` +/var/www/ekdos/ +├── current -> releases/ was nginx liest +├── previous Notiz für rollback.sh +├── releases/ +│ └── / +│ ├── public/ das Vite-Bündel -> nginx root +│ └── backend/ PHP + vendor/ -> fastcgi +│ ├── .env -> ../../../shared/.env +│ └── var/cache/ kompilierter DI-Container, je Veröffentlichung +├── shared/ +│ └── .env überlebt jedes Ausrollen +└── deploy/ + ├── release.sh + └── rollback.sh +``` + +Oberfläche und Backend liegen **in derselben Veröffentlichung** und werden mit +einem einzigen Symlink-Tausch gemeinsam sichtbar. Eine neue Oberfläche kann +daher nie gegen ein altes Backend laufen. + ## 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-mbstring php8.5-curl php8.5-intl php8.5-xml \ + composer rsync curl ``` +`composer` wird auf dem Webserver gebraucht: die PHP-Abhängigkeiten werden dort +aufgelöst, nicht im Gitea-Runner. Nur dort stimmen PHP-Version und Erweiterungen, +sodass die Plattformprüfung von Composer etwas Echtes prüft. + `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. @@ -29,41 +57,54 @@ Redis braucht nichts weiter -- EK-DOS legt seine Schlüssel unter dem Präfix > angemeldet und jede Ansicht fragt wieder direkt bei n8n an. Die Anwendung > läuft weiter, aber langsamer und mit erneuter Anmeldung. -## 3. Verzeichnisse +## 3. Benutzer und Rechte + +Zwei Konten: `ekdos` führt PHP aus, `deploy` nimmt die Dateien entgegen. Über +die gemeinsame Gruppe kommt php-fpm an das, was `deploy` hochlädt. ```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 useradd --create-home --shell /bin/bash deploy +sudo usermod -aG ekdos deploy + +sudo mkdir -p /var/www/ekdos/{releases,shared,deploy} +sudo chown -R deploy:ekdos /var/www/ekdos +sudo chmod -R g+rX /var/www/ekdos + +# setgid: alles neu Angelegte erbt die Gruppe ekdos, sonst käme php-fpm +# nach dem nächsten Ausrollen nicht mehr an die Dateien. +sudo find /var/www/ekdos -type d -exec chmod g+s {} + + sudo mkdir -p /var/log/php && sudo chown ekdos:ekdos /var/log/php ``` -## 4. Backend einrichten +`release.sh` lädt php-fpm neu. Dafür genau dieses eine Kommando ohne Passwort: ```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 +echo 'deploy ALL=(root) NOPASSWD: /bin/systemctl reload php8.5-fpm' \ + | sudo tee /etc/sudoers.d/ekdos-deploy +sudo chmod 440 /etc/sudoers.d/ekdos-deploy +sudo visudo -c ``` -`.env` gehört dem Benutzer `ekdos` und sollte `chmod 600` sein. Sie wird beim -Ausrollen ausdrücklich nicht überschrieben. +## 4. Konfiguration anlegen + +```bash +sudo -u deploy cp backend/.env.example /var/www/ekdos/shared/.env +sudo -u deploy editor /var/www/ekdos/shared/.env +sudo chown deploy:ekdos /var/www/ekdos/shared/.env +sudo chmod 640 /var/www/ekdos/shared/.env +``` + +`640` mit Gruppe `ekdos`: php-fpm liest sie, sonst niemand. Sie wird beim +Ausrollen ausdrücklich nie ü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(, '')` angesprochen und geht über -die RFC1918-Adresse -- er ist nach aussen nicht veröffentlicht. +| 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 | ## 5. php-fpm @@ -75,66 +116,160 @@ 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. +**erst nach einem `systemctl reload php8.5-fpm`** wirksam -- `release.sh` 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 -``` +Die nginx-Konfiguration wird auf dem Server gepflegt und gehört bewusst nicht +ins Repository. Was sie leisten muss: -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. +| Pfad | Ziel | +| ------------- | ------------------------------------------------------------------------ | +| `/api/` | `fastcgi_pass unix:/run/php/ekdos.sock`, `SCRIPT_FILENAME` auf `/var/www/ekdos/current/backend/public/index.php` | +| `/assets/` | `try_files $uri =404` -- kein SPA-Rückfall, sonst wird eine fehlende JS-Datei zu HTML mit Status 200 | +| alles andere | `root /var/www/ekdos/current/public`, `try_files $uri $uri/ /index.html` | -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`. +Drei Punkte, die erfahrungsgemäss Ärger machen: -## 7. Ausrollen +- **`HTTP_X_FORWARDED_FOR` auf `$remote_addr` setzen**, nicht auf + `$proxy_add_x_forwarded_for`. Letzteres hängt die Adresse nur an einen vom + Client mitgeschickten Header an -- damit liesse sich die Anmeldebremse mit + einem selbst gesetzten `X-Forwarded-For` umgehen. Steht ein Proxy davor, + dessen Netze über `set_real_ip_from` eintragen. +- **`add_header` wird nicht vererbt**, sobald eine `location` eine eigene + Kopfzeile setzt. Ein Block mit eigenem `Cache-Control` verliert damit alle + Sicherheitskopfzeilen des Serverblocks. +- **`/api/health` von einer HTTPS-Umleitung ausnehmen.** `release.sh` prüft + lokal über HTTP; eine 301 wäre für `curl -f` ein Erfolg, ohne dass PHP je + erreicht würde. Alternativ `EKDOS_HEALTH_URL` auf die HTTPS-Adresse setzen. -Die Gitea-Strecke (`.gitea/workflows/build.yml`) macht das bei jedem Push auf -`main`. Nötig sind: +Läuft der Zugriff nur intern über HTTP, muss `SESSION_COOKIE_SECURE=false` +gesetzt sein -- ein Secure-Cookie wird über reines HTTP nicht gesendet. -- 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 +## 7. Erstes Ausrollen von Hand -``` -deploy ALL=(root) NOPASSWD: /bin/systemctl reload php8.5-fpm -``` - -Jede Veröffentlichung landet unter `releases/`; 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 +Die Strecke übernimmt das später, aber einmal von Hand zeigt, ob alles steht: ```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' +# auf dem Arbeitsplatz +cd frontend && pnpm install && pnpm build && cd .. + +SHA=erste +ssh deploy@10.0.11.131 "mkdir -p /var/www/ekdos/releases/$SHA" +rsync -az --delete frontend/dist/ deploy@10.0.11.131:/var/www/ekdos/releases/$SHA/public/ +rsync -az --delete --exclude vendor/ --exclude .env --exclude var/ \ + backend/ deploy@10.0.11.131:/var/www/ekdos/releases/$SHA/backend/ +rsync -az --chmod=F755 deploy/release.sh deploy/rollback.sh \ + deploy@10.0.11.131:/var/www/ekdos/deploy/ + +ssh deploy@10.0.11.131 /var/www/ekdos/deploy/release.sh $SHA ``` +`release.sh` installiert die Composer-Pakete, schreibt das Schema fort, schaltet +den Symlink um, lädt php-fpm neu, verwirft den n8n-Cache und prüft +`/api/health`. Bricht etwas vorher ab, bleibt die laufende Fassung unberührt. + +Danach den ersten Administrator anlegen: + +```bash +ssh deploy@10.0.11.131 'php /var/www/ekdos/current/backend/bin/ekdos user:create' +ssh deploy@10.0.11.131 'php /var/www/ekdos/current/backend/bin/ekdos check' +``` + +## 8. Gitea-Strecke + +`.gitea/workflows/build.yml` läuft bei jedem Push auf `main`: + +| Auftrag | Container | tut | +| ---------------------- | ------------------ | ------------------------------------------------------------- | +| `backend-pruefen` | `php:8.5-cli` | `php -l` über alle Dateien, `composer validate` | +| `bauen-und-ausrollen` | `node:22-bookworm` | `pnpm typecheck` / `build`, dann rsync über SSH und `release.sh` | + +Bei einem Pull Request wird nur geprüft und gebaut; ausgerollt wird +ausschliesslich aus `main`. + +Bauen und Ausrollen liegen bewusst in **einem** Auftrag: das Bündel bleibt im +Arbeitsverzeichnis und geht von dort per rsync raus. `upload-artifact` / +`download-artifact` würden nur eine Fassungsabhängigkeit einführen (v3 oder v4, +je nach Gitea-Version) und damit einen verlässlichen Fehlschlag beim ersten Lauf. + +**Es gibt keinen automatischen Rückschritt.** `release.sh` tauscht den Symlink +erst nach Composer und Migration. Scheitert es davor, läuft die alte Fassung +unverändert weiter -- ein automatischer Rückschritt würde dann auf die +*vorletzte* schalten und aus einem folgenlosen Fehlschlag einen Ausfall machen. +Der Lauf berichtet stattdessen, was gerade aktiv ist und ob `/api/health` +antwortet; zurückgeschaltet wird bei Bedarf von Hand. + +### Schlüssel einrichten + +Auf dem Arbeitsplatz: + +```bash +ssh-keygen -t ed25519 -f gitea-deploy -C 'gitea-runner -> ekdos' -N '' +ssh-copy-id -i gitea-deploy.pub deploy@10.0.11.131 +ssh-keyscan -t ed25519 10.0.11.131 +``` + +In Gitea unter *Settings -> Secrets* zwei Einträge: + +| Secret | Inhalt | +| -------------------- | ----------------------------------------------------------- | +| `DEPLOY_SSH_KEY` | der Inhalt von `gitea-deploy` (der **private** Schlüssel) | +| `DEPLOY_KNOWN_HOSTS` | die Ausgabe von `ssh-keyscan -t ed25519 10.0.11.131` | + +`DEPLOY_KNOWN_HOSTS` ist optional -- fehlt es, übernimmt der Lauf den +Fingerabdruck ungeprüft und schreibt einen Hinweis ins Protokoll. Für den +Dauerbetrieb setzen. + +Den privaten Schlüssel danach vom Arbeitsplatz löschen; er liegt in Gitea. + +### Lockfiles + +Beide Aufträge bestehen auf Sperrdateien, sonst löst jeder Lauf neu auf und +bekommt womöglich andere Fassungen als geprüft: + +```bash +cd backend && composer update # erzeugt composer.lock +cd frontend && pnpm install # erzeugt pnpm-lock.yaml +``` + +Beide einchecken. + +## Zurückschalten + +```bash +ssh deploy@10.0.11.131 /var/www/ekdos/deploy/rollback.sh --list # was liegt da +ssh deploy@10.0.11.131 /var/www/ekdos/deploy/rollback.sh # eine zurück +ssh deploy@10.0.11.131 /var/www/ekdos/deploy/rollback.sh 2f9c1ab # auf eine bestimmte +``` + +Es wird nur der Symlink getauscht und php-fpm neu geladen -- Sekunden. Die +letzten fünf Veröffentlichungen bleiben liegen. + +> **Migrationen werden nicht zurückgenommen.** Hat eine Fassung das Schema +> geändert, muss die alte damit umgehen können. Bei den bisherigen Migrationen +> (nur Tabellen anlegen) ist das der Fall. + ## Prüfen, wenn etwas klemmt ```bash -php backend/bin/ekdos check # Postgres, Redis, n8n -curl -sS https:///api/health # ohne Anmeldung erreichbar -tail -f /var/log/php/ekdos-error.log # Ausnahmen aus dem Backend +php /var/www/ekdos/current/backend/bin/ekdos check # Postgres, Redis, n8n +curl -fsS http://127.0.0.1/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` | +| Symptom | meistens | +| ----------------------------------------------- | ------------------------------------------------------------------ | +| 502 auf `/api/` | Socket fehlt oder falsche Rechte; `systemctl status php8.5-fpm` | +| 403 auf `/api/`, im Fehlerlog „Permission denied" | setgid fehlt, php-fpm kommt nicht an die neue Veröffentlichung | +| Anmeldung wirkt, danach sofort abgemeldet | Redis nicht erreichbar, oder `SESSION_COOKIE_SECURE=true` ohne TLS | +| Listen leer, „n8n 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; `sudo systemctl reload php8.5-fpm` | +| Ausrollen bricht bei „composer install" ab | Erweiterung fehlt; `composer check-platform-reqs` auf dem Server | ## Alle Benutzer aussperren diff --git a/docs/N8N.md b/docs/N8N.md index ced2990..57b8345 100644 --- a/docs/N8N.md +++ b/docs/N8N.md @@ -107,3 +107,41 @@ 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. + +## Wo die Dateien tatsächlich liegen + +EK-DOS-WEB liest **keine einzige Datei**. Weder die Next.js-Fassung noch das +PHP-Backend enthalten einen Dateisystemzugriff; der alte Container hatte auch +keinen Mount. Es gibt auch keinen PDF-Renderer: nichts wird erzeugt, nur +weitergereicht. + +Alle Dokumente kommen als HTTP-Antwort aus n8n: + +| Route | n8n liefert | EK-DOS-WEB tut | +| --------------------------------- | ---------------------- | ---------------------------- | +| `/api/tickets/digitale-akte` | PDF-Bytes | durchreichen | +| `/api/tickets/servicebericht` | PDF-Bytes | durchreichen | +| `/api/customers/digitale-akte` | PDF-Bytes | durchreichen | +| `/api/customer-invoices/pdf` | PDF-Bytes | Pfad prüfen, durchreichen | +| `/api/invoices-create/digitale-akte` | base64 im JSON | dekodieren, durchreichen | +| `/api/hours` | JSON-Report | Felder abbilden | + +Der Mount `/mnt/n8n-nas` gehört zum **n8n-Container**, nicht zu dieser +Anwendung. Dort wird die Datei gelesen; EK-DOS-WEB fragt nur über HTTP danach. +Für den Webserver heisst das: er braucht keinen NAS-Zugang, keine SMB-Zugangs- +daten und keinen Mount. Ein Umzug des Webservers auf eine andere Maschine +berührt die Dateiablage nicht. + +Zwei Dinge, die häufig hierher vermutet werden, aber nicht hier liegen: + +- **Stundennachweise** sind kein Dokument. `/api/hours` liefert einen JSON-Report, + den n8n aus mehreren Quellen zusammensetzt. Es wird keine Datei gelesen. +- **Tageszusammenfassung und Tagesübersicht** kommen in der Anwendung nicht vor. + „Tageszusammenfassung" ist die Beschriftung von Workflow 9/13 im Schulungs- + diagramm; „TAGESÜBERSICHT" ist die Überschrift über der Begrüssung auf der + Startseite. Keines von beidem lädt Daten. + +**Zeitgesteuerte Abläufe (Druckaufträge, morgendliche Berichte) gibt es in +EK-DOS-WEB nicht.** Es existiert kein Zeitplaner, kein Cron-Eintrag und kein +Druckcode -- weder alt noch neu. Solche Abläufe sind Schedule-Trigger in n8n und +laufen unabhängig von dieser Anwendung weiter. Der Umbau auf PHP berührt sie nicht.