- 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
347 lines
11 KiB
Markdown
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)
|