docs: update design doc + add infrastructure runbook
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
bf1af0a0bf
commit
371213dbbb
2 changed files with 560 additions and 24 deletions
|
|
@ -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: <rawKey>` 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
|
||||
|
||||
|
|
|
|||
546
docs/superpowers/specs/2026-05-19-infrastructure.md
Normal file
546
docs/superpowers/specs/2026-05-19-infrastructure.md
Normal file
|
|
@ -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=<random-32-char-hex>
|
||||
POSTGRES_PASSWORD=<sicheres-passwort>
|
||||
REDIS_PASSWORD=<sicheres-passwort>
|
||||
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 <gitea-url>/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`.
|
||||
Loading…
Reference in a new issue