transceiver-db/CODEX-TASK-pi-scraper-deploy.md
Rene Fichtmueller 0edc6e3f3a feat: Pi scraper fleet — fetch-only index-pi.ts + FS.COM/NADDOD via SOCKS5
- index-pi.ts: removed Playwright scrapers (FS.COM, eBay enricher, switch assets)
  added NADDOD (fetch-based, benefits from residential IP)
  now 32 fetch-only queues safe for ARM/Pi without Chromium
- index-fs-only.ts: new dedicated FS.COM + NADDOD worker for Erik
  routes through Pi SOCKS5 via PROXY_URLS=socks5://10.10.0.6:1080
  Crawlee ProxyConfiguration automatically applies to Playwright crawler
- pi-scraper-setup.sh: removed inline index-pi.ts override (repo version now authoritative)
- CODEX-TASK-pi-scraper-deploy.md: full 9-step Codex spec for Pi fleet setup
  covers WireGuard keypair, Erik peer config, setup script, ecosystem.config.js
- CODEX-TASK-zero-manual-review.md: deterministic equivalence matcher spec
2026-05-10 09:53:55 +02:00

11 KiB

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

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

ssh erik-cf

Datei bearbeiten: /etc/wireguard/wg0.conf

Füge am Ende hinzu:

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

sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
# oder bei komplettem Reload:
sudo systemctl restart wg-quick@wg0

Verify:

sudo wg show wg0   # muss den Pi-Peer als "Peer:" zeigen

PostgreSQL Firewall auf Erik — Pi-IP erlauben:

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

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

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

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

{
  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

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

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:

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

PI_NAME=pi2 WG_ADDR=10.10.0.7 WG_PRIVKEY=<anderer-key> ...

Erik's wg0.conf:

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