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

347 lines
11 KiB
Markdown

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