§ 01

專案概況

IMMI-Case- 是澳洲移民法庭案例的下載、管理與分析平台。從 AustLII 抓取 9 個法院 / 仲裁庭(149,016 筆案件,2000–2026)。提供全文搜尋、法官排行榜、視覺化分析,以及 LLM Council 智能摘要。 主要使用者是自助申請的移民申請人——非法律專業人士,在壓力情境下使用。

品牌定位:權威 · 精準 · 學術。美學方向:「法律典籍」(暖米白 + 深海軍藍 + 琥珀金)。

案件記錄
149K
9 個法院,2000–2026
React 頁面
28
含 auth sprint 新增 LoginPage
Worker 行數
2,889
proxy.js,44 個 handler
judge-leaderboard
383x
13.1s → 0.034s warm
主包瘦身
−51%
460 KB → 225 KB (gzip 146→72)
暖路徑延遲
<55ms
所有分析端點,Cache API 命中
§ 02

架構快照

系統架構圖
flowchart TB subgraph USER["User / Browser"] B["React SPA /app/*"] end subgraph CF["Cloudflare Edge proxy.js"] direction TB CRON["Cron 5min 8 endpoints"] CACHE["Cache API caches.default 19 handlers TTL 120-600s"] HD["Hyperdrive PG pool max-age 10s"] DO["FlaskBackend DO flask-v15 writes+LLM"] AUTH["AuthNonce DO replay protect"] R2["R2 judge-photos"] end subgraph SUPABASE["Supabase PostgreSQL"] DB["immigration_cases 149016 rows"] RLS["Row Level Security multi-tenant JWT"] end subgraph FLASK["Flask Container"] APP["Python 3.14 POST/PUT/DELETE LLM Council"] end B -->|GET /api/v1/*| CF B -->|writes /app/*| CF CRON -->|pre-warm every 5min| CACHE CF -->|cache hit return| CACHE CACHE -->|cache miss| HD HD -->|connection pool| DB DB --> RLS CF -->|writes + LLM| DO DO --> APP APP --> DB CF -->|Telegram Login| AUTH CF -->|photos| R2 style CACHE fill:#d4edd9,stroke:#2d7a57,color:#1e4d3a style CRON fill:#fdf0d5,stroke:#c47a15,color:#7a4a00 style HD fill:#d8eaf5,stroke:#2e5f8a,color:#1e3a5f style CF fill:#f4f2ee,stroke:#4a8ab5 style SUPABASE fill:#f0f7ff,stroke:#b8d4ef
讀路徑(GET): Browser → Worker → Cache API 命中直接回傳;未命中 → Hyperdrive → Supabase。 寫路徑: Worker → FlaskBackend DO → Flask → Supabase。Cron 每 5 分鐘預熱 8 個端點的 Cache API。

三層快取架構

  • L1 · Cache API — per-PoP HTTP 快取,caches.default.match/put,key = Request URL,TTL = Cache-Control max-age。零費用,<1ms 命中延遲。
  • L2 · Hyperdrive — PostgreSQL TCP 連線池 + query-result 快取(max_age=10s)。跨請求保持連線存活,Worker 只傳 connectionString。
  • L3 · Cron Pre-warm — */5 * * * * 主動預熱 L1,讓第一個使用者不用等冷啟動(5 分鐘窗口內)。

技術棧一覽

  • Cloudflare Workers — Edge runtime,nodejs_compat,60s CPU cap
  • React 18 + Vite 6 + TS — 28 頁,lazy code-split,SPA at /app/
  • 🐍 Flask Python 3.14 — 寫路徑,LLM Council,CSV 匯出
  • 🐘 Supabase PostgreSQL — 149K 案件,Row Level Security
  • 🔐 Telegram Login + JWT — HS256,5min access + 7d refresh cookie,multi-tenant RLS
§ 03

近期動態(過去 2 週,50 commits)

Performance · Waves 1–6 效能優化

P0-1 · judge-leaderboard:SQL 索引 + Cache API

根本原因:LATERAL unnest 在 149K 行上展開法官陣列,執行 3 次全表掃描。加索引 7.3x → 再加 Cache API TTL=600s → 合計 383x。

13.12s → 0.034s warm (383x) 88b2d2b

P0-2 · 主包瘦身(i18n chunk 分割)

vite.config.ts manualChunks 從 object 改為 arrow function。i18next + react-i18next + 2x locale JSON(115 KB)拆成獨立 lazy chunk。

460 KB → 225 KB raw (−51%) 146 KB → 72 KB gzip 3ceb05e

P1-3 · Cron Warm-up(*/5 * * * *)

wrangler.toml 加 cron trigger,scheduled() 執行 SELECT 1 保持 Worker isolate + Hyperdrive TCP 連線池存活,消除冷啟動。

冷啟動 ~4.2s → 5min 後不再發生 71b302b

Wave 2 · judges / legal-concepts / visa-families / success-rate

success-rate 冷啟動 8.9s(LATERAL unnest judges 欄位),visa-families 5.5s。四個端點加 Cache API,納入 cron 預熱。

success-rate 278x visa-families 133x 75049d3

Wave 3 · concept-effectiveness / cooccurrence / trends

三個 concept 分析端點,LATERAL unnest legal_concepts 欄位,其中 concept-cooccurrence 冷啟動 13.0s。全部加 Cache API TTL=600s,URL key 含 limit/min_count 參數。

concept-cooccurrence 174x 764ed1c

Wave 4–5 · analytics/filter-options / judge-profile / judge-compare / taxonomy/countries

judge-compare 冷啟動 6.6s(平行 LATERAL unnest 最多 4 法官)、taxonomy/countries 4.3s(GROUP BY country_of_origin 149K 行)。

judge-compare 95x taxonomy/countries 68x 4c24ef6 / e9a2ff5

Wave 6 · Cron 預熱從 4 → 8 個端點

Wave 5 部署後全部端點顯示冷啟動。根本原因:cron 只預熱 4 個端點,curl 打到不同 CF PoP。scheduled() 擴展至 8 個 handler,覆蓋所有高影響分析頁。

8 endpoints pre-warmed every 5min aba5e8a
Auth · Telegram Login + Multi-tenant JWT + RLS

Auth Sprint · 完整 auth 流程

Telegram Widget → Worker HMAC 驗證 → AuthNonce DO replay 防護 → DB upsert user → HS256 JWT(5min access + 7d refresh cookie)→ Supabase RLS 透過 SET LOCAL request.jwt.claims 實現多租戶隔離。

multi-tenant RLS kid rotation ba9fba0

Auth 強化 · 結構化日誌 + UX 硬化 + Sidebar 接入

每個認證 DB 查詢發出 JSON 結構化日誌(kid、tenant_id、user_id、query_ms、ok)。4 個 schema mismatch 修正。Sidebar + Topbar 接入 auth state。

b2fa78d / 6b622bb / f069d1e
Testing · RLS 整合測試 + k6 壓力測試

新增 AC5/7/8/9/10 驗證腳本

tests/integration/rls_isolation.sql(跨租戶查詢驗證)、test_revoke_member.py(成員移除閉環,高風險 race condition 修正)、tests/k6/auth-latency.js(auth 端點壓力測試,thresholds:p95<800ms)。

729c750 / 55374af
§ 04

決策記錄

效能
Cache API 而非 Workers KV 或 Redis
分析結果快取選用 caches.default,TTL 120–600s。
Workers KV:$5/百萬寫入,全球複製 ~60s 一致性延遲。Redis:需獨立部署,RTT 20–50ms。Cache API:零費用,與 CDN 同 PoP,<1ms 命中延遲。對快取失效敏感度低的分析數據,per-PoP 特性無妨。
→ KV 適合全球靜態配置;Redis 適合 pub/sub 或計數器場景。
效能
Cron 5 分鐘(非 1 或 10 分鐘)
*/5 * * * * 作為 Cache API 預熱頻率。
Hyperdrive TCP 連線池空閒逾時 ~3–5 分鐘。最短 TTL 端點 analytics/filter-options=120s。5 分鐘確保:(1) 連線池不斷;(2) 最短 TTL 端點在過期前已被預熱。1 分鐘浪費 DB;10 分鐘連線池可能斷開。
→ 若 TTL 調短至 <5min,cron 頻率需同步調整。
產品
接受 Recharts 413 KB(不換 visx)
P1-4 評估後,owner 決定接受現狀。
用戶原話:「I don't think 200 KB is gonna be a lot to speed up the web app.」Recharts chunk 是 lazy-loaded,gzip 後 120 KB,只在導航到圖表頁時載入。切換 visx 需重寫 24 個元件——投資報酬率極低。
→ 若 Sankey 圖日後被移除,可節省 30–50 KB 再評估。
效能
Hyperdrive max_age 從 60s → 10s
wrangler hyperdrive update 設 caching.max_age=10s,stale_while_revalidate=0s。
前端 setTimeout(invalidate, 10s) workaround 需要寫入後 ≤10s 可見。預設 60s 導致刪除/新增後 UI 不一致。
→ 可回滾:wrangler hyperdrive update <id> --max-age 60 --swr 15
安全
JWT 雙密鑰輪換(kid claim)
JWT_SECRET_CURRENT + JWT_SECRET_PREVIOUS,token 帶 kid。驗證時雙密鑰嘗試。
允許無停機密鑰輪換:部署新密鑰時舊 token 在 5min access TTL 內仍有效。
→ 已知限制:refresh token 無 jti,無法即時撤銷(7 天盲點)。
架構
getSql(env) 每請求創建,絕不快取模組層級
每個 handler 開頭呼叫 getSql(env),不快取為 module-level singleton。
Workers I/O context 綁定於單一請求。模組層級 singleton 跨請求重用 → "Cannot perform I/O on behalf of a different request" 錯誤。Hyperdrive 負責真正的 TCP 連線池。
→ CLAUDE.md 明確列為 Gotcha,任何未來 Worker 開發者必須知道。
§ 05

狀態儀表板

✓ 運行正常
  • 19 個 handler 已加 Cache API,warm <55ms
  • 8 個端點 cron 每 5min 預熱
  • judge-leaderboard:13.1s → 0.034s
  • 主包:460 KB → 225 KB (−51%)
  • Telegram Login + JWT + RLS
  • R2 法官照片 bucket
  • LLM Council(CF AI Gateway)
  • RLS 隔離整合測試通過
  • k6 auth 壓力測試通過
→ 進行中
  • Wave 6 後穩定性觀察中
  • CLAUDE.md 頁數待更新 27→28
  • (無主動 sprint)
⚠ 已接受限制
  • Recharts 413 KB(owner 接受)
  • per-PoP cache:非 APAC 首次冷
  • Federal Court DNS 斷線
  • AATA 2025+ → 改用 ARTA
✗ 已知技術債
  • Refresh token 無 jti,7d 撤銷盲點
  • judge-profile/compare 未加入 cron
  • CLAUDE.md 頁數 27(實際 28)
  • LATERAL unnest 未 pre-materialized
  • lib/api.ts 1304 行(超上限)
§ 06

心智模型精要

§ 07

認知技術債熱點

高風險
Refresh Token 7 天撤銷盲點
無 jti claim,無伺服器端 session 表。被盜的 refresh token 最多 7 天內可持續 mint 新 access token。5min access TTL 限制讀路徑損害;寫路徑每次重新驗證 JWT。
修復:建立 refresh_sessions(jti, user_id, expires_at, revoked_at) 表,refresh token 加 jti claim,AuthNonce DO 或 DB 驗證 jti 未撤銷。
高風險
CLAUDE.md 頁數過時(27 → 實際 28)
Auth sprint 新增了 LoginPage.tsx,但 CLAUDE.md §架構 仍寫 "27 React pages"。P2-5 事實刷新沒有包含這個 auth sprint 的變更。
一行修復:CLAUDE.md "27 React pages" → "28 React pages(含 auth sprint 新增 LoginPage)"。
中等風險
judge-profile / judge-compare 未加入 Cron
兩個端點冷啟動 3.4s / 6.6s(已加 Cache API,warm <70ms)。Wave 6 cron 擴展到 8 個端點,但未包含這兩個。部署後、首個使用者訪問 Judge Detail/Compare 前仍是冷的。
修復:scheduled() 加 handleAnalyticsJudgeProfile + handleAnalyticsJudgeCompare 的預熱呼叫(含典型 name/names 參數)。
中等風險
per-PoP Cache 限制未對開發者透明
Cron 預熱只覆蓋一個 PoP。非 APAC 開發者 / curl 測試可能命中不同 PoP,看到冷啟動而困惑(「為什麼有 cache 還是冷?」)。容易誤診為 cache 失效。
文件化:CLAUDE.md 加「Cache API per-PoP」說明,解釋 cron 覆蓋範圍 + 真實 APAC 用戶的穩定暖路徑。
中等風險
LATERAL unnest 未 Pre-materialized
judges / legal_concepts 在查詢時即時 unnest。Cache API 是目前的緩解,但 TTL 到期後仍需跑一次 3–13s 的昂貴 SQL。每次 cache miss 即是一次對 DB 的重擊。
長期修復:建立 judge_appearances materialized view 或 generated column,定期 refresh。查詢從 O(n×m) 降至 O(n),TTL 可縮短或移除。
低風險
lib/api.ts 1304 行(超 800 行上限)
所有端點 fetch helper 集中在一個檔案。P0-2 bundle 分析時未出現在熱點中,未進行拆分。隨著功能增長(auth、LLM Council、RLS headers),此檔案持續增長。
建議:若出現 bundle 回歸或端點數突破 40+,按 domain 拆分(cases-api.ts、analytics-api.ts、auth-api.ts)。
§ 08

下一步方向

根據近期動態與未閉環事項推斷。非指令,而是「動能指向哪裡」的描述。

安全 · 高優先
Refresh Token 撤銷機制
refresh_sessions 表 + jti claim,完成 auth sprint 最後的已知限制閉環。
效能 · 中優先
Cron 加入 judge-profile/compare
scheduled() 擴展至 10 個端點,覆蓋 JudgeDetail/Compare 頁面,實現 zero-cold-start。
文件 · 低優先
CLAUDE.md 第三輪事實刷新
頁數 27→28,per-PoP Cache 說明,proxy.js 行數 2889,cron 端點數 8。
架構 · 長期
judge_appearances Materialized View
消除 LATERAL unnest 根本原因。所有法官分析端點 cache miss 從 3–13s 降至 <0.5s。
功能 · 待決定
austlii-scraper Worker 整合
workers/austlii-scraper/ 已有 Queue + R2 架構。UI 觸發入口或主 Worker 整合尚未規劃。
可觀測性 · 待決定
Cloudflare Logpush → 可觀測性
getSqlAsUser.js 已有結構化日誌(event/kid/tenant_id/query_ms)。接 Logpush → Grafana 可實現 per-tenant 延遲儀表板。