Files

1163 lines
44 KiB
Markdown
Raw Permalink Normal View History

# 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