Files
hms-licht-ton/docs/architecture.md
T

32 KiB
Raw Blame History

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:

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:

: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:

{
  "items": [...],
  "total": 500,
  "page": 1,
  "page_size": 20,
  "total_pages": 25
}

EquipmentItem Schema:

{
  "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):

{
  "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:

{
  "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:

{
  "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

-- 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

# 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

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

# 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)
  • 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