Files
erp-nutzfahrzeuge/architecture.md
T

1163 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ERP Nutzfahrzeuge Architekturdokument
> **Version**: 1.0
> **Datum**: 2026-07-12
> **Status**: Approved for Implementation
> **Solution Architect**: A0 Orchestrator
---
## 1. Übersicht
ERP-System für den Handel mit Nutzfahrzeugen (LKW, Baumaschinen, PKW, Stapler, Transporter).
Backend in Python/FastAPI, Frontend in React/Next.js (mobile-first), PostgreSQL als Datenbank,
Redis für Caching/Queues. Hosting via Coolify self-hosted mit Docker-Compose (4 Container).
### 1.1 Stakeholder & Rollen
| Rolle | Berechtigungen | Nutzer (~10) |
|---|---|---|
| Admin | Vollzugriff, User-Management, Systemeinstellungen | 1-2 |
| Verkäufer | Fahrzeuge, Kontakte, Verkäufe, OCR, KI-Copilot | 5-7 |
| Buchhaltung | Verkäufe (read), DATEV-Export, USt-IdNr.-Prüfung | 1-2 |
### 1.2 Sprachen
- Deutsch (DE) Primärsprache
- Englisch (EN) Sekundärsprache
---
## 2. Tech-Stack
| Schicht | Technologie | Version |
|---|---|---|
| Backend | Python / FastAPI | Python 3.12, FastAPI 0.111+ |
| Frontend | React / Next.js | Next.js 15 (App Router), React 19 |
| CSS | Tailwind CSS | 3.4+ |
| Datenbank | PostgreSQL | 16+ |
| Cache/Queue | Redis | 7+ |
| ORM | SQLAlchemy 2.0 + Alembic | async |
| KI | OpenRouter API | Qwen2.5-VL (OCR), Flux.1-Pro (Bild), Claude/GPT-4 (Copilot) |
| mobile.de | Seller API (Push only) | REST |
| OCR | OpenRouter Vision (Qwen2.5-VL) | |
| DATEV | CSV-Export (DATEV-Format) | |
| USt-IdNr. | BZSt API (geplant, aktuell kein Zugang) | REST |
| Deployment | Docker-Compose via Coolify | 4 Container |
| Auth | JWT (access + refresh) | |
| Testing | pytest + httpx (Backend), Vitest + Playwright (Frontend) | |
---
## 3. Deployment-Architektur
### 3.1 Docker-Compose (4 Container)
```
┌─────────────────────────────────────────────────┐
│ Coolify (46.225.91.159) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ frontend │ │ backend │ │ redis │ │
│ │ Next.js │ │ FastAPI │ │ cache │ │
│ │ :3000 │ │ :8000 │ │ :6379 │ │
│ └────┬─────┘ └────┬─────┘ └──────────┘ │
│ │ │ │
│ │ ┌────┴─────┐ │
│ │ │ postgres │ │
│ │ │ :5432 │ │
│ │ └──────────┘ │
│ │ │
│ Traefik (SSL, Let's Encrypt) │
└─────────────────────────────────────────────────┘
```
### 3.2 Netzwerk
- Traefik als Reverse Proxy mit automatischem SSL
- Frontend erreichbar unter `https://erp.media-on.de`
- Backend API unter `https://erp.media-on.de/api`
- Interne Kommunikation über Docker-Netzwerk
### 3.3 Volumes
- `postgres_data`: Datenbank-Persistenz
- `redis_data`: Redis-Persistenz (optional)
- `uploads_data`: Fahrzeug-Dateien (Bilder, Dokumente, OCR-Scans)
- `retouched_images`: Retuschierte Bilder
---
## 4. Modulstruktur
### 4.1 Backend-Struktur (FastAPI)
```
backend/
├── app/
│ ├── main.py # FastAPI app entry, CORS, routers
│ ├── config.py # Settings (Pydantic BaseSettings)
│ ├── database.py # async engine, session factory
│ ├── dependencies.py # Common deps (auth, pagination)
│ ├── models/ # SQLAlchemy models
│ │ ├── user.py
│ │ ├── vehicle.py
│ │ ├── contact.py
│ │ ├── sale.py
│ │ ├── document.py
│ │ └── ocr_result.py
│ ├── schemas/ # Pydantic schemas (request/response)
│ │ ├── user.py
│ │ ├── vehicle.py
│ │ ├── contact.py
│ │ ├── sale.py
│ │ └── ocr.pyn│ ├── routers/ # API route handlers
│ │ ├── auth.py
│ │ ├── vehicles.py
│ │ ├── contacts.py
│ │ ├── sales.py
│ │ ├── ocr.py
│ │ ├── files.py
│ │ ├── copilot.py
│ │ └── image_retouch.py
│ ├── services/ # Business logic
│ │ ├── auth_service.py
│ │ ├── vehicle_service.py
│ │ ├── mobilede_service.py
│ │ ├── contact_service.py
│ │ ├── sale_service.py
│ │ ├── ocr_service.py
│ │ ├── file_service.py
│ │ ├── copilot_service.py
│ │ ├── retouch_service.py
│ │ └── datev_service.py
│ ├── tasks/ # Background tasks (async)
│ │ ├── mobilede_push.py
│ │ └── ocr_processing.py
│ └── utils/
│ ├── openrouter.py # OpenRouter API client
│ ├── i18n.py # Backend i18n helpers
│ └── datev.py # DATEV export formatter
├── alembic/ # DB migrations
├── tests/ # pytest test suite
├── requirements.txt
├── Dockerfile
└── pyproject.toml
```
### 4.2 Frontend-Struktur (Next.js)
```
frontend/
├── app/ # App Router
│ ├── layout.tsx # Root layout (i18n provider, auth)
│ ├── page.tsx # Dashboard
│ ├── (auth)/
│ │ └── login/page.tsx
│ ├── vehicles/
│ │ ├── page.tsx # List + filter
│ │ ├── [id]/page.tsx # Detail
│ │ └── new/page.tsx # Create form
│ ├── contacts/
│ │ ├── page.tsx # List
│ │ ├── [id]/page.tsx # Detail
│ │ └── new/page.tsx # Create form
│ ├── sales/
│ │ ├── page.tsx # List
│ │ ├── [id]/page.tsx # Detail + contract
│ │ └── new/page.tsx # Create sale
│ ├── ocr/
│ │ └── page.tsx # OCR upload + results
│ ├── copilot/
│ │ └── page.tsx # KI-Copilot chat
│ └── settings/
│ └── page.tsx # Admin settings
├── components/ # Reusable components
│ ├── ui/ # Base UI (Button, Input, Card, Table)
│ ├── vehicles/ # Vehicle-specific components
│ ├── contacts/ # Contact-specific components
│ ├── sales/ # Sale-specific components
│ ├── ocr/ # OCR components
│ └── copilot/ # Copilot chat components
├── lib/ # Client utilities
│ ├── api.ts # API client (fetch wrapper)
│ ├── auth.ts # Auth context/hooks
│ └── i18n.ts # i18n config
├── messages/ # i18n translation files
│ ├── de.json
│ └── en.json
├── public/
├── Dockerfile
├── tailwind.config.ts
└── package.json
```
---
## 5. Datenmodell
### 5.1 Entity-Relationship-Übersicht
```
User ──< Vehicle ──< File (Dateiablage)
│ │
│ ├──< OCRResult
│ ├──< RetouchedImage
│ └──< Sale ──< ContractDocument
│ └──< DATEVExport
└──< Contact (Kunde/Lieferant)
```
### 5.2 Kern-Entitäten
#### User
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| email | VARCHAR(255) | unique, not null |
| full_name | VARCHAR(200) | not null |
| role | ENUM('admin','verkaeufer','buchhaltung') | not null |
| password_hash | VARCHAR(255) | not null |
| is_active | BOOLEAN | default true |
| created_at | TIMESTAMPTZ | default now() |
#### Vehicle
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| vehicle_type | ENUM('lkw','baumaschine','pkw','stapler','transporter') | not null |
| brand | VARCHAR(100) | not null |
| model | VARCHAR(150) | not null |
| vin | VARCHAR(17) | unique |
| registration_plate | VARCHAR(20) | |
| first_registration | DATE | |
| mileage_km | INTEGER | |
| power_kw | INTEGER | |
| fuel_type | ENUM('diesel','petrol','electric','hybrid','other') | |
| purchase_price | DECIMAL(12,2) | not null |
| sale_price | DECIMAL(12,2) | |
| status | ENUM('in_stock','reserved','sold','deleted') | default 'in_stock' |
| mobilede_ad_id | VARCHAR(50) | |
| mobilede_synced_at | TIMESTAMPTZ | |
| notes | TEXT | |
| created_by | UUID | FK → User |
| created_at | TIMESTAMPTZ | default now() |
| updated_at | TIMESTAMPTZ | default now() |
#### Contact
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| contact_type | ENUM('kunde','lieferant','beide') | not null |
| company_name | VARCHAR(200) | |
| first_name | VARCHAR(100) | |
| last_name | VARCHAR(100) | |
| street | VARCHAR(255) | |
| zip_code | VARCHAR(10) | |
| city | VARCHAR(100) | |
| country | VARCHAR(2) | default 'DE' |
| is_eu | BOOLEAN | default false |
| ust_id_nr | VARCHAR(20) | |
| phone | VARCHAR(30) | |
| email | VARCHAR(255) | |
| created_at | TIMESTAMPTZ | default now() |
#### Sale
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| vehicle_id | UUID | FK → Vehicle, not null |
| buyer_contact_id | UUID | FK → Contact, not null |
| seller_user_id | UUID | FK → User, not null |
| sale_price | DECIMAL(12,2) | not null |
| sale_date | DATE | not null |
| payment_method | ENUM('bank_transfer','cash','financing','other') | |
| is_gwg | BOOLEAN | default false (Geringwertiges Wirtschaftsgut) |
| gwg_amount | DECIMAL(12,2) | |
| vat_rate | DECIMAL(5,2) | default 19.00 |
| ust_id_nr_verified | BOOLEAN | default false |
| contract_pdf_path | VARCHAR(500) | |
| datev_export_id | UUID | FK → DATEVExport (nullable) |
| created_at | TIMESTAMPTZ | default now() |
#### File (Dateiablage pro Fahrzeug)
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| vehicle_id | UUID | FK → Vehicle, not null |
| file_type | ENUM('image','document','ocr_scan','retouched_image') | not null |
| original_filename | VARCHAR(255) | not null |
| stored_path | VARCHAR(500) | not null |
| mime_type | VARCHAR(100) | not null |
| file_size_bytes | BIGINT | not null |
| uploaded_by | UUID | FK → User |
| created_at | TIMESTAMPTZ | default now() |
#### OCRResult
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| vehicle_id | UUID | FK → Vehicle, not null |
| file_id | UUID | FK → File, not null |
| raw_text | TEXT | extracted text |
| structured_data | JSONB | parsed fields (brand, model, vin, etc.) |
| ocr_type | ENUM('zb_i','zb_ii') | not null |
| confidence_score | FLOAT | |
| status | ENUM('pending','completed','failed') | default 'pending' |
| created_at | TIMESTAMPTZ | default now() |
#### DATEVExport
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| export_date | DATE | not null |
| period_from | DATE | not null |
| period_to | DATE | not null |
| file_path | VARCHAR(500) | generated CSV path |
| record_count | INTEGER | |
| created_by | UUID | FK → User |
| created_at | TIMESTAMPTZ | default now() |
---
## 6. API Design
### 6.1 Auth
| Method | Endpoint | Beschreibung |
|---|---|---|
| POST | `/api/auth/login` | Login → JWT (access + refresh) |
| POST | `/api/auth/refresh` | Token refresh |
| GET | `/api/auth/me` | Aktuellen User abrufen |
| PUT | `/api/auth/me` | Profil aktualisieren |
### 6.2 Vehicles (M1)
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | `/api/vehicles` | List with pagination, filter (type, status, brand), sort |
| GET | `/api/vehicles/:id` | Detail view |
| POST | `/api/vehicles` | Create new vehicle |
| PUT | `/api/vehicles/:id` | Update vehicle |
| DELETE | `/api/vehicles/:id` | Soft delete (status='deleted') |
| POST | `/api/vehicles/:id/mobilede-push` | Push to mobile.de Seller API |
| GET | `/api/vehicles/:id/mobilede-status` | Sync status abrufen |
### 6.3 OCR (M2)
| Method | Endpoint | Beschreibung |
|---|---|---|
| POST | `/api/ocr/upload` | Upload ZB I/II scan → async OCR processing |
| GET | `/api/ocr/results/:id` | OCR result abrufen |
| GET | `/api/ocr/results?vehicle_id=X` | OCR results für Fahrzeug |
| POST | `/api/ocr/results/:id/apply` | OCR-Daten auf Vehicle anwenden |
### 6.4 Contacts (M3)
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | `/api/contacts` | List with pagination, filter (type, eu/inland), search |
| GET | `/api/contacts/:id` | Detail view |
| POST | `/api/contacts` | Create contact |
| PUT | `/api/contacts/:id` | Update contact |
| DELETE | `/api/contacts/:id` | Delete contact |
### 6.5 Files (M4)
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | `/api/vehicles/:id/files` | List files for vehicle |
| POST | `/api/vehicles/:id/files` | Upload file (multipart) |
| GET | `/api/vehicles/:id/files/:fileId` | Download file |
| DELETE | `/api/vehicles/:id/files/:fileId` | Delete file |
### 6.6 Sales (M5)
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | `/api/sales` | List with pagination, filter (date, status) |
| GET | `/api/sales/:id` | Detail view |
| POST | `/api/sales` | Create sale (generates contract PDF) |
| PUT | `/api/sales/:id` | Update sale |
| DELETE | `/api/sales/:id` | Cancel/delete sale |
| POST | `/api/sales/:id/contract` | Re-generate contract PDF |
| GET | `/api/sales/:id/contract` | Download contract PDF |
| POST | `/api/sales/:id/verify-ust-id` | USt-IdNr. BZSt API Prüfung (geplant) |
| POST | `/api/datev/export` | DATEV CSV Export für Zeitraum |
| GET | `/api/datev/exports` | List DATEV exports |
| GET | `/api/datev/exports/:id/download` | Download DATEV CSV |
### 6.7 KI-Copilot (M7)
| Method | Endpoint | Beschreibung |
|---|---|---|
| POST | `/api/copilot/chat` | Text chat → OpenRouter (Claude/GPT-4) |
| POST | `/api/copilot/voice` | Voice input (STT) → chat response |
| GET | `/api/copilot/history` | Chat history (paginated) |
| POST | `/api/copilot/action` | System action ausführen (vehicle create, search, etc.) |
### 6.8 Image Retouch (M8)
| Method | Endpoint | Beschreibung |
|---|---|---|
| POST | `/api/retouch/process` | Bild → Flux.1-Pro Retusche |
| GET | `/api/retouch/results/:id` | Retuschiertes Bild abrufen |
| POST | `/api/retouch/price-compare` | Preisvergleich mit mobile.de Listings |
### 6.9 Users (Admin)
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | `/api/users` | List users (admin only) |
| POST | `/api/users` | Create user (admin only) |
| PUT | `/api/users/:id` | Update user (admin only) |
| DELETE | `/api/users/:id` | Deactivate user (admin only) |
---
## 7. Test-Strategie
### 7.1 Backend (pytest)
- **Unit Tests**: Services isoliert mit Mocks (OpenRouter, mobile.de API)
- **Integration Tests**: API Endpoints mit Test-DB (SQLite/PostgreSQL testcontainer)
- **Coverage Target**: ≥ 80% pro Modul
- **Fixtures**: DB session, test client, mock OpenRouter responses
### 7.2 Frontend (Vitest + Playwright)
- **Unit Tests**: Komponenten mit Vitest + React Testing Library
- **E2E Tests**: Playwright für kritische User-Flows (Login, Vehicle CRUD, OCR, Sale)
- **Coverage Target**: ≥ 70% pro Komponentengruppe
### 7.3 Test-Commands
```bash
# Backend
cd backend && pytest --cov=app --cov-report=term-missing
# Frontend
cd frontend && npm run test
cd frontend && npm run e2e
# Full integration
docker compose -f docker-compose.test.yml up --abort-on-container-exit
```
---
## 8. Integrationsarchitektur
### 8.1 OpenRouter KI-Pipeline
```
User Request → FastAPI → OpenRouter API → Response
├── OCR: Qwen2.5-VL (Vision) → structured JSON
├── Retouch: Flux.1-Pro → Image URL/base64
└── Copilot: Claude/GPT-4 → Text/Action JSON
```
- API-Key via Environment Variable `OPENROUTER_API_KEY`
- Rate-Limiting via Redis Token Bucket
- Fehlerbehandlung: Retry mit exponential backoff (max 3)
- Response-Caching für identische OCR-Scans
### 8.2 mobile.de Seller API
```
Vehicle Update → FastAPI → mobile.de Seller API (POST/PUT)
Ad ID gespeichert in Vehicle.mobilede_ad_id
```
- **Nur Push-Richtung** (kein Import von mobile.de)
- Auth: OAuth2 oder API-Key (in mobile.de Seller Portal konfigurierbar)
- Mapping: ERP Vehicle → mobile.de Ad Format
- Retry-Queue bei Fehlern (Redis)
- Status-Tracking: `mobilede_synced_at`, `mobilede_ad_id`
### 8.3 DATEV-Export
```
Buchhaltung User → DATEV Export Request → FastAPI
→ Sammle Sales im Zeitraum → Format CSV (DATEV-Format) → Download
```
- Format: DATEV CSV (Buchungsstapel)
- Felder: Datum, Konto, Gegenkonto, Betrag, Belegfeld, Buchungstext
- GwG-Behandlung: Sofortabzug bei ≤ 800€ (§6 Abs. 2 EStG)
- USt-IdNr.-Prüfung: BZSt API (geplant, Feature-Flag `bzst_api_enabled`)
---
## 9. ADRs (Architecture Decision Records)
### ADR-001: FastAPI statt Django
**Status**: Accepted
**Entscheidung**: FastAPI als Backend-Framework.
**Begründung**: Async-native, bessere Performance, automatische OpenAPI-Dokumentation,
lichtgewichtiger als Django, native Pydantic-Integration.
**Alternativen**: Django REST Framework (zu schwerfällig), Flask (kein async).
### ADR-002: Next.js App Router statt Pages Router
**Status**: Accepted
**Entscheidung**: Next.js 15 mit App Router.
**Begründung**: Server Components, Streaming, bessere Code-Organisation,
Zukunftssicherheit.
**Alternativen**: Pages Router (deprecated), CRA (deprecated).
### ADR-003: OpenRouter als KI-Gateway
**Status**: Accepted
**Entscheidung**: Alle KI-Aufrufe über OpenRouter API.
**Begründung**: Ein API-Key für multiple Modelle, einheitliche API,
kein Vendor Lock-in, schnelle Modellwechsel.
**Alternativen**: Direkte OpenAI/Anthropic API (Vendor Lock-in),
lokale Modelle (zu langsam für OCR).
### ADR-004: OCR via Vision-Modell statt Tesseract
**Status**: Accepted
**Entscheidung**: OCR über OpenRouter Qwen2.5-VL Vision-Modell.
**Begründung**: Bessere Erkennung bei handschriftlichen und undeutlichen ZB-Scans,
keine Infrastruktur für Tesseract nötig, strukturierbare JSON-Ausgabe.
**Alternativen**: Tesseract (schlechte Qualität bei Scans),
Google Cloud Vision (Privacy-Bedenken, kostenpflichtig).
### ADR-005: Soft-Delete für Vehicles
**Status**: Accepted
**Entscheidung**: Vehicles werden nicht physisch gelöscht, sondern status='deleted'.
**Begründung**: Referentielle Integrität (Sales, Files, OCR Results),
Audit-Trail für Buchhaltung.
**Alternativen**: Hard Delete (Datenverlust, FK-Constraints).
### ADR-006: JWT statt Session-based Auth
**Status**: Accepted
**Entscheidung**: JWT (access + refresh token) für Authentifizierung.
**Begründung**: Stateless, geeignet für API + SPA, keine Server-Side-Session nötig.
**Alternativen**: Session-Cookies (CSRF-Risiko, Server-State nötig).
### ADR-007: Redis für Caching und Background Queues
**Status**: Accepted
**Entscheidung**: Redis als Cache und Task-Queue.
**Begründung**: Schnelles In-Memory-Caching für OCR-Results und mobile.de-Status,
Background-Queues für async OCR und mobile.de Push.
**Alternativen**: Celery+RabbitMQ (zu komplex für ~10 Nutzer),
keine Queue (blocking API).
### ADR-008: Docker-Compose mit 4 Containern statt K8s
**Status**: Accepted
**Entscheidung**: Docker-Compose mit frontend, backend, postgres, redis.
**Begründung**: Einfach zu deployen via Coolify, ausreichend für ~10 Nutzer,
keine K8s-Komplexität.
**Alternativen**: Kubernetes (overkill), einzelne Coolify Services
(mehr Konfigurationsaufwand).
---
## 10. Nicht-funktionale Anforderungen
### 10.1 Performance
- API-Response < 500ms für Standard-CRUD (ohne KI-Calls)
- OCR-Verarbeitung < 30s pro Scan (async, User wird benachrichtigt)
- Bildretusche < 60s pro Bild (async)
- Frontend LCP < 2.5s (mobile.de Performance Budget)
### 10.2 Sicherheit
- JWT mit kurzer Access-Token-Gültigkeit (15min) + Refresh (7d)
- Role-based Access Control (RBAC) auf Endpoint-Ebene
- Input-Validierung via Pydantic auf allen Endpoints
- SQL-Injection-Schutz durch SQLAlchemy parameterized queries
- File-Upload: MIME-Type-Validierung, max 20MB pro Datei
- OpenRouter API-Key in Environment Variables, nie im Code
- HTTPS-only via Traefik + Let's Encrypt
### 10.3 Skalierbarkeit
- Horizontal skalierbar (stateless Backend, sticky sessions nicht nötig)
- Redis als verteiltes Cache → mehrere Backend-Instanzen möglich
- PostgreSQL Connection Pooling via SQLAlchemy async engine
- Für ~10 Nutzer: Single-Instance ausreichend
### 10.4 Verfügbarkeit
- Docker-Compose mit `restart: unless-stopped`
- PostgreSQL mit WAL-Archiving für Point-in-Time-Recovery
- Daily Backup via Coolify (PostgreSQL dump)
- Health-Checks auf `/api/health` (Backend) und `/` (Frontend)
---
## 11. Risiken & Mitigation
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|---|---|---|---|
| OpenRouter API-Ausfall | Mittel | Hoch | Retry mit Backoff, Fallback-Modell, Cache |
| mobile.de API-Änderung | Niedrig | Mittel | API-Version pinnen, Monitoring |
| OCR-Qualität unzureichend | Mittel | Mittel | Confidence-Score → Manual Review bei < 0.7 |
| BZSt API kein Zugang | Hoch | Niedrig | Feature-Flag, manuelle Prüfung als Fallback |
| Datenverlust PostgreSQL | Niedrig | Kritisch | Daily Backups, WAL-Archiving |
| Token-Kosten OpenRouter | Mittel | Mittel | Caching, Rate-Limiting pro User |
---
## 12. i18n Architektur
- **Backend**: Fehlermeldungen auf Deutsch/Englisch via Accept-Language Header
- **Frontend**: next-intl oder react-i18next
- **Translation Files**: `frontend/messages/de.json`, `frontend/messages/en.json`
- **Sprachumschaltung**: User-Settings → Locale in localStorage + Cookie
- **Backend-Fehler**: Error-Codes + lokalisierte Messages
---
## 13. Konventionen
## 14. PDF-Generierung & Briefpapier (Letterhead)
### 14.1 Library-Wahl: WeasyPrint
**Entscheidung**: WeasyPrint als PDF-Generierungs-Library.
**Begründung**: WeasyPrint rendert HTML/CSS → PDF. Dadurch können PDF-Templates
mit Standard-Web-Technologien (HTML, CSS, Jinja2) erstellt werden. Briefpapier
(Letterhead) wird über CSS `@page` Rules mit `@top`/`@bottom` Margin-Boxes
umgesetzt inkl. Logo, Firmenadresse, Footer, Seitenzahlen.
**Vergleich**:
| Kriterium | WeasyPrint | ReportLab |
|---|---|---|
| Template-Erstellung | HTML/CSS + Jinja2 | Python-Code (Programmatic) |
| Briefpapier/Header/Footer | CSS @page Rules, repeating on every page | PageTemplate + Frame + onPage callback |
| Logo/Bild-Embedding | `<img>` oder CSS `background-image` | `Image()` flowable |
| Lernkurve | Niedrig (Web-Entwickler kennen HTML/CSS) | Mittel (ReportLab-spezifische API) |
| Flexibilität | Hoch (volle CSS-Unterstützung) | Mittel (eingeschränkte Layout-Optionen) |
| Wartbarkeit | Hoch (Templates separiert von Code) | Niedrig (Layout im Python-Code) |
**Alternativen**: ReportLab (pixel-präzise Kontrolle, aber komplexer),
fpdf2 (einfach aber weniger Features), wkhtmltopdf (veraltet).
### 14.2 Briefpapier-Implementierung
**Template-Struktur**:
```
backend/app/utils/pdf/
├── letterhead_template.html # Jinja2 HTML-Template mit Briefpapier
├── letterhead_style.css # CSS mit @page Rules (Header/Footer/Logo)
├── contract_template.html # Vertrag-Template (erbt Briefpapier)
├── invoice_template.html # Rechnung-Template (erbt Briefpapier)
└── pdf_generator.py # WeasyPrint Wrapper-Service
```
**CSS @page für Briefpapier**:
```css
@page {
size: A4;
margin: 120px 40px 80px 40px; /* top right bottom left */
@top-left {
content: url('/data/letterhead/logo.png');
width: 180px;
}
@top-right {
content: 'Firmenname GmbH';
font-size: 9pt;
color: #666;
}
@bottom-left {
content: 'Firmenname GmbH · Straßenname 1 · 12345 Stadt';
font-size: 8pt;
color: #999;
}
@bottom-right {
content: 'Seite ' counter(page) ' von ' counter(pages);
font-size: 8pt;
color: #999;
}
}
```
**Briefpapier-Elemente** (konfigurierbar über Admin-Settings):
- Logo (PNG/SVG, Position: top-left oder top-center)
- Firmenname & Adresse (Header oder Footer)
- Kontaktdaten (Tel, Email, Website)
- Steuernummer / USt-IdNr. (Footer)
- Seitenzahlen (Footer rechts)
- Hintergrund-Wasserzeichen (optional, z.B. "ENTWURF")
- Bankverbindung (Footer)
**Admin-Settings für Briefpapier**:
- Endpoint: `PUT /api/settings/letterhead` (admin only)
- Felder: logo_path, company_name, street, zip_city, phone, email, website,
tax_number, ust_id_nr, bank_name, bank_iban, bank_bic
- Briefpapier-Vorschau: `GET /api/settings/letterhead/preview` → PDF Preview
### 14.3 Dokument-Typen
| Dokument | Template | Briefpapier |
|---|---|---|
| Kaufvertrag (Sale Contract) | contract_template.html | Ja (Full Letterhead) |
| Rechnung (Invoice) | invoice_template.html | Ja (Full Letterhead) |
| DATEV-Export | (CSV, kein PDF) | Nein |
| Fahrzeug-Inventarliste | inventory_template.html | Ja (Full Letterhead) |
### ADR-009: WeasyPrint statt ReportLab
**Status**: Accepted
**Entscheidung**: WeasyPrint für alle PDF-Generierungen.
**Begründung**: HTML/CSS-Templates sind einfacher zu erstellen und zu warten
als ReportLab-Code. Briefpapier über CSS @page ist flexibler und
Web-Entwickler können Templates ohne Python-Kenntnisse anpassen.
**Alternativen**: ReportLab (pixel-präzise aber komplex), wkhtmltopdf (veraltet).
---
## 15. mobile.de Seller API Detaillierte Integration
### 15.1 API-Übersicht
mobile.de bietet zwei Schnittstellen:
| Schnittstelle | Format | Use Case |
|---|---|---|
| **Seller API (REST)** | XML (`application/vnd.de.mobile.seller-ad-v1.1+xml`) | Granulare CRUD-Operationen, Real-Time Sync |
| **CSV Upload Interface** | CSV (Semikolon-getrennt, ISO-8859-15) | Bulk-Upload aller Fahrzeuge auf einmal |
**Entscheidung**: Seller API (REST) als primäre Schnittstelle. CSV Upload als
Fallback/Initial-Bulk-Import.
### 15.2 Authentifizierung
**HTTP Basic Authentication** (empfohlen):
- Username + Password aus mobile.de Seller Portal
- `Authorization: Basic <base64(username:password)>`
- Alternativ (deprecated): `X-MOBILE-SELLER-TOKEN` Header
**Credentials in Environment Variables**:
```
MOBILEDE_SELLER_API_USERNAME=<username>
MOBILEDE_SELLER_API_PASSWORD=<password>
MOBILEDE_SELLER_KEY=<seller-key>
MOBILEDE_SELLER_API_URL=https://services.mobile.de
```
### 15.3 REST API Endpunkte
Base URL: `https://services.mobile.de`
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | `/seller-api/sellers` | Alle Sellers abrufen |
| GET | `/seller-api/sellers/{seller-key}` | Einzelnen Seller abrufen |
| GET | `/seller-api/sellers/{seller-key}/ads` | Alle Anzeigen eines Sellers |
| POST | `/seller-api/sellers/{seller-key}/ads` | Anzeige erstellen (XML Body) |
| GET | `/seller-api/sellers/{seller-key}/ads/{adId}` | Einzelne Anzeige abrufen |
| PUT | `/seller-api/sellers/{seller-key}/ads/{adId}` | Anzeige aktualisieren (XML Body) |
| DELETE | `/seller-api/sellers/{seller-key}/ads/{adId}` | Anzeige löschen |
| POST | `/seller-api/sellers/{seller-key}/ads/{adId}/vehicle-attribute/renewal-date` | Anzeige erneuern |
| PUT | `/seller-api/sellers/{seller-key}/ads/{adId}/images` | Bilder hochladen/ändern |
| GET | `/seller-api/sellers/{seller-key}/ads/{adId}/images` | Bild-URLs abrufen |
| DELETE | `/seller-api/sellers/{seller-key}/ads/{adId}/images` | Alle Bilder löschen |
| GET | `/seller-api/sellers/{seller-key}/ads/{ad-key}/statistic` | Anzeigen-Statistiken |
### 15.4 XML Ad-Format (Beispiel)
```xml
<ad:ad xmlns:ad="http://services.mobile.de/schema/ad">
<ad:vehicle>
<ad:category>Lkw</ad:category>
<ad:make>Mercedes-Benz</ad:make>
<ad:model>Actros</ad:model>
<ad:kilometre>150000</ad:kilometre>
<ad:firstRegistration>2020-03-15</ad:firstRegistration>
<ad:fuel>Diesel</ad:fuel>
<ad:gearbox>Manual</ad:gearbox>
<ad:price consumerPrice="true">45000.00</ad:price>
<ad:vat>1</ad:vat>
<ad:damagedVehicle>0</ad:damagedVehicle>
<ad:images>
<ad:image url="https://erp.media-on.de/files/vehicle-123/img1.jpg"/>
</ad:images>
</ad:vehicle>
</ad:ad>
```
### 15.5 Mapping: ERP Vehicle → mobile.de Ad
| ERP Feld | mobile.de XML Feld | Anmerkung |
|---|---|---|
| vehicle_type | ad:category | Lkw/Baumaschine/Pkw/Stapler/Transporter |
| brand | ad:make | |
| model | ad:model | |
| mileage_km | ad:kilometre | |
| first_registration | ad:firstRegistration | ISO Datum |
| power_kw | ad:power | kW-Wert |
| fuel_type | ad:fuel | Diesel/Petrol/Electric/Hybrid |
| sale_price | ad:price | consumerPrice="true" |
| registration_plate | ad:licensePlate | |
| vin | ad:vin | Fahrzeug-Identifikationsnummer |
| notes | ad:description | Freitext |
### 15.6 CSV Upload Interface (Fallback)
Format: Semikolon-getrennt, ISO-8859-15, `.csv` in `.zip` verpackt.
Pflichtfelder: internal number, category, make, model, kilometre, VAT,
damaged_vehicle, one-year-old car, new car, our recommendation, metallic, warranty.
**Use Case**: Initial-Bulk-Import aller Fahrzeuge nach mobile.de, oder wenn
Seller API nicht verfügbar.
### 15.7 Rate-Limits
Die mobile.de Dokumentation nennt keine expliziten Rate-Limits. Dennoch
implementieren wir einen eigenen Rate-Limiter (Redis Token Bucket):
- Max 10 Requests/Sekunde an mobile.de
- Retry-Queue bei HTTP 429 oder 5xx Fehlern (max 3 Retries mit Backoff)
### ADR-010: Seller API (REST) statt CSV Upload als primäre Schnittstelle
**Status**: Accepted
**Entscheidung**: REST Seller API als primäre mobile.de Schnittstelle.
**Begründung**: Granulare CRUD-Operationen, Real-Time Sync, Bild-Management,
Statistiken. CSV nur für Initial-Import oder Bulk-Operationen.
**Alternativen**: CSV Upload nur (kein Real-Time, kein Bild-Management).
---
## 16. Buchhaltungsschnittstellen
### 16.1 DATEV-Export (primär)
**Format**: DATEV CSV (ASCII, semikolon-getrennt) Buchungsstapel.
**DATEV CSV Felder** (Buchungsstapel-Format):
```csv
"Buchungsstapel";"01.01.2026";"31.01.2026";"0001";"Mandantenname";"1000";"Beraternummer"
"Umsatz";"Soll";"Haben";"Konto";"Gegenkonto";"Datum";"Belegfeld";"Buchungstext"
45000.00;"S";4200;0840;15.01.2026;"RE-2026-001";"Fahrzeugverkauf Mercedes Actros"
```
**DATEV XML-Schnittstelle** (erweitert):
- DATEV unterstützt zusätzlich XML-Format für Belegdaten
- Belegbilder + Belegdaten als DATEV-konforme XML
- Vorteil: Belegbilder (Rechnungen/Verträge) können direkt mitgeliefert werden
- Implementiert als Feature-Flag `datev_xml_export_enabled`
### 16.2 Weitere Buchhaltungsschnittstellen
| System | Format | Status | Implementierung |
|---|---|---|---|
| DATEV | CSV (ASCII) + XML | Geplant (primär) | T06 datev_service |
| Lexware | DATEV-CSV kompatibel | Über DATEV-Export nutzbar | Keine separate Implementierung nötig |
| SAP | CSV/IDoc | Zukunft (v2) | Feature-Flag, separates Export-Modul |
| DATEV Belegbilder | XML + Bilder | Zukunft (v2) | Feature-Flag `datev_xml_export_enabled` |
**Lexware-Kompatibilität**: Lexware unterstützt den DATEV-Import (CSV-Format).
Daher ist der DATEV-CSV-Export automatisch Lexware-kompatibel.
Lexware nutzt denselben DATEV-Datenexport "Belegbilder mit Belegdaten".
### 16.3 Export-Service Architektur
```
backend/app/utils/datev.py # DATEV CSV Formatter
backend/app/utils/accounting_export.py # Generic Export Base (für zukünftige Formate)
backend/app/services/datev_service.py # DATEV Export Logic
```
**Generic Export Pattern** (für zukünftige SAP/Weitere):
```python
class AccountingExporter(ABC):
@abstractmethod
def export(self, sales: list[Sale], period: DateRange) -> bytes: ...
class DatevCSVExporter(AccountingExporter): ...
class DatevXMLExporter(AccountingExporter): ... # v2
class SAPExporter(AccountingExporter): ... # v2
```
### ADR-011: DATEV CSV als primäre Buchhaltungsschnittstelle
**Status**: Accepted
**Entscheidung**: DATEV CSV-Format als primärer Export. Generic Export Base
für zukünftige Formate (XML, SAP).
**Begründung**: DATEV ist der deutsche Standard, Lexware-kompatibel,
einfach zu generieren. Abstract Base Class ermöglicht Erweiterung ohne
Code-Duplikation.
**Alternativen**: Direkte DATEV online API (zu komplex, benötigt
Zertifizierung), SAP IDoc (overkill für ~10 Nutzer).
---
## 17. KI-Copilot: Vollständige Systemsteuerung
### 17.1 Anforderung
Der KI-Copilot soll **ALLE** System-Funktionen steuern können. Der User
kann per Text oder Sprache mit dem Copilot interagieren und beliebige
Aktionen ausführen lassen.
### 17.2 Technische Umsetzung: Function Calling
**Mechanismus**: OpenRouter Function Calling / Tool Use
Der LLM (Claude/GPT-4) erhält einen System-Prompt mit allen verfügbaren
Functions (Tools). Bei einer User-Anfrage entscheidet der LLM, welche
Function aufgerufen werden muss, und gibt strukturiertes JSON zurück.
```
User: "Erstelle ein neues Fahrzeug: Mercedes Actros, 150000 km, 45000 EUR"
LLM Output: {
"function": "create_vehicle",
"arguments": {
"vehicle_type": "lkw",
"brand": "Mercedes-Benz",
"model": "Actros",
"mileage_km": 150000,
"purchase_price": 45000.00
}
}
Copilot Service: POST /api/vehicles (intern, mit User-JWT)
Response to User: "Fahrzeug erstellt ✓ (ID: abc-123). Soll ich es
nach mobile.de pushen?"
```
### 17.3 Verfügbare System-Functions (Tools)
| Function | Beschreibung | Endpoint |
|---|---|---|
| search_vehicles | Fahrzeuge suchen/filtern | GET /api/vehicles |
| create_vehicle | Neues Fahrzeug anlegen | POST /api/vehicles |
| update_vehicle | Fahrzeug aktualisieren | PUT /api/vehicles/:id |
| delete_vehicle | Fahrzeug löschen (soft) | DELETE /api/vehicles/:id |
| push_to_mobilede | Nach mobile.de pushen | POST /api/vehicles/:id/mobilede-push |
| upload_ocr | OCR-Scan hochladen | POST /api/ocr/upload |
| apply_ocr | OCR-Daten anwenden | POST /api/ocr/results/:id/apply |
| search_contacts | Kontakte suchen | GET /api/contacts |
| create_contact | Kontakt anlegen | POST /api/contacts |
| update_contact | Kontakt aktualisieren | PUT /api/contacts/:id |
| upload_file | Datei hochladen | POST /api/vehicles/:id/files |
| list_files | Dateien auflisten | GET /api/vehicles/:id/files |
| create_sale | Verkauf anlegen | POST /api/sales |
| get_sale | Verkaufsdetails | GET /api/sales/:id |
| generate_contract | Vertrag generieren | POST /api/sales/:id/contract |
| export_datev | DATEV-Export | POST /api/datev/export |
| retouch_image | Bild retuschieren | POST /api/retouch/process |
| price_compare | Preisvergleich | POST /api/retouch/price-compare |
| get_statistics | Dashboard-Statistiken | GET /api/statistics |
| search_copilot | Eigene Historie durchsuchen | GET /api/copilot/history |
**Echt ALLES** inkl. Admin-Functions (nur für Admin-Role):
| Function | Beschreibung | Endpoint |
|---|---|---|
| list_users | User auflisten | GET /api/users |
| create_user | User anlegen | POST /api/users |
| deactivate_user | User deaktivieren | DELETE /api/users/:id |
| update_letterhead | Briefpapier ändern | PUT /api/settings/letterhead |
### 17.4 Safety & Confirmation
**Action Preview Pattern** (wie in T07 definiert):
1. LLM schlägt Action vor (Function + Arguments)
2. Frontend zeigt Action Preview: "Ich möchte folgendes tun: [Action]"
3. User bestätigt oder ablehnt
4. Bei Bestätigung: Copilot führt Action aus (interner API-Call mit User-JWT)
5. Ergebnis wird im Chat angezeigt
**Auto-Execute bei Read-Only Actions**:
- search_vehicles, search_contacts, get_sale, list_files, get_statistics,
price_compare → automatisch ausgeführt (kein Bestätigung nötig)
- Alle schreibenden Actions → User-Bestätigung erforderlich
**RBAC durchgesetzt**: Copilot nutzt den JWT des aktuellen Users.
Ein Verkäufer kann keine User verwalten, auch nicht über den Copilot.
### 17.5 System-Prompt Struktur
```
Du bist der KI-Assistent für ein Nutzfahrzeug-ERP-System.
Du kannst Fahrzeuge verwalten, Kontakte anlegen, Verkäufe erstellen,
OCR-Scans verarbeiten, Bilder retuschieren und DATEV-Exporte erstellen.
Verfügbare Aktionen:
[Liste aller Functions mit Beschreibung und Parameters]
Regeln:
- Bei schreibenden Aktionen: schlage die Aktion vor und warte auf Bestätigung
- Bei Lese-Aktionen: führe sie direkt aus und präsentiere die Ergebnisse
- Antworte auf Deutsch (oder Englisch je nach User-Setting)
- Wenn Informationen fehlen: frage nach
```
### ADR-012: Function Calling für KI-Vollsteuerung
**Status**: Accepted
**Entscheidung**: OpenRouter Function Calling für alle System-Aktionen.
**Begründung**: Strukturierte JSON-Ausgabe, zuverlässig, von Claude/GPT-4
unterstützt. User-Bestätigung für schreibende Aktionen als Safety-Mechanismus.
**Alternativen**: Free-text parsing (fehleranfällig), MCP Protocol
(zu neu, noch nicht etabliert).
---
## 18. KI Multi-Provider Support
### 18.1 OpenRouter als Unified Gateway
**Entscheidung**: OpenRouter als alleiniges KI-Gateway. Keine direkten
Provider-Anbindungen.
**Begründung**: OpenRouter bietet:
- **400+ Modelle** von allen major providers (OpenAI, Anthropic, Google, Meta, Mistral, etc.)
- **Ein API-Key** für alle Modelle (kein separates Key-Management)
- **OpenAI-kompatible API** (drop-in replacement)
- **Provider-Routing**: Automatic fallback auf Backup-Provider bei Ausfall
- **Side-by-side Pricing**: Kostenvergleich aller Modelle
- **Tool Calling Support**: Modelle mit Function Calling verfügbar
### 18.2 Modell-Konfiguration pro Use-Case
Der User kann in den Admin-Settings konfigurieren, welches Modell für
welchen Use-Case verwendet wird:
| Use Case | Default Modell | Alternative |
|---|---|---|
| OCR (ZB I/II) | qwen/qwen-2.5-vl-72b-instruct | openai/gpt-4o |
| Bild-Retusche | black-forest-labs/flux-1.1-pro | stabilityai/stable-diffusion-xl |
| Copilot (Chat) | anthropic/claude-3.5-sonnet | openai/gpt-4o, google/gemini-flash-1.5 |
| Copilot (Voice STT) | openai/whisper-large-v3 | |
**Settings Endpoint**:
```
GET /api/settings/ai-models → Aktuelle Modell-Konfiguration
PUT /api/settings/ai-models → Modell-Konfiguration aktualisieren (admin only)
```
**Config in DB** (Settings-Tabelle):
```json
{
"ocr_model": "qwen/qwen-2.5-vl-72b-instruct",
"retouch_model": "black-forest-labs/flux-1.1-pro",
"copilot_chat_model": "anthropic/claude-3.5-sonnet",
"copilot_voice_model": "openai/whisper-large-v3"
}
```
### 18.3 Provider-Fallback
OpenRouter bietet automatisches Provider-Routing:
- Bei Ausfall des primären Providers → automatischer Fallback auf Backup-Provider
- Beispiel: anthropic/claude-3.5-sonnet → bei Anthropic-Ausfall →
OpenRouter versucht alternative Provider für dasselbe Modell
**Eigenes Fallback-Handling** (zusätzlich):
```python
COPILOT_MODELS = [
"anthropic/claude-3.5-sonnet",
"openai/gpt-4o",
"google/gemini-flash-1.5",
"meta-llama/llama-3.1-70b-instruct"
]
# Bei Fehler: nächstes Modell in der Liste probieren
```
### 18.4 Kostenkontrolle
- OpenRouter zeigt Token-Kosten pro Request
- Logging: ai_usage_log Tabelle (model, tokens_in, tokens_out, cost, user_id, timestamp)
- Rate-Limiting pro User (Redis Token Bucket)
- Monatsbudget konfigurierbar (Admin-Setting ai_monthly_budget_eur)
- Bei Überschreitung: Warnung + ggf. Drosselung
### ADR-013: OpenRouter als alleiniges KI-Gateway
**Status**: Accepted
**Entscheidung**: Alle KI-Aufrufe über OpenRouter, keine direkten Provider-APIs.
**Begründung**: Ein API-Key, 400+ Modelle, Provider-Routing, einheitliche API,
keine Multi-SDK-Wartung. Modell-Konfiguration pro Use-Case ermöglicht
flexible Anpassung ohne Code-Änderung.
**Alternativen**: Direkte OpenAI + Anthropic APIs (Multi-Key-Management,
höhere Wartung), lokale Modelle via Ollama (zu langsam für OCR/Retusche,
aber möglich als Fallback für Copilot-Chat).
### 18.5 Optional: Lokale Modelle via Ollama (Copilot Fallback)
Für den Copilot-Chat (nicht OCR/Retusche) kann optional ein lokales Modell
via Ollama als Offline-Fallback konfiguriert werden:
- Feature-Flag local_model_fallback_enabled
- Ollama läuft als 5. Container (optional, nur bei Bedarf)
- Modell: llama3.1:8b-instruct oder qwen2.5:14b
- Use Case: Offline-Betrieb oder Kostenersparnis für einfache Chat-Tasks
- Function Calling Support: Llama 3.1 unterstützt native Tool-Calling
**Architektur-Erweiterung** (v2, Feature-Flag):
```
Copilot Request → Try OpenRouter → Fail → Try local Ollama → Fail → Error
```
---
## 13. Konventionen
### 13.1 Backend
- async/await für alle DB-Operationen und externen API-Calls
- Pydantic v2 für alle Request/Response-Schemas
- SQLAlchemy 2.0 mit typed Mapped columns
- Alembic für DB-Migrationen (kein auto-create in Produktion)
- Router-Prefix: `/api/<resource>`
- Error-Format: `{"error": {"code": "VEHICLE_NOT_FOUND", "message": "...", "details": {}}}`
### 13.2 Frontend
- TypeScript strict mode
- Tailwind CSS für Styling (kein CSS-in-JS)
- Server Components für statische Daten, Client Components für Interaktion
- API-Client: zentrale fetch-Wrapper mit Auth-Header-Injection
- Date-Naming: kebab-case für Dateien, camelCase für Variablen
### 13.3 Git
- Conventional Commits: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`
- Branch-Naming: `feature/T01-vehicle-module`, `fix/...`, `hotfix/...`
- PRs erforderlich für main-Branch
## 19. Provider Architecture (Multi-Provider mit Ollama Cloud)
### 19.1 Provider Abstraktion (ABC Pattern)
```python
from abc import ABC, abstractmethod
class BaseAIProvider(ABC):
@abstractmethod
async def chat(self, messages, tools=None, model=None) -> dict: ...
@abstractmethod
async def vision(self, image_base64, prompt, model=None) -> str: ...
@abstractmethod
async def image_edit(self, image_base64, instruction, model=None) -> str: ...
@abstractmethod
def list_models(self) -> list[str]: ...
@abstractmethod
def health_check(self) -> bool: ...
class OpenRouterProvider(BaseAIProvider): ...
class OllamaCloudProvider(BaseAIProvider): ...
class OllamaLocalProvider(BaseAIProvider): ...
```
Neue Provider durch Implementierung von BaseAIProvider + Eintrag in providers.yaml hinzufügbar.
### 19.2 providers.yaml
```yaml
providers:
openrouter:
enabled: true
api_base: https://openrouter.ai/api/v1
api_key: ${OPENROUTER_API_KEY}
models: [anthropic/claude-3.5-sonnet, openai/gpt-4o, qwen/qwen2.5-vl]
ollama_cloud:
enabled: true
api_base: ${OLLAMA_CLOUD_URL}
api_key: ${OLLAMA_CLOUD_API_KEY}
models: [glm-5.2:cloud, qwen2.5:14b, llava:13b]
ollama_local:
enabled: false
api_base: http://ollama:11434
models: [llama3.1:8b-instruct, qwen2.5:14b]
```
### 19.3 Use-Case → Provider Mapping
| Use Case | Default | Fallback |
|---|---|---|
| Copilot Chat | ollama_cloud (glm-5.2) | openrouter (claude-3.5) |
| OCR | openrouter (qwen2.5-vl) | ollama_cloud (llava) |
| Bildretusche | openrouter (flux.1-pro) | - |
| Voice | openrouter | browser-native |
Admin konfigurierbar via PUT /api/settings/ai-providers
### ADR-014: Multi-Provider mit Ollama Cloud
- Ollama Cloud als ECHTER Provider (nicht nur Fallback)
- Provider-Abstraktion ermöglicht beliebige zukünftige Provider
- Admin kann pro Use-Case Provider wählen