diff --git a/.claude/JARVIS_FEATURES.md b/.claude/JARVIS_FEATURES.md new file mode 100644 index 0000000..66fb9b0 --- /dev/null +++ b/.claude/JARVIS_FEATURES.md @@ -0,0 +1,11 @@ +### Funktionen ### +- Bestellungen erfassen. Eventuell ein KanBanboard, Nextcloud Deck und/oder eine Excel-Tabelle. Spracheingaben als Bestellung/Auftrag anlegen. +- Lernmodus fΓΌr zBsp. Dokumente, Betriebsanleitungen, technische Daten usw. Also eine Wissensdatenbank aufnauen. +- Anbindung an Lexware.de ΓΌber API um Angebote und Artikel anzulegen. +- Mehrbenutzerfunktion +- Multitenant Setup fΓΌr mehrere Kunden. +- SSO Anbindung mittels Authentik. +- GIT Anbindung an gitea.mbo-tech-it.de Repro: https://gitea.mbo-tech-it.de/claude/jarvis-assist.git +- Einstellungen, extra MenΓΌ Punkt mit den einzelnen Einstellungen wie E-Mailadresse Firmennamen. Namen des Assistenten usw. +- Zugriff auf E-Mails und den kompletten Text um Termine zu filtern und Zusammenfassungen zu schreiben. +- E-Mails im Postfach in Unterordner verschieben. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..9dc6a1c --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "additionalDirectories": [ + "C:\\Users\\Jonny\\.superpowers" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd42457 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Python bytecode cache +__pycache__/ +*.pyc + +# Node dependencies & build output +node_modules/ +dist/ + +# Real environment files (secrets/config) - see *.example files for templates +.env +.env.local +.env.production + +# Local, personal Claude Code settings (not shared) +.claude/settings.local.json diff --git a/Claude outputs/JARVIS_HANDOFF.md b/Claude outputs/JARVIS_HANDOFF.md new file mode 100644 index 0000000..d58a724 --- /dev/null +++ b/Claude outputs/JARVIS_HANDOFF.md @@ -0,0 +1,666 @@ +# πŸ€– JARVIS - Übergabe Dokumentation + +**Projekt:** JARVIS KI-Assistent + Business Automation +**Erstes Deployment:** 12.09.2026 +**Status:** βœ… Live auf VPS 72.61.186.98 (Postgres, Redis, Ollama, n8n, JARVIS-API, JARVIS-Web) +**Owner:** Jonny (MBO-Tech-IT) + +--- + +## πŸ“‹ Übersicht + +JARVIS ist ein **vollstΓ€ndig selbst gehosteter AI-Assistant + Business Automation Stack** auf einem VPS mit Docker-Orchestrierung. Chat-Zugriff lΓ€uft ΓΌber eine eigene Web-OberflΓ€che (React) und spricht ΓΌber Claude Tool Use direkt mit Kalender und E-Mail-Postfach. + +**Kernkomponenten:** +- πŸ€– **KI-Backend**: Claude API (Chat + Tool Use) + Ollama (lokale Embeddings) +- πŸ’Ύ **Datenbank**: PostgreSQL mit `pgvector`-Extension (strukturierte Daten + Vector Search in einer DB, kein separates Milvus) +- ⚑ **Cache**: Redis (aktuell nur fΓΌrs Health-Check vorbereitet, noch nicht aktiv genutzt) +- πŸ”„ **Automation**: n8n (Workflow Engine) - Wetter- und E-Mail-Cache laufen bereits darΓΌber +- πŸ“‘ **API**: FastAPI (Python Backend, `main.py`) +- 🌐 **Web-Frontend**: React/Vite, nginx-served +- πŸ”Œ **Externe Integrationen**: Nextcloud-Kalender (CalDAV), E-Mail-Postfach (IMAP/SMTP) +- 🌐 **Reverse Proxy**: bereits vorhandener, JARVIS-fremder Traefik auf dem VPS (siehe unten) +- 🐳 **Container**: 6 Docker-Container (siehe "Docker Container Status") + +--- + +## πŸ”‘ Zugriffsdaten + +``` +VPS IP: 72.61.186.98 +User: jarvis-core +SSH Key: ~/.ssh/jarvis_core_key +Domain: jarvis.mbo-tech-it.de +Location: /home/jarvis-core/jarvis +``` + +### SSH-Zugriff +```bash +ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98 +``` +Die Flags `-F /dev/null -o IdentitiesOnly=yes` sind nΓΆtig, damit die globale +lokale `~/.ssh/config` nicht in die SchlΓΌsselauswahl reinpfuscht. + +**Wichtig:** `jarvis-core` muss in `/etc/ssh/sshd_config` unter `AllowUsers` stehen, sonst wird jeder Key mit "Permission denied" abgelehnt, obwohl der Key korrekt ist (passiert am 12.09.2026, gefixt durch Ergaenzen von `jarvis-core` in der `AllowUsers`-Zeile + `systemctl reload ssh`). + +### n8n-Zugriff +``` +URL: https://n8n.jarvis.mbo-tech-it.de +User: jonny@mbo-tech-it.de (Owner-Account, Passwort nicht in dieser Doku) +``` +API-Keys fuer die n8n-REST-API werden unter Settings -> n8n API verwaltet +(pro Key eigenes Ablaufdatum). Aktuell existieren: "JARVIS Weather Workflow" +(Ablauf 12.10.2026) und "JARVIS Email Workflow Runtime" (Ablauf 13.10.2026, +fuer den E-Mail-Cache-Workflow angelegt). Die Key-Werte selbst sind nirgends +dauerhaft gespeichert (nur beim Erstellen einmalig sichtbar) - bei Bedarf +einfach einen neuen erstellen. + +### Database Credentials +``` +PostgreSQL User: jarvis +PostgreSQL Database: jarvis (n8n hat eine eigene DB "n8n" in derselben Instanz) +PostgreSQL Port: 5432 +Password: in .env (DB_PASSWORD) +``` + +### Externe Dienste (Zugangsdaten in `.env` auf dem VPS) +- **Nextcloud-Kalender** (`NEXTCLOUD_CALDAV_URL`, `NEXTCLOUD_USER`, `NEXTCLOUD_APP_PASSWORD`): `https://cloud.ffw-onza.de/.../ffw-onza-alle/`, App-Passwort-Auth +- **E-Mail-Postfach** (`EMAIL_USER`, `EMAIL_PASSWORD`, `EMAIL_IMAP_HOST/PORT`, `EMAIL_SMTP_HOST/PORT`): `kontakt@mbo-tech-it.de` auf `mx2f35.netcup.net` (IMAP 143 STARTTLS, SMTP 465 implizites TLS) +- **Claude API** (`CLAUDE_API_KEY`): console.anthropic.com, separat vom Claude-Pro-Abo + +--- + +## πŸ“ Services & Ports + +| Service | URL | Port | Status | Notes | +|---------|-----|------|--------|-------| +| **Web-Frontend** | https://jarvis.mbo-tech-it.de | 80 (intern, nginx) | 🟒 Live | Chat + Dashboard, Shared-Secret-Login mit `API_KEY_ADMIN` | +| **API** | https://api.jarvis.mbo-tech-it.de | 8000 (intern) | 🟒 Live | FastAPI Swagger UI: `/docs`. Alle `/api/v1/*` Routen ausser `/health` brauchen Header `X-Admin-Key` | +| **n8n** | https://n8n.jarvis.mbo-tech-it.de | 5678 (intern) | 🟒 Live | Workflow Automation - 2 aktive Workflows (Wetter, E-Mail-Cache) | +| **Traefik** | - | 80/443 | 🟒 Genutzt (fremd) | Server hat bereits einen eigenen Traefik fuer andere Projekte. JARVIS-eigener Traefik-Service wurde entfernt (Port-Konflikt), stattdessen haengt JARVIS per Labels + externem Netzwerk `proxy-network` am bestehenden Traefik. TLS ueber Let's-Encrypt-Resolver `netcup` (DNS-01) | +| **PostgreSQL** | localhost | 5432 | 🟒 Live | Hauptdatenbank + Vector Search, Image `pgvector/pgvector:pg16` | +| **Redis** | localhost | 6379 | 🟒 Live | LΓ€uft und wird im Health-Check geprΓΌft, im Code aber noch nicht aktiv fΓΌrs Caching genutzt | +| **Ollama** | http://72.61.186.98:11434 | 11434 | 🟒 Live | Local LLM Runtime, genutzt fΓΌr Embeddings (`nomic-embed-text` gepullt) | +| **Milvus** | - | - | β›” Nie deployt | Ersetzt durch `pgvector` direkt in Postgres (siehe Phase 3a). Platzhalter-Services `milvus`/`etcd` stehen noch in `docker-compose.yml`, laufen aber nicht | + +**Traefik-Integration (12.09.2026):** JARVIS ist am bestehenden VPS-weiten Traefik +(`docker-compose.yml` Projekt `traefik`, `/data/docker/compose/traefik/`) angehaengt: +externes Docker-Netzwerk `proxy-network`, Labels an `jarvis-api`, `jarvis-web` und `n8n` +(`traefik.docker.network=proxy-network`, `entrypoints=websecure`, +`tls.certresolver=netcup`). DNS (`jarvis` A-Record + `*.jarvis` Wildcard) liegt +bei Netcup. Falls eine neue Subdomain (z.B. `foo.jarvis.mbo-tech-it.de`) mal +nicht auflΓΆst, obwohl der Wildcard existiert: pruefen, ob ein expliziter +(auch leerer) Eintrag exakt fuer diesen Namen existiert - der blockiert laut +DNS-Wildcard-Regeln (RFC 1034) die Wildcard-Aufloesung nur fuer diesen einen +Namen. **Beobachtet (12.09.2026):** Auch ohne sichtbaren blockierenden +Eintrag im Netcup-Panel kann die Aufloesung eines frisch fuer eine +DNS-01-Challenge verwendeten Namens (z.B. `api.jarvis...`) deutlich laenger +brauchen als ein unberuehrter Wildcard-Name (bis zu ~20 Minuten beobachtet, +vermutlich Netcup-interner Nebeneffekt der ACME-TXT-Record-Erstellung) - +einfach abwarten, es loest sich von selbst. + +--- + +## πŸ—οΈ Architektur + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Bestehender, JARVIS-fremder Traefik β”‚ +β”‚ (VPS-weit, andere Projekte inklusive) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ proxy-network (Labels) + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚jarvis-webβ”‚ β”‚jarvis-apiβ”‚ β”‚ n8n β”‚ +β”‚(nginx) β”‚ β”‚(FastAPI) β”‚ β”‚ :5678 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ + β”‚ jarvis-net (intern) β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β–Ό β–Ό β–Ό β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ Postgres β”‚ β”‚ Redis β”‚ β”‚ Ollama β”‚ β”‚ +β”‚ +pgvector β”‚ β”‚ :6379 β”‚ β”‚ :11434 β”‚ β”‚ +β”‚ :5432 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ + └─── n8n schreibt Cache-Tabellen β”€β”€β”€β”€β”€β”˜ + (weather_cache, email_cache) + +Externe Integrationen (kein eigener Container, direkter API-Zugriff aus jarvis-api): + - Nextcloud CalDAV (Kalender lesen/schreiben) + - IMAP/SMTP-Postfach kontakt@mbo-tech-it.de (E-Mails lesen/senden) + - Claude API (Chat + Tool Use) +``` + +--- + +## 🐳 Docker Container Status + +```bash +cd /home/jarvis-core/jarvis +docker compose ps + +# Erwartete Ausgabe (6 Container): +# jarvis-postgres Up (healthy) +# jarvis-redis Up (healthy) +# jarvis-ollama Up +# jarvis-n8n Up +# jarvis-api Up +# jarvis-web Up +``` +`milvus`/`etcd` stehen zwar noch als Service-Definitionen in +`docker-compose.yml` (Platzhalter, nie funktionsfΓ€hig konfiguriert), werden +aber mit `docker compose up -d` normalerweise nicht mit hochgezogen, wenn man +gezielt einzelne Services neu startet (`docker compose up -d jarvis-api` etc., +so wie es in diesem Projekt durchgΓ€ngig gemacht wird statt einem globalen +`up -d`). + +--- + +## πŸ“ Projektstruktur + +``` +/home/jarvis-core/jarvis/ +β”œβ”€β”€ docker-compose.yml # Container-Orchestrierung (Docker Compose v2 - "docker compose", kein Bindestrich) +β”œβ”€β”€ .env # Umgebungsvariablen (GEHEIM!) +β”œβ”€β”€ init-db.sql # PostgreSQL Basis-Initialisierung (users/conversations/messages/tasks/documents/audit_logs) +β”œβ”€β”€ migrations/ # NachtrΓ€gliche Schema-Γ„nderungen, manuell per psql angewendet +β”‚ β”œβ”€β”€ 002_document_chunks.sql +β”‚ β”œβ”€β”€ 003_drop_low_data_ivfflat_index.sql +β”‚ β”œβ”€β”€ 004_weather_cache.sql +β”‚ └── 005_email_cache.sql +β”œβ”€β”€ api/ +β”‚ β”œβ”€β”€ main.py # FastAPI Hauptanwendung (einzige Backend-Datei) +β”‚ └── requirements.txt # Python Dependencies +β”œβ”€β”€ web/ # React/Vite Frontend-Quellcode + Dockerfile + nginx.conf +β”œβ”€β”€ config/ # (ungenutzt) +β”œβ”€β”€ data/ # (ungenutzt - Postgres/Redis/n8n/Ollama persistieren tatsΓ€chlich ΓΌber benannte Docker-Volumes, siehe docker-compose.yml `volumes:`) +└── logs/ # (ungenutzt, kein Service schreibt aktuell hierhin) +``` + +Lokal (Entwicklung, dieses Repo): Backend-Code liegt unter `Claude outputs/`, +Frontend unter `web/`. Kein dediziertes Git-Repo fΓΌr dieses Projekt - Dateien +werden per `scp` direkt auf den VPS deployt. + +--- + +## πŸ”§ Wichtige Befehle + +**Hinweis:** Die installierte Docker-Compose-Version ist v2 (Plugin-Syntax +`docker compose`, ohne Bindestrich) - das alte `docker-compose` (mit +Bindestrich) ist auf dem VPS nicht garantiert vorhanden. + +### Container Management +```bash +cd /home/jarvis-core/jarvis + +# Alle starten +docker compose up -d + +# Alle stoppen +docker compose down + +# Logs anschauen +docker compose logs -f + +# Spezifischen Service neustarten (Backend-CodeΓ€nderung reicht ein restart, +# da main.py per Volume gemountet ist und uvicorn mit --reload lΓ€uft) +docker compose restart jarvis-api + +# Frontend-Γ„nderung braucht einen echten Rebuild (Multi-Stage-Dockerfile) +docker compose build jarvis-web && docker compose up -d jarvis-web + +# In Container gehen +docker compose exec jarvis-api bash +docker compose exec postgres bash +``` + +### Database Management +```bash +# PostgreSQL CLI ΓΆffnen +docker exec -it jarvis-postgres psql -U jarvis -d jarvis + +# Query ausfΓΌhren +docker exec jarvis-postgres psql -U jarvis -d jarvis -c "SELECT * FROM users;" + +# Migration anwenden (nach scp der .sql-Datei nach migrations/) +docker exec -i jarvis-postgres psql -U jarvis -d jarvis < migrations/00X_name.sql + +# Backup erstellen +docker exec jarvis-postgres pg_dump -U jarvis jarvis > jarvis_backup.sql + +# Backup wiederherstellen +docker exec -i jarvis-postgres psql -U jarvis jarvis < jarvis_backup.sql +``` + +### API Testing +```bash +# Health Check (kein Header noetig) +curl http://localhost:8000/health + +# Admin Stats (Header noetig) +curl -H "X-Admin-Key: $API_KEY_ADMIN" http://localhost:8000/api/v1/admin/stats + +# Swagger UI im Browser +https://api.jarvis.mbo-tech-it.de/docs +``` + +### Backend-Tests lokal ausfΓΌhren (vor jedem Deploy) +Lokales Python (3.14) kann `psycopg2-binary` nicht bauen (kein Wheel) - Tests +laufen deshalb in einem `python:3.11-slim`-Container per SSH: +```bash +scp -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key \ + "Claude outputs/main.py" "Claude outputs/requirements.txt" \ + "Claude outputs/requirements-dev.txt" "Claude outputs/pytest.ini" \ + jarvis-core@72.61.186.98:/tmp/jarvis-test/ +scp -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key \ + "Claude outputs/tests/"*.py jarvis-core@72.61.186.98:/tmp/jarvis-test/tests/ +ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98 \ + "docker run --rm -v /tmp/jarvis-test:/app -w /app python:3.11-slim bash -c \ + 'pip install -q -r requirements-dev.txt -r requirements.txt && python -m pytest tests/ -v'" +``` + +--- + +## βš™οΈ Konfiguration (.env, auf dem VPS unter `/home/jarvis-core/jarvis/.env`) + +``` +# Domain +DOMAIN=jarvis.mbo-tech-it.de + +# Database +DB_PASSWORD= + +# Claude / Ollama +CLAUDE_API_KEY= +CLAUDE_MODEL=claude-sonnet-5 # (Default in main.py, muss nicht in .env stehen) +OLLAMA_EMBED_MODEL=nomic-embed-text # (Default in main.py) + +# Admin-Zugriff (Shared Secret fΓΌrs Frontend UND alle /api/v1/* Routen) +API_KEY_ADMIN= + +# Nextcloud-Kalender +NEXTCLOUD_APP_PASSWORD= + +# E-Mail-Postfach kontakt@mbo-tech-it.de +EMAIL_PASSWORD= + +LOG_LEVEL=info +``` + +**⚠️ WICHTIG:** `.env` enthΓ€lt Secrets - nicht in Git committen (fΓΌr dieses +Projekt ohnehin kein Git-Repo im Einsatz)! + +--- + +## πŸ”Œ Datenbankschema + +### PostgreSQL Tables (Datenbank `jarvis`) + +**users** (`init-db.sql`) +- Felder: id, username, email, password_hash, api_key, role, is_active +- Es gibt kein echtes Login-System pro Nutzer (siehe "Security" unten) - die + einzige vorhandene Zeile ist ein Seed-User `admin@jarvis.local` aus + `init-db.sql`. `ensure_default_user()` in `main.py` nimmt schlicht den + ersten User per ID als `DEFAULT_USER_ID` fΓΌr alle Chat-AktivitΓ€t (in der + Praxis also dieser `admin`-Seed-User, nicht ein separat angelegter + "jarvis-service"-User, obwohl der Code-Pfad dafΓΌr existiert) + +**conversations** / **messages** (`init-db.sql`) +- Chat-Verlauf, Multi-Turn ΓΌber `conversation_id` +- `messages.tokens_used` trackt Claude-Token-Verbrauch pro Antwort + +**tasks** (`init-db.sql`) +- Tabelle existiert, wird aber von den `/api/v1/tasks`-Endpoints noch NICHT + genutzt (die sind aktuell Stubs mit hartcodierten Werten, siehe "API + Endpoints" unten) - offener Punkt fΓΌr Phase 4b + +**documents** + **document_chunks** (`init-db.sql` + `migrations/002_document_chunks.sql`) +- Knowledge Base fΓΌrs RAG: `documents` hΓ€lt Titel/Volltext, + `document_chunks` die 768-dim `pgvector`-Embeddings (Ollama + `nomic-embed-text`) pro Chunk +- `ivfflat`-Index wurde wieder entfernt (`003_drop_low_data_ivfflat_index.sql`), + da er bei wenig Daten 0 Treffer lieferte - aktuell Sequential Scan + +**weather_cache** (`migrations/004_weather_cache.sql`) +- Wird ausschließlich vom n8n-Workflow "Wetter Crailsheim" befΓΌllt (Cache, + kein Live-API-Call aus `main.py`) + +**email_cache** (`migrations/005_email_cache.sql`) +- Wird ausschließlich vom n8n-Workflow "Neue E-Mails Cache" befΓΌllt + (Absender/Betreff/Empfangsdatum, keine Volltexte). Der Chat greift fΓΌr + Live-Anfragen separat direkt per IMAP zu, nicht ΓΌber diesen Cache + +**audit_logs** (`init-db.sql`) +- Tabelle existiert, wird aber aktuell von keinem Code-Pfad beschrieben + (vorbereitet fΓΌr spΓ€tere Compliance-Anforderungen) + +### Redis +- LΓ€uft und wird im Health-Check geprΓΌft (`check_redis()`), im Code aber + noch nirgends fΓΌr Sessions/Caching/Queues genutzt - reiner Platzhalter fΓΌr + spΓ€tere Erweiterung + +--- + +## πŸš€ API Endpoints + +Alle `/api/v1/*`-Routen (ausser keine - `/health` ist die einzige offene +Route) verlangen den Header `X-Admin-Key: `. + +### Health +``` +GET /health # Oeffentlich, kein Header noetig +GET /api/v1/admin/stats # Conversations/Tasks/Documents-Zaehler +GET /api/v1/admin/health/detailed # Postgres/Redis/Milvus(TCP-Check)/Ollama/n8n +``` + +### Chat (Claude, mit Tool Use fΓΌr Kalender + E-Mail) +``` +POST /api/v1/chat + Body: {"conversation_id": 1, "message": "...", "context": {}} + -> {"conversation_id", "response", "tokens_used", "timestamp"} + +GET /api/v1/conversations/{id} # Voller Nachrichtenverlauf +``` +Claude kann in `run_chat_completion()` bis zu `MAX_TOOL_ROUNDS = 5` Tool-Runden +hintereinander ausfΓΌhren: `list_calendar_events`, `create_calendar_event`, +`update_calendar_event`, `delete_calendar_event` (LΓΆschen nur nach expliziter +Chat-BestΓ€tigung), `list_recent_emails`, `send_email` (Senden nur nach +expliziter Chat-BestΓ€tigung). + +### Kalender +``` +GET /api/v1/calendar/events?days=14 # Naechste Termine (Nextcloud CalDAV) +``` + +### Wetter +``` +GET /api/v1/weather # Neuester Cache-Eintrag (503 wenn leer) +``` + +### E-Mail +``` +GET /api/v1/emails?limit=10 # Neueste gecachte Mails ([] wenn leer, kein 503) +``` + +### Tasks (⚠️ noch nicht mit Postgres verbunden - TODO) +``` +POST /api/v1/tasks # Stub: gibt immer id=1 zurueck, speichert nichts +GET /api/v1/tasks?status=pending # Stub: gibt immer leere Liste zurueck +``` + +### Documents (Knowledge Base) +``` +POST /api/v1/documents?title=...&content=...&document_type=... + # Chunking + Ollama-Embedding + Insert + +GET /api/v1/documents?query=...&limit=10 + # Vector-Similarity-Suche ueber document_chunks +``` + +### Workflows (⚠️ noch nicht mit n8n verbunden - TODO) +``` +POST /api/v1/workflows/trigger # Stub: ruft n8n nicht wirklich auf +``` + +--- + +## πŸ” Security + +Es gibt **kein** vollwertiges Auth-System mit Login pro Nutzer. Stattdessen: +- Frontend-Login = ein geteiltes Secret (`API_KEY_ADMIN`), das im Browser in + `localStorage` liegt und bei jedem Request als `X-Admin-Key`-Header + mitgeschickt wird (`require_admin_key`-Dependency in `main.py`) +- Der `admin`-User in der `users`-Tabelle (aus `init-db.sql`) ist nur ein + DB-Seed fΓΌr die `user_id`-Fremdschluessel, kein aktives Login - das Feld + `password_hash` wird von keinem Code-Pfad geprΓΌft +- FΓΌr ein echtes Multi-User-System (eigene Logins, Rollen) wΓ€re eine + separate Architektur-Runde nΓΆtig - bewusst "Out of Scope" laut Frontend-Spec + +### Secrets (alle in `.env` auf dem VPS, nicht im Repo) +- **API_KEY_ADMIN**: Shared Secret fΓΌrs Frontend-Login + alle geschΓΌtzten API-Routen +- **DB_PASSWORD**: Postgres-Passwort +- **CLAUDE_API_KEY**: Anthropic API Key +- **NEXTCLOUD_APP_PASSWORD**: Nextcloud CalDAV App-Passwort +- **EMAIL_PASSWORD**: IMAP/SMTP-Passwort fΓΌr kontakt@mbo-tech-it.de + +--- + +## πŸ“Š Monitoring & Logs + +```bash +# Real-time Logs +docker compose logs -f + +# Logs fΓΌr spezifischen Service +docker compose logs -f jarvis-api + +# Alte Logs anschauen +docker logs --tail 100 jarvis-api + +# System Resources +docker stats +``` + +--- + +## πŸ› οΈ Troubleshooting + +### Container startet nicht +```bash +docker compose logs jarvis-api +docker compose restart jarvis-api +docker compose down && docker compose up -d +``` + +### Database Connection Error +```bash +docker exec jarvis-postgres pg_isready -U jarvis +docker exec jarvis-postgres psql -U jarvis -d jarvis -c "SELECT 1" +``` + +### API antwortet nicht +```bash +docker compose ps +curl -v http://localhost:8000/health +docker compose logs jarvis-api +``` + +### Frontend zeigt eine neue Aenderung nicht an, obwohl deployt +`web/nginx.conf` setzt `index.html` auf `no-cache` und `/assets/*` auf +`immutable` - trotzdem: harter Reload (Strg+Shift+R) probieren, bevor man +tiefer sucht. Ursache war einmal ein fehlender Cache-Header (siehe Phase 4d). + +### n8n-Workflow lΓ€uft nicht wie erwartet +Executions-Tab des jeweiligen Workflows in der n8n-UI ansehen (zeigt Input/ +Output pro Node) - schneller als Logs raten. + +--- + +## πŸ“ˆ Entwicklungsverlauf (chronologisch) + +### Phase 1: Grundsetup βœ… +- [x] Docker Stack deployed, alle Kern-Services laufen, Datenbank initialisiert, API verfΓΌgbar + +### Phase 2: KI-Integration βœ… +- [x] Claude API Key eingetragen (console.anthropic.com, separat vom Claude Pro Abo) +- [x] Claude Integration in API (Multi-Turn ueber `conversation_id`) +- [x] Token-Tracking implementiert (`tokens_used` pro Message + Response) +- [x] Postgres-Persistenz fuer Conversations/Messages +- [x] Echte Health-Checks (Postgres, Redis, Ollama, n8n; Milvus als TCP-Check vorbereitet) + +### Phase 3a: Knowledge Base βœ… +- [x] pgvector-Extension in Postgres (Image gewechselt auf `pgvector/pgvector:pg16`) +- [x] `document_chunks`-Tabelle (768-dim Embeddings, Chunking 1000/100 Zeichen Overlap) +- [x] Embeddings via Ollama `nomic-embed-text` (lokal, keine externen Kosten) +- [x] `POST/GET /api/v1/documents` funktionsfaehig +- [x] Bug gefunden+gefixt: `ivfflat`-Index lieferte bei wenig Daten 0 Treffer - Index vorerst entfernt, Sequential Scan aktiv +- [ ] Follow-up: `ivfflat`/`hnsw`-Index neu anlegen, sobald genug Dokumente vorhanden sind + +### Phase 3b: Frontend βœ… +- [x] React/Vite Web UI (`jarvis-web` Container, nginx-served) unter https://jarvis.mbo-tech-it.de +- [x] Shared-Secret-Login (wiederverwendet `API_KEY_ADMIN`, Header `X-Admin-Key`) +- [x] Chat Interface (Multi-Turn) + Dashboard (Stats + Health-Tabelle) +- [x] API auf eigene Subdomain umgezogen: https://api.jarvis.mbo-tech-it.de +- [x] Bug gefixt (13.09.2026): Chat verlor Verlauf bei Tab-Wechsel/Reload, da + `conversationId` nur im React-State lag. Fix: `conversationId` in + `localStorage`, Verlauf wird beim Mounten via `GET /api/v1/conversations/{id}` + wiederhergestellt (`web/src/components/Chat.tsx`) +- [ ] Vollwertiges User-/Auth-System (Login pro Nutzer, Rollen) - weiterhin offen + +### Phase 4a: Wetter-Widget βœ… (erster echter n8n-Workflow) +- [x] n8n Owner-Account eingerichtet (`jonny@mbo-tech-it.de`) +- [x] n8n-Workflow "Wetter Crailsheim" (id `BwNCJ2TkuZfqUzst`): Schedule-Trigger + (alle 30 Min) -> Open-Meteo API (kein Key noetig) -> Code-Node (WMO-Code + -> deutscher Text) -> Postgres-Insert in `weather_cache` +- [x] n8n-Credential "JARVIS Postgres" (zeigt auf dieselbe `jarvis`-DB wie die API) +- [x] `GET /api/v1/weather` liest die neueste Zeile +- [x] `WeatherWidget` oben rechts in der Frontend-Nav-Leiste + +### Phase 4d: Nextcloud-Kalender-Integration βœ… +- [x] CalDAV-Zugriff auf `https://cloud.ffw-onza.de/.../ffw-onza-alle/` via + App-Passwort, Python-Library `caldav` +- [x] `GET /api/v1/calendar/events` + "Naechste Termine"-Widget im Dashboard +- [x] Chat kann Kalender abfragen, Termine anlegen und verschieben (direkt, + ohne Rueckfrage) sowie loeschen (nur nach Bestaetigung im Chat) - + Claude Tool Use mit `list_calendar_events` / `create_calendar_event` / + `update_calendar_event` / `delete_calendar_event` +- [x] Bug gefixt (13.09.2026): `run_chat_completion` unterstuetzte nur eine + Tool-Runde - "Termin verschieben" braucht aber zwei (erst uid per + `list_calendar_events` finden, dann `update_calendar_event`), die + zweite Runde wurde still verworfen (leere Antwort). Jetzt eine echte + Schleife (`MAX_TOOL_ROUNDS = 5`) +- [x] Bug gefixt: neu angelegte Termine landeten als UTC statt Europe/Berlin + (2h Verschiebung) - naive Datumswerte werden jetzt explizit lokalisiert + (`_as_calendar_local`) +- [x] Bug gefixt: `web/nginx.conf` hatte keine Cache-Header - `index.html` + wurde vom Browser gecacht, wodurch ein Deploy im Browser nicht ankam, + obwohl der Server bereits den neuen Build auslieferte. Jetzt: + `index.html` = `no-cache`, `/assets/*` = `immutable` + +### Phase 4e: Sprach-Ein-/Ausgabe im Chat βœ… +- [x] Mikrofon-Button (🎀) nutzt die Browser-eigene Web Speech API + (`SpeechRecognition`, `de-DE`) - reine Frontend-Loesung, blendet sich + selbst aus, wenn der Browser das nicht unterstuetzt (v.a. Firefox) +- [x] Assistant-Antworten werden automatisch per `SpeechSynthesis` + vorgelesen, Standard "an"; Mute-Button (πŸ”Š/πŸ”‡) merkt sich Zustand in + `localStorage` (`jarvis_speech_muted`) +- [x] Neues Modul `web/src/speech.ts` buendelt beide APIs (nach Vorbild von `api.ts`) +- [x] Bug gefixt (13.09.2026): `continuous=false` beendete die Erkennung bei + jeder Sprechpause. Jetzt `continuous=true` mit automatischem Neustart + bei Chrome's internem Session-Timeout (akkumuliert Text ueber + Neustarts hinweg, damit nichts verloren geht) +- [x] Sprachausgabe filtert vor dem Vorlesen: Emojis (inkl. Zahlen-Emoji wie + 1⃣2⃣3⃣), Markdown-Formatierung (`**fett**`, `#Ueberschriften`) und + dekorative Trennlinien/Tabellenzeichen (`---`, `═══`, `|`) - siehe + `cleanForSpeech()` in `web/src/speech.ts` + +### Phase 4f: E-Mail-Integration βœ… +- [x] Postfach `kontakt@mbo-tech-it.de` (IMAP `mx2f35.netcup.net:143` STARTTLS, + SMTP `mx2f35.netcup.net:465` implizites TLS) via Python-Standardbibliothek + (`imaplib`, `smtplib`, `email`) angebunden - keine neue Dependency +- [x] Chat-Tools `list_recent_emails` (liest live per IMAP, `BODY.PEEK` damit + der Lese-Status nicht veraendert wird, RFC-2047-Betreffs dekodiert) und + `send_email` (SMTP_SSL) in `run_chat_completion()`, analog zum Kalender. + `send_email` verlangt wie `delete_calendar_event` immer erst eine + Bestaetigung im Chat, bevor tatsaechlich versendet wird +- [x] n8n-Workflow "Neue E-Mails Cache" (id `VaAQdw18ElYWkEfr`, aktiv): + eingebauter `Email Trigger (IMAP)`-Node (`postProcessAction: "nothing"`, + damit das Cachen den echten Lese-Status im Postfach nicht veraendert) + -> Postgres-Insert in `email_cache` (nur Metadaten, keine Volltexte) +- [x] `GET /api/v1/emails` liest die neuesten Zeilen; leerer Cache liefert + `[]` statt 503 (anders als beim Wetter kein Fehlerzustand) +- [x] `EmailWidget` ("Neue E-Mails") im Dashboard, nach demselben Muster wie + `CalendarWidget`/`WeatherWidget` +- [x] Live end-to-end getestet: Chat hat echte Inbox-Mails aufgelistet, + Testmail per Chat mit Bestaetigungsdialog versendet und im Postfach + verifiziert, n8n-Workflow hat dieselbe Mail in `email_cache` erfasst, + Dashboard-Widget zeigt sie an +- [x] Kleiner Fix waehrend der Verifikation: von JARVIS selbst versendete + Mails hatten keinen `Date`-Header (smtplib/EmailMessage setzt den nicht + automatisch) - `_send_email_sync` setzt jetzt `Date` und `Message-ID` +- Out of Scope (siehe Spec): Anhaenge, Antworten/Weiterleiten, Loeschen/ + Verschieben von Mails, mehrere Postfaecher/Ordner + +### Phase 4b: Weitere Automation ⏳ (offen) +- [ ] `POST/GET /api/v1/tasks` an die vorhandene `tasks`-Tabelle anbinden (aktuell Stub) +- [ ] `POST /api/v1/workflows/trigger` tatsaechlich an die n8n-API anbinden (aktuell Stub) +- [ ] Weitere n8n Business-Workflows definieren (Kundenbericht-Automation o.Γ€.) + +### Phase 5: Production ⏳ (offen) +- [ ] Backup-Strategie (Postgres-Dumps automatisieren, aktuell nur manuell per `pg_dump`) +- [ ] Monitoring & Alerts (aktuell nur manuelles Ansehen von `/api/v1/admin/health/detailed`) +- [ ] Sicherheits-Hardening / echtes Multi-User-Auth-System + +--- + +## πŸ“ž Kontakt & Support + +**Owner:** Jonny (Markus) +**Email:** jonny@mbo-tech-it.de +**Company:** MBO-Tech-IT +**Location:** Crailsheim, Baden-WΓΌrttemberg + +--- + +## πŸ“š Dokumentation & Links + +- [FastAPI Docs](https://api.jarvis.mbo-tech-it.de/docs) +- [n8n Docs](https://docs.n8n.io) +- [PostgreSQL Docs](https://www.postgresql.org/docs/) +- [pgvector Docs](https://github.com/pgvector/pgvector) +- [Claude API Docs](https://docs.anthropic.com) +- [Docker Docs](https://docs.docker.com) +- [Traefik Docs](https://doc.traefik.io) + +Spezifikationen und ImplementierungsplΓ€ne aller Phasen liegen lokal unter +`docs/superpowers/specs/` und `docs/superpowers/plans/`. + +--- + +## 🎯 Quick Reference + +### Start JARVIS +```bash +cd /home/jarvis-core/jarvis +docker compose up -d +``` + +### Check Status +```bash +docker compose ps +curl http://localhost:8000/health +``` + +### View Logs +```bash +docker compose logs -f +``` + +### Backend-Code aktualisieren +```bash +scp -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key \ + "Claude outputs/main.py" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/api/main.py +ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98 \ + "cd /home/jarvis-core/jarvis && docker compose restart jarvis-api" +``` + +### Frontend-Code aktualisieren +```bash +scp -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key -r \ + web/src jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/web/ +ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98 \ + "cd /home/jarvis-core/jarvis && docker compose build jarvis-web && docker compose up -d jarvis-web" +``` + +### Backup Database +```bash +docker exec jarvis-postgres pg_dump -U jarvis jarvis > backup.sql +``` + +--- + +**πŸŽ‰ JARVIS ist bereit!** + +*Letzte Aktualisierung: 13.09.2026* +*Deployment Status: βœ… LIVE* diff --git a/Claude outputs/JARVIS_SETUP.md b/Claude outputs/JARVIS_SETUP.md new file mode 100644 index 0000000..5fa4606 --- /dev/null +++ b/Claude outputs/JARVIS_SETUP.md @@ -0,0 +1,319 @@ +# πŸ€– JARVIS - KI-Assistent + Business Automation + +**VollstΓ€ndiges Docker-Stack fΓΌr JARVIS: KI-Backend + Datenbanken + Workflow-Automation** + +## Architektur-Übersicht + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ JARVIS CORE (VPS) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ TRAEFIK (Reverse Proxy / Load Balancer) β”‚ β”‚ +β”‚ β”‚ Ports: 80, 443, 8080 β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ WEB β”‚ API β”‚ n8n β”‚ β”‚ +β”‚ β”‚ (Frontend) β”‚ (FastAPI) β”‚ (Automation) β”‚ β”‚ +β”‚ β”‚ Port: 3000 β”‚ Port: 8000 β”‚ Port: 5678 β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ PostgreSQL β”‚ Redis β”‚ Milvus β”‚ β”‚ +β”‚ β”‚ Port: 5432 β”‚ Port: 6379 β”‚ Port: 19530 β”‚ β”‚ +β”‚ β”‚ (Structured) β”‚ (Cache) β”‚ (Vector DB) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ OLLAMA (Local LLM - Optional) β”‚ β”‚ +β”‚ β”‚ Port: 11434 β”‚ β”‚ +β”‚ β”‚ Alternative: Claude API (via Python) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Schnelstart + +### 1️⃣ SSH-Zugriff einrichten + +```bash +# Speichern der Private Key +# Putty: key β†’ jarvis_ed25519.ppk +# SSH: ssh -i jarvis_ed25519 jarvis-core@72.61.186.98 +``` + +### 2️⃣ Setup-Script ausfΓΌhren + +```bash +ssh -i jarvis_ed25519 jarvis-core@72.61.186.98 + +# Download setup script +curl -o setup-jarvis-complete.sh https://path/to/setup-jarvis-complete.sh +chmod +x setup-jarvis-complete.sh + +# Run with domain +./setup-jarvis-complete.sh jarvis.mbo-tech-it.de +``` + +### 3️⃣ Konfigurieren + +```bash +cd /home/jarvis-core/jarvis + +# Edit .env - WICHTIG: API-Keys eintragen! +nano .env + +# Erforderlich: +# - CLAUDE_API_KEY (fΓΌr Claude API) +# - DB_PASSWORD (wird generiert, aber prΓΌfe es) +# - Alle anderen Keys werden auto-generiert +``` + +### 4️⃣ Docker-Stack starten + +```bash +# Starten +docker-compose up -d + +# Logs prΓΌfen +docker-compose logs -f + +# Status +docker-compose ps +``` + +## Services & Ports + +| Service | URL | Port | Beschreibung | +|---------|-----|------|-------------| +| **Web Dashboard** | http://localhost:3000 | 3000 | Frontend UI | +| **API Backend** | http://localhost:8000 | 8000 | FastAPI Server | +| **n8n Workflows** | http://localhost:5678 | 5678 | Automatisierung | +| **Traefik Dashboard** | http://localhost:8080 | 8080 | Proxy Management | +| **PostgreSQL** | localhost | 5432 | Hauptdatenbank | +| **Redis** | localhost | 6379 | Cache/Sessions | +| **Milvus** | localhost | 19530 | Vector Search | +| **Ollama** | http://localhost:11434 | 11434 | Local LLM (opt.) | + +## Datenbankstruktur + +### PostgreSQL (jarvis) +- **users** - Benutzer & Authentifizierung +- **conversations** - Chat-Verlauf +- **messages** - Einzelne Nachrichten +- **tasks** - Automatisierungsaufgaben +- **documents** - Knowledge Base +- **audit_logs** - Audit Trail + +### Milvus (Vector DB) +- Semantic Search in Dokumenten +- Embeddings fΓΌr RAG (Retrieval-Augmented Generation) + +### Redis +- Session Management +- Caching +- Job Queue (fΓΌr n8n) + +## API Endpoints + +### Chat +```bash +POST /api/v1/chat +{ + "conversation_id": 1, + "message": "Deine Frage", + "context": {} +} + +GET /api/v1/conversations/{id} +``` + +### Tasks +```bash +POST /api/v1/tasks +{ + "title": "Task Name", + "description": "Beschreibung", + "task_type": "automation", + "priority": 1 +} + +GET /api/v1/tasks?status=pending +``` + +### Documents (Knowledge Base) +```bash +POST /api/v1/documents +{ + "title": "Dokument", + "content": "...", + "document_type": "manual" +} + +GET /api/v1/documents?query=search+term +``` + +### Workflows (n8n) +```bash +POST /api/v1/workflows/trigger +{ + "workflow_id": "workflow_name", + "data": {} +} +``` + +## Umgebungsvariablen (.env) + +```bash +# Domain +DOMAIN=jarvis.mbo-tech-it.de + +# Database +DB_PASSWORD=SecurePassword123! + +# KI / LLM +CLAUDE_API_KEY=sk-ant-... +OLLAMA_MODEL=mistral + +# Security +JWT_SECRET=random-secret-key +API_KEY_ADMIN=admin-api-key + +# Logging +LOG_LEVEL=info +``` + +## HΓ€ufige Befehle + +```bash +cd /home/jarvis-core/jarvis + +# Alle Container starten +docker-compose up -d + +# Spezifischen Service neu starten +docker-compose restart jarvis-api + +# Logs ansehen +docker-compose logs -f jarvis-api + +# In Container gehen +docker-compose exec jarvis-api bash + +# Database mit psql verbinden +docker-compose exec postgres psql -U jarvis -d jarvis + +# Alle Container stoppen +docker-compose down + +# Mit Daten lΓΆschen (Vorsicht!) +docker-compose down -v +``` + +## Integration mit Claude API + +### Python-Client Beispiel + +```python +import os +import anthropic + +client = anthropic.Anthropic(api_key=os.getenv("CLAUDE_API_KEY")) + +message = client.messages.create( + model="claude-3-5-sonnet-20241022", + max_tokens=1024, + messages=[ + {"role": "user", "content": "Deine Frage"} + ] +) + +print(message.content[0].text) +``` + +## n8n Workflows + +### Beispiel-Automation: Kundenbericht generieren +1. Trigger: Zeitplan (tΓ€glich) +2. Node: PostgreSQL - Daten abrufen +3. Node: Claude API - Bericht generieren +4. Node: PDF generieren +5. Node: Email versenden + +## Security Best Practices + +βœ… **Production Ready:** +- [ ] HTTPS/SSL konfigurieren (Let's Encrypt) +- [ ] Firewall-Regeln (nur erlaubte IPs) +- [ ] Secrets in .env nicht in Git +- [ ] RegelmÀßige DB-Backups +- [ ] Monitoring & Alerts einrichten +- [ ] Rate Limiting aktivieren +- [ ] API-Keys rotieren + +⚠️ **Aktuell (Test):** +- HTTP nur lokal/intern +- Standard-PasswΓΆrter (Γ„NDERN!) +- Keine Authentifizierung erzwungen + +## Troubleshooting + +### Container startet nicht +```bash +docker-compose logs jarvis-api +# β†’ Fehlerlog prΓΌfen +``` + +### Datenbank nicht erreichbar +```bash +docker-compose exec postgres psql -U jarvis -d jarvis -c "SELECT 1" +``` + +### Redis Cache Fehler +```bash +docker-compose exec redis redis-cli ping +# β†’ PONG = OK +``` + +### Milvus Vector DB Fehler +```bash +docker-compose exec milvus curl http://localhost:9091/healthz +``` + +## Monitoring + +```bash +# System-Stats +docker stats + +# Container Logs (real-time) +docker-compose logs -f + +# Health Check +curl http://localhost:8000/health +curl http://localhost:8000/api/v1/admin/health/detailed +``` + +## NΓ€chste Schritte + +1. βœ… Docker-Stack deployed +2. ⏳ Claude API Integration (Python) +3. ⏳ Web-Frontend (React/Vue) +4. ⏳ n8n Workflows konfigurieren +5. ⏳ Knowledge Base aufbauen +6. ⏳ Production Deployment (SSL, Backups, etc.) + +## Support & Dokumentation + +- [FastAPI Docs](http://localhost:8000/docs) - Swagger UI +- [n8n Docs](https://docs.n8n.io) +- [PostgreSQL Docs](https://www.postgresql.org/docs/) +- [Milvus Docs](https://milvus.io/docs) +- [Claude API Docs](https://docs.anthropic.com) + +--- + +πŸš€ **JARVIS ist bereit fΓΌr die KI-Integration!** diff --git a/Claude outputs/deploy-jarvis.sh b/Claude outputs/deploy-jarvis.sh new file mode 100644 index 0000000..d5edc60 --- /dev/null +++ b/Claude outputs/deploy-jarvis.sh @@ -0,0 +1,382 @@ +#!/bin/bash + +set -e + +echo "πŸ€– ============ JARVIS FULL DEPLOYMENT ============" +echo "" + +# ============ COLORS ============ +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +# ============ CONFIGURATION ============ +DOMAIN="${1:-jarvis.local}" +JARVIS_HOME="/home/jarvis-core/jarvis" + +echo -e "${BLUE}πŸ“ Setup Directory: $JARVIS_HOME${NC}" +echo -e "${BLUE}πŸ“ Domain: $DOMAIN${NC}" +echo "" + +# ============ 1. SYSTEM UPDATE ============ +echo -e "${YELLOW}[1/9] System Update...${NC}" +sudo apt-get update -qq +sudo apt-get upgrade -y -qq +sudo apt-get install -y -qq curl wget git htop net-tools openssl python3-pip > /dev/null 2>&1 +echo -e "${GREEN}βœ… System updated${NC}" + +# ============ 2. DOCKER INSTALLATION ============ +echo -e "${YELLOW}[2/9] Installing Docker...${NC}" +if ! command -v docker &> /dev/null; then + curl -fsSL https://get.docker.com -o get-docker.sh > /dev/null 2>&1 + bash get-docker.sh > /dev/null 2>&1 + rm -f get-docker.sh + sudo usermod -aG docker jarvis-core + echo -e "${GREEN}βœ… Docker installed${NC}" +else + echo -e "${GREEN}βœ… Docker already installed${NC}" +fi + +# ============ 3. DOCKER COMPOSE ============ +echo -e "${YELLOW}[3/9] Installing Docker Compose...${NC}" +if ! command -v docker-compose &> /dev/null; then + COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d'"' -f4) + sudo curl -L "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose > /dev/null 2>&1 + sudo chmod +x /usr/local/bin/docker-compose + echo -e "${GREEN}βœ… Docker Compose installed${NC}" +else + echo -e "${GREEN}βœ… Docker Compose already installed${NC}" +fi + +# ============ 4. PROJECT STRUCTURE ============ +echo -e "${YELLOW}[4/9] Creating project structure...${NC}" +mkdir -p $JARVIS_HOME/{config,data,logs,api,web} +cd $JARVIS_HOME +mkdir -p data/{postgres,redis,milvus,etcd,ollama,n8n} +mkdir -p logs/{api,n8n,nginx} +echo -e "${GREEN}βœ… Project structure created${NC}" + +# ============ 5. DOCKER NETWORK ============ +echo -e "${YELLOW}[5/9] Creating Docker network...${NC}" +docker network create jarvis-net 2>/dev/null || true +echo -e "${GREEN}βœ… Docker network ready${NC}" + +# ============ 6. DOCKER-COMPOSE.YML ============ +echo -e "${YELLOW}[6/9] Writing docker-compose.yml...${NC}" +cat > docker-compose.yml << 'EOF' +version: '3.9' + +services: + traefik: + image: traefik:v2.10 + container_name: jarvis-traefik + command: + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + ports: + - "80:80" + - "443:443" + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - jarvis-net + restart: unless-stopped + + postgres: + image: postgres:16-alpine + container_name: jarvis-postgres + environment: + POSTGRES_DB: jarvis + POSTGRES_USER: jarvis + POSTGRES_PASSWORD: ${DB_PASSWORD:-ChangeMe123!} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init-db.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "5432:5432" + networks: + - jarvis-net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U jarvis"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + redis: + image: redis:7-alpine + container_name: jarvis-redis + command: redis-server --appendonly yes + volumes: + - redis_data:/data + ports: + - "6379:6379" + networks: + - jarvis-net + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + milvus: + image: milvusdb/milvus:v0.4.0 + container_name: jarvis-milvus + environment: + ETCD_ENDPOINTS: etcd:2379 + COMMON_STORAGETYPE: local + volumes: + - milvus_data:/var/lib/milvus + ports: + - "19530:19530" + - "9091:9091" + depends_on: + etcd: + condition: service_healthy + networks: + - jarvis-net + restart: unless-stopped + + etcd: + image: quay.io/coreos/etcd:v3.5.5 + container_name: jarvis-etcd + environment: + - ETCD_AUTO_COMPACTION_MODE=revision + - ETCD_AUTO_COMPACTION_RETENTION=1000 + - ETCD_QUOTA_BACKEND_BYTES=4294967296 + volumes: + - etcd_data:/etcd + ports: + - "2379:2379" + networks: + - jarvis-net + healthcheck: + test: ["CMD", "etcdctl", "endpoint", "health"] + interval: 10s + timeout: 5s + retries: 5 + command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd + restart: unless-stopped + + ollama: + image: ollama/ollama:latest + container_name: jarvis-ollama + environment: + - OLLAMA_HOST=0.0.0.0:11434 + volumes: + - ollama_data:/root/.ollama + ports: + - "11434:11434" + networks: + - jarvis-net + restart: unless-stopped + + n8n: + image: n8n:latest + container_name: jarvis-n8n + environment: + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=postgres + - DB_POSTGRESDB_USER=jarvis + - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD:-ChangeMe123!} + - DB_POSTGRESDB_DATABASE=n8n + - N8N_HOST=${DOMAIN:-localhost} + - N8N_PORT=5678 + - WEBHOOK_TUNNEL_URL=http://n8n:5678/ + - GENERIC_TIMEZONE=Europe/Berlin + volumes: + - n8n_data:/home/node/.n8n + ports: + - "5678:5678" + depends_on: + postgres: + condition: service_healthy + networks: + - jarvis-net + labels: + - "traefik.enable=true" + - "traefik.http.routers.n8n.rule=Host(`n8n.${DOMAIN:-localhost}`)" + - "traefik.http.services.n8n.loadbalancer.server.port=5678" + restart: unless-stopped + + jarvis-api: + image: python:3.11-slim + container_name: jarvis-api + working_dir: /app + command: > + bash -c "pip install -q -r requirements.txt && + uvicorn main:app --host 0.0.0.0 --port 8000 --reload" + environment: + - DATABASE_URL=postgresql://jarvis:${DB_PASSWORD:-ChangeMe123!}@postgres:5432/jarvis + - REDIS_URL=redis://redis:6379 + - MILVUS_HOST=milvus + - MILVUS_PORT=19530 + - OLLAMA_HOST=http://ollama:11434 + - CLAUDE_API_KEY=${CLAUDE_API_KEY:-} + - LOG_LEVEL=info + volumes: + - ./api:/app + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - jarvis-net + labels: + - "traefik.enable=true" + - "traefik.http.routers.api.rule=Host(`api.${DOMAIN:-localhost}`)" + - "traefik.http.services.api.loadbalancer.server.port=8000" + restart: unless-stopped + +networks: + jarvis-net: + driver: bridge + +volumes: + postgres_data: + redis_data: + milvus_data: + etcd_data: + ollama_data: + n8n_data: +EOF +echo -e "${GREEN}βœ… docker-compose.yml created${NC}" + +# ============ 7. ENV FILE ============ +echo -e "${YELLOW}[7/9] Creating .env file...${NC}" +cat > .env << EOF +DOMAIN=$DOMAIN +DB_PASSWORD=$(openssl rand -base64 32) +CLAUDE_API_KEY= +OLLAMA_MODEL=mistral +LOG_LEVEL=info +JWT_SECRET=$(openssl rand -base64 32) +API_KEY_ADMIN=$(openssl rand -base64 32) +N8N_ENCRYPTION_KEY=$(openssl rand -base64 32) +EOF +chmod 600 .env +echo -e "${GREEN}βœ… .env created${NC}" + +# ============ 8. DATABASE & API ============ +echo -e "${YELLOW}[8/9] Creating database schema...${NC}" +cat > init-db.sql << 'EOF' +CREATE DATABASE jarvis; +CREATE DATABASE n8n; +\c jarvis; +CREATE TABLE users (id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, api_key VARCHAR(255) UNIQUE, role VARCHAR(50) DEFAULT 'user', is_active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); +CREATE TABLE conversations (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(255), context JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); +CREATE INDEX idx_conversations_user_id ON conversations(user_id); +CREATE TABLE messages (id SERIAL PRIMARY KEY, conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, role VARCHAR(50) NOT NULL, content TEXT NOT NULL, tokens_used INTEGER, metadata JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); +CREATE INDEX idx_messages_conversation_id ON messages(conversation_id); +CREATE TABLE tasks (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, description TEXT, task_type VARCHAR(100), status VARCHAR(50) DEFAULT 'pending', priority INTEGER DEFAULT 0, data JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, completed_at TIMESTAMP); +CREATE INDEX idx_tasks_user_id ON tasks(user_id); +CREATE TABLE documents (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, document_type VARCHAR(100), metadata JSONB, embedding_id VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); +INSERT INTO users (username, email, password_hash, role) VALUES ('admin', 'admin@jarvis.local', '$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3djPvga3jaK', 'admin') ON CONFLICT DO NOTHING; +EOF + +# Create API directory and files +mkdir -p api +cat > api/requirements.txt << 'EOF' +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +python-dotenv==1.0.0 +pydantic==2.5.0 +psycopg2-binary==2.9.9 +redis==5.0.1 +pymilvus==2.3.4 +anthropic==0.14.0 +aiohttp==3.9.1 +requests==2.31.0 +EOF + +cat > api/main.py << 'EOF' +from fastapi import FastAPI +from datetime import datetime +import os + +app = FastAPI(title="JARVIS API", version="0.1.0") + +@app.get("/health") +async def health(): + return {"status": "healthy", "timestamp": datetime.now()} + +@app.get("/api/v1/admin/stats") +async def stats(): + return {"status": "ok", "timestamp": datetime.now()} + +@app.post("/api/v1/chat") +async def chat(message: str): + return {"response": f"Echo: {message}", "timestamp": datetime.now()} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +EOF + +echo -e "${GREEN}βœ… Database schema and API created${NC}" + +# ============ 9. PERMISSIONS ============ +echo -e "${YELLOW}[9/9] Setting permissions...${NC}" +sudo chown -R jarvis-core:jarvis-core $JARVIS_HOME +chmod -R 755 $JARVIS_HOME +chmod 600 .env +echo -e "${GREEN}βœ… Permissions set${NC}" + +# ============ DEPLOYMENT ============ +echo "" +echo -e "${GREEN}============================================${NC}" +echo -e "${GREEN}πŸš€ STARTING DOCKER CONTAINERS...${NC}" +echo -e "${GREEN}============================================${NC}" +echo "" + +docker-compose up -d + +echo "" +echo -e "${BLUE}Waiting for services to start... (30s)${NC}" +sleep 30 + +echo "" +echo -e "${GREEN}============================================${NC}" +echo -e "${GREEN}βœ… JARVIS DEPLOYED SUCCESSFULLY!${NC}" +echo -e "${GREEN}============================================${NC}" +echo "" +echo -e "${BLUE}πŸ“ Services:${NC}" +echo " 🌐 Web Dashboard: http://$DOMAIN:3000 (⏳ coming soon)" +echo " πŸ“‘ API: http://$DOMAIN:8000" +echo " πŸ“‘ API Docs: http://$DOMAIN:8000/docs" +echo " βš™οΈ n8n: http://$DOMAIN:5678" +echo " πŸ”Œ Traefik: http://$DOMAIN:8080" +echo "" +echo -e "${BLUE}πŸ“Š Database Access:${NC}" +echo " PostgreSQL: localhost:5432 (user: jarvis)" +echo " Redis: localhost:6379" +echo " Milvus: localhost:19530" +echo " Ollama: localhost:11434" +echo "" +echo -e "${BLUE}πŸ“ Next steps:${NC}" +echo " 1. Edit .env - Add CLAUDE_API_KEY" +echo " 2. Restart API: docker-compose restart jarvis-api" +echo " 3. Check logs: docker-compose logs -f" +echo "" +echo -e "${YELLOW}⚠️ Important:${NC}" +echo " β€’ Change default admin password in database" +echo " β€’ Setup SSL/HTTPS in production" +echo " β€’ Configure regular backups" +echo "" + +# Status +echo -e "${BLUE}πŸ“Š Container Status:${NC}" +docker-compose ps +echo "" + +echo -e "${GREEN}πŸŽ‰ Ready to go! Start building with JARVIS!${NC}" diff --git a/Claude outputs/docker-compose.yml b/Claude outputs/docker-compose.yml new file mode 100644 index 0000000..80b12a0 --- /dev/null +++ b/Claude outputs/docker-compose.yml @@ -0,0 +1,208 @@ +services: + postgres: + image: pgvector/pgvector:pg16 + container_name: jarvis-postgres + environment: + POSTGRES_DB: jarvis + POSTGRES_USER: jarvis + POSTGRES_PASSWORD: ${DB_PASSWORD:-ChangeMe123!} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init-db.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "5432:5432" + networks: + - jarvis-net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U jarvis"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + redis: + image: redis:7-alpine + container_name: jarvis-redis + command: redis-server --appendonly yes + volumes: + - redis_data:/data + ports: + - "6379:6379" + networks: + - jarvis-net + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + # NOTE: milvus/etcd are placeholders - milvusdb/milvus:v0.4.0 does not exist + # and this config format (etcd-only, no MinIO) doesn't match modern Milvus + # standalone anyway. Not started yet; needs a proper Phase 3 design. + milvus: + image: milvusdb/milvus:v0.4.0 + container_name: jarvis-milvus + environment: + ETCD_ENDPOINTS: etcd:2379 + COMMON_STORAGETYPE: local + volumes: + - milvus_data:/var/lib/milvus + ports: + - "19530:19530" + - "9091:9091" + depends_on: + etcd: + condition: service_healthy + networks: + - jarvis-net + restart: unless-stopped + + etcd: + image: quay.io/coreos/etcd:v3.5.5 + container_name: jarvis-etcd + environment: + - ETCD_AUTO_COMPACTION_MODE=revision + - ETCD_AUTO_COMPACTION_RETENTION=1000 + - ETCD_QUOTA_BACKEND_BYTES=4294967296 + volumes: + - etcd_data:/etcd + ports: + - "2379:2379" + networks: + - jarvis-net + healthcheck: + test: ["CMD", "etcdctl", "endpoint", "health"] + interval: 10s + timeout: 5s + retries: 5 + command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd + restart: unless-stopped + + ollama: + image: ollama/ollama:latest + container_name: jarvis-ollama + environment: + - OLLAMA_HOST=0.0.0.0:11434 + volumes: + - ollama_data:/root/.ollama + ports: + - "11434:11434" + networks: + - jarvis-net + restart: unless-stopped + + n8n: + image: n8nio/n8n:latest + container_name: jarvis-n8n + environment: + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=postgres + - DB_POSTGRESDB_USER=jarvis + - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD:-ChangeMe123!} + - DB_POSTGRESDB_DATABASE=n8n + - N8N_HOST=n8n.${DOMAIN:-localhost} + - N8N_PORT=5678 + - N8N_PROTOCOL=https + - WEBHOOK_TUNNEL_URL=https://n8n.${DOMAIN:-localhost}/ + - GENERIC_TIMEZONE=Europe/Berlin + volumes: + - n8n_data:/home/node/.n8n + ports: + - "5678:5678" + depends_on: + postgres: + condition: service_healthy + networks: + - jarvis-net + - proxy-network + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy-network" + - "traefik.http.routers.jarvis-n8n.rule=Host(`n8n.${DOMAIN:-localhost}`)" + - "traefik.http.routers.jarvis-n8n.entrypoints=websecure" + - "traefik.http.routers.jarvis-n8n.tls=true" + - "traefik.http.routers.jarvis-n8n.tls.certresolver=netcup" + - "traefik.http.services.jarvis-n8n.loadbalancer.server.port=5678" + restart: unless-stopped + + jarvis-api: + image: python:3.11-slim + container_name: jarvis-api + working_dir: /app + command: > + bash -c "pip install -q -r requirements.txt && + uvicorn main:app --host 0.0.0.0 --port 8000 --reload" + environment: + - DATABASE_URL=postgresql://jarvis:${DB_PASSWORD:-ChangeMe123!}@postgres:5432/jarvis + - REDIS_URL=redis://redis:6379 + - MILVUS_HOST=milvus + - MILVUS_PORT=19530 + - OLLAMA_HOST=http://ollama:11434 + - OLLAMA_EMBED_MODEL=nomic-embed-text + - N8N_URL=http://n8n:5678 + - CLAUDE_API_KEY=${CLAUDE_API_KEY:-} + - CLAUDE_MODEL=claude-sonnet-5 + - API_KEY_ADMIN=${API_KEY_ADMIN:-} + - NEXTCLOUD_CALDAV_URL=https://cloud.ffw-onza.de/remote.php/dav/calendars/jonny/ffw-onza-alle/ + - NEXTCLOUD_USER=jonny + - NEXTCLOUD_APP_PASSWORD=${NEXTCLOUD_APP_PASSWORD:-} + - EMAIL_IMAP_HOST=mx2f35.netcup.net + - EMAIL_IMAP_PORT=143 + - EMAIL_SMTP_HOST=mx2f35.netcup.net + - EMAIL_SMTP_PORT=465 + - EMAIL_USER=kontakt@mbo-tech-it.de + - EMAIL_PASSWORD=${EMAIL_PASSWORD:-} + - LOG_LEVEL=info + volumes: + - ./api:/app + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - jarvis-net + - proxy-network + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy-network" + - "traefik.http.routers.jarvis-api.rule=Host(`api.${DOMAIN:-localhost}`)" + - "traefik.http.routers.jarvis-api.entrypoints=websecure" + - "traefik.http.routers.jarvis-api.tls=true" + - "traefik.http.routers.jarvis-api.tls.certresolver=netcup" + - "traefik.http.services.jarvis-api.loadbalancer.server.port=8000" + restart: unless-stopped + + jarvis-web: + build: + context: ./web + dockerfile: Dockerfile + container_name: jarvis-web + networks: + - proxy-network + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy-network" + - "traefik.http.routers.jarvis-web.rule=Host(`${DOMAIN:-localhost}`)" + - "traefik.http.routers.jarvis-web.entrypoints=websecure" + - "traefik.http.routers.jarvis-web.tls=true" + - "traefik.http.routers.jarvis-web.tls.certresolver=netcup" + - "traefik.http.services.jarvis-web.loadbalancer.server.port=80" + restart: unless-stopped + +networks: + jarvis-net: + driver: bridge + proxy-network: + external: true + +volumes: + postgres_data: + redis_data: + milvus_data: + etcd_data: + ollama_data: + n8n_data: diff --git a/Claude outputs/env.example b/Claude outputs/env.example new file mode 100644 index 0000000..843cb92 --- /dev/null +++ b/Claude outputs/env.example @@ -0,0 +1,23 @@ +# ============ DOMAIN CONFIGURATION ============ +DOMAIN=jarvis.local +# DOMAIN=jarvis.mbo-tech-it.de (for production) + +# ============ DATABASE ============ +DB_PASSWORD=SecurePassword123!JarvisDB + +# ============ AI/LLM ============ +CLAUDE_API_KEY=sk-ant-...your-api-key-here... +CLAUDE_MODEL=claude-sonnet-5 +# Optional: Use local Ollama instead +OLLAMA_MODEL=mistral + +# ============ SECURITY ============ +JWT_SECRET=your-super-secret-jwt-key-here-change-this +API_KEY_ADMIN=admin-api-key-secure-change-this + +# ============ LOGGING ============ +LOG_LEVEL=info +# Log levels: debug, info, warn, error + +# ============ N8N ============ +N8N_ENCRYPTION_KEY=your-n8n-encryption-key-change-this diff --git a/Claude outputs/init-db.sql b/Claude outputs/init-db.sql new file mode 100644 index 0000000..1db0a22 --- /dev/null +++ b/Claude outputs/init-db.sql @@ -0,0 +1,141 @@ +-- ============ JARVIS DATABASE INITIALIZATION ============ + +-- "jarvis" is already created by the postgres image via POSTGRES_DB; +-- creating it again here would abort the whole init script. +CREATE DATABASE n8n; + +-- Connect to jarvis database +\c jarvis; + +-- ============ CORE TABLES ============ + +-- Users table +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + api_key VARCHAR(255) UNIQUE, + role VARCHAR(50) DEFAULT 'user', + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Conversations/Chat history +CREATE TABLE IF NOT EXISTS conversations ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(255), + context JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_conversations_user_id ON conversations(user_id); + +-- Chat messages +CREATE TABLE IF NOT EXISTS messages ( + id SERIAL PRIMARY KEY, + conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role VARCHAR(50) NOT NULL, + content TEXT NOT NULL, + tokens_used INTEGER, + metadata JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_messages_conversation_id ON messages(conversation_id); +CREATE INDEX idx_messages_user_id ON messages(user_id); + +-- Business automation tasks +CREATE TABLE IF NOT EXISTS tasks ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + description TEXT, + task_type VARCHAR(100), + status VARCHAR(50) DEFAULT 'pending', + priority INTEGER DEFAULT 0, + data JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP +); + +CREATE INDEX idx_tasks_user_id ON tasks(user_id); +CREATE INDEX idx_tasks_status ON tasks(status); + +-- Knowledge base documents +CREATE TABLE IF NOT EXISTS documents ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + document_type VARCHAR(100), + metadata JSONB, + embedding_id VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_documents_user_id ON documents(user_id); +CREATE INDEX idx_documents_embedding_id ON documents(embedding_id); + +-- Audit log +CREATE TABLE IF NOT EXISTS audit_logs ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, + action VARCHAR(255) NOT NULL, + resource_type VARCHAR(100), + resource_id INTEGER, + changes JSONB, + ip_address INET, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id); +CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at); + +-- ============ TRIGGERS ============ + +-- Auto-update timestamp function +CREATE OR REPLACE FUNCTION update_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Apply trigger to tables with updated_at +CREATE TRIGGER trigger_update_timestamp_users +BEFORE UPDATE ON users +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); + +CREATE TRIGGER trigger_update_timestamp_conversations +BEFORE UPDATE ON conversations +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); + +CREATE TRIGGER trigger_update_timestamp_tasks +BEFORE UPDATE ON tasks +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); + +CREATE TRIGGER trigger_update_timestamp_documents +BEFORE UPDATE ON documents +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); + +-- ============ DEFAULT DATA ============ + +-- Insert admin user (password: admin - CHANGE IN PRODUCTION!) +INSERT INTO users (username, email, password_hash, role) +VALUES ( + 'admin', + 'admin@jarvis.local', + '$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3djPvga3jaK', + 'admin' +) ON CONFLICT DO NOTHING; + +-- Permissions/Roles would go here +-- Add your own initialization data diff --git a/Claude outputs/migrations/002_document_chunks.sql b/Claude outputs/migrations/002_document_chunks.sql new file mode 100644 index 0000000..5b77c03 --- /dev/null +++ b/Claude outputs/migrations/002_document_chunks.sql @@ -0,0 +1,14 @@ +CREATE EXTENSION IF NOT EXISTS vector; + +CREATE TABLE document_chunks ( + id SERIAL PRIMARY KEY, + document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + chunk_index INTEGER NOT NULL, + content TEXT NOT NULL, + embedding vector(768) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_document_chunks_document_id ON document_chunks(document_id); +CREATE INDEX idx_document_chunks_embedding ON document_chunks + USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); diff --git a/Claude outputs/migrations/003_drop_low_data_ivfflat_index.sql b/Claude outputs/migrations/003_drop_low_data_ivfflat_index.sql new file mode 100644 index 0000000..c0a3fa0 --- /dev/null +++ b/Claude outputs/migrations/003_drop_low_data_ivfflat_index.sql @@ -0,0 +1,12 @@ +-- The ivfflat index built with lists=100 against a near-empty table has such +-- low recall that ORDER BY ... LIMIT queries silently return zero rows +-- (confirmed 2026-09-12: a single-row table returned no match under LIMIT, +-- and matched only after `SET ivfflat.probes = 10`). This is exactly what +-- Postgres warned about when creating the index ("This will cause low +-- recall... Drop the index until the table has more data"). +-- +-- Dropping it now: a sequential scan is correct and fast enough at this +-- data volume. Recreate an ivfflat (or hnsw) index once document_chunks +-- has enough rows for meaningful centroids (pgvector docs suggest +-- lists ~= sqrt(row_count)). +DROP INDEX IF EXISTS idx_document_chunks_embedding; diff --git a/Claude outputs/migrations/004_weather_cache.sql b/Claude outputs/migrations/004_weather_cache.sql new file mode 100644 index 0000000..710da23 --- /dev/null +++ b/Claude outputs/migrations/004_weather_cache.sql @@ -0,0 +1,10 @@ +CREATE TABLE weather_cache ( + id SERIAL PRIMARY KEY, + location VARCHAR(100) NOT NULL, + temperature_c NUMERIC NOT NULL, + condition_code INTEGER NOT NULL, + condition_text VARCHAR(100) NOT NULL, + fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_weather_cache_fetched_at ON weather_cache(fetched_at); diff --git a/Claude outputs/migrations/005_email_cache.sql b/Claude outputs/migrations/005_email_cache.sql new file mode 100644 index 0000000..d9ae331 --- /dev/null +++ b/Claude outputs/migrations/005_email_cache.sql @@ -0,0 +1,9 @@ +CREATE TABLE email_cache ( + id SERIAL PRIMARY KEY, + sender VARCHAR(255) NOT NULL, + subject VARCHAR(500) NOT NULL, + received_at TIMESTAMP NOT NULL, + cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_email_cache_received_at ON email_cache(received_at); diff --git a/Claude outputs/pytest.ini b/Claude outputs/pytest.ini new file mode 100644 index 0000000..2f4c80e --- /dev/null +++ b/Claude outputs/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +asyncio_mode = auto diff --git a/Claude outputs/requirements-dev.txt b/Claude outputs/requirements-dev.txt new file mode 100644 index 0000000..9868557 --- /dev/null +++ b/Claude outputs/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest==8.3.3 +httpx==0.27.2 +pytest-asyncio==0.24.0 diff --git a/Claude outputs/requirements.txt b/Claude outputs/requirements.txt new file mode 100644 index 0000000..8acc6db --- /dev/null +++ b/Claude outputs/requirements.txt @@ -0,0 +1,9 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 +psycopg2-binary==2.9.9 +redis==5.0.1 +anthropic +aiohttp==3.9.1 +caldav +icalendar diff --git a/Claude outputs/setup-jarvis-complete.sh b/Claude outputs/setup-jarvis-complete.sh new file mode 100644 index 0000000..2b7de77 --- /dev/null +++ b/Claude outputs/setup-jarvis-complete.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +set -e + +echo "πŸ€– ============ JARVIS CORE SETUP ============" +echo "" + +# ============ COLORS ============ +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# ============ CONFIGURATION ============ +JARVIS_HOME="/home/jarvis-core/jarvis" +DOMAIN="${1:-jarvis.local}" + +echo -e "${BLUE}πŸ“ Setup Directory: $JARVIS_HOME${NC}" +echo -e "${BLUE}πŸ“ Domain: $DOMAIN${NC}" +echo "" + +# ============ 1. SYSTEM UPDATE ============ +echo -e "${YELLOW}[1/7] System Update...${NC}" +sudo apt-get update -qq +sudo apt-get upgrade -y -qq +sudo apt-get install -y -qq \ + curl \ + wget \ + git \ + htop \ + net-tools \ + openssl > /dev/null +echo -e "${GREEN}βœ… System updated${NC}" + +# ============ 2. DOCKER ============ +echo -e "${YELLOW}[2/7] Installing Docker...${NC}" +if ! command -v docker &> /dev/null; then + curl -fsSL https://get.docker.com -o get-docker.sh + sudo bash get-docker.sh > /dev/null 2>&1 + rm get-docker.sh + sudo usermod -aG docker jarvis-core + echo -e "${GREEN}βœ… Docker installed${NC}" +else + echo -e "${GREEN}βœ… Docker already installed${NC}" +fi + +# ============ 3. DOCKER COMPOSE ============ +echo -e "${YELLOW}[3/7] Installing Docker Compose...${NC}" +if ! command -v docker-compose &> /dev/null; then + COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d'"' -f4) + sudo curl -L "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose > /dev/null 2>&1 + sudo chmod +x /usr/local/bin/docker-compose + echo -e "${GREEN}βœ… Docker Compose installed${NC}" +else + echo -e "${GREEN}βœ… Docker Compose already installed${NC}" +fi + +# ============ 4. PROJECT STRUCTURE ============ +echo -e "${YELLOW}[4/7] Creating project structure...${NC}" +mkdir -p $JARVIS_HOME/{config,data,logs,api,web} +cd $JARVIS_HOME + +# Create directories +mkdir -p data/{postgres,redis,milvus,etcd,ollama,n8n} +mkdir -p logs/{api,n8n,nginx} + +echo -e "${GREEN}βœ… Project structure created${NC}" + +# ============ 5. CONFIGURATION FILES ============ +echo -e "${YELLOW}[5/7] Creating configuration files...${NC}" + +# Copy docker-compose.yml (you'll need to provide this) +# This assumes files are available locally +if [ ! -f "docker-compose.yml" ]; then + echo -e "${YELLOW}⚠️ docker-compose.yml not found. Please copy it to $JARVIS_HOME${NC}" +fi + +# Create .env file +cat > .env << EOF +DOMAIN=$DOMAIN +DB_PASSWORD=$(openssl rand -base64 32) +CLAUDE_API_KEY= +OLLAMA_MODEL=mistral +LOG_LEVEL=info +JWT_SECRET=$(openssl rand -base64 32) +API_KEY_ADMIN=$(openssl rand -base64 32) +N8N_ENCRYPTION_KEY=$(openssl rand -base64 32) +EOF + +echo -e "${GREEN}βœ… Configuration created${NC}" +echo -e "${YELLOW} πŸ“ Edit .env with your API keys!${NC}" + +# ============ 6. VOLUMES & PERMISSIONS ============ +echo -e "${YELLOW}[6/7] Setting up volumes...${NC}" +sudo chown -R jarvis-core:jarvis-core $JARVIS_HOME +chmod -R 755 $JARVIS_HOME +chmod 600 .env +echo -e "${GREEN}βœ… Volumes configured${NC}" + +# ============ 7. DOCKER NETWORK ============ +echo -e "${YELLOW}[7/7] Creating Docker network...${NC}" +docker network create jarvis-net 2>/dev/null || true +echo -e "${GREEN}βœ… Docker network ready${NC}" + +echo "" +echo -e "${GREEN}============================================${NC}" +echo -e "${GREEN}πŸš€ JARVIS CORE READY FOR DEPLOYMENT${NC}" +echo -e "${GREEN}============================================${NC}" +echo "" +echo -e "${BLUE}Next steps:${NC}" +echo "1. cd $JARVIS_HOME" +echo "2. Edit .env - Add your CLAUDE_API_KEY" +echo "3. docker-compose up -d" +echo "" +echo -e "${BLUE}Services:${NC}" +echo " β€’ API: http://$DOMAIN:8000" +echo " β€’ n8n: http://$DOMAIN:5678" +echo " β€’ Web: http://$DOMAIN:3000" +echo " β€’ Traefik: http://$DOMAIN:8080" +echo " β€’ Postgres: 5432" +echo " β€’ Redis: 6379" +echo " β€’ Milvus: 19530" +echo " β€’ Ollama: 11434" +echo "" +echo -e "${YELLOW}⚠️ IMPORTANT: Change default passwords in production!${NC}" +echo "" diff --git a/Claude outputs/tests/test_auth.py b/Claude outputs/tests/test_auth.py new file mode 100644 index 0000000..e7a33fd --- /dev/null +++ b/Claude outputs/tests/test_auth.py @@ -0,0 +1,37 @@ +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import pytest +from fastapi.testclient import TestClient + +import main + +main.API_KEY_ADMIN = "test-secret" + + +@pytest.fixture() +def client(): + with TestClient(main.app) as c: + yield c + + +def test_missing_key_returns_401(client): + response = client.get("/api/v1/admin/stats") + assert response.status_code == 401 + + +def test_wrong_key_returns_401(client): + response = client.get("/api/v1/admin/stats", headers={"X-Admin-Key": "nope"}) + assert response.status_code == 401 + + +def test_correct_key_passes_auth_gate(client): + response = client.get("/api/v1/admin/stats", headers={"X-Admin-Key": "test-secret"}) + assert response.status_code != 401 + + +def test_health_stays_open_without_key(client): + response = client.get("/health") + assert response.status_code != 401 diff --git a/Claude outputs/tests/test_chunking.py b/Claude outputs/tests/test_chunking.py new file mode 100644 index 0000000..79bf520 --- /dev/null +++ b/Claude outputs/tests/test_chunking.py @@ -0,0 +1,20 @@ +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from main import chunk_text + + +def test_chunk_text_empty_returns_empty_list(): + assert chunk_text("") == [] + + +def test_chunk_text_shorter_than_chunk_size_returns_single_chunk(): + assert chunk_text("hello", chunk_size=1000, overlap=100) == ["hello"] + + +def test_chunk_text_exact_multiple_of_chunk_size(): + text = "a" * 20 + chunks = chunk_text(text, chunk_size=10, overlap=2) + assert chunks == [text[0:10], text[8:18], text[16:20]] diff --git a/Claude outputs/tests/test_email.py b/Claude outputs/tests/test_email.py new file mode 100644 index 0000000..1460abc --- /dev/null +++ b/Claude outputs/tests/test_email.py @@ -0,0 +1,68 @@ +import base64 +import os +import sys +from unittest.mock import MagicMock, patch + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import main + + +def _encoded_subject(text: str) -> bytes: + return b"=?utf-8?B?" + base64.b64encode(text.encode("utf-8")) + b"?=" + + +def _header_bytes(subject_bytes: bytes) -> bytes: + return ( + b"From: Sender \r\n" + b"Subject: " + subject_bytes + b"\r\n" + b"Date: Fri, 11 Sep 2026 15:47:02 +0000\r\n\r\n" + ) + + +def test_list_recent_emails_decodes_subject_and_marks_unread(): + fake_conn = MagicMock() + fake_conn.search.return_value = ("OK", [b"1"]) + fake_conn.fetch.return_value = ( + "OK", + [(b"1 (FLAGS () BODY[HEADER.FIELDS (FROM SUBJECT DATE)] {123}", _header_bytes(_encoded_subject("Ümlaut Betreff")))], + ) + + with patch.object(main.imaplib, "IMAP4", return_value=fake_conn): + emails = main._list_recent_emails_sync(limit=1) + + assert len(emails) == 1 + assert emails[0]["subject"] == "Ümlaut Betreff" + assert emails[0]["from"] == "Sender " + assert emails[0]["unread"] is True + fake_conn.login.assert_called_once_with(main.EMAIL_USER, main.EMAIL_PASSWORD) + + +def test_list_recent_emails_marks_seen_messages_as_read(): + fake_conn = MagicMock() + fake_conn.search.return_value = ("OK", [b"1"]) + fake_conn.fetch.return_value = ( + "OK", + [(b"1 (FLAGS (\\Seen) BODY[HEADER.FIELDS (FROM SUBJECT DATE)] {123}", _header_bytes(b"Plain Subject"))], + ) + + with patch.object(main.imaplib, "IMAP4", return_value=fake_conn): + emails = main._list_recent_emails_sync(limit=1) + + assert emails[0]["unread"] is False + assert emails[0]["subject"] == "Plain Subject" + + +def test_send_email_calls_smtp_with_composed_message(): + fake_smtp = MagicMock() + + with patch.object(main.smtplib, "SMTP_SSL", return_value=fake_smtp): + result = main._send_email_sync("empfaenger@example.com", "Betreff", "Text") + + fake_smtp.login.assert_called_once_with(main.EMAIL_USER, main.EMAIL_PASSWORD) + fake_smtp.send_message.assert_called_once() + sent_msg = fake_smtp.send_message.call_args[0][0] + assert sent_msg["To"] == "empfaenger@example.com" + assert sent_msg["Subject"] == "Betreff" + assert sent_msg["Date"] is not None + assert result == {"to": "empfaenger@example.com", "subject": "Betreff", "sent": True} diff --git a/Claude outputs/tests/test_weather.py b/Claude outputs/tests/test_weather.py new file mode 100644 index 0000000..0ab217c --- /dev/null +++ b/Claude outputs/tests/test_weather.py @@ -0,0 +1,48 @@ +import os +import sys +from datetime import datetime +from unittest.mock import AsyncMock, patch + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import pytest +from fastapi.testclient import TestClient + +import main + +main.API_KEY_ADMIN = "test-secret" +HEADERS = {"X-Admin-Key": "test-secret"} + + +@pytest.fixture() +def client(): + with TestClient(main.app) as c: + yield c + + +def test_weather_returns_503_when_no_data(client): + with patch.object(main, "pg_pool", "not-none"), patch.object( + main, "get_latest_weather", new=AsyncMock(return_value=None) + ): + response = client.get("/api/v1/weather", headers=HEADERS) + assert response.status_code == 503 + + +def test_weather_returns_latest_row(client): + row = { + "location": "Crailsheim", + "temperature_c": 18.4, + "condition_text": "Bewoelkt", + "fetched_at": datetime(2026, 9, 12, 16, 0, 3, 123456), + } + with patch.object(main, "pg_pool", "not-none"), patch.object( + main, "get_latest_weather", new=AsyncMock(return_value=row) + ): + response = client.get("/api/v1/weather", headers=HEADERS) + assert response.status_code == 200 + assert response.json()["location"] == "Crailsheim" + + +def test_weather_requires_admin_key(client): + response = client.get("/api/v1/weather") + assert response.status_code == 401 diff --git a/docs/superpowers/plans/2026-09-12-knowledge-base.md b/docs/superpowers/plans/2026-09-12-knowledge-base.md new file mode 100644 index 0000000..60fc1c5 --- /dev/null +++ b/docs/superpowers/plans/2026-09-12-knowledge-base.md @@ -0,0 +1,434 @@ +# JARVIS Knowledge Base (Phase 3) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the TODO-stub `POST/GET /api/v1/documents` endpoints with a real document-upload + semantic-search knowledge base, backed by pgvector in the existing Postgres instance and Ollama for embeddings. + +**Architecture:** Documents are chunked (character-based, with overlap) on upload, each chunk is embedded via Ollama's `nomic-embed-text` model, and stored in a new `document_chunks` table (pgvector column). Search embeds the query the same way and ranks chunks by cosine distance (`<=>`). + +**Tech Stack:** FastAPI, psycopg2 (existing `db_query` helper), pgvector Postgres extension, aiohttp (existing Ollama HTTP pattern), pytest (new, local-only dev dependency). + +**Spec:** `docs/superpowers/specs/2026-09-12-knowledge-base-design.md` + +## Global Constraints + +- No dedicated git repository exists for this project yet (the working directory sits inside an unrelated, very large home-directory-level repo that must not be touched). **Every "Commit" step below is replaced by "confirm the file is saved" β€” do not run `git add`/`git commit`.** +- Runtime is the live VPS at `72.61.186.98`, container `jarvis-api` (image `python:3.11-slim`, code bind-mounted from `/home/jarvis-core/jarvis/api`, `uvicorn --reload`). SSH: `ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98`. Local files live in `Claude outputs/` (note the space β€” quote the path in shell commands) and must be scp'd to `/home/jarvis-core/jarvis/...` to take effect. +- Reuse the existing `db_query(query, params, fetch)` async helper in `main.py` for all DB access β€” do not introduce an ORM. +- Follow the existing code style in `main.py`: section comments (`# ============ X ============`), try/except around endpoint bodies that logs and raises `HTTPException`, async endpoints. +- Embedding model: `nomic-embed-text` (768 dimensions) β€” this fixes the `vector(768)` column width used below. +- Chunking: ~1000 characters, 100 character overlap (from the spec) β€” do not change these defaults without updating the spec. + +--- + +### Task 1: pgvector extension + `document_chunks` table + +**Files:** +- Modify: `Claude outputs/docker-compose.yml` (postgres image) +- Create: `Claude outputs/migrations/002_document_chunks.sql` + +**Interfaces:** +- Produces: Postgres table `document_chunks(id, document_id, chunk_index, content, embedding vector(768), created_at)`, used by Task 4 and Task 5's DB helpers. + +**Context:** the plain `postgres:16-alpine` image does not ship the pgvector extension. `pgvector/pgvector:pg16` is a drop-in image (built from postgres:16) with the extension precompiled β€” swapping the image and recreating the container is safe, the existing `postgres_data` volume is untouched. + +- [ ] **Step 1: Swap the Postgres image to the pgvector-enabled build** + +Edit `Claude outputs/docker-compose.yml`: + +```yaml + postgres: + image: pgvector/pgvector:pg16 +``` + +(only the `image:` line under the `postgres:` service changes β€” everything else stays as-is). + +- [ ] **Step 2: Deploy the compose change and recreate the postgres container** + +```bash +SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key" +scp $SSHOPTS "C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs\docker-compose.yml" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/docker-compose.yml +ssh $SSHOPTS jarvis-core@72.61.186.98 "cd /home/jarvis-core/jarvis && docker compose up -d postgres && sleep 5 && docker compose ps postgres" +``` + +Expected: `jarvis-postgres` shows `Up ... (healthy)` with the new image. + +- [ ] **Step 3: Write the migration file** + +Create `Claude outputs/migrations/002_document_chunks.sql`: + +```sql +CREATE EXTENSION IF NOT EXISTS vector; + +CREATE TABLE document_chunks ( + id SERIAL PRIMARY KEY, + document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + chunk_index INTEGER NOT NULL, + content TEXT NOT NULL, + embedding vector(768) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_document_chunks_document_id ON document_chunks(document_id); +CREATE INDEX idx_document_chunks_embedding ON document_chunks + USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); +``` + +- [ ] **Step 4: Apply the migration to the live database** + +```bash +SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key" +scp $SSHOPTS "C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs\migrations\002_document_chunks.sql" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/migrations/002_document_chunks.sql +ssh $SSHOPTS jarvis-core@72.61.186.98 "docker exec -i jarvis-postgres psql -U jarvis -d jarvis < /home/jarvis-core/jarvis/migrations/002_document_chunks.sql" +``` + +Expected output ends with `CREATE INDEX` (no errors). If `CREATE EXTENSION` fails with "could not open extension control file", Step 1/2 didn't take effect β€” re-check the image was actually swapped (`docker inspect jarvis-postgres | grep Image`). + +- [ ] **Step 5: Verify** + +```bash +ssh $SSHOPTS jarvis-core@72.61.186.98 "docker exec jarvis-postgres psql -U jarvis -d jarvis -c '\d document_chunks'" +``` + +Expected: column list showing `embedding | vector(768)` among others. + +- [ ] **Step 6: Confirm files saved** (no git repo for this project β€” see Global Constraints) + +--- + +### Task 2: Chunking function (TDD) + +**Files:** +- Modify: `Claude outputs/main.py` (add `chunk_text`) +- Test: `Claude outputs/tests/test_chunking.py` +- Create: `Claude outputs/requirements-dev.txt` + +**Interfaces:** +- Produces: `chunk_text(text: str, chunk_size: int = 1000, overlap: int = 100) -> list[str]` in `main.py`, consumed by Task 4's upload endpoint. + +- [ ] **Step 1: Add pytest as a local-only dev dependency** + +Create `Claude outputs/requirements-dev.txt`: + +``` +pytest==8.3.3 +``` + +- [ ] **Step 2: Install it locally** + +```bash +cd "C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs" +python -m pip install -r requirements-dev.txt -r requirements.txt +``` + +- [ ] **Step 3: Write the failing tests** + +Create `Claude outputs/tests/test_chunking.py`: + +```python +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from main import chunk_text + + +def test_chunk_text_empty_returns_empty_list(): + assert chunk_text("") == [] + + +def test_chunk_text_shorter_than_chunk_size_returns_single_chunk(): + assert chunk_text("hello", chunk_size=1000, overlap=100) == ["hello"] + + +def test_chunk_text_exact_multiple_of_chunk_size(): + text = "a" * 20 + chunks = chunk_text(text, chunk_size=10, overlap=2) + assert chunks == [text[0:10], text[8:18], text[16:20]] +``` + +- [ ] **Step 4: Run tests to verify they fail** + +```bash +cd "C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs" +python -m pytest tests/test_chunking.py -v +``` + +Expected: FAIL with `ImportError: cannot import name 'chunk_text'` (or similar β€” `chunk_text` doesn't exist yet). Note: importing `main.py` will also try to import `psycopg2`, `redis`, `anthropic`, `aiohttp` β€” these must already be installed locally via Step 2 for the import to get far enough to fail on `chunk_text` specifically, not on a missing package. + +- [ ] **Step 5: Implement `chunk_text`** + +In `Claude outputs/main.py`, add near the other pure helpers (e.g. above `# ============ DB HELPERS ============`): + +```python +def chunk_text(text: str, chunk_size: int = 1000, overlap: int = 100) -> list: + if not text: + return [] + if len(text) <= chunk_size: + return [text] + chunks = [] + step = chunk_size - overlap + start = 0 + while start < len(text): + end = start + chunk_size + chunks.append(text[start:end]) + if end >= len(text): + break + start += step + return chunks +``` + +- [ ] **Step 6: Run tests to verify they pass** + +```bash +cd "C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs" +python -m pytest tests/test_chunking.py -v +``` + +Expected: 3 passed. + +- [ ] **Step 7: Confirm files saved** (no git repo for this project β€” see Global Constraints) + +--- + +### Task 3: Ollama embedding helper + +**Files:** +- Modify: `Claude outputs/main.py` (add `OLLAMA_EMBED_MODEL` config + `get_embedding`) +- Modify: `Claude outputs/docker-compose.yml` (jarvis-api environment) + +**Interfaces:** +- Consumes: `OLLAMA_HOST` (already defined in `main.py`), `aiohttp` (already imported). +- Produces: `async def get_embedding(text: str) -> list` in `main.py`, consumed by Task 4 and Task 5. + +- [ ] **Step 1: Add the embed-model config constant** + +In `Claude outputs/main.py`, next to the other `# ============ CONFIG ============` constants: + +```python +OLLAMA_EMBED_MODEL = os.getenv("OLLAMA_EMBED_MODEL", "nomic-embed-text") +``` + +- [ ] **Step 2: Add `get_embedding`** + +In `Claude outputs/main.py`, add after `chunk_text`: + +```python +async def get_embedding(text: str) -> list: + timeout = aiohttp.ClientTimeout(total=30) + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.post( + f"{OLLAMA_HOST}/api/embeddings", + json={"model": OLLAMA_EMBED_MODEL, "prompt": text}, + ) as resp: + if resp.status != 200: + body = await resp.text() + raise RuntimeError(f"Ollama embedding failed ({resp.status}): {body}") + data = await resp.json() + return data["embedding"] +``` + +- [ ] **Step 3: Add `OLLAMA_EMBED_MODEL` to the deployed environment** + +In `Claude outputs/docker-compose.yml`, under `jarvis-api: environment:`, add a line next to `OLLAMA_HOST`: + +```yaml + - OLLAMA_EMBED_MODEL=nomic-embed-text +``` + +- [ ] **Step 4: Confirm files saved** (no automated test here β€” `get_embedding` is verified end-to-end in Task 4; see Global Constraints for why there's no commit step) + +--- + +### Task 4: Wire up document upload + +**Files:** +- Modify: `Claude outputs/main.py` (`insert_document`, `insert_document_chunks`, `upload_document`) + +**Interfaces:** +- Consumes: `chunk_text` (Task 2), `get_embedding` (Task 3), `db_query` (existing), `DEFAULT_USER_ID` (existing global). +- Produces: DB helpers `insert_document(title, content, document_type) -> int` and `insert_document_chunks(document_id: int, chunks: list) -> None`, where `chunks` is `list[tuple[str, list]]` of `(chunk_text, embedding)` pairs. + +- [ ] **Step 1: Add a vector-literal formatting helper and the DB helpers** + +In `Claude outputs/main.py`, add near the other DB helpers (after `save_message`): + +```python +def _vector_literal(embedding: list) -> str: + return "[" + ",".join(repr(float(x)) for x in embedding) + "]" + + +async def insert_document(title: str, content: str, document_type: str) -> int: + row = await db_query( + "INSERT INTO documents (user_id, title, content, document_type) VALUES (%s, %s, %s, %s) RETURNING id", + (DEFAULT_USER_ID, title, content, document_type), + fetch="one", + ) + return row["id"] + + +async def insert_document_chunks(document_id: int, chunks: list): + for index, (content, embedding) in enumerate(chunks): + await db_query( + """ + INSERT INTO document_chunks (document_id, chunk_index, content, embedding) + VALUES (%s, %s, %s, %s::vector) + """, + (document_id, index, content, _vector_literal(embedding)), + ) +``` + +- [ ] **Step 2: Replace the `upload_document` endpoint** + +In `Claude outputs/main.py`, replace the existing stub: + +```python +@app.post("/api/v1/documents") +async def upload_document(title: str, content: str, document_type: str = "general"): + """Upload document to knowledge base""" + if not pg_pool: + raise HTTPException(status_code=503, detail="Database is not configured") + if not content.strip(): + raise HTTPException(status_code=422, detail="content must not be empty") + + try: + logger.info(f"Uploading document: {title}") + document_id = await insert_document(title, content, document_type) + chunks = chunk_text(content) + embedded_chunks = [(c, await get_embedding(c)) for c in chunks] + await insert_document_chunks(document_id, embedded_chunks) + + return { + "document_id": document_id, + "title": title, + "status": "indexed", + "chunk_count": len(chunks), + } + except HTTPException: + raise + except Exception as e: + logger.error(f"Document upload error: {str(e)}") + raise HTTPException(status_code=503, detail=f"Embedding failed: {str(e)}") +``` + +- [ ] **Step 3: Deploy** + +```bash +SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key" +OUT="C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs" +scp $SSHOPTS "$OUT/main.py" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/api/main.py +scp $SSHOPTS "$OUT/docker-compose.yml" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/docker-compose.yml +ssh $SSHOPTS jarvis-core@72.61.186.98 "cd /home/jarvis-core/jarvis && docker compose up -d jarvis-api && sleep 8 && docker logs --tail 20 jarvis-api" +``` + +Expected: log ends with `Application startup complete.` (no traceback). + +- [ ] **Step 4: Pull the embedding model** + +```bash +ssh $SSHOPTS jarvis-core@72.61.186.98 "docker exec jarvis-ollama ollama pull nomic-embed-text" +``` + +Expected: ends with `success`. + +- [ ] **Step 5: Test the upload endpoint against the live API** + +```bash +ssh $SSHOPTS jarvis-core@72.61.186.98 "curl -s -X POST 'http://localhost:8000/api/v1/documents?title=Oeffnungszeiten&content=Wir%20haben%20Montag%20bis%20Freitag%20von%209%20bis%2017%20Uhr%20geoeffnet.&document_type=faq'; echo" +``` + +Expected: JSON with `"status":"indexed"` and `"chunk_count":1`. + +- [ ] **Step 6: Confirm files saved** (no git repo for this project β€” see Global Constraints) + +--- + +### Task 5: Wire up document search + end-to-end verification + +**Files:** +- Modify: `Claude outputs/main.py` (`search_chunks`, `search_documents`) + +**Interfaces:** +- Consumes: `get_embedding` (Task 3), `db_query` (existing). +- Produces: DB helper `search_chunks(query_embedding: list, limit: int) -> list[dict]`. + +- [ ] **Step 1: Add the search DB helper** + +In `Claude outputs/main.py`, add next to `insert_document_chunks`: + +```python +async def search_chunks(query_embedding: list, limit: int): + return await db_query( + """ + SELECT dc.content, dc.chunk_index, d.id AS document_id, d.title, + dc.embedding <=> %s::vector AS distance + FROM document_chunks dc + JOIN documents d ON d.id = dc.document_id + ORDER BY distance ASC + LIMIT %s + """, + (_vector_literal(query_embedding), limit), + fetch="all", + ) +``` + +- [ ] **Step 2: Replace the `search_documents` endpoint** + +In `Claude outputs/main.py`, replace the existing stub: + +```python +@app.get("/api/v1/documents") +async def search_documents(query: str, limit: int = 10): + """Search knowledge base using vector similarity""" + if not pg_pool: + raise HTTPException(status_code=503, detail="Database is not configured") + if not query.strip(): + raise HTTPException(status_code=422, detail="query must not be empty") + + try: + query_embedding = await get_embedding(query) + rows = await search_chunks(query_embedding, limit) + return { + "query": query, + "results": [ + { + "document_id": r["document_id"], + "title": r["title"], + "content": r["content"], + "chunk_index": r["chunk_index"], + "distance": float(r["distance"]), + } + for r in rows + ], + "count": len(rows), + } + except HTTPException: + raise + except Exception as e: + logger.error(f"Document search error: {str(e)}") + raise HTTPException(status_code=503, detail=f"Search failed: {str(e)}") +``` + +- [ ] **Step 3: Deploy** + +```bash +SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key" +OUT="C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs" +scp $SSHOPTS "$OUT/main.py" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/api/main.py +ssh $SSHOPTS jarvis-core@72.61.186.98 "cd /home/jarvis-core/jarvis && docker compose restart jarvis-api && sleep 8 && docker logs --tail 20 jarvis-api" +``` + +Expected: log ends with `Application startup complete.` (no traceback). + +- [ ] **Step 4: End-to-end test (matches spec's manual verification requirement)** + +Using the document uploaded in Task 4 Step 5 ("Wir haben Montag bis Freitag von 9 bis 17 Uhr geoeffnet."), search with a thematically related query: + +```bash +ssh $SSHOPTS jarvis-core@72.61.186.98 "curl -s 'http://localhost:8000/api/v1/documents?query=Wann%20habt%20ihr%20auf%3F'; echo" +``` + +Expected: `results[0].title == "Oeffnungszeiten"` and `results[0].content` contains the opening-hours text, with the lowest `distance` of any returned result. + +- [ ] **Step 5: Confirm files saved** (no git repo for this project β€” see Global Constraints) diff --git a/docs/superpowers/plans/2026-09-12-weather-widget.md b/docs/superpowers/plans/2026-09-12-weather-widget.md new file mode 100644 index 0000000..b2617dc --- /dev/null +++ b/docs/superpowers/plans/2026-09-12-weather-widget.md @@ -0,0 +1,293 @@ +# JARVIS Weather Widget Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Expose the weather data the new n8n workflow writes to Postgres via a protected API endpoint, and show it top-right in the frontend nav bar. + +**Architecture:** `GET /api/v1/weather` reads the newest row from `weather_cache` (already populated by the "Wetter Crailsheim" n8n workflow, verified working). A new `WeatherWidget` React component polls that endpoint and renders in the nav bar. + +**Tech Stack:** FastAPI (existing `db_query`/`require_admin_key` patterns), React (existing `apiFetch` pattern). + +**Spec:** `docs/superpowers/specs/2026-09-12-weather-widget-design.md` + +## Global Constraints + +- No dedicated git repository for this project (see prior plans) β€” every "Commit" step is replaced by "confirm the file is saved". +- SSH: `ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98`. Local backend files: `Claude outputs/` (quote the path). Local frontend files: `web/`. +- The n8n workflow "Wetter Crailsheim" (id `BwNCJ2TkuZfqUzst`), its Postgres credential "JARVIS Postgres", and the `weather_cache` table already exist and are verified working (one row present from a manual test run) β€” this plan does not touch them. +- `weather_cache` schema: `id, location, temperature_c NUMERIC, condition_code INTEGER, condition_text VARCHAR(100), fetched_at TIMESTAMP`. +- The endpoint must use `dependencies=[Depends(require_admin_key)]` like every other `/api/v1/*` route (see `Claude outputs/main.py`). + +--- + +### Task 1: `GET /api/v1/weather` endpoint + +**Files:** +- Modify: `Claude outputs/main.py` (add `get_latest_weather` DB helper + `GET /api/v1/weather` endpoint) +- Test: `Claude outputs/tests/test_weather.py` + +**Interfaces:** +- Produces: `GET /api/v1/weather` returning `{"location": str, "temperature_c": float, "condition_text": str, "fetched_at": str}` on success, `503` when `weather_cache` is empty. Consumed by Task 2's `WeatherWidget`. + +- [ ] **Step 1: Write the failing test** + +Create `Claude outputs/tests/test_weather.py`: + +```python +import os +import sys +from datetime import datetime +from unittest.mock import AsyncMock, patch + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import pytest +from fastapi.testclient import TestClient + +import main + +main.API_KEY_ADMIN = "test-secret" +HEADERS = {"X-Admin-Key": "test-secret"} + + +@pytest.fixture() +def client(): + with TestClient(main.app) as c: + yield c + + +def test_weather_returns_503_when_no_data(client): + with patch.object(main, "pg_pool", "not-none"), patch.object( + main, "get_latest_weather", new=AsyncMock(return_value=None) + ): + response = client.get("/api/v1/weather", headers=HEADERS) + assert response.status_code == 503 + + +def test_weather_returns_latest_row(client): + row = { + "location": "Crailsheim", + "temperature_c": 18.4, + "condition_text": "Bewoelkt", + "fetched_at": datetime(2026, 9, 12, 16, 0, 3, 123456), + } + with patch.object(main, "pg_pool", "not-none"), patch.object( + main, "get_latest_weather", new=AsyncMock(return_value=row) + ): + response = client.get("/api/v1/weather", headers=HEADERS) + assert response.status_code == 200 + assert response.json()["location"] == "Crailsheim" + + +def test_weather_requires_admin_key(client): + response = client.get("/api/v1/weather") + assert response.status_code == 401 +``` + +Note: `patch.object(main, "pg_pool", "not-none")` stands in for "database configured" β€” the endpoint only checks `if not pg_pool`, so any truthy value works without needing a real connection pool. + +- [ ] **Step 2: Run test to verify it fails** + +```bash +SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key" +ssh $SSHOPTS jarvis-core@72.61.186.98 "mkdir -p /tmp/jarvis-test/tests" +OUT="C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs" +scp $SSHOPTS "$OUT/main.py" "$OUT/requirements.txt" "$OUT/requirements-dev.txt" jarvis-core@72.61.186.98:/tmp/jarvis-test/ +scp $SSHOPTS "$OUT/tests/test_weather.py" jarvis-core@72.61.186.98:/tmp/jarvis-test/tests/ +ssh $SSHOPTS jarvis-core@72.61.186.98 "docker run --rm -v /tmp/jarvis-test:/app -w /app python:3.11-slim bash -c 'pip install -q -r requirements-dev.txt -r requirements.txt && python -m pytest tests/test_weather.py -v'" +``` + +Expected: FAIL β€” `get_latest_weather` doesn't exist / `/api/v1/weather` is 404. + +- [ ] **Step 3: Add the DB helper and endpoint** + +In `Claude outputs/main.py`, add near the other DB helpers (after `search_chunks`): + +```python +async def get_latest_weather(): + return await db_query( + """ + SELECT location, temperature_c, condition_text, fetched_at + FROM weather_cache ORDER BY fetched_at DESC LIMIT 1 + """, + fetch="one", + ) +``` + +Add the endpoint near the other admin/status endpoints (after `detailed_health`): + +```python +@app.get("/api/v1/weather", dependencies=[Depends(require_admin_key)]) +async def get_weather(): + """Latest cached weather reading, populated by the n8n 'Wetter Crailsheim' workflow""" + if not pg_pool: + raise HTTPException(status_code=503, detail="Database is not configured") + + row = await get_latest_weather() + if not row: + raise HTTPException(status_code=503, detail="Weather data not available yet") + + return { + "location": row["location"], + "temperature_c": float(row["temperature_c"]), + "condition_text": row["condition_text"], + "fetched_at": row["fetched_at"].isoformat(), + } +``` + +- [ ] **Step 4: Run test to verify it passes** + +Re-run the Step 2 command. Expected: 3 passed. + +- [ ] **Step 5: Deploy and verify against the live API** + +```bash +SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key" +OUT="C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs" +scp $SSHOPTS "$OUT/main.py" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/api/main.py +ssh $SSHOPTS jarvis-core@72.61.186.98 "cd /home/jarvis-core/jarvis && docker compose restart jarvis-api && sleep 8 && docker logs --tail 15 jarvis-api" +``` + +Expected: log ends with `Application startup complete.` (no traceback). Then: + +```bash +ADMIN_KEY=$(ssh $SSHOPTS jarvis-core@72.61.186.98 "grep API_KEY_ADMIN /home/jarvis-core/jarvis/.env" | cut -d= -f2-) +ssh $SSHOPTS jarvis-core@72.61.186.98 "curl -s -H 'X-Admin-Key: $ADMIN_KEY' http://localhost:8000/api/v1/weather; echo" +``` + +Expected: JSON with `"location":"Crailsheim"` and the temperature/condition from the test run done during n8n workflow setup. + +- [ ] **Step 6: Confirm files saved** (no git repo for this project β€” see Global Constraints) + +--- + +### Task 2: `WeatherWidget` in the nav bar + +**Files:** +- Create: `web/src/components/WeatherWidget.tsx` +- Modify: `web/src/App.tsx` (render the widget in the nav) +- Modify: `web/src/index.css` (right-align the widget in the nav) + +**Interfaces:** +- Consumes: `apiFetch` from `./api` (existing), `GET /api/v1/weather` (Task 1). +- Produces: `WeatherWidget` default export, no props, rendered inside the existing `