# 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 = 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= \ DB_NAME=transceiver_db \ WG_PRIVKEY= \ 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: "", 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): Promise { 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= ... ``` Erik's wg0.conf: ```ini [Peer] PublicKey = 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)