From 4fb17a09e6aec81838303f59572bb7fcef09d89b Mon Sep 17 00:00:00 2001 From: Micha Date: Tue, 2 Jun 2026 06:28:01 +0200 Subject: [PATCH] apps: add n8n + mail-to-gitea-issue workflow (n8n.kaleschke.info) --- HOMELAB_ARCHITECTURE_MASTER_V2.md | 3 + apps/n8n/docker-compose.yml | 53 ++++ apps/n8n/workflows/mail-to-gitea-issue.json | 284 ++++++++++++++++++++ docs/SECRETS_MAP.md | 5 + docs/SERVICE_CATALOG.md | 1 + 5 files changed, 346 insertions(+) create mode 100644 apps/n8n/docker-compose.yml create mode 100644 apps/n8n/workflows/mail-to-gitea-issue.json diff --git a/HOMELAB_ARCHITECTURE_MASTER_V2.md b/HOMELAB_ARCHITECTURE_MASTER_V2.md index 6d40804..0a291d3 100644 --- a/HOMELAB_ARCHITECTURE_MASTER_V2.md +++ b/HOMELAB_ARCHITECTURE_MASTER_V2.md @@ -163,6 +163,7 @@ Diese Dienste sind **keine Public Apps**: - `monitoring-grafana` — monitoring.kaleschke.info (Middleware) - `hermes-dashboard` — hermes.kaleschke.info (Middleware) - `super-productivity` — sp.kaleschke.info (Middleware) +- `n8n` — n8n.kaleschke.info (Traefik ohne pauschale Middleware, native Auth + Webhook-Ausnahme analog Komodo) - `Traefik-Dashboard` - `AdGuard Home` — Admin-UI auf Port 8082 (`80` im Container), kein Traefik, nur Tailscale-IP `100.80.98.33`; 2026-05-26 bewusst keine 2FA-/Traefik-Umstellung @@ -274,6 +275,7 @@ Legende Status: | `nextcloud` | ✅ | `frontend_net`, `nextcloud_internal` | Traefik | aktiv via `cloud.kaleschke.info`, nativer Nextcloud-Login, WebDAV/CardDAV faehig | CalDAV/CardDAV-Redirect via Traefik-Labels | | `plex` | ✅ | `host` | Plex native, **LAN/Tailscale-only** (Remote Access aus seit 2026-05-28) | Compose-Stack unter `host-services/plex/`; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme; Server geclaimt von `Xeridos`; Smart-TVs (Schlafzimmer, Wohnzimmer) ueber WLAN-LAN per mDNS | — | | `super-productivity` | ✅ vorbereitet | `frontend_net` | Traefik + Middleware | Persoenliche Task-PWA des Operators; Issues kommen aus Gitea `Micha/mails` via n8n-Mail-Workflow | Deploy + Webhook + DNS-Eintrag offen | +| `n8n` | ✅ vorbereitet | `frontend_net` | Traefik, native Auth (keine pauschale Authelia) | Workflow-Automation; erster Workflow: GMX-Mail -> OpenAI-Extraktion -> Gitea-Issue in `Micha/mails`; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret | Deploy + Webhook + Owner-Setup offen | ### 7.5 Admin / Operations @@ -404,6 +406,7 @@ Für den laufenden Betrieb gilt stattdessen: | `nextcloud` | keine zentrale ForwardAuth-Middleware | Nextcloud bringt eigene Auth, Clients und WebDAV/CardDAV-Endpunkte mit; Traefik bleibt Reverse Proxy, Auth bleibt app-nativ | | `monitoring-influxdb3-core` | Host-Port 8181 auf LAN-IP; `user: "0"` | Home Assistant laeuft in einer VM ausserhalb des Compose-Netzes und muss Metriken schreiben koennen; keine Traefik-Route, kein `frontend_net`, Zugriff nur ueber Token und LAN-IP `INFLUXDB_BIND_IP`; InfluxDB 3 Core benoetigt im aktuellen Container-Setup Root-Rechte fuer den lokalen Object-Store-Pfad im named volume | | `monitoring-promtail` | Docker-Socket read-only | Docker-Log-Discovery fuer Loki; keine Schreibrechte, keine Appdaten-Persistenz ueber den Socket | +| `n8n` | keine pauschale Authelia-Middleware | Webhook-Endpunkte (`/webhook/*`, `/webhook-test/*`) muessen ohne ForwardAuth erreichbar bleiben; n8n bringt eigene Owner-/Login-Auth mit (analog Komodo/Nextcloud) | --- diff --git a/apps/n8n/docker-compose.yml b/apps/n8n/docker-compose.yml new file mode 100644 index 0000000..56f54c4 --- /dev/null +++ b/apps/n8n/docker-compose.yml @@ -0,0 +1,53 @@ +services: + n8n: + # TODO (Codex, erster Deploy): Digest am Container per `docker inspect` auslesen + # und Tag durch `docker.n8n.io/n8nio/n8n:2.22.6@sha256:` ersetzen. + image: docker.n8n.io/n8nio/n8n:2.22.6 + container_name: n8n + restart: unless-stopped + + security_opt: + - no-new-privileges:true + + dns: + - 1.1.1.1 + - 8.8.8.8 + + environment: + TZ: Europe/Berlin + GENERIC_TIMEZONE: Europe/Berlin + + N8N_HOST: n8n.kaleschke.info + N8N_PORT: "5678" + N8N_PROTOCOL: https + N8N_EDITOR_BASE_URL: https://n8n.kaleschke.info/ + WEBHOOK_URL: https://n8n.kaleschke.info/ + N8N_PROXY_HOPS: "1" + + N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY} + + N8N_DIAGNOSTICS_ENABLED: "false" + N8N_PERSONALIZATION_ENABLED: "false" + N8N_HIRING_BANNER_ENABLED: "false" + N8N_RUNNERS_ENABLED: "true" + N8N_BLOCK_ENV_ACCESS_IN_NODE: "true" + + volumes: + - /mnt/user/appdata/n8n/data:/home/node/.n8n + + networks: + - frontend_net + + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend_net" + - "traefik.http.routers.n8n.rule=Host(`n8n.kaleschke.info`)" + - "traefik.http.routers.n8n.entrypoints=websecure" + - "traefik.http.routers.n8n.tls=true" + - "traefik.http.routers.n8n.tls.certresolver=le" + - "traefik.http.routers.n8n.middlewares=secure-headers@file" + - "traefik.http.services.n8n.loadbalancer.server.port=5678" + +networks: + frontend_net: + external: true diff --git a/apps/n8n/workflows/mail-to-gitea-issue.json b/apps/n8n/workflows/mail-to-gitea-issue.json new file mode 100644 index 0000000..96d81a5 --- /dev/null +++ b/apps/n8n/workflows/mail-to-gitea-issue.json @@ -0,0 +1,284 @@ +{ + "name": "GMX -> OpenAI -> Gitea Issue (Super Productivity)", + "nodes": [ + { + "parameters": { + "pollTimes": { + "item": [ + { + "mode": "everyMinute" + } + ] + }, + "format": "simple", + "options": { + "customEmailConfig": "[\"UNSEEN\"]", + "forceReconnect": 15 + }, + "postProcessAction": "read" + }, + "id": "11111111-1111-1111-1111-111111111111", + "name": "IMAP: GMX UNSEEN", + "type": "n8n-nodes-base.emailReadImap", + "typeVersion": 2, + "position": [ + 240, + 300 + ], + "credentials": { + "imap": { + "id": "REPLACE_GMX_IMAP_CRED_ID", + "name": "GMX IMAP" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "a1", + "name": "from", + "value": "={{ $json.from }}", + "type": "string" + }, + { + "id": "a2", + "name": "subject", + "value": "={{ $json.subject }}", + "type": "string" + }, + { + "id": "a3", + "name": "date", + "value": "={{ $json.date }}", + "type": "string" + }, + { + "id": "a4", + "name": "messageId", + "value": "={{ $json.messageId || $json['message-id'] || '' }}", + "type": "string" + }, + { + "id": "a5", + "name": "text", + "value": "={{ ($json.text || $json.textPlain || '').toString().slice(0, 8000) }}", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "22222222-2222-2222-2222-222222222222", + "name": "Extract mail fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 460, + 300 + ] + }, + { + "parameters": { + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"model\": \"gpt-4o-mini\",\n \"temperature\": 0.2,\n \"response_format\": {\n \"type\": \"json_schema\",\n \"json_schema\": {\n \"name\": \"issue_extraction\",\n \"strict\": true,\n \"schema\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"title\", \"body_md\", \"priority\", \"due_date\", \"category\"],\n \"properties\": {\n \"title\": { \"type\": \"string\", \"maxLength\": 80 },\n \"body_md\": { \"type\": \"string\" },\n \"priority\": { \"type\": \"string\", \"enum\": [\"niedrig\", \"normal\", \"hoch\"] },\n \"due_date\": { \"type\": [\"string\", \"null\"], \"description\": \"ISO YYYY-MM-DD oder null\" },\n \"category\": { \"type\": \"string\" }\n }\n }\n }\n },\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Du extrahierst aus einer E-Mail eine Aufgabe fuer ein Issue-Tracking-System. Antworte ausschliesslich gemaess JSON-Schema. Sprache: Deutsch.\\n- title: imperativ, max. 80 Zeichen, ohne abschliessenden Punkt.\\n- body_md: 2 bis 6 Saetze. Was ist zu tun, warum, bis wann. Keine Begruessungen.\\n- priority: niedrig | normal | hoch.\\n- due_date: ISO YYYY-MM-DD wenn aus Mail ableitbar, sonst null.\\n- category: kurzes Schlagwort (rechnung, termin, technik, familie, sonstiges, ...).\"\n },\n {\n \"role\": \"user\",\n \"content\": {{ JSON.stringify('Absender: ' + $json.from + '\\nDatum: ' + $json.date + '\\nBetreff: ' + $json.subject + '\\n\\nMailtext:\\n' + $json.text) }}\n }\n ]\n}", + "options": { + "timeout": 60000 + } + }, + "id": "33333333-3333-3333-3333-333333333333", + "name": "OpenAI: extract issue", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 680, + 300 + ], + "credentials": { + "httpHeaderAuth": { + "id": "REPLACE_OPENAI_HEADER_AUTH_CRED_ID", + "name": "OpenAI Bearer" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "b1", + "name": "extracted", + "value": "={{ JSON.parse($json.choices[0].message.content) }}", + "type": "object" + }, + { + "id": "b2", + "name": "mail", + "value": "={{ $('Extract mail fields').item.json }}", + "type": "object" + } + ] + }, + "options": {} + }, + "id": "44444444-4444-4444-4444-444444444444", + "name": "Parse OpenAI JSON", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 900, + 300 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "c1", + "name": "title", + "value": "={{ ($json.extracted.priority === 'hoch' ? '[P1] ' : '') + $json.extracted.title }}", + "type": "string" + }, + { + "id": "c2", + "name": "body", + "value": "={{ $json.extracted.body_md + '\\n\\n---\\n**Kategorie:** ' + $json.extracted.category + '\\n**Prioritaet:** ' + $json.extracted.priority + ($json.extracted.due_date ? '\\n**Faellig:** ' + $json.extracted.due_date : '') + '\\n**Quelle:** Mail von ' + $json.mail.from + ' (' + $json.mail.date + ')\\n**Betreff:** ' + $json.mail.subject + '\\n**Message-ID:** ' + $json.mail.messageId }}", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "55555555-5555-5555-5555-555555555555", + "name": "Build issue payload", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1120, + 300 + ] + }, + { + "parameters": { + "method": "POST", + "url": "https://git.kaleschke.info/api/v1/repos/Micha/mails/issues", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Accept", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"title\": {{ JSON.stringify($json.title) }},\n \"body\": {{ JSON.stringify($json.body) }},\n \"assignees\": [\"Micha\"]\n}", + "options": { + "timeout": 30000 + } + }, + "id": "66666666-6666-6666-6666-666666666666", + "name": "Gitea: create issue", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1340, + 300 + ], + "credentials": { + "httpHeaderAuth": { + "id": "REPLACE_GITEA_HEADER_AUTH_CRED_ID", + "name": "Gitea Token" + } + } + } + ], + "connections": { + "IMAP: GMX UNSEEN": { + "main": [ + [ + { + "node": "Extract mail fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract mail fields": { + "main": [ + [ + { + "node": "OpenAI: extract issue", + "type": "main", + "index": 0 + } + ] + ] + }, + "OpenAI: extract issue": { + "main": [ + [ + { + "node": "Parse OpenAI JSON", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse OpenAI JSON": { + "main": [ + [ + { + "node": "Build issue payload", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build issue payload": { + "main": [ + [ + { + "node": "Gitea: create issue", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "meta": { + "instanceId": "homelab-n8n" + }, + "tags": [] +} diff --git a/docs/SECRETS_MAP.md b/docs/SECRETS_MAP.md index 8e832f1..b9642d1 100644 --- a/docs/SECRETS_MAP.md +++ b/docs/SECRETS_MAP.md @@ -55,6 +55,10 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb | Monitoring Grafana -> InfluxDB | Datasource Token | `/mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt` -> Docker Secret `/run/secrets/monitoring_grafana_influxdb_token` | aktiv | | Home Assistant -> InfluxDB | HA InfluxDB Token | `/homeassistant/secrets.yaml` -> `influxdb3_homeassistant_token` | geplant | | Renovate Bot | Gitea Service-Account PAT | `/mnt/user/appdata/secrets/renovate_token.txt` -> Host-Datei (chmod 600), gelesen von `ops/renovate/run-renovate.sh` und an Renovate-Container als `RENOVATE_TOKEN` weitergegeben | aktiv nach Operator-Setup (siehe `docs/RENOVATE.md`) | +| n8n | Encryption Key fuer interne Credential-Verschluesselung | Komodo Stack ENV `${N8N_ENCRYPTION_KEY}`; kein `_FILE`-Support im Upstream-Image | aktiv | +| n8n | GMX IMAP Login (Mail-Trigger Workflow) | n8n Credentials Store (Typ `imap`), nur in `/mnt/user/appdata/n8n/data` mit `N8N_ENCRYPTION_KEY` verschluesselt | aktiv | +| n8n | OpenAI API Key (LLM-Extraktion Workflow) | n8n Credentials Store (Typ `httpHeaderAuth`, Header `Authorization: Bearer ...`) | aktiv | +| n8n | Gitea PAT fuer `n8n-bot` (Issue-Erstellung Workflow) | n8n Credentials Store (Typ `httpHeaderAuth`, Header `Authorization: token ...`); separater Bot-User mit Scope `write:issue` auf `Micha/mails` | aktiv | --- @@ -128,6 +132,7 @@ Einige Secrets liegen bewusst nur als Komodo Stack Environment Variables vor, we | `komodo-core` | `KOMODO_SECRET_KEY`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET`, `KOMODO_MONGO_PASSWORD`, `KOMODO_PERIPHERY_PASSKEY` | Vaultwarden -> externe Notiz (Henne-Ei: Komodo-Mongo-Dump ist hier **nicht** Restore-Quelle, weil Komodo dafuer schon laufen muesste) | siehe `docs/SERVICES_RECOVERY.md` Komodo-Bootstrap; ohne diese Werte ist der Self-Stack nicht reproduzierbar | | `hermes-agent` | `HERMES_DASHBOARD_HOST` plus Provider-/API-/Home-Assistant-Tokens in Host-`.env` | Vaultwarden -> externe Notiz | Stack ist aktuell geparkt (Review 2026-07-25); ohne Werte bleibt der Stack deaktiviert, kein Schaden am Rest | | `glance` | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` | Provider-UIs (Immich, AdGuard, Speedtest-Tracker) neu erzeugen | rebuildbar; Widgets bleiben leer bis Tokens neu erzeugt sind, kein kritischer Datentopf | +| `n8n` | `N8N_ENCRYPTION_KEY` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | Bei Verlust aller Quellen: n8n startet, aber **alle gespeicherten Credentials sind unbrauchbar** (Re-Eingabe noetig: GMX IMAP, OpenAI, Gitea PAT). Workflows bleiben strukturell erhalten. | ### Komodo-Sonderfall diff --git a/docs/SERVICE_CATALOG.md b/docs/SERVICE_CATALOG.md index c96b43e..57209b3 100644 --- a/docs/SERVICE_CATALOG.md +++ b/docs/SERVICE_CATALOG.md @@ -78,6 +78,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und | `monitoring-influxdb3-core` | InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten | `monitoring/docker-compose.yml` | Host-Port `8181` je `INFLUXDB_BIND_IP`, keine Public URL | Monitoring-Grafana, Home Assistant Writer | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | Tier 3 | nein | 2026-05-31 effektiv auf `127.0.0.1:8181` gebunden, also nicht LAN-exponiert; `user: "0"` ist fuer den lokalen Object-Store-Pfad dokumentiert; uebernimmt den bisherigen InfluxDB-Daten-/Token-Katalog; `401 Unauthorized` beim Curl ohne Token ist erwarteter Reachability-Test | | `hermes-gateway` | Hermes Agent Gateway/API intern | `ops/hermes-agent/docker-compose.yml` | intern `8642` auf `hermes_net` | SSH Runner (VM 192.168.178.143), LLM Provider, optional Home Assistant | `/mnt/user/appdata/hermes-agent/data`, SSH key path | Tier 3, Borg/Share | nein | NAS-Stack bleibt deaktiviert, solange die separate Hermes-VM/Runner-Seite nicht wiederhergestellt ist; kein Docker-Socket | | `hermes-dashboard` | Hermes Dashboard | `ops/hermes-agent/docker-compose.yml` | `https://hermes.kaleschke.info` via `${HERMES_DASHBOARD_HOST}` | `hermes-gateway`, Traefik + Authelia | shared read-only data mount | Tier 3, Borg/Share | ja + Authelia | Compose-Profil `dashboard`; aktuell VM-seitig offen, nicht Teil des NAS-Finalstarts | +| `n8n` | Workflow-Automation; aktuell genutzt fuer Mail->LLM->Gitea-Issue (Inbox `Micha/mails`) | `apps/n8n/docker-compose.yml`, `apps/n8n/workflows/*.json` | `https://n8n.kaleschke.info` | Traefik (ohne pauschale Authelia, analog Komodo/Nextcloud), GMX IMAP, OpenAI API, Gitea API | `/mnt/user/appdata/n8n/data` (SQLite, Credentials, Workflows) | Tier 2, Borg + `n8n-data` (Credentials sind nur mit `N8N_ENCRYPTION_KEY` entschluesselbar) | ja, native Auth | Wegen Webhook-Endpunkten (`/webhook/*`) bewusst ohne `authelia@file`; eigene Login-/Owner-Auth bleibt Pflicht; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret, Verlust macht Credentials unbrauchbar. | ## Host Operations