# Performance & Architektur Report (Pounce)

**Stand (Codebase):** `d08ca33fe3c88b3b2d716f0bdf22b71f989a5eb9`  
**Datum:** 2025-12-12  
**Scope:** `frontend/` (Next.js 14 App Router) + `backend/` (FastAPI + async SQLAlchemy + APScheduler) + DB + Docker/Deploy.

## Status (umgesetzt)

- ✅ **Phase 0**: Scheduler split, Market-Feed bounded paging, Health cache-first, PriceTracker N+1 fix (`2e8ff50`)
- ✅ **Phase 1**: DB migrations (indexes + optional columns), persisted `pounce_score`, Admin N+1 removal, Radar summary endpoint (`ee4266d`)
- ✅ **Phase 2**: Redis + ARQ worker scaffolding, Prometheus metrics (`/metrics`), load test scaffolding, Docker hardening (`5d23d34`)

---

## Executive Summary (die 5 größten Hebel)

1. **Scheduler aus dem API-Prozess herauslösen**  
   Aktuell startet der Scheduler in `backend/app/main.py` im App-Lifespan. Bei mehreren Uvicorn/Gunicorn-Workern laufen Jobs **mehrfach parallel** → doppelte Scrapes/Checks, DB-Last, E-Mail-Spam, inkonsistente Zustände.

2. **Market Feed Endpoint (`/api/v1/auctions/feed`) DB-seitig paginieren/sortieren**  
   `backend/app/api/auctions.py` lädt derzeit **alle aktiven Auktionen + alle aktiven Listings** in Python, berechnet Score, sortiert, und paginiert erst am Ende. Das skaliert schlecht sobald ihr > ein paar hundert Auktionen habt.

3. **Price Tracker N+1 eliminieren**  
   `backend/app/services/price_tracker.py::detect_price_changes()` macht aktuell: *distinct(tld, registrar) → pro Paar query(limit 2)*. Das ist ein klassischer N+1 und wird bei 800+ TLDs schnell sehr langsam.

4. **Health-Cache wirklich nutzen**  
   Es gibt `DomainHealthCache`, und der Scheduler schreibt Status/Score. Aber `GET /domains/{id}/health` macht immer einen **Live-Check** (`domain_health.py` mit HTTP/DNS/SSL). Für UI/Performance besser: default **cached**, live nur “Refresh”.

5. **Valuation im Request-Path reduzieren (Auctions)**  
   `backend/app/api/auctions.py` berechnet pro Auction im Response optional valuation, und `valuation_service` fragt pro Domain auch DB-Daten ab (TLD cost). Das ist pro Request potenziell **sehr teuer**.

---

## Messwerte (Frontend Build)

Aus `frontend/` → `npm run build` (Next.js 14.0.4):

- **First Load JS (shared):** ~81.9 kB  
- **Größte Pages (First Load):**
  - `/terminal/watchlist`: ~120 kB
  - `/terminal/radar`: ~120 kB
  - `/terminal/intel/[tld]`: ~115 kB
  - `/terminal/market`: ~113 kB
- **Warnings:** Einige Routen “deopted into client-side rendering” (z.B. `/terminal/radar`, `/terminal/listing`, `/unsubscribe`, `/terminal/welcome`). Das ist nicht zwingend schlimm, aber ein Hinweis: dort wird kein echtes SSR/Static genutzt.

**Interpretation:** Das Frontend ist vom Bundle her bereits recht schlank. Die größten Performance-Risiken liegen aktuell eher im **Backend (Queries, Jobs, N+1, Caching)**.

---

## Backend – konkrete Hotspots & Fixes

### 1) Scheduler: Architektur & Skalierung

**Ist-Zustand**
- `backend/app/main.py`: `start_scheduler()` im `lifespan()` → Scheduler läuft im selben Prozess wie die API.
- `backend/app/scheduler.py`: viele Jobs (Domain Checks, Health Checks, TLD Scrape, Auction Scrape, Cleanup, Sniper Matching).

**Probleme**
- Multi-worker Deployment (Gunicorn/Uvicorn) → Scheduler läuft pro Worker → Job-Duplikate.
- Jobs sind teils sequentiell (Domain Checks), teils N+1 (Health Cache, Digests, Sniper Matching).

**Empfehlung (Best Practice)**
- **Scheduler als separater Service/Container** laufen lassen (z.B. eigener Docker Service `scheduler`, oder systemd/cron job, oder Celery Worker + Beat).
- Wenn Scheduler im selben Code bleiben soll: **Leader-Lock** (Redis/DB advisory lock) einbauen, sodass nur ein Prozess Jobs ausführt.

---

### 2) Market Feed (`backend/app/api/auctions.py::get_market_feed`)

**Ist-Zustand**
- Holt Listings und Auktionen ohne DB-Limit/Offset, baut `items` in Python, sortiert in Python, paginiert erst am Ende.

**Warum das weh tut**
- Bei z.B. 10’000 aktiven Auktionen ist jeder Request an `/feed` ein “Full table scan + Python sort + JSON build”.

**Fix-Strategie**
- **Score persistieren**: `pounce_score` in `DomainAuction` und `DomainListing` speichern/aktualisieren (beim Scrape bzw. beim Listing Create/Update).  
  Dann kann man DB-seitig `WHERE pounce_score >= :min_score` und `ORDER BY pounce_score DESC` machen.
- **DB-Pagination**: `LIMIT/OFFSET` in SQL, nicht in Python.
- **Filter DB-seitig**: `keyword`, `tld`, `price range`, `ending_within` in SQL.
- **Response caching**: Für public feed (oder häufige Filterkombos) Redis TTL 15–60s.

---

### 3) Auction Search (`backend/app/api/auctions.py::search_auctions`)

**Ist-Zustand**
- Nach Query werden Auktionen in Python gefiltert (Vanity Filter) und dann pro Auction in einer Schleife `valuation_service.estimate_value(...)` aufgerufen.

**Probleme**
- Valuation kann DB-Queries pro Item auslösen (TLD cost avg), und läuft seriell.

**Fix-Strategie**
- Valuations **vorberechnen** (Background Job) und in einer Tabelle/Spalte cachen.
- Alternativ: Valuation nur **für Top-N** (z.B. 20) berechnen und für den Rest weglassen.
- TLD-Cost als **in-memory cache** (LRU/TTL) oder einmal pro Request prefetchen.

---

### 4) Price Tracker (`backend/app/services/price_tracker.py`)

**Ist-Zustand**
- N+1 Queries: distinct(tld, registrar) → pro Paar 1 Query für die letzten 2 Preise.

**Fix-Strategie**
- SQL Window Function (Postgres & SQLite können das):
  - `ROW_NUMBER() OVER (PARTITION BY tld, registrar ORDER BY recorded_at DESC)`  
  - dann self-join oder `LAG()` für vorherigen Preis.
- Zusätzlich DB-Index: `tld_prices(tld, registrar, recorded_at DESC)`

---

### 5) Domain Health (`backend/app/services/domain_health.py` + `backend/app/api/domains.py`)

**Ist-Zustand**
- Live Health Check macht pro Request echte DNS/HTTP/SSL Checks.
- Scheduler schreibt `DomainHealthCache`, aber Endpoint nutzt ihn nicht.

**Fix-Strategie**
- Neue Endpoints:
  - `GET /domains/health-cache` → cached health für alle Domains eines Users (1 Request für UI)
  - `POST /domains/{id}/health/refresh` → live refresh (asynchron, job queue)  
- `DomainHealthCache` auch mit `dns_data/http_data/ssl_data` befüllen (ist im Model vorgesehen).

---

## Datenbank – Indexing & Query Patterns

### Empfohlene Indizes (High Impact)

- **Domain Checks**
  - `domain_checks(domain_id, checked_at DESC)` für `/domains/{id}/history`
- **TLD Prices**
  - `tld_prices(tld, registrar, recorded_at DESC)` für “latest two prices” und history queries
- **Health Cache**
  - `domain_health_cache(domain_id)` (unique ist vorhanden), optional `checked_at`

### Query-Patterns (Quick Wins)

- In `backend/app/api/domains.py::add_domain()` wird aktuell `len(current_user.domains)` genutzt → lädt potenziell viele Rows.  
  Besser: `SELECT COUNT(*) FROM domains WHERE user_id = ...`.

- Admin “Users list”: vermeidet N+1 (Subscription + Domain Count pro User) → `JOIN` + `GROUP BY`.

---

## Frontend – Verbesserungen (gezielt, nicht “blind refactor”)

### 1) Reduziere API-Calls pro Screen (Dashboard/Watchlist)

Aktuell holen manche Screens mehrere Endpoints und rechnen Stats client-side:
- `/terminal/radar`: holt u.a. Auctions und `GET /listings/my` nur um Stats zu zählen.

**Empfehlung**
- Ein Endpoint: `GET /dashboard/summary` (counts + small previews) → 1 Request statt 3–5.

### 2) Tabellen/Listen skalieren

- Für sehr große Listen (Market Feed / TLDs / Admin Users) mittelfristig:
  - Pagination + “infinite scroll”
  - ggf. Virtualisierung (`react-window`) falls 1000+ Rows.

### 3) Kleine Code-Health Fixes (auch Performance)

- Achtung bei `.sort()` auf State-Arrays: `.sort()` mutiert. Immer vorher kopieren (`[...arr].sort(...)`), sonst entstehen subtile Bugs und unnötige Re-Renders.

---

## Deployment/Infra – “Production grade” Performance

### Backend
- **Gunicorn + Uvicorn Workers** (oder Uvicorn `--workers`) ist gut für CPU/IO – aber **nur wenn Scheduler separat läuft**.
- **DB Pooling**: `create_async_engine(..., pool_size=..., max_overflow=...)` für Postgres (nicht bei SQLite).
- **slowapi**: in Production Redis storage verwenden (sonst pro Worker eigener limiter state).

### Frontend
- Dockerfile erwartet `.next/standalone`. Dafür in `frontend/next.config.js` `output: 'standalone'` aktivieren (oder Dockerfile anpassen).

---

## Priorisierte Roadmap

### Phase 0 (0–1 Tag, Quick Wins)
- Scheduler entkoppeln ODER Leader-Lock einbauen
- `/auctions/feed`: DB-limit + offset + order_by (keine full scans)
- `PriceTracker.detect_price_changes`: Window-Query statt N+1
- Cached Health Endpoint für Watchlist

### Phase 1 (1–2 Wochen)
- Precompute `pounce_score` + valuations (Background Jobs), persistieren & cachen
- Admin N+1 entfernen (Users list)
- DB Indizes ergänzen (DomainCheck, TLDPrice)
- “Dashboard summary” Endpoint + Frontend umstellen

### Phase 2 (2–6 Wochen)
- Background-Job System (Celery/RQ/Dramatiq) + Redis
- Observability: Request timing, DB query timing, Prometheus metrics, tracing
- Load testing + Performance budgets (API p95, page LCP/TTFB)

---

## Mess-/Monitoring Plan (damit wir nicht im Dunkeln optimieren)

- **Backend**
  - Log: Request duration + endpoint + status
  - DB: slow query logging / EXPLAIN ANALYZE (prod-like)
  - Metrics: p50/p95 latency pro endpoint, queue depth, job runtime
- **Frontend**
  - Core Web Vitals Tracking (ist bereits angelegt in `frontend/src/lib/analytics.ts`)
  - “API Timing” (TTFB + payload size) für Market/Watchlist


