diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..61687e3 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,943 @@ +# HMS Licht & Ton – System Architecture + +> **Projekt:** hms-licht-ton (ID 30) +> **Datum:** 2026-07-08 +> **Status:** Draft – Ready for Review +> **Autor:** Solution Architect (A0 Orchestrator) + +--- + +## 1. Architektur-Überblick + +### 1.1 System-Komponenten + +``` +┌─────────────────────────────────────────────────────────┐ +│ Coolify (Docker) │ +│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │ +│ │ Nuxt 3 SSR │ │ FastAPI │ │ PostgreSQL │ │ +│ │ (Frontend) │ │ (Backend) │ │ (equipment_cache│ │ +│ │ Port 3000 │ │ Port 8000 │ │ rental_requests)│ │ +│ └──────┬───────┘ └──────┬──────┘ └────────┬─────────┘ │ +│ │ │ │ │ +│ │ /api/* │ SQLAlchemy │ │ +│ └────────────────►│ │ │ +│ │ ┌───────────┐ │ │ +│ │ │ Redis │ │ │ +│ │ │ (cache, │ │ │ +│ │ │ rate-lim)│ │ │ +│ │ └─────┬─────┘ │ │ +│ │ │ │ │ +│ ┌───────────────────────┴────────┴─────────┴──────┐ │ +│ │ Traefik (TLS, Routing) │ │ +│ │ hms.media-on.de → Nuxt 3000 │ │ +│ │ hms.media-on.de/api → FastAPI 8000 │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ + │ HTTPS + ▼ +┌─────────────────┐ +│ Rentman API │ +│ api.rentman.net│ +│ (Bearer Token) │ +└─────────────────┘ +``` + +### 1.2 Container-Topologie + +| Container | Image | Port | Abhängigkeiten | +|-----------|-------|------|----------------| +| `hms-frontend` | Node 20 + Nuxt 3 | 3000 | `hms-backend` | +| `hms-backend` | Python 3.12 + FastAPI | 8000 | `hms-postgres`, `hms-redis` | +| `hms-postgres` | PostgreSQL 16 | 5432 | – | +| `hms-redis` | Redis 7 | 6379 | – | + +Traefik (Coolify-eigen) routet: +- `hms.media-on.de/*` → `hms-frontend:3000` +- `hms.media-on.de/api/*` → `hms-backend:8000` + +--- + +## 2. Frontend-Architektur (Nuxt 3) + +### 2.1 Rendering-Strategie + +| Route | Modus | Begründung | +|-------|-------|------------| +| `/` (Home) | SSG (`nuxt generate`) | Statischer Content, SEO/SSR nicht nötig (noindex) | +| `/referenzen` | SSG | Statische Bilder, keine API nötig | +| `/mietkatalog` | SSR (`server`) | Dynamische Equipment-Daten vom Backend | +| `/mietkatalog/:id` | SSR | Equipment-Detail vom Backend | +| `/warenkorb` | CSR | Reine Client-State (localStorage) | +| `/mietanfrage` | CSR | Formular mit Client-State (Warenkorb) | +| `/kontakt` | SSG | Statisches Formular, API-Call bei Submit | +| `/admin` | CSR | Auth-basiert, keine Public-SSR nötig | +| `/impressum` | SSG | Statischer Content | +| `/datenschutz` | SSG | Statischer Content | +| `/agb-vermietung` | SSG | Statischer Content | + +**Konfiguration:** `nuxt.config.ts` mit `routeRules`: +```typescript +export default defineNuxtConfig({ + routeRules: { + '/': { prerender: true }, + '/referenzen': { prerender: true }, + '/mietkatalog': { ssr: true }, + '/mietkatalog/**': { ssr: true }, + '/warenkorb': { ssr: false }, + '/mietanfrage': { ssr: false }, + '/kontakt': { prerender: true }, + '/admin': { ssr: false }, + '/impressum': { prerender: true }, + '/datenschutz': { prerender: true }, + '/agb-vermietung': { prerender: true }, + } +}) +``` + +### 2.2 Verzeichnisstruktur + +``` +frontend/ +├── nuxt.config.ts +├── tailwind.config.ts +├── package.json +├── tsconfig.json +├── app.vue +├── assets/ +│ ├── css/ +│ │ └── main.css (Design Tokens, Tailwind imports) +│ └── img/ +│ ├── logo.svg +│ ├── hero-bg.jpg +│ └── ref/ (Referenz-Bilder ref1-9.jpg) +├── components/ +│ ├── HmsLogo.vue +│ ├── SpeakerIcon.vue +│ ├── AppHeader.vue +│ ├── AppFooter.vue +│ ├── ServiceCard.vue +│ ├── EquipmentCard.vue +│ ├── LoadingSkeleton.vue +│ ├── EmptyState.vue +│ ├── ErrorState.vue +│ ├── LegalContentPage.vue +│ ├── Lightbox.vue +│ └── CartDrawer.vue +├── composables/ +│ ├── useEquipment.ts (API fetch + caching) +│ ├── useCart.ts (Pinia store wrapper) +│ └── useApi.ts (Base API client) +├── layouts/ +│ └── default.vue (Header + Footer + slot) +├── pages/ +│ ├── index.vue (Home) +│ ├── referenzen.vue +│ ├── mietkatalog/ +│ │ ├── index.vue +│ │ └── [id].vue +│ ├── warenkorb.vue +│ ├── mietanfrage.vue +│ ├── kontakt.vue +│ ├── admin.vue +│ ├── impressum.vue +│ ├── datenschutz.vue +│ ├── agb-vermietung.vue +│ └── [...slug].vue (404 catch-all) +├── stores/ +│ └── cart.ts (Pinia store, localStorage persist) +├── plugins/ +│ └── pinia-persist.client.ts +├── public/ +│ └── robots.txt +└── server/ + └── middleware/ + └── headers.ts (noindex headers) +``` + +### 2.3 State Management + +| State | Lösung | Persistenz | +|-------|--------|------------| +| Warenkorb (Artikel + Mengen) | Pinia store `cart.ts` | localStorage (client-side) | +| UI-State (mobile menu, lightbox) | Component-local `ref()` | keine | +| Equipment-Daten | Nuxt `useFetch` / `useAsyncData` | Nuxt payload cache (SSR) | +| Admin-Auth | JWT in HttpOnly Cookie | Cookie (24h) | + +### 2.4 Design Token System + +Implementiert als CSS Custom Properties in `assets/css/main.css` und Tailwind-Config: + +```css +:root { + /* A0 Dark Theme */ + --bg: #131313; + --panel: #1a1a1a; + --surface: #212121; + --row: #272727; + --card: #2d2d2d; + --border: #444444a8; + --secondary: #656565; + --primary: #737a81; + --text: #ffffff; + --text-muted: #d4d4d4; + + /* Orange Akzent */ + --color-accent: #EC6925; + --color-accent-hover: #d4581a; + --color-accent-light: rgba(236, 105, 37, 0.08); + --color-accent-border: rgba(236, 105, 37, 0.25); + + /* Status */ + --color-success: #4ade80; + --color-error: #f87171; + --color-warning: #fbbf24; + --color-info: #60a5fa; + + /* Radius */ + --radius-sm: 2px; + --radius-md: 3px; + --radius-lg: 4px; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-base: 250ms ease; + --transition-slow: 400ms ease; +} +``` + +Tailwind-Config extend mit denselben Werten als utility classes. + +--- + +## 3. Backend-Architektur (FastAPI) + +### 3.1 Verzeichnisstruktur + +``` +backend/ +├── main.py (FastAPI app, CORS, routers) +├── config.py (Pydantic Settings, env vars) +├── database.py (SQLAlchemy engine, session) +├── models/ +│ ├── __init__.py +│ ├── equipment.py (EquipmentCache model) +│ ├── rental_request.py (RentalRequest + RentalRequestItem) +│ ├── contact.py (Contact model) +│ ├── admin_user.py (AdminUser model) +│ └── sync_log.py (SyncLog model) +├── schemas/ +│ ├── __init__.py +│ ├── equipment.py (Pydantic schemas) +│ ├── rental_request.py +│ ├── contact.py +│ ├── auth.py +│ └── sync.py +├── routers/ +│ ├── __init__.py +│ ├── equipment.py (GET /equipment, GET /equipment/{id}) +│ ├── rental_requests.py (POST /rental-requests) +│ ├── contact.py (POST /contact) +│ ├── auth.py (POST /admin/login, GET /admin/me) +│ ├── admin.py (POST /admin/sync, GET /admin/sync-status) +│ └── health.py (GET /health) +├── services/ +│ ├── rentman_import.py (Equipment-Import pipeline) +│ ├── rentman_request.py (Mietanfrage pipeline) +│ ├── email_service.py (SMTP email send) +│ └── auth_service.py (JWT create/verify) +├── cache.py (Redis client, cache helpers) +├── requirements.txt +├── Dockerfile +├── alembic.ini +└── migrations/ + └── versions/ +``` + +### 3.2 API-Design + +Alle Endpunkte unter Prefix `/api`. + +#### Equipment Endpoints + +| Method | Path | Beschreibung | Query Params | Response | +|--------|------|--------------|--------------|----------| +| GET | `/api/equipment` | Equipment-Liste | `search`, `category`, `sort` (name_asc, name_desc), `page`, `page_size` | `200` PaginatedResponse | +| GET | `/api/equipment/{id}` | Equipment-Detail | – | `200` EquipmentDetail / `404` | +| GET | `/api/equipment/categories` | Alle Kategorien | – | `200` string[] | + +**PaginatedResponse Schema:** +```json +{ + "items": [...], + "total": 500, + "page": 1, + "page_size": 20, + "total_pages": 25 +} +``` + +**EquipmentItem Schema:** +```json +{ + "id": 123, + "rentman_id": "456", + "name": "L-Acoustics K2", + "number": "K2-001", + "category": "Lautsprecher", + "description": "Line Array Element", + "image_url": "/img/equipment/k2-001.jpg", + "rental_price": null, + "available": true +} +``` + +**EquipmentDetail Schema (erweitert):** +```json +{ + "id": 123, + "rentman_id": "456", + "name": "L-Acoustics K2", + "number": "K2-001", + "category": "Lautsprecher", + "description": "Full description...", + "specifications": {"weight": 56, "power": 750, ...}, + "images": ["url1", "url2"], + "rental_price": null, + "available": true, + "brand": "L-Acoustics" +} +``` + +#### Rental Request Endpoints + +| Method | Path | Beschreibung | Body | Response | +|--------|------|--------------|------|----------| +| POST | `/api/rental-requests` | Mietanfrage erstellen | RentalRequestCreate | `201` {reference_number} / `422` | + +**RentalRequestCreate Schema:** +```json +{ + "event_name": "Sommerfest 2026", + "date_start": "2026-08-15", + "date_end": "2026-08-16", + "location": "München", + "person_count": 100, + "contact_name": "Max Mustermann", + "contact_company": "Firma GmbH", + "contact_email": "max@example.com", + "contact_phone": "+49 170 1234567", + "contact_street": "Hauptstr. 1", + "contact_postalcode": "80000", + "contact_city": "München", + "message": "Brauchen PA für Open-Air", + "items": [ + {"equipment_id": 123, "quantity": 2}, + {"equipment_id": 456, "quantity": 4} + ] +} +``` + +#### Contact Endpoint + +| Method | Path | Beschreibung | Body | Response | +|--------|------|--------------|------|----------| +| POST | `/api/contact` | Kontaktformular | ContactCreate | `200` {success: true} / `422` | + +**ContactCreate Schema:** +```json +{ + "name": "Max Mustermann", + "email": "max@example.com", + "phone": "+49 170 1234567", + "message": "Anfrage bezüglich...", + "privacy_consent": true +} +``` + +#### Admin/Auth Endpoints + +| Method | Path | Beschreibung | Auth | Response | +|--------|------|--------------|------|----------| +| POST | `/api/admin/login` | Admin-Login | – | `200` {token} / `401` | +| GET | `/api/admin/me` | Aktueller Admin | JWT | `200` {username} / `401` | +| POST | `/api/admin/sync` | Sync triggern | JWT | `200` {sync_id} / `401` | +| GET | `/api/admin/sync-status` | Sync-Status | JWT | `200` SyncStatus | +| GET | `/api/admin/sync-log` | Sync-Log (paginiert) | JWT | `200` SyncLog[] | + +#### Health Endpoint + +| Method | Path | Response | +|--------|------|----------| +| GET | `/api/health` | `200` {status: "ok", db: "connected", redis: "connected"} | + +--- + +## 4. Datenbank-Schema (PostgreSQL) + +### 4.1 Tabellen + +```sql +-- Equipment Cache (aus Rentman importiert) +CREATE TABLE equipment_cache ( + id SERIAL PRIMARY KEY, + rentman_id VARCHAR(64) UNIQUE NOT NULL, + name VARCHAR(255) NOT NULL, + number VARCHAR(64), + category VARCHAR(128), + subcategory VARCHAR(128), + description TEXT, + specifications JSONB, + images JSONB, + rental_price DECIMAL(10, 2), + brand VARCHAR(128), + available BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_equipment_category ON equipment_cache(category); +CREATE INDEX idx_equipment_name ON equipment_cache(name); +CREATE INDEX idx_equipment_number ON equipment_cache(number); + +-- Rental Requests (Mietanfragen) +CREATE TABLE rental_requests ( + id SERIAL PRIMARY KEY, + reference_number VARCHAR(32) UNIQUE NOT NULL, + event_name VARCHAR(255) NOT NULL, + date_start DATE NOT NULL, + date_end DATE NOT NULL, + location VARCHAR(255), + person_count INTEGER, + contact_name VARCHAR(255) NOT NULL, + contact_company VARCHAR(255), + contact_email VARCHAR(255) NOT NULL, + contact_phone VARCHAR(64), + contact_street VARCHAR(255), + contact_postalcode VARCHAR(16), + contact_city VARCHAR(128), + message TEXT, + status VARCHAR(32) DEFAULT 'pending', + rentman_request_id VARCHAR(64), + rentman_sync_status VARCHAR(32) DEFAULT 'pending', + rentman_sync_error TEXT, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_rental_status ON rental_requests(status); + +-- Rental Request Items +CREATE TABLE rental_request_items ( + id SERIAL PRIMARY KEY, + rental_request_id INTEGER REFERENCES rental_requests(id) ON DELETE CASCADE, + equipment_id INTEGER REFERENCES equipment_cache(id), + equipment_name VARCHAR(255), + rentman_equipment_id VARCHAR(64), + quantity INTEGER NOT NULL DEFAULT 1, + unit_price DECIMAL(10, 2), + rentman_sync_status VARCHAR(32) DEFAULT 'pending' +); + +CREATE INDEX idx_rental_items_request ON rental_request_items(rental_request_id); + +-- Contacts (Kontaktformular) +CREATE TABLE contacts ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + phone VARCHAR(64), + message TEXT NOT NULL, + privacy_consent BOOLEAN NOT NULL, + email_sent BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Admin Users +CREATE TABLE admin_users ( + id SERIAL PRIMARY KEY, + username VARCHAR(64) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Sync Log +CREATE TABLE sync_log ( + id SERIAL PRIMARY KEY, + sync_type VARCHAR(32) NOT NULL DEFAULT 'equipment', + status VARCHAR(32) NOT NULL, + items_processed INTEGER DEFAULT 0, + items_failed INTEGER DEFAULT 0, + error_message TEXT, + started_at TIMESTAMP DEFAULT NOW(), + completed_at TIMESTAMP +); + +CREATE INDEX idx_sync_log_started ON sync_log(started_at DESC); +``` + +### 4.2 Redis Usage + +| Key Pattern | TTL | Zweck | +|-------------|-----|-------| +| `equipment:list:{hash}` | 3600s (1h) | Cached equipment list responses | +| `equipment:detail:{id}` | 3600s (1h) | Cached equipment detail | +| `equipment:categories` | 3600s (1h) | Cached category list | +| `rate:contact:{ip}` | 60s | Rate limit contact form (5/min) | +| `rate:rental:{ip}` | 60s | Rate limit rental requests (5/min) | +| `rate:login:{ip}` | 60s | Rate limit admin login (5/min) | + +--- + +## 5. Rentman-Integration + +### 5.1 Equipment-Import Pipeline + +``` +[Trigger: Cron 6h ODER Admin POST /api/admin/sync] + │ + ▼ +[1] sync_log Eintrag: status='running', started_at=NOW() + │ + ▼ +[2] RentmanAPI GET /equipment?limit=100&offset=0 + │ ┌─────────────────────────────────┐ + │ │ Loop: solange data nicht leer │ + │ │ - Parse response │ + │ │ - Upsert in equipment_cache │ + │ │ - offset += 100 │ + │ │ - Nächste Seite │ + │ └─────────────────────────────────┘ + │ + ▼ +[3] Redis Cache invalidieren (equipment:*) + │ + ▼ +[4] sync_log Update: status='completed', items_processed=N, completed_at=NOW() + │ + ▼ +[5] Fertig. Frontend liest aus PostgreSQL + Redis Cache. +``` + +**Fehlerbehandlung:** +- API nicht erreichbar → sync_log status='failed', error_message, bestehende Daten bleiben +- Partial failure (Seite 3/10) → bereits importierte Seiten bleiben, Fehler geloggt, Retry nur für fehlgeschlagene +- Retry-Mechanismus: exponential backoff, max 3 retries pro Seite + +### 5.2 Mietanfrage Pipeline + +``` +[POST /api/rental-requests] + │ + ▼ +[1] Validierung (Pydantic): Event-Daten, Kontakt-Daten, Items + │ + ▼ +[2] rental_requests Eintrag in DB (status='pending', reference_number generiert) + │ + ▼ +[3] rental_request_items in DB speichern + │ + ▼ +[4] RentmanAPI POST /projectrequests + │ → rentman_project_request_id erhalten + │ → rental_requests.rentman_request_id aktualisieren + │ + ▼ +[5] Für jedes Item: RentmanAPI POST /projectrequests/{id}/projectrequestequipment + │ → rental_request_items.rentman_sync_status='success' / 'failed' + │ + ▼ +[6] Bestätigungs-E-Mail an Kunde (reference_number + Item-Liste) + │ + ▼ +[7] Response 201 an Frontend: {reference_number: "HMS-2026-00123"} +``` + +**Fehlerbehandlung:** +- POST /projectrequests schlägt fehl → status='failed', Rentman-Sync-Retry-Queue, User sieht trotzdem Success ('Anfrage eingegangen, wird verarbeitet') +- Equipment-POST schlägt fehl → item-level retry, Admin-Benachrichtigung +- SMTP nicht erreichbar → E-Mail in Retry-Queue, Admin-Benachrichtigung + +### 5.3 Rentman API Mapping + +**Import (GET /equipment → equipment_cache):** + +| Rentman Feld | DB Feld | Anmerkung | +|--------------|---------|-----------| +| `id` | `rentman_id` | String, unique | +| `name` | `name` | – | +| `number` / `code` | `number` | Artikelnummer | +| `equipment_group.name` | `category` | Kategorie | +| `description` | `description` | – | +| `specifications` | `specifications` | JSONB | +| `images` / `files` | `images` | JSONB array of URLs | +| `rental_price` | `rental_price` | Decimal, nullable | +| `brand` | `brand` | – | + +**Anfrage (POST /projectrequests):** + +| Frontend Feld | Rentman Feld | +|---------------|-------------| +| `event_name` | `name` | +| `date_start` + `T08:00:00+02:00` | `planperiod_start` | +| `date_end` + `T02:00:00+02:00` | `planperiod_end` | +| `date_start` + `T18:00:00+02:00` | `usageperiod_start` | +| `date_end` + `T23:59:00+02:00` | `usageperiod_end` | +| `contact_company` oder `contact_name` | `contact_name` | +| `contact_name` (split) | `contact_person_first_name` / `contact_person_lastname` | +| `contact_email` | `contact_person_email` | +| `location` | `location_name` | +| `contact_street` | `location_mailing_street` | +| `contact_postalcode` | `location_mailing_postalcode` | +| `contact_city` | `location_mailing_city` | +| `message` | `remark` | + +**Equipment POST (POST /projectrequests/{id}/projectrequestequipment):** + +| Frontend Feld | Rentman Feld | +|---------------|-------------| +| `equipment_name` | `name` | +| `quantity` | `quantity` / `quantity_total` | +| `unit_price` (oder 0) | `unit_price` | +| `/equipment/{rentman_id}` | `linked_equipment` | + +--- + +## 6. Security-Konzept + +### 6.1 Authentifizierung + +| Bereich | Methode | +|---------|---------| +| Admin-Login | JWT (python-jose), HttpOnly Cookie, 24h Expiry | +| Public API | Keine Auth nötig (Equipment GET, Contact POST, Rental POST) | +| Admin API | JWT required (sync, sync-status, sync-log) | + +**JWT Implementation:** +- Algorithm: HS256 +- Secret: `JWT_SECRET` env var +- Payload: `{sub: username, exp: timestamp}` +- Cookie: `hms_admin_token`, HttpOnly, Secure, SameSite=Strict + +### 6.2 Rate Limiting + +| Endpoint | Limit | Window | +|----------|-------|--------| +| `POST /api/contact` | 5 requests | 60s per IP | +| `POST /api/rental-requests` | 5 requests | 60s per IP | +| `POST /api/admin/login` | 5 requests | 60s per IP | + +Implementiert via Redis (INCR + EXPIRE). + +### 6.3 Input Validation + +- **Backend:** Pydantic schemas (stricter than DB constraints) + - Email: EmailStr validator + - Strings: min/max length, strip whitespace + - Dates: date format, date_start ≤ date_end + - Numbers: positive integers + - Boolean: privacy_consent must be True + +- **Frontend:** Client-side validation (Vue reactive) vor Submit + - Gleiche Regeln wie Backend + - Inline Fehler-Meldungen (aria-invalid, aria-describedby) + +### 6.4 CORS + +```python +# FastAPI CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["https://hms.media-on.de"], + allow_methods=["GET", "POST"], + allow_headers=["*"], + allow_credentials=True, # für Admin-Cookie +) +``` + +### 6.5 Secrets Management + +| Secret | Environment Variable | Usage | +|--------|---------------------|-------| +| Rentman API Token | `RENTMAN_API_TOKEN` | Backend Service | +| JWT Secret | `JWT_SECRET` | Auth Service | +| Database URL | `DATABASE_URL` | SQLAlchemy | +| Redis URL | `REDIS_URL` | Redis client | +| SMTP Host | `SMTP_HOST` | Email Service | +| SMTP Port | `SMTP_PORT` | Email Service | +| SMTP User | `SMTP_USER` | Email Service | +| SMTP Password | `SMTP_PASSWORD` | Email Service | +| SMTP From | `SMTP_FROM` | Email Service | + +Alle via Coolify Environment Variables. Nie im Frontend, nie im Git. + +--- + +## 7. Deployment-Architektur + +### 7.1 Docker Compose + +```yaml +version: '3.8' +services: + frontend: + build: ./frontend + ports: + - "3000:3000" + environment: + - NUXT_PUBLIC_API_BASE=https://hms.media-on.de/api + depends_on: + - backend + restart: unless-stopped + + backend: + build: ./backend + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql+asyncpg://hms:${DB_PASSWORD}@postgres:5432/hms + - REDIS_URL=redis://redis:6379/0 + - RENTMAN_API_TOKEN=${RENTMAN_API_TOKEN} + - JWT_SECRET=${JWT_SECRET} + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_USER=${SMTP_USER} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - SMTP_FROM=${SMTP_FROM} + depends_on: + - postgres + - redis + restart: unless-stopped + + postgres: + image: postgres:16-alpine + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=hms + - POSTGRES_USER=hms + - POSTGRES_PASSWORD=${DB_PASSWORD} + restart: unless-stopped + + redis: + image: redis:7-alpine + volumes: + - redis_data:/data + restart: unless-stopped + +volumes: + postgres_data: + redis_data: +``` + +### 7.2 Coolify Deployment + +- **Platform:** Coolify auf coolify-01 (46.225.91.159) +- **Domain:** hms.media-on.de +- **TLS:** Let's Encrypt via Traefik (Coolify automatisch) +- **Resource Limits:** + - Frontend: 512MB RAM, 0.5 CPU + - Backend: 512MB RAM, 0.5 CPU + - PostgreSQL: 1GB RAM, 1 CPU + - Redis: 256MB RAM + +### 7.3 Environments + +| Environment | Domain | Zweck | +|-----------|-------|------| +| Dev | dev.hms.media-on.de | Entwicklung, Testing | +| Staging | staging.hms.media-on.de | Pre-Production Verification | +| Prod | hms.media-on.de | Live | + +### 7.4 CI/CD Pipeline (Forgejo Actions) + +``` +[Push to main] + → [Build Frontend: npm ci && npm run build] + → [Build Backend: pip install && pytest] + → [Build Docker Images] + → [Deploy to Coolify via API] +``` + +--- + +## 8. Test-Strategie + +### 8.1 Frontend Tests + +| Typ | Tool | Scope | Coverage Target | +|-----|------|-------|----------------| +| Unit | Vitest | Composables, Stores, Utils | 80% | +| Component | Vitest + Vue Test Utils | Component rendering, props, emits | 80% | +| E2E | Playwright | Page flows, forms, navigation | Critical paths | + +**Critical E2E Paths:** +1. Home → Mietkatalog → Equipment Detail → Add to Cart → Warenkorb → Mietanfrage → Submit +2. Home → Kontakt → Form submit → Success +3. Admin Login → Sync trigger → Status update +4. Referenzen → Filter → Lightbox +5. 404 page on invalid route + +### 8.2 Backend Tests + +| Typ | Tool | Scope | Coverage Target | +|-----|------|-------|----------------| +| Unit | Pytest | Services, Validators, Helpers | 80% | +| Integration | Pytest + TestClient | API endpoints, DB | 80% | +| E2E | Pytest | Rentman mock → full pipeline | Critical paths | + +**Critical Backend Tests:** +1. Equipment import (mocked Rentman API, paginated) +2. Rental request submission (mocked Rentman, email mock) +3. Contact form submission (email mock) +4. Admin login (valid/invalid credentials) +5. Rate limiting (6th request blocked) +6. Equipment search/filter/pagination + +### 8.3 Test Infrastructure + +- **Frontend:** Vitest config in `frontend/vitest.config.ts`, Playwright config in `frontend/playwright.config.ts` +- **Backend:** Pytest config in `backend/pytest.ini`, conftest.py with fixtures +- **Mocking:** Rentman API mocked via `unittest.mock` / `responses` library +- **Test DB:** SQLite in-memory oder test PostgreSQL instance + +--- + +## 9. E-Mail-Service + +### 9.1 Templates + +- **Kontaktformular:** HTML + Text template, Reply-To = sender email, To = info@hms-licht-ton.de +- **Mietanfrage-Bestätigung:** HTML + Text template, To = customer email, enthält reference_number + item list + +### 9.2 Implementierung + +```python +# services/email_service.py +import aiosmtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +class EmailService: + async def send_contact_email(self, contact: ContactCreate) -> bool: + # Build MIME message + # Send via aiosmtplib (async) + # On failure: enqueue in retry queue + ... + + async def send_rental_confirmation(self, request: RentalRequest, items: list) -> bool: + # Build confirmation email with reference_number + # Send via aiosmtplib + ... +``` + +--- + +## 10. Architecture Decision Records (ADRs) + +### ADR-001: Nuxt 3 als Frontend Framework +- **Status:** Accepted +- **Entscheidung:** Nuxt 3 (Vue 3 + Tailwind CSS) für Frontend +- **Begründung:** SSR/SSG Support für SEO (trotz noindex, für KI-Crawler), Vue 3 Composition API, Mobile-First, große Community +- **Alternativen:** Next.js (React), Astro, plain Vue 3 SPA +- **Nachteile:** Build-Komplexität durch SSR/SSG-Mischung + +### ADR-002: FastAPI als Backend Framework +- **Status:** Accepted +- **Entscheidung:** FastAPI (Python, async) für Backend API +- **Begründung:** Rentman Python-Client bereits vorhanden, async für API-Calls, automatische OpenAPI-Doku, Pydantic Validation +- **Alternativen:** Django REST, Express.js, Go +- **Nachteile:** Python ecosystem Abhängigkeiten (SQLAlchemy, etc.) + +### ADR-003: PostgreSQL + Redis +- **Status:** Accepted +- **Entscheidung:** PostgreSQL als Primary DB, Redis als Cache + Rate Limiter +- **Begründung:** PostgreSQL für ACID (rental_requests), JSONB für flexible specs; Redis für Caching + Rate Limiting +- **Alternativen:** SQLite (zu einfach), MongoDB (overkill) + +### ADR-004: localStorage für Warenkorb (keine Session-Backend) +- **Status:** Accepted +- **Entscheidung:** Client-side localStorage für Warenkorb, kein Backend-Session +- **Begründung:** Keine User-Auth nötig, keine Server-Session-Storage, persistiert über Reloads, einfach zu implementieren +- **Alternativen:** Server-side session (Redis), Cookie-based cart +- **Risiko:** localStorage limit ~5MB, aber Warenkorb-Items sind klein (IDs + quantities) + +### ADR-005: noindex/nofollow + JSON-LD +- **Status:** Accepted +- **Entscheidung:** Alle Seiten noindex/nofollow/noarchive/nosnippet, aber JSON-LD LocalBusiness auf Home +- **Begründung:** User möchte keine öffentliche Indexierung, aber KI-Crawler sollen strukturierte Daten erkennen +- **Alternativen:** Vollständige Indexierung (abgelehnt), keine Meta-Tags (zu wenig Info für KI) + +### ADR-006: SSG für statische Seiten, SSR für Mietkatalog +- **Status:** Accepted +- **Entscheidung:** Statische Seiten (Home, Referenzen, Legal) als SSG, Mietkatalog als SSR +- **Begründung:** Statische Seiten schnell + cached, Mietkatalog braucht aktuelle Equipment-Daten vom Backend +- **Alternativen:** Alles SSR (zu langsam für statische Seiten), Alles SSG (Mietkatalog nicht aktuell) + +### ADR-007: JWT HttpOnly Cookie für Admin-Auth +- **Status:** Accepted +- **Entscheidung:** JWT in HttpOnly Cookie (nicht localStorage), 24h Expiry +- **Begründung:** HttpOnly schützt vor XSS, SameSite=Strict vor CSRF, 24h Balance zwischen Security und Usability +- **Alternativen:** Session in Redis (mehr Complexity), Basic Auth (zu simpel) + +### ADR-008: Cron-basierter Equipment-Sync (6h) + manueller Trigger +- **Status:** Accepted +- **Entscheidung:** Equipment-Sync alle 6h via Cron + manuelle Admin-Trigger +- **Begründung:** Equipment ändert sich nicht stündlich, 6h ist aktuell genug, manueller Trigger für Ad-hoc Updates +- **Alternativen:** Webhook von Rentman (nicht verfügbar), Real-time (overkill) + +--- + +## 11. Non-Functional Requirements + +### 11.1 Performance +- Page Load < 2s auf 3G (Lighthouse Mobile Score >= 90) +- API Response < 500ms für Equipment-Liste (Redis cached) +- Images: WebP/AVIF, lazy-loading, responsive sizes +- Frontend Bundle < 200KB gzipped + +### 11.2 Availability +- Uptime target: 99.5% (kleines Unternehmen, nicht kritisch) +- Health check: GET /api/health (Docker restart if unhealthy) +- Graceful degradation: Equipment aus Cache anzeigen wenn Backend down + +### 11.3 Scalability +- Single-tenant, moderate traffic expected +- Redis Cache handles read load +- PostgreSQL handles write load (rental requests) +- Horizontal scaling: post-MVP (mehrere Frontend Container) + +--- + +## 12. Risiken + +| Risiko | Wahrscheinlichkeit | Impact | Mitigation | +|--------|-------------------|--------|------------| +| Rentman API Felder unklar | Hoch | Mittel | GET /equipment/{id} testen, flexible JSONB-Spalten | +| Rentman API Rate Limits | Niedrig | Mittel | Paginated import, 6h interval, caching | +| SMTP nicht erreichbar | Niedrig | Niedrig | Retry-Queue, Admin-Notification | +| Logo SVG nicht verfügbar | Mittel | Niedrig | Fallback-Text 'HMS Licht & Ton' | +| Referenz-Bilder fehlen | Mittel | Niedrig | Placeholder images, Unsplash fallback | +| localStorage Limit | Niedrig | Niedrig | Nur IDs + quantities, max ~1000 items | + +--- + +## 13. Offene Fragen + +1. Rentman Equipment-Felder: Welche Felder sind im `equipment`-Objekt verfügbar? → GET /equipment/{id} testen +2. Equipment-Kategorien: Wie strukturiert? Flaches Feld oder nested object? → API testen +3. AGB-Texte: Vom User als Text/Markdown bereitstellen? +4. Referenz-Bilder: 9 Dateien vom User bereitstellen? +5. SMTP-Zugangsdaten: Welcher Server? +6. USt-IdNr: Für Impressum benötigt? +7. Mietpreise: Anzeigen oder 'auf Anfrage'? + +--- + +## 14. Handoff Summary + +- **architecture status:** COMPLETE – Alle Komponenten, API-Endpunkte, DB-Schema, Security, Deployment dokumentiert +- **8 ADRs** mit Begründung und Alternativen +- **6-8 Tasks** im task_graph.json mit Test-Specs und Abhängigkeiten +- **ready for review:** YES – Qualitätssicherung durch quality_reviewer empfohlen \ No newline at end of file