# CODEX TASK: Raspberry Pi Scraper Fleet Deployment

## Ziel

Einen oder mehrere Raspberry Pis als dedizierte Scraper-Nodes in die TIP-Infrastruktur
integrieren — mit WireGuard VPN zu Erik's PostgreSQL und SOCKS5-Proxy für FS.COM.

**Warum Pi statt Erik:**
- Residential IP → FS.COM, NADDOD blockieren Datacenter-IPs (IONOS wird erkannt)
- Starlink oder Kabel-IP = andere IP-Range = höhere Scraper-Erfolgsrate
- Keine Kosten für separate Proxies (€50-150/Monat bei kommerziellen Diensten)
- Pi 4B (4GB) kostet ~60€ einmalig und läuft dauerhaft

**Architektur nach Deployment:**
```
Raspberry Pi(s)                    Erik (82.165.222.127)
┌────────────────────────┐         ┌─────────────────────────┐
│  index-pi.ts           │  VPN    │  PostgreSQL + pg-boss   │
│  32 fetch-only scrapers│◄───────►│  tip-api, tip-mcp       │
│  (NADDOD, GBICS, etc.) │         │  tip-scraper (Playwright)│
│                        │         │    ↑ PROXY_URLS          │
│  dante SOCKS5 :1080    │◄────────│    socks5://Pi:1080      │
│  (WireGuard IP only)   │         │    für FS.COM scraper    │
└────────────────────────┘         └─────────────────────────┘
       WG: 10.10.0.6                       WG: 10.10.0.1
```

---

## Repository

```
Pfad lokal:  /Users/renefichtmueller/Desktop/Claude Code/github-repos/transceiver-db/
Pfad Erik:   /opt/tip/
Gitea:       http://192.168.178.196:3000/rene/transceiver-db.git
```

---

## SCHRITT 1: WireGuard — Pi-Keypair generieren (lokal oder auf Erik)

Führe dies auf einem Linux-System aus (oder auf dem Pi selbst):

```bash
# WireGuard keypair für Pi1 generieren
wg genkey | tee /tmp/pi1-privkey | wg pubkey > /tmp/pi1-pubkey
cat /tmp/pi1-privkey   # → wird PI_PRIVKEY beim Pi-Setup
cat /tmp/pi1-pubkey    # → wird in Erik's wg0.conf eingetragen
```

Notiere:
- `PI_PRIVKEY` = privater Schlüssel (bleibt auf dem Pi, NIEMALS in Git)
- `PI_PUBKEY`  = öffentlicher Schlüssel (wird auf Erik konfiguriert)

---

## SCHRITT 2: WireGuard auf Erik — Pi als Peer hinzufügen

**SSH auf Erik:**
```bash
ssh erik-cf
```

**Datei bearbeiten:** `/etc/wireguard/wg0.conf`

Füge am Ende hinzu:
```ini
# Raspberry Pi 1 — Scraper Node
[Peer]
PublicKey = <PI_PUBKEY aus Schritt 1>
AllowedIPs = 10.10.0.6/32
PersistentKeepalive = 25
```

**WireGuard neu laden (ohne Verbindungsabbruch):**
```bash
sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
# oder bei komplettem Reload:
sudo systemctl restart wg-quick@wg0
```

**Verify:**
```bash
sudo wg show wg0   # muss den Pi-Peer als "Peer:" zeigen
```

**PostgreSQL Firewall auf Erik — Pi-IP erlauben:**
```bash
# UFW: WireGuard-Traffic von Pi zur DB erlauben
sudo ufw allow in on wg0 from 10.10.0.6 to 10.10.0.1 port 5433 proto tcp comment "Pi1 → PostgreSQL"
sudo ufw status | grep 5433
```

**pg_hba.conf auf Erik anpassen** (damit Pi sich zur DB verbinden kann):
```bash
# Falls nicht schon eingetragen:
echo "host transceiver_db tip 10.10.0.0/24 scram-sha-256" | sudo tee -a /etc/postgresql/17/main/pg_hba.conf
sudo systemctl reload postgresql
```

---

## SCHRITT 3: Pi physisch einrichten

Raspberry Pi OS Lite (64-bit) flashen und SSH aktivieren.

**Auf dem Pi ausführen:**
```bash
# Einmalig: TIP Scraper Setup mit WireGuard + SOCKS5
PI_NAME=pi1 \
DB_HOST=10.10.0.1 \
DB_PORT=5433 \
DB_USER=tip \
DB_PASS=<tip-db-passwort-aus-env> \
DB_NAME=transceiver_db \
WG_PRIVKEY=<PI_PRIVKEY aus Schritt 1> \
WG_ADDR=10.10.0.6 \
PROXY_AGENT=1 \
bash <(curl -sL http://192.168.178.196:3000/rene/transceiver-db/raw/branch/main/scripts/pi-scraper-setup.sh)
```

**Was das Script macht:**
1. Node.js 22 + tsx + pm2 installieren
2. Repo von Gitea klonen nach `/opt/tip-scraper/`
3. npm install --ignore-scripts (kein Playwright!)
4. .env schreiben (DB via WireGuard: 10.10.0.1:5433)
5. WireGuard konfigurieren (Pi → Erik)
6. PM2 mit `index-pi.ts` starten (32 fetch-only scrapers)
7. dante SOCKS5-Proxy auf 10.10.0.6:1080 starten

**Verify auf Pi:**
```bash
pm2 status                           # tip-pi-scraper: online
pm2 logs tip-pi-scraper --lines 20   # muss "32 queues / workers active" zeigen
sudo wg show wg0                     # muss Handshake mit Erik zeigen
ss -tlnp | grep 1080                 # dante: listening
```

---

## SCHRITT 4: Erik — FS.COM Scraper durch Pi-Proxy routen

**Datei:** `/opt/tip/ecosystem.config.js`

Den bestehenden `tip-scraper` Eintrag aufsplitten in:
- `tip-scraper-pi` — läuft auf dem Pi (kein Eintrag nötig hier, Pi macht's selbst)
- `tip-scraper-fs` — FS.COM via Pi SOCKS5 (neuer PM2-Prozess auf Erik)
- `tip-scraper` — alle anderen Playwright-Scrapers auf Erik

**Füge folgenden Eintrag in `ecosystem.config.js` hinzu** (neben dem bestehenden `tip-scraper`):

```javascript
{
  name: "tip-scraper-fs",
  script: "./node_modules/.bin/tsx",
  args: "packages/scraper/src/index-fs-only.ts",
  cwd: "/opt/tip",
  interpreter: "none",
  exec_mode: "fork",
  env: {
    NODE_ENV: "production",
    POSTGRES_HOST: "localhost",
    POSTGRES_PORT: "5433",
    POSTGRES_DB: "transceiver_db",
    POSTGRES_USER: "tip",
    POSTGRES_PASSWORD: "<CHANGE_ME>",
    PROXY_URLS: "socks5://10.10.0.6:1080",   // ← Pi SOCKS5
    CRAWLEE_STORAGE_DIR: "/tmp/tip-crawlee-fs",
  },
  max_memory_restart: "800M",
  instances: 1,
  autorestart: true,
},
```

---

## SCHRITT 5: index-fs-only.ts erstellen (Erik-side, nur FS.COM)

**Erstelle Datei:** `packages/scraper/src/index-fs-only.ts`

```typescript
/**
 * TIP FS.COM Dedicated Scraper
 *
 * Runs on ERIK but routes traffic through Pi's SOCKS5 proxy
 * so FS.com sees a residential IP instead of IONOS datacenter IP.
 *
 * PROXY_URLS=socks5://10.10.0.6:1080 must be set in environment.
 */
import { config } from "dotenv";
import { join } from "path";
config({ path: join(__dirname, "..", "..", "..", ".env") });

import PgBoss from "pg-boss";
import { mkdirSync, rmSync } from "fs";

const connectionString = `postgres://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_HOST}:${process.env.POSTGRES_PORT || "5433"}/${process.env.POSTGRES_DB}`;

async function withIsolatedStorage(name: string, fn: () => Promise<void>): Promise<void> {
  const dir = `/tmp/tip-crawlee-${name}-${Date.now()}`;
  mkdirSync(join(dir, "request_queues", "default"), { recursive: true });
  mkdirSync(join(dir, "datasets", "default"), { recursive: true });
  mkdirSync(join(dir, "key_value_stores", "default"), { recursive: true });
  const prev = process.env.CRAWLEE_STORAGE_DIR;
  process.env.CRAWLEE_STORAGE_DIR = dir;
  try { await fn(); }
  finally {
    process.env.CRAWLEE_STORAGE_DIR = prev ?? "";
    try { rmSync(dir, { recursive: true, force: true }); } catch {}
  }
}

async function main() {
  const proxy = process.env.PROXY_URLS;
  console.log(`\n=== TIP FS.COM Scraper (proxy: ${proxy ?? "none"}) ===\n`);
  if (!proxy) {
    console.warn("WARNING: PROXY_URLS not set — FS.com will see IONOS IP (may be blocked)");
  }

  const boss = new PgBoss({
    connectionString,
    retryLimit: 3,
    retryDelay: 300,       // 5 min retry on failure
    expireInSeconds: 7200,  // 2h timeout for full FS catalog run
    monitorStateIntervalSeconds: 60,
  });

  boss.on("error", (e: Error) => console.error("pg-boss error:", e.message));
  await boss.start();

  await boss.createQueue("scrape:pricing:fs").catch(() => {});
  await boss.createQueue("scrape:pricing:naddod").catch(() => {});

  const { scrapeFs }     = await import("./scrapers/fs-com");
  const { scrapeNaddod } = await import("./scrapers/naddod");

  await boss.work("scrape:pricing:fs", async () => {
    console.log(`[${new Date().toISOString()}] FS.COM via ${proxy ?? "direct"}`);
    await withIsolatedStorage("fs", scrapeFs);
  });

  // NADDOD also benefits from residential IP (less aggressive rate limiting)
  await boss.work("scrape:pricing:naddod", async () => {
    console.log(`[${new Date().toISOString()}] NADDOD via ${proxy ?? "direct"}`);
    await scrapeNaddod();
  });

  console.log("FS.COM + NADDOD workers active — waiting for scheduled jobs\n");
  process.on("SIGTERM", async () => { await boss.stop(); process.exit(0); });
  process.on("SIGINT",  async () => { await boss.stop(); process.exit(0); });
}

main().catch((e) => { console.error("Fatal:", e); process.exit(1); });
```

**Hinweis:** Wenn `PROXY_URLS` gesetzt ist, nutzt der FS.COM PlaywrightCrawler automatisch
den SOCKS5-Proxy via Crawlee's ProxyConfiguration. Kein weiterer Code nötig.

---

## SCHRITT 6: Scheduler — NADDOD-Job aus index.ts entfernen

Da NADDOD nun durch `index-fs-only.ts` (auf Erik) und `index-pi.ts` (auf Pi) gehandhabt wird,
muss `scrape:pricing:naddod` nur noch EINMAL registriert werden.

**Prüfe:** `packages/scraper/src/scheduler.ts` — suche nach `naddod` und stelle sicher,
dass der `scrape:pricing:naddod` Job-Schedule NUR EINMAL registriert ist.

Falls doppelt vorhanden: Im `scheduler.ts` den zweiten Eintrag entfernen.

---

## SCHRITT 7: Erik — PM2 neu starten

```bash
ssh erik-cf
cd /opt/tip
git pull origin main
pm2 start ecosystem.config.js --only tip-scraper-fs
pm2 save
pm2 status   # tip-scraper-fs muss online sein
```

---

## SCHRITT 8: Verifikation (automatisch — kein manuelles Debugging)

Die Scrapers verifizieren sich selbst. Folgende Checks laufen automatisch:

**Pi selbst:**
```bash
# 5 min nach Setup auf dem Pi:
pm2 logs tip-pi-scraper --lines 50 | grep -E "\[(scrape|enrich)\]"
# Erwartete Ausgabe: Timestamp + Queue-Name alle paar Minuten
```

**FS.COM über Proxy:**
- FS.COM scraper läuft auf Erik um 02:00 und 14:00 (scheduled via pg-boss)
- Preise landen in `price_observations` Tabelle mit `vendor_id = fs-com`
- Automatische Diagnose: wenn `price` weiterhin NULL → Scraper-eigene Logs prüfen

**Self-healing:**
- pg-boss retryLimit=3: Jobs werden automatisch 3x wiederholt bei Fehler
- PM2 autorestart: Prozess startet automatisch neu wenn er crasht
- WireGuard PersistentKeepalive=25: VPN bleibt auch bei langer Inaktivität aktiv

---

## SCHRITT 9: Zweiter Pi (optional — für mehr IP-Diversität)

Gleiche Prozedur, aber:
```bash
PI_NAME=pi2 WG_ADDR=10.10.0.7 WG_PRIVKEY=<anderer-key> ...
```

Erik's wg0.conf:
```ini
[Peer]
PublicKey = <PI2_PUBKEY>
AllowedIPs = 10.10.0.7/32
PersistentKeepalive = 25
```

`ecosystem.config.js` auf Erik: zweiten `tip-scraper-fs` mit `PROXY_URLS: "socks5://10.10.0.7:1080"` 
für einen weiteren Proxy-Pfad (Round-robin über beide Pis).

---

## Zusammenfassung der neuen Dateien

| Datei | Status |
|-------|--------|
| `packages/scraper/src/index-pi.ts` | ✅ Geändert: fetch-only, Playwright entfernt, NADDOD hinzugefügt |
| `packages/scraper/src/index-fs-only.ts` | 🆕 Erstellen (Schritt 5) |
| `scripts/pi-scraper-setup.sh` | ✅ Geändert: Kein inline index-pi.ts Override mehr |
| `ecosystem.config.js` (auf Erik) | ✅ Ändern: tip-scraper-fs Eintrag hinzufügen |
| Erik `/etc/wireguard/wg0.conf` | ✅ Manuell: Pi-Peer hinzufügen |
| Erik `/etc/postgresql/17/main/pg_hba.conf` | ✅ Manuell: 10.10.0.0/24 Zugang |

## Manuelle Schritte (nicht von Codex ausführbar)

1. **WireGuard Keypair generieren** (Schritt 1) — muss manuell auf sicherem System passieren
2. **Erik wg0.conf bearbeiten** (Schritt 2) — SSH auf Erik nötig
3. **Pi physisch aufsetzen** (Schritt 3) — Hardware + pi-scraper-setup.sh ausführen
4. **DB-Passwort** in setup-Befehl einsetzen (steht in `/opt/tip/.env` auf Erik)
