diff --git a/QUICKSTART.md b/QUICKSTART.md deleted file mode 100644 index f32086e..0000000 --- a/QUICKSTART.md +++ /dev/null @@ -1,129 +0,0 @@ -# QUICKSTART - -從零把整條 pipeline 跑起來。大約 5 分鐘。 - -完整規格看 `README.md`,設計背景看 `SUMMARY.md`。 - -## 需求 - -- [`uv`](https://github.com/astral-sh/uv)(Python 套件管理) -- Docker(用來跑本機 Redis) -- 能連到 Function Server `http://192.168.42.124:13000/function`(內網) -- 有效的 `state.json`(twpkinfo.com 登入快照,放 repo 根目錄) - -## 1. 起本機 Redis - -```bash -docker run -d --name pokemon-radar-redis \ - --restart unless-stopped \ - -p 6379:6379 \ - redis:7-alpine - -docker exec pokemon-radar-redis redis-cli PING # 應回 PONG -``` - -管理指令:`docker stop/start/rm pokemon-radar-redis`、`docker logs -f pokemon-radar-redis`。 - -## 2. 設定 `.env` - -複製或編輯 `.env`,本機跑的最小設定: - -```ini -FUNCTION_URL=http://192.168.42.124:13000/function -FUNCTION_TOKEN=6R0W53R135510 - -SCRIPT_PATH=twpk_radar.mjs -STATE_PATH=state.json -UA_FROM_STATE=true - -RADAR_LAT=25.0478 -RADAR_LNG=121.5170 -RADAR_ZOOM=11 -MIN_REMAINING=300 - -REDIS_HOST=127.0.0.1 -REDIS_PORT=6379 -REDIS_DB=0 -REDIS_PASSWORD= -REDIS_TTL=300 -REDIS_KEY=pokemon:radar:latest -``` - -連正式 Redis(`192.168.42.211`)時把 `REDIS_HOST` 和 `REDIS_PASSWORD` 改回去即可。 - -## 3. 拉一次資料進 Redis - -```bash -uv run python radar_fetcher.py -``` - -成功的 log: - -``` -INFO ENV: RADAR_ZOOM=11, MIN_REMAINING=300, ... -INFO Loaded state: cookies=7, ls=2, ua=N -INFO Calling Function Server: http://192.168.42.124:13000/function -INFO Got 120 results (before filters) -SUCCESS Redis write OK host=127.0.0.1 key=pokemon:radar:latest ttl=300s -``` - -跑一次約 30-60 秒。 - -## 4. 檢查 Redis 內容 - -```bash -uv run python reader.py -``` - -會列出目前 TTL、總筆數,以及 `rich` 表格(名稱 / IV / CP-Lv / 技能 / 剩餘時間 / 座標)。 - -想看 raw JSON 用 `uv run python check_redis.py`。 - -## 5. 起 API - -```bash -./run_pokemon_radar_api.sh -# 或:uv run uvicorn pokemon_radar_api:app --host 0.0.0.0 --port 8008 --log-config logging.ini -``` - -另開一個 shell 測: - -```bash -curl -s http://127.0.0.1:8008/ping -# {"status":"ok","backend":"redis"} - -curl -s -X POST http://127.0.0.1:8008/scan \ - -H "Content-Type: application/json" \ - -d '{"lat":25.0478,"lng":121.5170,"min_iv":90}' -``` - -看到 `"source":"redis"` 且 `count_filtered` > 0 就成功。 - -## 6.(選用)定期自動更新 - -Redis TTL 預設 300s,過期後 API 會回空陣列。要讓資料持續可用,把 fetcher 排成 cron: - -```cron -*/3 * * * * cd /path/to/pokemon-radar-api && /root/.local/bin/uv run python radar_fetcher.py >> /var/log/radar_fetcher.log 2>&1 -``` - -生產環境另有 `fetch_and_upload.sh`,走的是**舊 pipeline**(本地檔 + scp),不經 Redis,見 `SUMMARY.md`。 - ---- - -## 常見問題 - -**`FUNCTION_TOKEN 未設定`** -`.env` 少 `FUNCTION_TOKEN`。 - -**`Redis 連線失敗: Timeout connecting to server`** -Redis 連不到。確認容器在跑(`docker ps | grep redis`),或 `.env` 的 `REDIS_HOST` 是否正確。API 端此時會回 `source: "redis (unreachable)"` 不會 500。 - -**`Got 0 results` 或 `Filtered to 0`** -通常是 `state.json` 過期,網站把你擋下來。重新登入 twpkinfo.com 匯出新的 state。`radar_fetcher` 會印 `remaining_min / remaining_max / perfect` 做診斷;若 `raw_items` 本身就 0,就是 state 問題。 - -**API 回 `source: "redis (empty)"`** -Redis 裡沒資料或 key 已過期。跑 `radar_fetcher.py` 重灌。 - -**`MJS regex 沒套到`(例如座標沒改)** -表示 `twpk_radar.mjs` 裡的 `const TARGET = {...};` 或 `const DO_SCREENSHOT = ...;` 格式被改動。Python 端用 regex 字串替換,形狀要維持。 diff --git a/README.md b/README.md index 32bf70d..56e86cf 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,63 @@ # pokemon-radar-api -把 twpkinfo.com 的寶可夢雷達資料包成 Redis-backed 的 HTTP API,給前端 / n8n 呼叫。 +把 twpkinfo.com 的寶可夢雷達資料,透過 Redis 快取成一支 FastAPI 給前端 / n8n 呼叫。 -- 上手操作:`QUICKSTART.md` -- 設計脈絡與演進:`SUMMARY.md` -- Claude Code 指引:`CLAUDE.md` - -本檔為參考手冊:組件一覽、指令、環境變數、HTTP API、Redis schema。 +這份 README 是給**一年後回來看的你自己**:最常用的指令放最上面,架構與決策放最下面。`CLAUDE.md` 是給 AI 助手用的,可以忽略。 --- -## 組件 +## 最常用的指令 -| 檔案 | 角色 | -|---|---| -| `radar_fetcher.py` | **現行 fetcher**:呼叫 Function Server 抓資料 → 篩選 → 寫入 Redis(`SETEX`,預設 TTL 300s) | -| `pokemon_radar_api.py` | **FastAPI 服務**(`:8008`):`/ping`、`/scan`;從 Redis 讀取,依 request 參數即時二次篩選 | -| `twpk_radar.mjs` | Function Server 上跑的 Puppeteer worker,負責開瀏覽器拉 `twpkinfo.com/ipoke.aspx` 的資料 | -| `pokemon_location_fetcher.py` | **舊版 CLI**:直接呼叫 Function Server,輸出本地 `pokemon_data.{json,csv}` + `screenshot.png`。API 仍重用它的 `PokemonRadarClient.postprocess` 做二次篩選 | -| `pokemon_radar_example.py` | 舊 CLI 的 Python 範例程式(非 CLI argparse 介面) | -| `reader.py` | 從 Redis 讀取並用 `rich` 表格顯示目前快取內容 | -| `check_redis.py` | 從 Redis 讀取並 dump 原始 JSON | -| `fetch_and_upload.sh` | 生產環境 cron:以 `flock` 鎖定,呼叫**舊 CLI**產 `pokemon_data.json` 後 `scp` 到 `192.168.42.104:/opt/caddy/www/pokemon/spots.json` | -| `run_pokemon_radar_api.sh` | 啟動 FastAPI(`uvicorn`,port 8008)的 shell 腳本 | -| `state.json` | twpkinfo.com 登入後的 cookies / localStorage / UA / langs 快照,注入到 MJS 繞人機驗證 | -| `logging.ini` | uvicorn 的 logging 設定 | +全部用 `uv`(Python 3.12)。設定讀 `.env`(不在 git 裡,本機有)。 -## 執行指令 +```bash +# 起本機 Redis(一次性) +docker run -d --name pokemon-radar-redis --restart unless-stopped -p 6379:6379 redis:7-alpine -全部以 `uv` 跑(Python 3.12,見 `.python-version`、`pyproject.toml`)。 +# 跑一次 fetcher,把資料抓進 Redis(TTL 300s) +uv run python radar_fetcher.py -| 目的 | 指令 | -|---|---| -| 啟動 API | `./run_pokemon_radar_api.sh`(= `uv run uvicorn pokemon_radar_api:app --host 0.0.0.0 --port 8008 --log-config logging.ini`) | -| 跑 fetcher(寫入 Redis) | `uv run python radar_fetcher.py` | -| 跑舊 CLI(輸出本地檔案) | `uv run python pokemon_location_fetcher.py --script twpk_radar.mjs --lat 25.0478 --lng 121.5170 --zoom 16 --state state.json --ua-from-state --min-remaining 300 --min-iv 90` | -| 查 Redis 內容(表格) | `uv run python reader.py` | -| 查 Redis 內容(raw JSON) | `uv run python check_redis.py` | -| 生產排程(含 scp 上傳) | `./fetch_and_upload.sh` | +# 看 Redis 目前有什麼(rich 表格) +uv run python reader.py -本專案沒有 test / lint / build 流程。 +# 看 Redis raw JSON +uv run python check_redis.py -## 環境變數(`.env`) +# 起 API +./run_pokemon_radar_api.sh +# 內部執行:uv run uvicorn pokemon_radar_api:app --host 0.0.0.0 --port 8008 --log-config logging.ini -| 變數 | 預設 | 誰用 | 說明 | +# 測 API +curl -s http://127.0.0.1:8008/ping +curl -s -X POST http://127.0.0.1:8008/scan -H "Content-Type: application/json" \ + -d '{"lat":25.0478,"lng":121.5170,"min_iv":90}' +``` + +**生產環境 cron**:`./fetch_and_upload.sh` — 走的是**舊 pipeline**(`pokemon_location_fetcher.py` 直接輸出本地 JSON,`scp` 到 `192.168.42.104` 的 caddy),不經 Redis,不經 API。跟 fetcher/API 這條線沒關係、請不要混淆。 + +沒有 test / lint / build。 + +--- + +## `.env` 速查 + +Secrets 放 `.env`(gitignore)。本機與生產共用同一份格式、只改 `REDIS_HOST` / `REDIS_PASSWORD`。 + +| 變數 | 誰讀 | 預設 | 備註 | |---|---|---|---| -| `FUNCTION_URL` | `http://192.168.42.124:13000/function` | `radar_fetcher` | Function Server endpoint | -| `FUNCTION_TOKEN` | (必填) | `radar_fetcher` | 未設會直接 `raise` | -| `SCRIPT_PATH` | `twpk_radar.mjs` | `radar_fetcher` | JS worker 檔名 | -| `STATE_PATH` | `state.json` | `radar_fetcher` | 登入狀態快照路徑 | -| `UA_FROM_STATE` | `true` | `radar_fetcher` | 是否從 state 帶 UA / langs | -| `RADAR_LAT` / `RADAR_LNG` | `25.0478` / `121.5170` | `radar_fetcher` | 掃描中心座標 | -| `RADAR_ZOOM` | `11` | `radar_fetcher` | 地圖縮放 | -| `RADAR_SHOT` | `false` | `radar_fetcher` | 是否擷取畫面(base64) | -| `MIN_REMAINING` | `300` | `radar_fetcher` | 剩餘秒數門檻 | -| `MIN_IV` | 空 | `radar_fetcher` | IV% 下限 | -| `PERFECT_ONLY` | `false` | `radar_fetcher` | 只保留 100% IV | -| `SPECIES` | 空 | `radar_fetcher` | 逗號分隔的 dex id 或名稱關鍵字(例:`133,伊布`) | -| `HTTP_TIMEOUT` | `160` | `radar_fetcher` | 呼叫 Function Server 的 HTTP timeout(秒) | -| `REDIS_HOST` | `localhost`(fetcher)/ `192.168.42.211`(API、reader) | 全部 | 注意兩邊預設不同,統一在 `.env` 指定 | -| `REDIS_PORT` | `6379` | 全部 | | -| `REDIS_DB` | `0` | 全部 | | -| `REDIS_PASSWORD` | 空 | 全部 | 空字串視為無密碼 | -| `REDIS_TTL` | `300` | `radar_fetcher` | 寫入 TTL | -| `REDIS_KEY` | `pokemon:radar:latest` | 全部 | | +| `FUNCTION_URL` | fetcher / 舊 CLI | `http://192.168.42.124:13000/function` | Function Server endpoint | +| `FUNCTION_TOKEN` | fetcher / 舊 CLI | (必填) | 空字串 fetcher 會 `raise` | +| `SCRIPT_PATH` | fetcher | `twpk_radar.mjs` | JS worker 檔名 | +| `STATE_PATH` | fetcher | `state.json` | twpkinfo.com 登入快照,**最常壞的地方** | +| `UA_FROM_STATE` | fetcher | `true` | 是否從 state 帶 UA / langs | +| `RADAR_LAT` / `RADAR_LNG` / `RADAR_ZOOM` | fetcher | `25.0478` / `121.5170` / `11` | 掃描中心 | +| `MIN_REMAINING` | fetcher | `300` | 寫入前先做一輪粗篩,低於此值不進 Redis | +| `MIN_IV` / `PERFECT_ONLY` / `SPECIES` | fetcher | 空 / `false` / 空 | fetcher 端篩選(通常留空,由 API 端即時篩) | +| `REDIS_HOST` / `REDIS_PORT` / `REDIS_DB` / `REDIS_PASSWORD` | 全部 | 本機 `127.0.0.1` / 生產 `192.168.42.211` | 預設本機無密碼 | +| `REDIS_TTL` | fetcher | `300` | 寫入 TTL | +| `REDIS_KEY` | 全部 | `pokemon:radar:latest` | | + +--- ## HTTP API @@ -73,50 +67,100 @@ ``` ### `POST /scan` -從 Redis 讀取目前快取,依 body 篩選後回傳。**永遠回 200**,Redis 連不上或空資料都回空陣列,不會 500。 +從 Redis 讀取 → 依 body 參數即時二次篩選 → 回傳。**永遠回 200**(Redis 空或不可用都回空陣列)。 -**Request body** - -| 欄位 | 型別 | 說明 | -|---|---|---| -| `lat`, `lng`, `zoom`, `shot`, `state_path`, `ua_from_state`, `save_files` | — | 相容性欄位,Redis 模式下**忽略** | -| `min_remaining` | `int ≥ 0` | 剩餘秒數下限 | -| `min_iv` | `int 0-100` | IV% 下限 | -| `perfect_only` | `bool` | 只保留 100% IV | -| `species` | `str` | 逗號分隔 dex id 或名稱關鍵字 | - -**Response** +**Body** | 欄位 | 說明 | |---|---| -| `source` | `"redis"` / `"redis (empty)"` / `"redis (unreachable)"` | -| `ttl` | Redis key 剩餘秒數 | -| `count_raw` | Redis 內筆數 | -| `count_filtered` | 經 request 參數篩選後筆數 | -| `items` | 物件陣列,見下 | -| `debug` | 含 `notes`(MJS 輸出)、`redis_ttl`、`note`(錯誤訊息) | +| `min_remaining` (int) | 剩餘秒數下限 | +| `min_iv` (int 0-100) | IV% 下限 | +| `perfect_only` (bool) | 只保留 100% IV | +| `species` (str) | 逗號分隔 dex id 或名稱關鍵字,如 `"133,伊布"` | +| `lat / lng / zoom / shot / state_path / ua_from_state / save_files` | 相容性欄位,**Redis 模式下忽略** | -**`items[]` 欄位** -`id / name / gender / remaining / expire_time / expire_at / latitude / longitude / iv_pct / iv_atk / iv_def / iv_sta / cp / level / is_perfect / fast_move / charge_move` - -## Redis schema - -| 項目 | 值 | -|---|---| -| Key | `pokemon:radar:latest` | -| 型別 | String(JSON) | -| TTL | `REDIS_TTL`(預設 300s) | -| Writer | `radar_fetcher.py` | -| Readers | `pokemon_radar_api.py`、`reader.py`、`check_redis.py` | - -**Value 結構** +**Response** ```json { - "source": "fetcher", - "generated_at": "2026-04-20T09:06:36+08:00", - "count_raw": 120, - "count_filtered": 79, - "items": [ /* 同上方 items[] */ ], - "debug": { "notes": ["..."], "api_source": "Fresh from Function Server" } + "source": "redis" | "redis (empty)" | "redis (unreachable)", + "ttl": 281, + "count_raw": 79, + "count_filtered": 24, + "items": [ /* id, name, gender, iv_pct, cp, level, latitude, longitude, + remaining, expire_at, fast_move, charge_move, is_perfect, ... */ ], + "debug": { "notes": [...], "redis_ttl": 281 } } ``` + +### Redis schema +Key `pokemon:radar:latest`,String(JSON),TTL 由 `REDIS_TTL` 控制。Value 結構: +```json +{ "source":"fetcher", "generated_at":"2026-04-20T09:06:36+08:00", + "count_raw":120, "count_filtered":79, + "items":[...], "debug":{ "notes":[...] } } +``` + +--- + +## 架構(一年後回來看用) + +### 資料流 + +``` +twpkinfo.com/ipoke.aspx + ▲ + │ Puppeteer (stealth + 等黑影消退 + 解析 DOM) + │ +Function Server (http://192.168.42.124:13000/function) + ▲ + │ POST { code: <注入後的 twpk_radar.mjs> } + │ +┌─────┴─────────────────────────┐ +│ │ +│ A) radar_fetcher.py │ B) pokemon_location_fetcher.py(舊 CLI) +│ → Redis (SETEX, 300s) │ → pokemon_data.{json,csv} + screenshot.png +│ │ → fetch_and_upload.sh 跑這條,scp 給 caddy +└─────┬─────────────────────────┘ + ▼ +pokemon_radar_api.py :8008 + POST /scan → 從 Redis 讀,依 request 即時二次篩選 +``` + +### 非看不可的設計決策 + +- **為什麼要 Redis**:v1 是每個 `/scan` 都打 Function Server,一次 browser automation 要 30-60s,多人同時打直接壞。拆成 writer(fetcher 定期 cron)+ reader(API 讀快取)後壓力從「每 request」降到「每 TTL」。 +- **為什麼兩條 pipeline 並存**:caddy 前端走靜態 JSON(舊 CLI 的產物),沒理由為它引 Redis。所以 A 走 Redis 給 API,B 走本地檔給靜態前端,**兩條都還在跑**。 +- **`twpk_radar.mjs` 靠 regex 注入參數**:Function Server 的契約是「你丟 JS 過來我跑」,所以參數化只能在字串層做。三個錨點別改形狀: + - `const TARGET = { lat: ..., lng: ..., zoom: ... };` + - `const DO_SCREENSHOT = true|false;` + - `globalThis.__POKE_STATE__ = ...;` + 動到形狀 regex 會靜默失敗、用錯參數跑。 +- **`pokemon_radar_api.py` 重用舊 CLI 的 postprocess**:為了省一份篩選邏輯,API import `PokemonRadarClient` 但傳空 url/token,只用它的 `postprocess` 方法。這是 workaround,理想做法是把篩選抽成獨立模組,兩份 fetcher 共用。 +- **fetcher 端先做一輪粗篩再寫 Redis**(`MIN_REMAINING` 等),API 端再做第二輪 per-request 篩選。Redis 裡不是「原始資料」而是「粗篩後資料」,如果要在 API 端篩出 fetcher 濾掉的東西是做不到的。 + +### 常見故障(依機率排序) + +1. **`state.json` 過期** → Function Server 回 200 但 results 是空 / 登入頁 DOM,pipeline 不會炸只會吐 0 筆。`radar_fetcher.py` 有診斷:`raw_items` 非空但篩完為 0 時 log `remaining_min / remaining_max / perfect` 分佈。重登 twpkinfo.com 匯出新 state。 +2. **Redis 連不上** → API 回 `source:"redis (unreachable)"`(不會 500)、`reader.py` 回 `Timeout connecting to server`。確認 docker container / `.env` 的 `REDIS_HOST`。 +3. **`FUNCTION_TOKEN` 未設定** → fetcher `raise RuntimeError`。檢查 `.env`。 +4. **`/scan` 回空但 Redis 其實有資料** → body 的篩選條件太嚴,或 fetcher 的 `MIN_REMAINING` 已把你要的過濾掉了。 + +--- + +## 組件索引 + +| 檔案 | 角色 | +|---|---| +| `radar_fetcher.py` | 現行 fetcher,寫入 Redis | +| `pokemon_radar_api.py` | FastAPI,讀 Redis | +| `twpk_radar.mjs` | Function Server 上跑的 Puppeteer worker | +| `pokemon_location_fetcher.py` | 舊 CLI,`fetch_and_upload.sh` 還在用 | +| `pokemon_radar_example.py` | 舊 CLI 的 Python 呼叫範例 | +| `reader.py` | Redis 內容表格檢視 | +| `check_redis.py` | Redis raw JSON dump | +| `fetch_and_upload.sh` | 生產 cron:舊 CLI → scp 到 caddy | +| `run_pokemon_radar_api.sh` | 啟動 uvicorn | +| `state.json` | twpkinfo.com 登入快照(gitignore) | +| `.env` | 所有 secret(gitignore) | +| `logging.ini` | uvicorn logging | +| `CLAUDE.md` | 給 AI 助手的指引 | diff --git a/SUMMARY.md b/SUMMARY.md deleted file mode 100644 index b4adb4c..0000000 --- a/SUMMARY.md +++ /dev/null @@ -1,92 +0,0 @@ -# SUMMARY - -這份文件講**為什麼長這樣**:設計脈絡、演進、取捨、目前還沒處理乾淨的地方。要查指令看 `README.md`,要跑起來看 `QUICKSTART.md`。 - -## 一句話 - -把一個只能「給座標、即時回寶可夢清單」的網站(twpkinfo.com),變成「前端 / n8n 能隨時打 API 拿最新資料」的服務。 - -## 為什麼需要這個專案 - -twpkinfo.com 的雷達頁(`/ipoke.aspx`)有完整的物種 / IV / CP / 技能 / 座標資訊,但: - -1. 沒有 API,資料只在網頁 DOM 裡 -2. 有人機驗證與前端黑影遮罩,直接 `curl` 拿不到 -3. 前端(地圖標點)和 n8n(通知高 IV 寶可夢)都需要結構化資料 - -所以這個專案做兩件事:**把網頁變成 JSON**、**把 JSON 包成 API**。 - -## 架構演進 - -這條 pipeline 不是一次設計出來的,是三個階段堆疊出來的,中間每個階段的產物都還在 repo 裡。 - -### 第一階段:CLI 一把梭 - -`pokemon_location_fetcher.py` + `twpk_radar.mjs` + `fetch_and_upload.sh`。 - -- `twpk_radar.mjs` 送到 Function Server(一台 `browserless` / 類似的 browser automation 服務)執行,繞 stealth、等黑影消退、解析資料回傳 JSON -- `pokemon_location_fetcher.py` 是 Python CLI,讀 MJS、用 **regex 注入座標和 state.json**、POST 到 Function Server、對回來的資料做 IV%/remaining/species 篩選,最後輸出 `pokemon_data.json` + `pokemon_data.csv` + `screenshot.png` -- `fetch_and_upload.sh` 用 `flock` 避免重入,跑完後 `scp` 把 JSON 丟到另一台 caddy 主機,由前端靜態讀取 - -**為什麼 regex 注入**:Function Server 的契約是「你丟一段 JS 過來,我幫你跑在無頭瀏覽器」。參數化只能在字串層做 — 所以 `twpk_radar.mjs` 裡的 `const TARGET = {...};`、`const DO_SCREENSHOT = ...;`、`globalThis.__POKE_STATE__ = ...;` 三個錨點變成 Python / JS 之間的介面。改 MJS 時若動到這些行的形狀,Python regex 會失效但不會報錯,只會靜默用錯誤參數跑。 - -這階段到今天仍在生產跑,因為 caddy 前端是靜態檔,沒理由為了它多引入 Redis。 - -### 第二階段:FastAPI v1(已不存在) - -`pokemon_radar_api.py_BACKUP_20251208225502`(已清除)是第一版 API:每個 `/scan` request 都呼叫一次 Function Server。 - -問題: - -- Function Server 單次 browser automation 要 30-60 秒,前端打一次就等一次 -- 多人同時打會超過 Function Server 的 concurrency -- 資料源本身是頁面抓取,3 秒打一次跟 3 分鐘打一次拿到的結果幾乎一樣(寶可夢生成間隔長) - -### 第三階段:Redis backed API(現行) - -把 fetch 和 serve 拆開: - -- `radar_fetcher.py` 當 writer,cron 定期跑,結果 `SETEX` 到 Redis,TTL 300s -- `pokemon_radar_api.py` 當 reader,每個 request 從 Redis 讀快取資料,依 request 參數(`min_iv` / `perfect_only` / `species`)做二次篩選後回傳 -- API 的 `ScanRequest` 保留了 `lat / lng / zoom / shot` 欄位**但忽略它們**,純粹相容舊前端不必改 client - -Reader 端沒有 breaking change、Writer 端可以獨立調速,Function Server 壓力從「每 request 一次」降到「每 TTL 一次」。 - -## 目前還沒處理乾淨的地方 - -### 兩條 pipeline 共存 - -`radar_fetcher.py`(新)和 `pokemon_location_fetcher.py`(舊)都能抓資料,**篩選邏輯各寫了一份**(`_postprocess` vs `PokemonRadarClient.postprocess`)。修改篩選規則要兩邊同步改。 - -合併不是單純 refactor:舊 CLI 還在 `fetch_and_upload.sh` 裡給前端生靜態檔,短期不會下掉。 - -### API 重用舊 client 的 postprocess - -`pokemon_radar_api.py` 為了不重寫一次篩選,`import PokemonRadarClient` 然後 `_filter_helper = PokemonRadarClient(url="", token="")`,只用它的 `postprocess`。這個 workaround 讓新 API 與舊 CLI 有了非必要的耦合。長期方向應該把 `_postprocess` 抽成獨立模組,兩邊共用。 - -### `.env` 的環境耦合 - -`radar_fetcher.py` 預設 `REDIS_HOST=localhost`,`pokemon_radar_api.py` 和 `reader.py` 預設 `192.168.42.211` — 因為 fetcher 歷史上跑在 Redis 本機,API 跑在遠端。實務上**一定要透過 `.env` 顯式設定**,不要依賴程式內預設。 - -### `state.json` 的脆弱性 - -整條 pipeline 最常壞的點是 state 過期。過期時 Function Server 會正常 200,但回來的資料是空陣列或登入頁 DOM — pipeline 不會炸,只會靜默吐 0 筆。`radar_fetcher._postprocess` 有個診斷分支:當 `raw_items` 非空但篩完為 0 時會 log `remaining_min / remaining_max / perfect` 分佈,優先看這個判斷是過濾太嚴還是資料本身有問題。 - -## 最近處理過的問題 - -### `/scan` 在 Redis 不可用時會 500 - -`get_data_from_redis()` 原本只 `try/except json.JSONDecodeError`,沒處理 `redis.exceptions.RedisError`。結果: -- Redis key 不存在 → 照原本設計回空陣列(OK) -- Redis 連不上 → 例外沒接,FastAPI 直接 500 - -修法:`get_data_from_redis()` 改回傳 `(data, ttl, err)` 三元組,整段 Redis 操作包 `try/except RedisError`;`scan()` 依 `err` 切換 `source` 為 `redis (unreachable)` 並把錯誤訊息放進 `debug.note`。三種情況(連線成功、key 不存在、連不上)都回 200,前端不會炸。 - -### 備份檔清理 - -repo 曾保留多份 `*_BACKUP_` 檔案當作版本保險,但有 git 就沒必要。已清除。 - -## 執行環境現況 - -- **本機開發**:Docker 起 `redis:7-alpine` 在 `127.0.0.1:6379`,`.env` 指本機、無密碼。Function Server 走 VPN 或同網段連 `192.168.42.124`。 -- **生產**:Redis 在 `192.168.42.211`(有密碼)、API 在某台 8008 port、`fetch_and_upload.sh` 由 cron 觸發並 scp 到 `192.168.42.104` 的 caddy。兩條 pipeline 並存。