Replace Clay with n8n, Ollama, SearXNG and browserless; add Telegram assistant

Remove the Clay role (Claude Code web UI) and its Anthropic API key.

New roles:
- ollama: local CPU LLM (qwen3:4b-instruct), internal network only
- browserless: headless Chromium for JavaScript-heavy pages
- searxng: metasearch backend for the chat web_search tool
- n8n at agent.ladkau.de: web chat (/chat), scheduled workflows,
  Telegram bot; workflows and credentials are imported from the repo

Telegram bot: Gemini 3.8 -> Gemini 3.5 -> local Qwen fallback chain,
/qwen to force the local model, /stocks and a weekday schedule
(07:30, 13:00, 17:40) for the MTE.DE / PTX.F stock report.

Also: dashboard and check-services entries for the chat, vault checks
for the new secrets (n8n, SearXNG, Gemini, Telegram, SMTP), updated
CLAUDE.md, runbooks and README.
This commit is contained in:
ml
2026-10-03 17:10:33 +02:00
parent b3cf4e250a
commit 974486bc53
37 changed files with 2805 additions and 290 deletions
@@ -0,0 +1,77 @@
# Managed by Ansible — do not edit manually
services:
n8n:
image: n8nio/n8n:{{ n8n_version }}
container_name: n8n
restart: unless-stopped
environment:
N8N_HOST: "{{ domain_n8n }}"
N8N_PROTOCOL: "https"
N8N_PORT: "5678"
# Public URL used to build webhook / chat links behind Traefik
WEBHOOK_URL: "https://{{ domain_n8n }}/"
N8N_PROXY_HOPS: "1"
N8N_ENCRYPTION_KEY: "{{ n8n_encryption_key }}"
GENERIC_TIMEZONE: "{{ n8n_timezone }}"
TZ: "{{ n8n_timezone }}"
N8N_DIAGNOSTICS_ENABLED: "false"
N8N_VERSION_NOTIFICATIONS_ENABLED: "false"
N8N_TEMPLATES_ENABLED: "false"
# Keep the SQLite database small: drop execution history after 7 days
EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: "168"
volumes:
- {{ n8n_data_dir }}/data:/home/node/.n8n
# Workflows and credentials imported by Ansible (see roles/n8n/tasks/main.yml)
- {{ n8n_data_dir }}/import:/import:ro
mem_limit: {{ n8n_mem_limit }}
networks:
- traefik_public
- ollama_internal
- browserless_internal
- searxng_internal
labels:
- "traefik.enable=true"
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
# Editor, webhooks and chat page — all behind basic auth
- "traefik.http.routers.n8n.rule=Host(`{{ domain_n8n }}`)"
- "traefik.http.routers.n8n.entrypoints=websecure"
- "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
- "traefik.http.routers.n8n.service=n8n"
- "traefik.http.routers.n8n.middlewares=n8n-auth"
# Short link: /chat redirects to the hosted chat page of the chat workflow
- "traefik.http.routers.n8n-chat.rule=Host(`{{ domain_n8n }}`) && Path(`/chat`)"
- "traefik.http.routers.n8n-chat.priority=100"
- "traefik.http.routers.n8n-chat.entrypoints=websecure"
- "traefik.http.routers.n8n-chat.tls.certresolver=letsencrypt"
- "traefik.http.routers.n8n-chat.service=n8n"
- "traefik.http.routers.n8n-chat.middlewares=n8n-auth,n8n-chat-redirect"
- "traefik.http.middlewares.n8n-chat-redirect.redirectregex.regex=^https://{{ domain_n8n | replace('.', '\\\\.') }}/chat/?$$"
- "traefik.http.middlewares.n8n-chat-redirect.redirectregex.replacement=https://{{ domain_n8n }}/webhook/{{ n8n_chat_webhook_id }}/chat"
# Telegram bot webhook: called by Telegram's servers, so no basic auth. Restricted to Telegram's
# address ranges; n8n additionally checks Telegram's secret token header and the workflow
# only answers the user id in telegram_user_id.
- "traefik.http.routers.n8n-telegram.rule=Host(`{{ domain_n8n }}`) && Path(`/webhook/{{ n8n_telegram_webhook_id }}/webhook`) && Method(`POST`)"
- "traefik.http.routers.n8n-telegram.priority=200"
- "traefik.http.routers.n8n-telegram.entrypoints=websecure"
- "traefik.http.routers.n8n-telegram.tls.certresolver=letsencrypt"
- "traefik.http.routers.n8n-telegram.service=n8n"
{% if telegram_webhook_source_ranges | length > 0 %}
- "traefik.http.routers.n8n-telegram.middlewares=n8n-telegram-ips"
- "traefik.http.middlewares.n8n-telegram-ips.ipallowlist.sourcerange={{ telegram_webhook_source_ranges | join(',') }}"
{% endif %}
{% set ns = namespace(entries=[]) %}
{% for user in n8n_users %}
{% set ns.entries = ns.entries + [user.username + ':' + (user.password | password_hash('bcrypt', (user.username | hash('md5'))[:22]) | replace('$', '$$'))] %}
{% endfor %}
- "traefik.http.middlewares.n8n-auth.basicauth.users={{ ns.entries | join(',') }}"
networks:
traefik_public:
external: true
ollama_internal:
external: true
browserless_internal:
external: true
searxng_internal:
external: true