From 371213dbbb324ee6fcf65bc2d6b3f2259ac58f41 Mon Sep 17 00:00:00 2001 From: Simon Kuehn Date: Tue, 19 May 2026 05:59:21 +0000 Subject: [PATCH] docs: update design doc + add infrastructure runbook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove Ollama as AI backend (never used); Mistral is primary throughout - Add infrastructure.md: full Docker setup, queue architecture, prod deployment steps, env vars — prod has no staging ERP container Co-Authored-By: Claude Sonnet 4.6 --- .../2026-05-13-superseller3000-design.md | 38 +- .../specs/2026-05-19-infrastructure.md | 546 ++++++++++++++++++ 2 files changed, 560 insertions(+), 24 deletions(-) create mode 100644 docs/superpowers/specs/2026-05-19-infrastructure.md diff --git a/docs/superpowers/specs/2026-05-13-superseller3000-design.md b/docs/superpowers/specs/2026-05-13-superseller3000-design.md index 2be931d..dc18f96 100644 --- a/docs/superpowers/specs/2026-05-13-superseller3000-design.md +++ b/docs/superpowers/specs/2026-05-13-superseller3000-design.md @@ -29,8 +29,8 @@ src/ # Orchestriert Domain über Interfaces (Ports) Infrastructure/ Channel/ # EbayAdapter, FrappeErpAdapter, [FutureAdapter] - AI/ # OllamaClient, MistralClient (beide hinter OllamaClientInterface) - # OllamaVisionAgent, SpecsResearchAgent, JsonCodingAgent, EbayTextAgent + AI/ # MistralClient hinter OllamaClientInterface (Interfacename historisch) + # VisionAgent, SpecsResearchAgent, JsonCodingAgent, EbayTextAgent Persistence/ # Doctrine Repositories (PostgreSQL) Logging/ # DatabaseLogHandler, ArchiveCommand Http/ # Symfony Controller, Webhook-Listener, EasyAdmin @@ -38,7 +38,7 @@ src/ Storage/ # StorageManager ``` -Jeder externe Dienst (eBay, Frappe ERP, Ollama, SMTP) implementiert ein Interface aus dem Application-Layer. Neue Plattform = neue Adapter-Klasse, Domain bleibt unberührt. +Jeder externe Dienst (eBay, Frappe ERP, Mistral, SMTP) implementiert ein Interface aus dem Application-Layer. Neue Plattform = neue Adapter-Klasse, Domain bleibt unberührt. ### 2.2 Tech-Stack @@ -51,7 +51,7 @@ Jeder externe Dienst (eBay, Frappe ERP, Ollama, SMTP) implementiert ein Interfac | Tests | PHPUnit 11 + Pest | | Datenbank | PostgreSQL 17 (eine Instanz, drei Schemas: `app`, `logs`, `logs_archive`) | | Cache / Queue | Redis 7 | -| AI | **Ollama** (lokal, SSH-Tunnel + autossh) **oder Mistral Cloud API** — per Alias in `services.yaml` umschaltbar; Modelle via `AI_TEXT_MODEL` / `AI_VISION_MODEL` env vars konfigurierbar | +| AI | **Mistral Cloud API** — Vision: `pixtral-12b-2409`, Text: `mistral-large-latest`; Modelle via `AI_TEXT_MODEL` / `AI_VISION_MODEL` env vars konfigurierbar | | Web-Suche | Tavily API (`TAVILY_API_KEY`) — liefert strukturierte Suchergebnisse für SpecsResearchAgent | | Admin-Panel | EasyAdmin 5 (`#[AdminDashboard]` Attribut, `type: easyadmin.routes` Loader) | | Auth | Symfony Security + scheb/two-factor-bundle (TOTP) | @@ -71,7 +71,7 @@ redis://...?queue_name=orders # Order-Processing, CustomerResolver, Invo redis://...?queue_name=channel_sync # Listing-Publish, Bestand-Sync, Deaktivierung, Tracking ``` -**Ollama-Ausfall:** AI-Pipeline-Jobs verbleiben in der Queue. Symfony Messenger wiederholt mit Backoff, nach 3 Versuchen → Failed Transport (persistent, nicht verloren). Operativer Betrieb (Verkäufe, Rechnungen, Bestand) läuft völlig unabhängig durch. Replay via `messenger:failed:retry` wenn Ollama wieder verfügbar. +**AI-Backend-Ausfall:** AI-Pipeline-Jobs verbleiben in der Queue. Symfony Messenger wiederholt mit Backoff, nach 3 Versuchen → Failed Transport (persistent, nicht verloren). Operativer Betrieb (Verkäufe, Rechnungen, Bestand) läuft völlig unabhängig durch. Replay via `messenger:failed:retry` wenn das AI-Backend wieder erreichbar ist. --- @@ -216,14 +216,14 @@ ApiKey ``` 1. Foto-Upload → PhotoUploadMessage in Queue (redis://ai_pipeline) -2. OllamaVisionAgent — LLaVA liest Typenschild +2. VisionAgent — Mistral Pixtral liest Typenschild Output: Hersteller, Modellname, Modellnummer, Seriennummer 3. Model-Cache-Check — findCompletedByModelNumber() in DB Treffer → copy ebayTitle/ebayDescription/specsText/attributes → Schritt 6 (kein AI mehr) Kein Treffer → weiter mit Schritt 4 4. SpecsResearchAgent — Tavily-Suche mit Modellbezeichnung → vollständige Specs (Freitext) Pflichtfeld-Liste kommt aus ArticleType.AttributeDefinitions ({{fields}}-Platzhalter im Prompt) -5. JsonCodingAgent — strukturierter Ollama-Call: Specs-Text → JSON gegen ArticleType-Schema +5. JsonCodingAgent — strukturierter Mistral-Call: Specs-Text → JSON gegen ArticleType-Schema 6. ValidationGate — alle Pflichtfelder gesetzt? (Schema + eBay-Kategorie-Pflichtfelder) ✓ → Schritt 7 ✗ → Retry ab Schritt 5 mit missing_fields im Prompt (max. 3×) @@ -352,7 +352,7 @@ Schema: logs_archive.log_entry # identische Struktur ### Dienste - **PostgreSQL:** App-User mit minimalen Rechten (kein Superuser) - **Redis:** `requirepass` gesetzt, nur intern erreichbar -- **Ollama:** lokal, Zugriff nur via SSH-Tunnel (autossh für Persistenz + Auto-Reconnect) +- **Mistral API:** Key in `.env.local`, nie in Git; HTTPS-only - **Gitea:** Registrierung deaktiviert, nur Admin-Anlage, SSH-Key-Auth ### Applikation @@ -440,31 +440,21 @@ docker compose exec app php bin/console app:api-keys:create --env=prod Der Key wird als bcrypt-Hash gespeichert. Prefix (erste 8 Zeichen) dient als Lookup-Key. Verwendung: `X-Api-Key: ` HTTP-Header. -### 11.3 AI-Backend wechseln +### 11.3 AI-Konfiguration -**Ollama (Standard, lokal):** +**Primär: Mistral Cloud API** (Standard, bereits so konfiguriert): ```yaml # config/services.yaml -App\Infrastructure\AI\OllamaClientInterface: - alias: App\Infrastructure\AI\OllamaClient -``` -```dotenv -# .env -AI_TEXT_MODEL=${OLLAMA_TEXT_MODEL} -AI_VISION_MODEL=${OLLAMA_VISION_MODEL} -``` - -**Mistral Cloud API:** -```yaml App\Infrastructure\AI\OllamaClientInterface: alias: App\Infrastructure\AI\MistralClient ``` ```dotenv -AI_TEXT_MODEL=${MISTRAL_TEXT_MODEL} -AI_VISION_MODEL=${MISTRAL_VISION_MODEL} +# .env.local MISTRAL_API_KEY=sk-... +TAVILY_API_KEY=tvly-... +# Modelle in .env vorbelegt: mistral-large-latest / pixtral-12b-2409 ``` -Vision erfordert Pixtral (`pixtral-12b-2409`). Nach Änderung: `cache:clear` + FPM reload. +Nach Schlüsseländerung: `docker compose exec app php bin/console cache:clear`. ### 11.4 Pipeline starten diff --git a/docs/superpowers/specs/2026-05-19-infrastructure.md b/docs/superpowers/specs/2026-05-19-infrastructure.md new file mode 100644 index 0000000..d4b22ce --- /dev/null +++ b/docs/superpowers/specs/2026-05-19-infrastructure.md @@ -0,0 +1,546 @@ +# SuperSeller3000 — Infrastruktur & Deployment + +**Zuletzt aktualisiert:** 2026-05-19 +**Zielumgebung:** VPS (Ubuntu/Debian), Docker Compose +**Domain:** `ss3k.schaunwama.de` + +> Dieses Dokument beschreibt den vollständigen Stand der Docker-Infrastruktur und alle Schritte, um das System auf einem neuen Server hochzuziehen. + +--- + +## 1. Übersicht der Services + +| Service | Image / Build | Zweck | +|---|---|---| +| `app` | Build `docker/app/Dockerfile` | PHP 8.4-FPM — Symfony-Applikation | +| `caddy` | `caddy:2-alpine` | Reverse Proxy, Auto-HTTPS (Let's Encrypt) | +| `postgres` | `postgres:17-alpine` | Datenbank (Schemas: `app`, `logs`, `logs_archive`) | +| `redis` | `redis:7-alpine` | Queue-Backend + Session-Cache | +| `worker-ai` | Build `docker/app/Dockerfile` | Messenger Worker — Transport `ai_pipeline` | +| `worker-orders` | Build `docker/app/Dockerfile` | Messenger Worker — Transport `orders` | +| `worker-channel` | Build `docker/app/Dockerfile` | Messenger Worker — Transport `channel_sync` | +| `cron` | Build `docker/app/Dockerfile` | Log-Rotation täglich (`app:logs:rotate`) | + +**Nicht im Prod-Setup:** kein Staging-ERP-Container. Frappe ERP läuft extern (eigene Instanz, `FRAPPE_ERP_BASE_URL` in `.env.local`). + +--- + +## 2. Queue-Architektur (Symfony Messenger + Redis Streams) + +Drei isolierte Redis-Streams — ein ausgefallener Worker blockiert die anderen nie. + +### Transports & Retry-Strategie + +| Transport | Redis Stream | Worker | max_retries | Delay / Backoff | +|---|---|---|---|---| +| `ai_pipeline` | `ai_pipeline` | `worker-ai` | 3 | 2 s · ×2 | +| `orders` | `orders` | `worker-orders` | 5 | 1 s · ×2 | +| `channel_sync` | `channel_sync` | `worker-channel` | 5 | 2 s · ×2 · max 60 s | +| `failed` | `failed` | — | — | persistent, nie verloren | + +Nach Erschöpfung aller Retries landet die Message im `failed`-Transport. Replay: `messenger:failed:retry`. + +### Messages pro Transport + +#### `ai_pipeline` → `worker-ai` + +Die Pipeline-Schritte werden sequenziell als separate Messages dispatcht — jeder Handler dispatcht den Nachfolger. + +| Message | Handler-Logik | +|---|---| +| `PhotoUploadMessage` | Startet Pipeline A: `VisionAgent` (Mistral Pixtral) liest Typenschild → Hersteller, Modell, SN | +| `SpecsResearchMessage` | `SpecsResearchAgent`: Tavily-Suche mit Modellbezeichnung → Specs-Freitext; Pflichtfelder aus `ArticleType.AttributeDefinitions` (`{{fields}}`-Platzhalter im Prompt) | +| `JsonCodingMessage` | `JsonCodingAgent`: Specs-Text → strukturiertes JSON gegen ArticleType-Schema | +| `ValidationMessage` | Pflichtfelder vollständig? → weiter; sonst Retry ab `JsonCodingMessage` (max. 3×, `missing_fields` im Prompt) → `needs_review` | +| `DraftArticleMessage` | Article anlegen (status: `draft`), Inventurnummer vergeben, Attribute + Foto persistieren | +| `EbayTextMessage` | `EbayTextAgent`: eBay-Titel + Beschreibung aus Attributen generieren, an Article speichern | +| `PxeInventoryMessage` | Startet Pipeline B (PXE): PXE-Dump direkt an `JsonCodingAgent` — SpecsResearch entfällt | + +**Model-Cache:** Nach `PhotoUploadMessage` prüft `findCompletedByModelNumber()` die DB. Treffer → alle Daten kopieren, `SpecsResearch`/`JsonCoding`/`EbayText` überspringen. +**Fehlerfall:** `PipelineJobFailureListener` fängt `WorkerMessageFailedEvent` ab und setzt `AIPipelineJob.status = failed` mit dem echten Fehlertext. + +#### `orders` → `worker-orders` + +| Message | Handler-Logik | +|---|---| +| `OrderReceivedMessage` | Vollständiger 13-Schritt-Order-Flow: Idempotenz-Check → atomarer Inventory-Lock (`stock - 1`) → `CustomerResolver` (Matching-Kaskade) → Order anlegen → Frappe Sales Invoice → PDF abrufen → `StorageManager` → Invoice-Record → PDF per SMTP an Lieferant → `UpdateStockOnChannelsMessage` oder `DeactivateListingMessage` dispatchen → Order: `completed` | + +#### `channel_sync` → `worker-channel` + +| Message | Handler-Logik | +|---|---| +| `PublishToChannelMessage` | `ChannelAdapterRegistry` → richtiger Adapter (z.B. `EbayAdapter`) → `publishListing()` → `ebay_listing_id` an Article zurückschreiben | +| `UpdateStockOnChannelsMessage` | `updateStock()` auf allen aktiven Plattformen des Artikels | +| `DeactivateListingMessage` | `deactivateListing()` — wird ausgelöst wenn `stock = 0` nach Verkauf | +| `TrackingPushMessage` | `pushTracking()` → Plattform als versandt markieren → `tracking_pushed_to_ebay_at` setzen | + +### AI-Backend-Ausfall + +AI-Worker-Messages verbleiben in der Queue. Messenger wiederholt mit Backoff, nach 3 Versuchen → `failed`-Transport. Operativer Betrieb (Verkäufe, Rechnungen, Bestand) läuft völlig unabhängig durch. Replay via `messenger:failed:retry` wenn das AI-Backend wieder erreichbar ist. + +--- + +## 3. Netzwerk + +Alle Services kommunizieren über ein internes Docker-Bridge-Network. Nur Caddy exponiert Ports nach außen. + +```yaml +networks: + default: + driver: bridge + ipam: + config: + - subnet: 172.18.0.0/24 + gateway: 172.18.0.1 +``` + +Das Gateway `172.18.0.1` ist die Host-Adresse innerhalb des Docker-Netzes (erreichbar von allen Containern). + +--- + +## 4. docker-compose.yml (Prod) + +```yaml +services: + app: + build: + context: . + dockerfile: docker/app/Dockerfile + user: "1000:1000" + environment: + HOME: /tmp + volumes: + - .:/var/www + - ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro + - ./docker/app/zz-fpm-pool.conf:/usr/local/etc/php-fpm.d/zzz-pool.conf:ro + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + env_file: + - path: .env + required: true + - path: .env.local + required: false + + caddy: + image: caddy:2-alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile + - .:/var/www + - ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro + - caddy_data:/data + depends_on: + - app + + postgres: + image: postgres:17-alpine + env_file: .env + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + worker-ai: + build: + context: . + dockerfile: docker/app/Dockerfile + user: "1000:1000" + environment: + HOME: /tmp + command: php bin/console messenger:consume ai_pipeline --time-limit=3600 --memory-limit=256M + volumes: + - .:/var/www + - ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro + depends_on: + - postgres + - redis + env_file: + - path: .env + required: true + - path: .env.local + required: false + restart: unless-stopped + + worker-orders: + build: + context: . + dockerfile: docker/app/Dockerfile + user: "1000:1000" + environment: + HOME: /tmp + command: php bin/console messenger:consume orders --time-limit=3600 --memory-limit=256M + volumes: + - .:/var/www + - ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro + depends_on: + - postgres + - redis + env_file: + - path: .env + required: true + - path: .env.local + required: false + restart: unless-stopped + + worker-channel: + build: + context: . + dockerfile: docker/app/Dockerfile + user: "1000:1000" + environment: + HOME: /tmp + command: php bin/console messenger:consume channel_sync --time-limit=3600 --memory-limit=256M + volumes: + - .:/var/www + - ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro + depends_on: + - postgres + - redis + env_file: + - path: .env + required: true + - path: .env.local + required: false + restart: unless-stopped + + cron: + build: + context: . + dockerfile: docker/app/Dockerfile + user: "1000:1000" + environment: + HOME: /tmp + command: > + sh -c "while true; do + php bin/console app:logs:rotate; + sleep 86400; + done" + volumes: + - .:/var/www + - ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro + depends_on: + - postgres + env_file: + - path: .env + required: true + - path: .env.local + required: false + restart: unless-stopped + +volumes: + postgres_data: + redis_data: + caddy_data: + +networks: + default: + driver: bridge + ipam: + config: + - subnet: 172.18.0.0/24 + gateway: 172.18.0.1 +``` + +**Hinweis:** `docker-compose.override.yml` existiert nur für lokale Entwicklung (Port-Bindings für Postgres/Redis zum direkten Zugriff vom Host). Auf dem Prod-Server nicht verwenden. + +--- + +## 4. Dockerfile — `docker/app/Dockerfile` + +```dockerfile +FROM php:8.4-fpm-alpine + +RUN apk add --no-cache \ + postgresql-dev \ + icu-dev \ + libzip-dev \ + unzip \ + git \ + $PHPIZE_DEPS \ + && docker-php-ext-install \ + pdo_pgsql \ + intl \ + zip \ + opcache \ + && pecl install redis \ + && docker-php-ext-enable redis \ + && apk del $PHPIZE_DEPS + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www + +COPY docker/app/php.ini /usr/local/etc/php/conf.d/app.ini +``` + +--- + +## 5. PHP-Konfiguration + +### `docker/app/php.ini` + +```ini +opcache.enable=0 +memory_limit=256M +upload_max_filesize=20M +post_max_size=20M +``` + +`opcache.enable=0` weil der Dev-Build keinen Preload hat — in Prod kann auf `1` + `opcache.preload` umgestellt werden, wenn gewünscht. + +### `docker/app/zz-fpm-pool.conf` + +```ini +[www] +; SSE connections hold a worker for up to 90 s each — raise the pool ceiling +; so regular requests are not starved. +pm = dynamic +pm.max_children = 30 +pm.start_servers = 4 +pm.min_spare_servers = 2 +pm.max_spare_servers = 8 +``` + +--- + +## 6. Caddyfile (Prod) — `docker/caddy/Caddyfile` + +``` +{ + admin off +} + +ss3k.schaunwama.de { + root * /var/www/public + php_fastcgi app:9000 + file_server + encode gzip + + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + } +} +``` + +**Prod enthält keinen `erpstaging`-Block.** Frappe ERP läuft auf einer eigenen Instanz (extern, `FRAPPE_ERP_BASE_URL` in `.env.local`). + +> Aktueller Dev-Stand hat noch den `erpstaging.schaunwama.de`-Block für die Staging-ERP-Weiterleitung — der muss vor dem ersten Prod-Deploy entfernt werden. + +--- + +## 7. Umgebungsvariablen + +### `.env` (ohne Credentials, in Git) + +```dotenv +APP_ENV=prod +APP_SECRET=change_me_in_env_local +POSTGRES_DB=superseller +POSTGRES_USER=superseller +POSTGRES_PASSWORD=change_me +DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?serverVersion=17&charset=utf8" +REDIS_PASSWORD=change_me +REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379 +MESSENGER_TRANSPORT_DSN=redis://:${REDIS_PASSWORD}@redis:6379/messages +MAILER_DSN=smtp://localhost +TAVILY_API_KEY= +MISTRAL_BASE_URL=https://api.mistral.ai +MISTRAL_API_KEY= +MISTRAL_VISION_MODEL=pixtral-12b-2409 +MISTRAL_TEXT_MODEL=mistral-large-latest +AI_TEXT_MODEL=${MISTRAL_TEXT_MODEL} +AI_VISION_MODEL=${MISTRAL_VISION_MODEL} +EBAY_CLIENT_ID= +EBAY_CLIENT_SECRET= +EBAY_MARKETPLACE_ID=EBAY_DE +EBAY_API_BASE_URL=https://api.ebay.com +EBAY_OAUTH_BASE_URL=https://api.ebay.com +EBAY_VERIFICATION_TOKEN= +EBAY_ENDPOINT_URL=https://ss3k.schaunwama.de/webhooks/ebay +FRAPPE_ERP_BASE_URL=https://erp.example.com +FRAPPE_ERP_API_KEY=changeme +FRAPPE_ERP_API_SECRET=changeme +FRAPPE_GENERIC_ITEM_CODE=REFURB-HW +SUPPLIER_EMAIL=lieferant@example.com +SENDER_EMAIL=noreply@superseller3000.de +``` + +### `.env.local` (nur auf Server, nie in Git) + +Alle echten Credentials überschreiben hier die Platzhalter aus `.env`: + +```dotenv +APP_SECRET= +POSTGRES_PASSWORD= +REDIS_PASSWORD= +MAILER_DSN=smtp://user:pass@mailserver:587 +TAVILY_API_KEY=tvly-... +MISTRAL_API_KEY=sk-... +EBAY_CLIENT_ID=... +EBAY_CLIENT_SECRET=... +EBAY_VERIFICATION_TOKEN=... # aus eBay Developer Portal +FRAPPE_ERP_BASE_URL=https://erp.meinefirma.de +FRAPPE_ERP_API_KEY=... +FRAPPE_ERP_API_SECRET=... +SUPPLIER_EMAIL=lieferant@meinefirma.de +SENDER_EMAIL=noreply@meinefirma.de +``` + +--- + +## 8. VPS-Voraussetzungen + +```bash +# Docker + Compose Plugin +apt install docker.io docker-compose-plugin + +# User 1000 muss existieren (PHP läuft als 1000:1000) +id -u superseller # sollte 1000 sein; ggf. anpassen + +# UFW: nur Caddy-Ports + SSH +ufw allow 22/tcp +ufw allow 80/tcp +ufw allow 443/tcp +ufw enable +``` + +--- + +## 9. Erste Inbetriebnahme (Schritt für Schritt) + +```bash +# 1. Repo klonen +git clone /superseller3000 /home/superseller/SuperSeller3000 +cd /home/superseller/SuperSeller3000 + +# 2. .env.local mit echten Credentials anlegen (siehe Abschnitt 7) +nano .env.local + +# 3. Caddyfile prüfen — kein erpstaging-Block! +# (docker/caddy/Caddyfile muss nur den ss3k.schaunwama.de-Block enthalten) + +# 4. Images bauen + DB hochfahren +docker compose up -d postgres redis +# warten bis postgres healthy + +# 5. Composer install +docker compose run --rm app composer install --no-dev --optimize-autoloader + +# 6. Datenbankmigrationen ausführen +docker compose run --rm app php bin/console doctrine:migrations:migrate --no-interaction + +# 7. Cache aufwärmen +docker compose run --rm app php bin/console cache:warmup --env=prod + +# 8. Ersten Admin-User anlegen +docker compose run --rm app php bin/console app:users:create --env=prod + +# 9. Ersten API-Key anlegen (Klartext wird nur einmal angezeigt) +docker compose run --rm app php bin/console app:api-keys:create --env=prod + +# 10. Alle Services starten +docker compose up -d + +# 11. eBay-Webhook registrieren (URL in .env.local: EBAY_ENDPOINT_URL) +# Im eBay Developer Portal: Notification URL = https://ss3k.schaunwama.de/webhooks/ebay +# Verification Token = EBAY_VERIFICATION_TOKEN +``` + +--- + +## 10. Laufender Betrieb + +### Worker-Status prüfen + +```bash +docker compose ps +docker compose logs -f worker-ai +docker compose logs -f worker-orders +docker compose logs -f worker-channel +``` + +### Failed Messages (nach Erschöpfung aller Retries) + +```bash +# Übersicht +docker compose exec app php bin/console messenger:failed:show + +# Einzelne Message erneut versuchen +docker compose exec app php bin/console messenger:failed:retry + +# Alle nochmal +docker compose exec app php bin/console messenger:failed:retry --all +``` + +### Updates deployen + +```bash +git pull +docker compose build app worker-ai worker-orders worker-channel cron +docker compose run --rm app composer install --no-dev --optimize-autoloader +docker compose run --rm app php bin/console doctrine:migrations:migrate --no-interaction +docker compose run --rm app php bin/console cache:warmup --env=prod +docker compose up -d +``` + +### Logs (Admin-Panel) + +`https://ss3k.schaunwama.de/admin` → **Log Entries** — Fulltext-Suche, Filter nach Level/Channel/Zeitraum. +Rotation läuft täglich automatisch (Cron-Service): Einträge > 90 Tage → `logs_archive`, danach gelöscht. + +### Backup + +```bash +# PostgreSQL-Dump +docker compose exec postgres pg_dump -U superseller superseller | gzip > backup_$(date +%Y%m%d).sql.gz + +# Artikel-Fotos und Rechnungs-PDFs (Docker Volumes) +docker run --rm -v superseller3000_postgres_data:/data -v $(pwd):/backup \ + alpine tar czf /backup/volume_postgres_$(date +%Y%m%d).tar.gz /data +``` + +--- + +## 11. AI-Konfiguration + +### Primäres Backend: Mistral Cloud API + +Standardkonfiguration — `config/services.yaml` bereits so ausgeliefert: + +```yaml +App\Infrastructure\AI\OllamaClientInterface: + alias: App\Infrastructure\AI\MistralClient +``` + +`.env.local`: +```dotenv +MISTRAL_API_KEY=sk-... +# Modelle sind in .env vorbelegt: +# AI_TEXT_MODEL=${MISTRAL_TEXT_MODEL} → mistral-large-latest +# AI_VISION_MODEL=${MISTRAL_VISION_MODEL} → pixtral-12b-2409 +``` + +Web-Suche (SpecsResearchAgent): **Tavily API** — `TAVILY_API_KEY=tvly-...` in `.env.local`. + +Nach Schlüsseländerung: `docker compose exec app php bin/console cache:clear`.