Skip to content

Maintenance

Hsiehting Lin edited this page Jul 29, 2026 · 2 revisions

維運手冊

日常維護這套系統需要知道的事。詳細的初次部署流程見 repo 內 CLAUDE.md("setup / deploy" 一節)與 README.md

本地開發

./scripts/setup.sh                    # 首次:互動式產生 config.toml / wrangler.toml / .env
pnpm install
cd frontend && pnpm install && cd ..
pnpm db:migrate:local                 # 本地 D1 schema + 樣本資料
pnpm dev                              # terminal A:wrangler dev (:8787)
cd frontend && pnpm dev               # terminal B:vite (:5173)
  • Vite proxy 會把 /api/img 轉到 :8787 並注入 X-Dev-Email.dev.varsCF_ACCESS_TEAM_DOMAIN=localhost 啟用 Access bypass。
  • 改本機登入 email:config.toml [dev] dev_email
  • 常見坑:port 8787 被占走。若本地所有 API 回 500/404,先查是不是 OpenEvidence MCP relay daemon 佔了 8787,不是程式碼問題。
  • 本地 D1 在 .wrangler/state/v3/d1/;砍掉重來:rip .wrangler/state

部署

./scripts/deploy.sh                               # D1 / R2 / Worker / Pages 一鍵,idempotent
node --experimental-strip-types scripts/sync-access.ts   # Access app + policy + Worker secrets + users 種子
./scripts/setup-public-bypass.sh                  # landing / OG / favicon 等 path-scoped bypass

.env 需要 CF_API_TOKENCF_ACCOUNT_IDPAGES_DOMAINADMIN_EMAILSROSTER_CSV_URL(範本見 .env.example)。部署失敗多半是 API token 缺 scope、zone 不在帳號下、或資源名衝突——讀錯誤訊息,不要跳步。

兩個部署前必check

# 1. wrangler 只認 CLOUDFLARE_API_TOKEN,.env 用的是舊名 CF_API_TOKEN
#    少這行會掛在「Failed to fetch auth token: 400」,看起來像 token 過期
export CLOUDFLARE_API_TOKEN="$CF_API_TOKEN" CLOUDFLARE_ACCOUNT_ID="$CF_ACCOUNT_ID"

# 2. 部署後確認 Pages 上的是 Production 不是 preview(頂列必須是 Production │ main)
wrangler pages deployment list --project-name hema-2026 | head -4

第 2 點特別容易在 worktree 裡踩到:wrangler pages deploy 從 branch name 推環境,而 worktree 永遠不在 main 上。Worker 不看 branch 照樣上線,結果是「新 Worker + 舊前端」,症狀看起來像快取問題。要嘛從主工作區的 main 部署,要嘛給 Pages 那步加 --branch main

資料庫

wrangler d1 migrations create <db> <name>   # 新增 migration(絕不改已套用的檔)
pnpm db:migrate:local                       # 先本地測
pnpm db:migrate:remote                      # 再上 prod
pnpm db:pull                                # 鏡像 remote → local(單向,local 會被覆蓋)

檢視資料:wrangler d1 execute hema-2026-db --local --command "SELECT ..."(prod 加 --remote)。

題庫匯入

CSV 格式:year,number,group,stem,option_a..e,answer,tags,difficulty,sourceyear 用民國(104–114),group ∈ {內科, 共同}。

node --experimental-strip-types scripts/import-questions.ts ./questions.csv          # prod
node --experimental-strip-types scripts/import-questions.ts ./questions.csv --local  # 本地
  • Pre-flight 驗證整批(年份範圍、1–70 內科 / 71–100 共同、(year,number) 唯一、answer 對應存在的選項),任一筆失敗整批拒絕。
  • 重要:importer 不會覆蓋社群已修訂的答案(answer challenge 升級過的題目)。這是修過的 bug,改 importer 時務必保住這個行為。

名單(roster)與 Access

  • 名單來源是 Google Sheet 發佈的 CSV(ROSTER_CSV_URL),email 欄位目前寫死在 column index 3(scripts/sync-access.ts:194)。
  • 每日 cron 會自動同步 roster → Access policy + D1 users;手動跑:pnpm sync-users
  • 新成員上線後即出現在 @mention picker(sync 會預先 seed users 列)。

講義(複習班 PDF)

pnpm import:lectures        # PDF → R2 + lecture_docs 登錄
pnpm seed:lecture-notes     # 預設頁面筆記

除錯

需求 指令 / 位置
Prod Worker log pnpm tail(wrangler tail)
Pages log Dashboard → Pages → 專案 → Functions
單元測試 pnpm testnode --test,涵蓋 worker 與前端的純函式;整合層無測試,見 技術債 第 1 項)
型別檢查 pnpm typecheck
跨瀏覽器驗證 見下節——改前端一定要跑,Chromium 過不代表 Safari 過
本地 DB 檢視 wrangler d1 execute <db> --local --command ...

跨瀏覽器驗證(改前端必做)

2026-07-29 之前,所有 iOS 使用者的題目頁都是白畫面,而沒有任何一道防線發現——因為驗證一直只跑 Chromium。iOS 強制所有瀏覽器使用 WebKit,所以「在 Chrome 上好好的」完全不構成證據(成因見 踩過的坑 十四)。

pnpm add -D playwright && pnpm exec playwright install webkit

要點:

  • 正式建置產物,不是 dev server。dev 有 React StrictMode 雙掛載,訊號不一樣(會多冒 Adding different instances of a keyed plugin,那是 StrictMode 的產物不是根因)。
  • deploy.sh 實際產出的那份 dist。worktree 與主工作區的 node_modules 解析版本可能不同,同一份原始碼會建出不同 bundle。
  • devices['iPhone 13'],並監聽 pageerror——白屏的特徵是 document.body.innerText.length === 0 且有一筆 pageerror。
const b = await webkit.launch()
const page = await (await b.newContext(devices['iPhone 13'])).newPage()
const errs = []
page.on('pageerror', e => errs.push(e.message))
await page.goto(`${BASE}/q/113-050`, { waitUntil: 'load' })
await page.waitForTimeout(3500)
// 斷言:innerText.length > 0 且 errs.length === 0

本機要同時跑 API:起 wrangler dev,再用一支靜態伺服器服務 dist 並把 /api/img 代理過去(注入 X-Dev-Email)。

教學影片策展

影片掛在主題上不掛題目(video_topicstag_topicsquestion_tags),所以補 tag 就會自動生效,不必重跑策展。主題白名單在 scripts/video-topics.json

python3 scripts/curate-videos.py search              # yt-dlp 搜尋 + 過濾 → 候選(有快取,可中斷續跑)
python3 scripts/score-batches.py split               # 切批次交給 Claude Code 派 Haiku 評分
python3 scripts/score-batches.py merge               # 合併評分(需 opencc 做繁體化)
python3 scripts/curate-videos.py publish --remote    # 每主題取前 8 → 縮圖進 R2 → 寫 D1
python3 scripts/curate-videos.py refresh --remote    # 只重抓 metadata,下架的標 status='dead'
  • 快取在 scripts/data/(gitignored)。留著它,重跑或調門檻不必再打一次 YouTube(約 30 分鐘)。
  • publish 重跑不會復活使用者刪掉的影片(status 刻意不在 upsert 的更新欄位裡)。
  • 沒有 YouTube API key,走 yt-dlp。它哪天被 YouTube 反制就會壞,但只影響這支離線腳本,站上資料不受影響。

安全底線(動code前先讀)

  • R2 bucket 永不公開;圖片/PDF 一律走 Worker proxy。
  • 不加 app 層 auth;身分只來自 Access JWT 的 email。
  • DB 只存 TipTap JSON,渲染只走 read-only TipTap。
  • 錯誤回應不外洩內部細節(已修過 error-detail leakage,維持住)。

Clone this wiki locally