# preisvergleich-tool

Internes Preisvergleichs-Tool fuer `preis.tooluna.de` mit Node.js, Express, einfachem Login, eBay Browse API, Google-Suche ueber SerpAPI und OpenAI-basierter Matching-Bewertung.

## Funktionen in Version 1

- Login mit Benutzername und Passwort aus `.env`
- Session-basierter Zugriffsschutz fuer Suchseite und API-Endpunkte
- Suchformular fuer Produktname, Zustand, EAN/GTIN, Hersteller, Artikelnummer und Modellnummer
- Modulares Provider-System unter `src/services/providers/`
- eBay Browse API mit OAuth Application Access Token und Token-Cache
- Google-Suche ueber SerpAPI mit bevorzugter Google-Shopping-Abfrage, Bildern und defensivem Zielseiten-Abruf fuer strukturierte Preisinfos
- Preisextraktion aus Text, JSON-LD, schema.org und Meta-Tags
- OpenAI Matching-Logik mit strengem JSON-Schema und Fallback
- Ergebnisansicht mit Sortierung, Filtern und responsivem Layout
- Gruppierte Treffer nach erkannter Produktidentitaet
- Dashboard-Kennzahlen fuer gepruefte, valide und ausgeblendete Treffer
- Detailansicht pro Treffer mit KI-Bewertung, Preislogik, Warnsignalen und Rohdaten-Auszug
- Optionaler EAN-Strengmodus fuer besonders praezise Suchen
- KI-Kernbezeichnungsanalyse aus allen Suchergebnis-Titeln, z. B. robuste Gleichbehandlung von `Black Pool`, `Blackpool` oder Modellcodes mit/ohne Bindestrich
- CSV- und Excel-Export
- PDF-Export als sauberer Erweiterungspunkt vorbereitet
- Rate-Limit und serverseitige Eingabevalidierung

## Projektstruktur

```text
preisvergleich-tool/
├── server.js
├── package.json
├── .env.example
├── README.md
├── public/
│   ├── index.html
│   ├── login.html
│   ├── app.js
│   └── styles.css
├── scripts/
│   └── check-syntax.js
└── src/
    ├── routes/
    ├── services/
    │   ├── providers/
    │   │   ├── ebayProvider.js
    │   │   └── googleProvider.js
    │   ├── openaiMatcher.js
    │   ├── priceExtractor.js
    │   ├── normalizer.js
    │   └── searchService.js
    ├── middleware/
    └── utils/
```

## Installation auf Ubuntu/V-Server

Empfohlen: Ubuntu 22.04 oder 24.04 mit Node.js 20 LTS.

```bash
sudo apt update
sudo apt install -y curl ca-certificates
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -v
```

Projekt auf den Server kopieren, z. B. nach `/var/www/preisvergleich-tool`.

```bash
cd /var/www/preisvergleich-tool
npm install
cp .env.example .env
nano .env
```

## .env einrichten

```env
SERPAPI_KEY=
GOOGLE_API_KEY=
GOOGLE_CX=
EBAY_CLIENT_ID=
EBAY_CLIENT_SECRET=
EBAY_MARKETPLACE_ID=EBAY_DE
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o-mini
OPENAI_MATCH_CONCURRENCY=2
MATCH_MIN_SCORE=45
EAN_STRICT_HIGH_SCORE=92
CANONICAL_SIGNAL_MIN_CONFIDENCE=70
CANONICAL_MISSING_PENALTY=28
CANONICAL_PRESENT_BONUS=8
CANONICAL_ANALYZER_RESULT_LIMIT=28
APP_USERNAME=
APP_PASSWORD=
SESSION_SECRET=
PORT=3000
NODE_ENV=production
TRUST_PROXY=1
SEARCH_RESULT_LIMIT=40
GOOGLE_TARGET_FETCH_LIMIT=8
SERPAPI_ORGANIC_FALLBACK_MIN=5
SERPAPI_PRODUCT_OFFER_LIMIT=3
SERPAPI_STORES_PER_PRODUCT=8
CLEAR_LOW_PRICE_RATIO=0.12
```

Wichtig:

- `.env` darf nicht committed werden.
- `SESSION_SECRET` in Produktion auf einen langen zufaelligen Wert setzen.
- `APP_PASSWORD` stark waehlen.
- API-Keys werden nur im Backend genutzt und nie ans Frontend ausgegeben.

Beispiel fuer einen Session-Secret:

```bash
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
```

## Start im Entwicklungsmodus

```bash
npm install
npm run dev
```

Danach lokal aufrufen:

```text
http://localhost:3000
```

## Start im Produktionsmodus

```bash
NODE_ENV=production npm start
```

## Betrieb mit PM2

```bash
sudo npm install -g pm2
cd /var/www/preisvergleich-tool
pm2 start server.js --name preisvergleich-tool
pm2 save
pm2 startup systemd
```

Logs:

```bash
pm2 logs preisvergleich-tool
```

Neustart nach Update:

```bash
cd /var/www/preisvergleich-tool
npm install --omit=dev
pm2 restart preisvergleich-tool
```

## Nginx Reverse Proxy fuer preis.tooluna.de

Beispiel fuer `/etc/nginx/sites-available/preis.tooluna.de`:

```nginx
server {
    listen 80;
    server_name preis.tooluna.de;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

Aktivieren:

```bash
sudo ln -s /etc/nginx/sites-available/preis.tooluna.de /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```

HTTPS mit Certbot:

```bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d preis.tooluna.de
```

In Produktion sollte `NODE_ENV=production` und `TRUST_PROXY=1` gesetzt sein, damit sichere Cookies hinter dem Reverse Proxy korrekt funktionieren.

## Apache Reverse Proxy fuer preis.tooluna.de

Module aktivieren:

```bash
sudo a2enmod proxy proxy_http headers ssl rewrite
sudo systemctl restart apache2
```

Beispiel-VHost:

```apache
<VirtualHost *:80>
    ServerName preis.tooluna.de

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    RequestHeader set X-Forwarded-Proto "http"
</VirtualHost>
```

HTTPS kann ebenfalls ueber Certbot eingerichtet werden:

```bash
sudo apt install -y certbot python3-certbot-apache
sudo certbot --apache -d preis.tooluna.de
```

## Beispiel-Suchanfrage

- Produktname: `Sony WH-1000XM5`
- Zustand: `Neuware`
- EAN/GTIN: `4548736132580`
- Hersteller: `Sony`
- Modellnummer: `WH-1000XM5`

## API-Hinweise und Limits

- SerpAPI hat tarifabhaengige Monatslimits und Kosten pro Anfrage.
- Das Tool erzeugt pro Suche mehrere Google-Shopping-Abfragen. Die normale Google-Websuche wird nur als Fallback genutzt, wenn zu wenige Shopping-Ergebnisse gefunden werden.
- Wenn SerpAPI Shopping einen Immersive-Product-Token liefert, ruft das Tool zusaetzliche Store-/Offer-Details ab. Das ist meist verlaesslicher als ein einzelner Shopping-Treffer.
- `GOOGLE_API_KEY` und `GOOGLE_CX` koennen leer bleiben, wenn `SERPAPI_KEY` gesetzt ist. Sie sind nur noch als Fallback fuer Google Custom Search vorgesehen.
- Zielseiten werden nur defensiv und mit Timeout abgerufen, wenn aus Google-Titel/Snippet/SerpAPI-Daten kein Preis erkennbar ist.
- `MATCH_MIN_SCORE` blendet schwache Treffer aus. Standard ist `45`; bei EAN, Modellnummer oder Artikelnummer wird intern mindestens `50` genutzt.
- `EAN_STRICT_HIGH_SCORE` steuert den EAN-Strengmodus. Wenn `EAN streng` aktiv ist, bleiben nur Treffer mit erkannter EAN/Modell-/Artikelnummer oder sehr hoher KI-Sicherheit sichtbar.
- `CANONICAL_SIGNAL_MIN_CONFIDENCE` steuert, ab welcher KI-Sicherheit eine aus den Ergebnissen erkannte Kernbezeichnung hart gewichtet wird.
- `CANONICAL_MISSING_PENALTY` zieht Treffer ab, denen die wahrscheinliche Kernbezeichnung fehlt.
- `CANONICAL_PRESENT_BONUS` belohnt Treffer, die diese Kernbezeichnung enthalten.
- `CANONICAL_ANALYZER_RESULT_LIMIT` begrenzt, wie viele Ergebnis-Titel fuer die Kernbezeichnungsanalyse an OpenAI gehen.
- `GOOGLE_TARGET_FETCH_LIMIT` begrenzt, wie viele Zielseiten pro Suche fuer fehlende Bilder/strukturierte Preise abgerufen werden.
- `SERPAPI_PRODUCT_OFFER_LIMIT` begrenzt, fuer wie viele Shopping-Produkte Detailangebote abgerufen werden.
- `SERPAPI_STORES_PER_PRODUCT` begrenzt, wie viele Haendlerangebote pro Detailprodukt uebernommen werden.
- `CLEAR_LOW_PRICE_RATIO` entfernt nur sehr eindeutige niedrige Preisausreisser, und auch nur mit Zusatzsignal wie Zubehoer, Ratenzahlung oder Option. Echte Schnaeppchen ohne Warnsignal bleiben erhalten.
- Der Suchmodus im Formular kann auf `Normal`, `Streng` oder `Sehr streng` gestellt werden. Strengere Modi verlangen hoehere MatchScores, bessere Preisverlaesslichkeit und verlaesslichere Quellen.
- Die Ergebnisansicht gruppiert Angebote nach erkannter Marke, Modell, Variante und Zustand. So lassen sich mehrere Haendlerpreise fuer dasselbe Produkt schneller vergleichen.
- Die Detailansicht pro Treffer zeigt erkannte Attribute, KI-Begruendung, Warnsignale, Preis-/Versandlogik und einen gekuerzten Rohdaten-Auszug.
- eBay nutzt den Client-Credentials-Flow und cached das Application Access Token bis kurz vor Ablauf.
- OpenAI wird pro normalisiertem Ergebnis fuer die Matching-Bewertung genutzt. Mit `SEARCH_RESULT_LIMIT` und `OPENAI_MATCH_CONCURRENCY` lassen sich Kosten und Parallelitaet steuern.
- Wenn eine API fehlt oder nicht antwortet, liefert das Tool nutzerfreundliche Fehler und nutzt beim Matching Fallback-Werte.

## Weitere Provider ergaenzen

Neue Quellen koennen unter `src/services/providers/` ergaenzt werden. Ein Provider sollte diese Form haben:

```js
module.exports = {
  id: 'mein-provider',
  name: 'Mein Provider',
  isConfigured() {
    return Boolean(process.env.MEIN_PROVIDER_KEY);
  },
  async search(product) {
    return [
      {
        source: 'Mein Provider',
        title: 'Angebotstitel',
        price: 129.99,
        shippingCost: 0,
        totalPrice: 129.99,
        currency: 'EUR',
        condition: 'Neuware',
        merchant: 'shop.example',
        url: 'https://shop.example/angebot',
        imageUrl: '',
        snippet: ''
      }
    ];
  }
};
```

Danach den Provider in `src/services/providerRegistry.js` importieren und in die `providers`-Liste aufnehmen.

## Spaetere SaaS-Erweiterung

Die aktuelle Version nutzt bewusst keine Datenbank. Fuer die SaaS-Version sind diese Erweiterungen naheliegend:

- Benutzer- und Rollenmodell in einer Datenbank, z. B. PostgreSQL
- Passwort-Hashing mit Argon2 oder bcrypt
- Persistente Sessions mit Redis
- Mandantenfaehige API-Limits pro Benutzer oder Team
- Suchhistorie und gespeicherte Exporte
- Abrechnung ueber Stripe oder einen vergleichbaren Anbieter

Die jetzige Struktur trennt Routen, Middleware, Services und Provider bereits so, dass diese Punkte ohne grossen Umbau ergaenzt werden koennen.

## Qualitaetscheck

```bash
npm run check
```

Der Check prueft die JavaScript-Dateien syntaktisch. Fuer echte API-Tests muessen gueltige Keys in `.env` gesetzt sein.
