chore(ratewise): 生產治理 — PWA precache-first、ETag 修復、SEO 治理#411
Conversation
- 修正測試檔動態 import type annotation 警告 - 新增生產治理 spec 與 implementation plan - 恢復 RateWise lint 0 warning 品質閘門 測試:pnpm --filter @app/ratewise lint;pnpm --filter @app/ratewise typecheck
- 將內部展示與測試頁排除於 production route surface - 補 route surface 測試,避免 prerender 與 app shell 漂移 - 同步 TypeScript / Node SEO path SSOT,並新增 changeset 測試:route surface、seo public surface、prerender、typecheck、build
- 將 unhandled rejection 改為集中分類 - 限制 production hydration suppression,避免遮蔽真錯誤 - 補 chunk、history 404 與 generic fetch 分類測試 測試:errorClassification、chunkLoadRecovery、sw、pwa recovery、typecheck
- 將核心無障礙與離線體驗納入可執行驗證 - 加入 scheduled production governance workflow - 將趨勢圖 latency budget 改為明確手動與排程 gate 測試:accessibility e2e、offline indicator e2e、trend latency e2e、typecheck、workflow prettier
- 拆分 live data、deterministic generation 與 artifact verify scripts - 移除本機 Lighthouse 與 tsbuildinfo 產物追蹤 - 同步 README、AGENTS 與 CLAUDE 的 artifact policy 測試:build-scripts vitest、verify:artifacts、generate:deterministic、typecheck、prettier
- 將幣別落地頁 route registration 收斂到 registry - 補齊 SEO paths、page entry 與 routes 消費的 parity tests - 同步 production route surface 測試期望與稽核紀錄 測試:currency route registry vitest、seo paths vitest、route surface vitest、verify:ssot、typecheck
- 將 color-scheme 測試改為確認 production 不輸出 internal-only HTML - 同步 002 稽核紀錄 測試:prerender vitest、pnpm test、build:ratewise
- prebuild 不再刷新 tracked live rate data - refresh:fallback-rates 更新 build-time fallback snapshot - runtime fallback 改讀 build-time snapshot SSOT 測試:pnpm test;typecheck;build;format;diff check
- 多幣別三態切換改用 pair-level availability - 修正換錢所反向 TWD 與 spot-only label 漂移 測試:focused MultiConverter/useCurrencyConverter vitest;ratewise typecheck - pnpm format;git diff --check;pnpm test;pnpm --filter @app/ratewise build
- 提升多幣別匯率資訊文字對比 - 讓主要滾動區域可鍵盤聚焦 - 補上 AppLayout 回歸測試並同步 changeset 與 002 紀錄 測試:pnpm --filter @app/ratewise typecheck 測試:pnpm --filter @app/ratewise exec vitest run AppLayout.safe-area.test.tsx MultiConverter.test.tsx 測試:pnpm --filter @app/ratewise build 測試:Playwright 多幣別 accessibility desktop/mobile 測試:pnpm format;git diff --check
- 修正 GA4 E2E 對建置 runtime 的誤判 - 修正 Firefox offline 測試呼叫 Chromium-only CDP 的失敗 測試:pnpm --filter @app/ratewise test:e2e;pnpm --filter @app/ratewise typecheck
- 線上遠端匯率來源全失敗且無快取時改用 build-time snapshot - 更新 RateWise 與 exchangeRateService 回歸測試 測試:pnpm --filter @app/ratewise test;pnpm --filter @app/ratewise typecheck
- SSG 金額頁注入改讀 build-time fallback snapshot - 補上 clean checkout 與 prerender 回歸測試保護 測試:vitest build-scripts/prerender;typecheck;build(無 public/rates.json)
- 更新 build-time fallback snapshot 至最新台銀快照 - 修正刷新腳本說明與幣別數量輸出 測試:build-scripts vitest;typecheck;verify-ssot-sync;git diff --check
- 移除每日匯率資料 PR 的 workflow 直接合併步驟 - 改由 branch protection 與 GitHub 原生合併控制接手 - 補上 workflow 回歸測試與治理文件 測試:build-scripts vitest;typecheck;verify-ssot-sync;git diff --check
- 補 Firefox NetworkError 訊息匹配,避免落入 unknown 觀測桶
- 補 Safari NSURLError 系列訊息(offline / network lost / hostname not found / cannot connect)匹配
- 補 Safari TypeError("Load failed") 仍走 chunk-load 的迴歸測試,避免新 pattern 搶走 chunk recovery
- 修正 prebuild-fetch-rates cache 分支幣別數量輸出單位,與成功路徑一致
- 同步更新 002 SSOT 紀錄與 changeset
測試:vitest errorClassification(11 passed);pnpm -r typecheck;prettier --check;verify-ssot-sync
- Sentry `beforeSend` 改用 `classifyUnhandledRejection`,避免兩處重複維護 fetch 字面比對 - Firefox / Safari 多種網路失敗訊息原本繞過 Sentry 過濾,現與跨瀏覽器 SSOT 一致 - 同步更新 002 SSOT 紀錄與 changeset 測試:vitest 29 passed;typecheck;Playwright AB 7 cases(6 fetch + 1 control)全綠
- client 加 aggregate-first:與 PROVIDER_RATES_PATH.aggregate 對齊台銀 history-30d 慣例 - 5 分鐘 memory cache 涵蓋 aggregate 命中與 fallback 兩條路徑,避免重複 fetch - aggregate 不存在時自動退回原本逐日 fetch,行為無回歸 - 4 個單測覆蓋命中 / 404 fallback / shape 不合法 / TTL cache 測試:moneyboxRateService 20 passed;typecheck;Playwright AB 50 reqs / ~5049ms → 1 req / ~2ms
- 移除說明性註解,改由函式名與結構自說明 - 抽出 fetchExchangeShopHistoricalRatesFromDailyEndpoints 與 aggregate 路徑對稱 - 兩處 historyAggregateCache 寫入收斂為單一寫入點 測試:moneyboxRateService 35 passed;typecheck
- Save snapshot 後讀本地 history/*.json 產生 history-30d.json(providerId/generatedAt/snapshots) - git add、CDN purge、post-push refresh 同步加入 aggregate file - 對應 client 端 fetchExchangeShopHistoricalRatesRange 的 aggregate-first 路徑 測試:本地 dry-run 3 fixture days 產生符合 client 預期 shape 的 aggregate JSON
- 建立 RateWise DESIGN/PRODUCT/SPEC,完成頁面盤點 - 收斂麵包屑、內容頁、App shell、通知與 PWA token - 刪除未使用 UI 死碼並補齊守門測試 測試:ratewise Vitest 全量通過 測試:ratewise typecheck、SSOT sync、build 通過 測試:Chrome /seo-tech console 0 error / 0 warning
- 恢復 Tailwind 標準 radius 預設值(lg/md/sm/xl),僅新增語義 token(card/panel/control/icon/compact)作為擴充 - 全站 UI 元件遷移至語義 radius token:rounded-card(主面板)、rounded-control(互動元素)、rounded-compact(密集元素) - 新增語義 shadow token 系統(shadow-soft/floating/brand/card/card-hover),取代散落的 Tailwind 預設 shadow - SingleConverter CTA hover 升級至 shadow-brand、SwapButton hover 升級至 shadow-card,修正互動層級回饋 - 品牌色從 Blue 遷移至 Violet/Indigo(Zen 主題),同步 themes.ts/index.css/DESIGN.md - 修正 classnames.ts 計算機按鍵 rounded-lg → rounded-compact(8px),rounded-t-lg → rounded-t-control - SkeletonLoader timeout 回退卡 rounded-control → rounded-card(對齊 feedbackSurfaceTokens 語義) - 同步 DESIGN.md radius 規格與 design-tokens.test.ts 守門測試 測試:typecheck 通過、vitest 2382 tests / 141 files 全綠 Co-authored-by: Cursor <cursoragent@cursor.com>
- PWA shell(index.html/offline.html/manifest/pwaOfflineFallback)品牌色同步至 Violet 主題 - 緊急離線卡片圓角與陰影對齊語義 token(rounded-card / shadow-card) - CDN 備援失敗日誌從 warn 降為 debug,減少正常 fallback 場景的噪音 - moneybox workflow 補齊 aggregate 檔案不存在時的初始化觸發邏輯 - AGENTS.md 新增使用者偏好與工作區事實(agents-memory-updater 產出) 測試:exchangeRateService 測試已涵蓋日誌層級變更,typecheck 通過 Co-authored-by: Cursor <cursoragent@cursor.com>
- 換錢所 computeConverterRate 依 rateMode 選價:mid=(買+賣)/2、sell 雙向賣出、auto 維持客戶視角 - rateMode 貫穿 getUnitExchangeRate、報價 adapter 與趨勢圖,修正選中間價無效的硬編碼問題 - PageNavHeader 砍掉 sticky/safe-area hack 改 in-flow pill 返回鈕,修復 iOS PWA 頂部內容遮擋 - RateSelector 以透明命中區補回 44px 觸控目標,gitignore 忽略本地 impeccable skill bundle - 同步收斂在途測試(themes SSR fallback、換錢所匯率期望值)並補 002 紀錄與 patch changeset 測試:typecheck 全 workspace 通過;ratewise Vitest 1800+ 全綠;scripts 7/7;prettier/eslint 通過
- RatingModal 星星按鈕由 32px 升至 44px 圓形命中區,新增觸控目標守門測試(TDD) - OpenData 複製鈕以透明命中區補 44px 並補 focus-visible ring;MailtoLink、計算機關閉鈕同步補齊 - OpenData/Footer bare rounded 收斂為 rounded-compact,對齊語義 radius token SSOT - 更新 002 紀錄與 patch changeset 測試:typecheck 通過;RatingModal/MailtoLink/OpenData/Footer/calculator 相關 Vitest 307+ 全綠
- 新增 radius-ssot.test.ts regex 守門:元件層禁用 bare rounded 與 Tailwind 原生圓角尺寸 - SkeletonLoader/ColorSchemeComparison/notificationTokens 殘留 bare rounded 收斂為 rounded-compact - 趨勢圖 rounded-b-xl 改 rounded-b-card,修正與 rounded-card 卡片底角不貼合 - 更新 002 紀錄與 patch changeset 測試:config 489 tests 全綠(含新守門);SkeletonLoader/趨勢圖測試通過;typecheck/prettier 通過
- Puppeteer 程式化掃描正式站 320/768/1280 視口,僅 open-data 端點 code 標籤溢出(right=449) - 端點路徑補 min-w-0 break-all,OpenData.test.tsx 新增窄視口防溢出守門 - 固定寬度全掃描無 320px 溢出風險,5 個表格均有 overflow-x-auto SSOT 包裹 - 更新 002 紀錄與 patch changeset 測試:OpenData 23 tests 全綠;typecheck/prettier 通過;首頁/multi/faq/guide/幣別頁 320px 零溢出
- 停止對 jsDelivr/GitHub Raw 發送 If-None-Match 條件式請求 - 移除 304 分支,保留 ETag 寫入快取供未來 Worker proxy 路徑 - 同步更新 exchangeRateService 單元測試 測試:pnpm --filter @app/ratewise test -- exchangeRateService(32 passed) Co-authored-by: Cursor <cursoragent@cursor.com>
- 整合二十線 UX 審查收斂為韓式 fintech 對標規格 - 定義 Hero、inline numpad、資訊去重與三階段路線圖 - 附 Phase 1 可執行任務拆解(CalculatorKeypad 抽出起) 測試:pnpm prettier --check docs/superpowers(格式已通過) Co-authored-by: Cursor <cursoragent@cursor.com>
- 新增 currencies.ts:TWD/KRW 幣別設定、formatAmount、detectCurrencyFromTimezone - 新增 exchangeRate.ts:從 jsDelivr CDN 取得換錢所賣出匯率(krwPerTwd) - 新增 useCurrencyAutoDetect hook:timezone 自動偵測幣別,手動切換後永不覆蓋 - useStore:新增 currency、currencyManuallySet、krwPerTwd、rateUpdatedAt 欄位 - HomeTab:金額顯示支援 KRW/TWD 格式,新增換算提示(≈ ₩X / ≈ NT$ X) - HistoryTab:全面使用 formatAmount 取代硬編碼 NT$ - SettingsTab:新增幣別切換按鈕、自動偵測 badge、匯率更新時間顯示 - i18n:補齊四語言 rate_updated、auto_detected 翻譯 測試:dev server 瀏覽器驗證 HomeTab KRW/TWD 顯示、換算提示、SettingsTab 幣別切換及自動偵測 badge 正常 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
- 移除已廢棄的 NAVIGATION_NETWORK_TIMEOUT_MS 與 Promise.race 斷言 - 改為驗證現行 precache-first SWR 方案(matchPrecache + fetchAndCacheNavigation) - 避免 iOS precache 被驅逐時誤回 offline.html 給在線用戶的問題 測試:pnpm vitest run sw.test.ts 全 35 個測試通過 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
- 移除 networkResponse.then(...) 舊斷言,改為驗證 fetchAndCacheNavigation SWR 模式 - 同步移除 NAVIGATION_NETWORK_TIMEOUT_MS / Promise.race 殘留斷言 測試:pnpm vitest run pwa-offline.test.ts 全 35 個測試通過 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
- themes.ts nitro primary 從 0 212 255(#00D4FF)改為 0 150 230(#0096E6) - 對齊 index.css [data-style='nitro'] 深電光霓虹藍(白字 3:1 AA 對比) - 修復 verify:artifacts DESIGN SSOT 漂移導致 build 失敗 測試:pnpm build 通過,DESIGN SSOT 驗證無漂移 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
- 移除 BottomSheet 與 useResponsiveSheetHeight,改為 position: fixed 面板 - 面板 bottom 使用 CSS calc(0.5rem + var(--nav-h) + env(safe-area-inset-bottom)) - 系統鍵盤開啟時(visualViewport.height 縮小)面板移至鍵盤上方(bottom: keyboardH px) - 鍵盤開啟時自動隱藏計算機,避免面板超出可視區域 - index.html 補充 interactive-widget=resizes-visual(Android Chrome 鍵盤感知) - ResizeObserver 追蹤面板高度,動態設定 content paddingBottom 測試:dev server 瀏覽器驗證 — 平分/個別輸入模式正常;鍵盤模擬(visualViewport override)顯示計算機隱藏符合預期;154 tests pass Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
- 移除 NAVIGATION_NETWORK_TIMEOUT_MS + Promise.race:iOS eviction 後 3s timeout
會將 offline.html 回傳給在線用戶(假離線症狀,看起來像未更新至最新版本)
- Cold start 改用 matchPrecache('index.html') 立即回傳,等同 createHandlerBoundToURL
行為且無需等待;背景同步更新 html-cache 供暖啟動使用
- ensureOfflineHtmlCached:同時備份 index.html 至 html-cache,使 index 與
offline.html 具有同等 iOS eviction 存活率
- resolveOfflineDocumentFallback 新增 matchIndexHtmlInAnyCache 第二層 fallback
測試:pwaOfflineFallback.test.ts 補齊 matchIndexHtmlInAnyCache 路徑;154 ratewise tests pass
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
- fetchFromCDN 不再傳入 cachedEtag,不發送 If-None-Match 標頭 - 移除 304 Not Modified 分支,改由 HTTP cache 管理重複請求 - 更新測試:驗證即使快取有 ETag 也不附帶 If-None-Match - 更新 OpenData.tsx、SeoTech.tsx、seo-metadata.ts FAQ 說明文字 - 同步更新 open-data.md Markdown mirror - AGENTS.md 補充 UI/UX 治理知識點 測試:pnpm --filter @app/ratewise test -- moneyboxRateService(passed) Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cbd116c1e0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
P0 收斂策略(6741e600 分析後執行)PR #411 混雜多個無關變更(split-meow、ci.yml revert、AppLayout 移除 PwaInstallGuide 等),P0 修復無法安全整包合併。 已自
新 PR:#425 驗證: |
🔬 PWA
|
| 驗證 | 結果 |
|---|---|
分支 offline e2e 全套(offline-cold-start + offline-pwa,offline-pwa-chromium project) |
22 passed / 9 skipped / 0 failed(29.9s);precache 暖機 78 JS + 2 CSS |
Targeted repro:刪除 workbox-precache + html-cache(模擬 iOS eviction)→ 硬重載 |
完整 app 1s 內復原(V2.23.0、匯率、幣別清單全正常);document 5ms、首屏 32ms、root 3 子節點 |
⚠️ 誠實限制:CDP page-session 的 Slow-3G 節流無法套到 SW 自身的 fetch(Chromium 已知限制,SW 在獨立 context),故 repro 量到的是快網復原,非慢網。但已證明復原路徑正確:雙快取全失 + 重載 → app 正常重掛、precache 自動重建,無黑屏/卡死。
風險定級:🟡 Low-Medium
handleNavigationRequest 三條路徑:
- html-cache 命中 → 立即回快取 + 背景 revalidate(零白屏)
- 冷快取,
matchPrecache('index.html')命中 → 立即回 shell(零白屏) - html-cache miss + precache 也 miss(iOS eviction) →
await fetchAndCacheNavigation()無 timeout
真正無上限白屏需同時滿足:① precache 被驅逐(iOS 罕見)+ ② 硬重載 + ③ 網路「連得上但不回應」(hung,非單純慢、非離線)。因為:
- 離線 → fetch 立即 throw →
offline.html即時 fallback(已驗證) - 單純慢 →
index.html是小 shell,1–3s 內到達;之後 JS 階段由index.html內建冷啟動 watchdog(__RATEWISE_COLD_START_TIMEOUT_MS__)保護 - 只有「TCP 連上但 server 掛住」這種 captive-portal 類邊緣才會真的無限等
修法(PR #426,case-3 only)
只在 case-3 對 fetch 加 bounded Promise.race(8s),逾時用 event.waitUntil 保住背景 fetch 再回 resolveNavigationFallback();case-1/2 維持零延遲不動。
為何此修法不會重現本 PR 要修的回歸:本 PR 移除舊 3s timeout 是因為 timeout 會在「precache 仍存在」時把 offline.html 誤送給在線用戶(false-offline)。而 case-3 的 precache 本來就已確認不存在,因此此分支的 timeout fallback 不可能造成 false-offline。
- 修法 PR:fix(ratewise): 為導覽 case-3 加上有界網路 fallback #426(base 指向本分支
chore/ratewise-production-governance) - 測試:
sw.test.ts新增 case-2/case-3 行為測試 → 37/37 passed;typecheck green
對本 PR 的其餘建議(不阻擋,供拆分參考)
- 規模 233 檔 / 23 changesets 建議拆分(至少分離 PWA+ETag 與 design-token/UX),以利審查與回滾。
- 本 PR 夾帶
apps/split-meow/*(10 檔)與 ratewise 治理無關,建議抽出為獨立 PR。
— 驗證與修法委派 Cursor(composer-2.5-fast)執行,主 checkout 全程未受影響。
#411 收斂進度更新(2026-06-26)已透過 #425 獨立 PR 收斂(P0)
本輪新增 follow-up PR
仍待獨立 PR(勿整包合併 #411)
#411 維持 open 作為收斂 tracker,不建議 squash merge。 |
* fix(ratewise): 為導覽 case-3 加上有界網路 fallback - precache 已驅逐時等網路設 8s 上限,避免連線掛住造成無限白屏 - timeout 後以 event.waitUntil 保留 in-flight fetch,成功仍寫入 html-cache - case 1 暖快取與 case 2 precache 命中維持零延遲不變 - 新增 handleNavigationRequest 單元測試覆蓋 case 2 即時與 case 3 逾時 fallback * fix(ratewise): 修正 sw 測試 mock 的 require-await lint 錯誤 - 將 matchPrecache mock 由 async 改為回傳 Promise.resolve,消除無 await 的 async - 不影響 case-2/case-3 測試行為 測試:vitest run src/__tests__/sw.test.ts 37/37 通過;eslint 該檔 0 錯誤 --------- Co-authored-by: haotool <haotool.org@gmail.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
- history 檔名改用 extractSeoulSnapshotDate 提取的 updateTime(首爾掛牌日) 與 fetch script 的 date-rollover 判斷同源 SSOT,提取失敗 fallback Asia/Seoul wall-clock - 修正 cron 每 5 分鐘全天執行時 UTC 15:00-15:59 視窗首爾新一日資料被寫進台北前一日檔名 - 補 002 條目 reward-moneybox-history-snapshot-date-ssot(本次 +1,累計 +62) 測試:本地三組情境驗證(updateTime 提取、UTC 跨日 timestamp、空資料 fallback)皆正確 Generated with Claude Code via Happy Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
- ExpenseRecord 新增 currency 與 exchangeRateKrwPerTwd 快照欄位,記帳時保存 - HistoryTab 個別金額改用各筆快照幣別,KRW 副標即時換算 TWD;彙總/結算採 trip 主導幣別 fallback - 新增 formatKrwAsTwd helper 與 store/currencies 單元測試 - 補 002 條目 reward-split-meow-expense-currency-rate-snapshot(本次 +1,累計 +63) 回應 Codex P2 (PR 411):先 TWD 記帳後切 KRW 會把舊金額誤格式化為 KRW 測試:split-meow vitest 162/162 通過、typecheck 通過、Playwright 瀏覽器驗收切回 TWD 後 KRW 記錄仍正確(截圖 screenshots/) Generated with Claude Code via Happy Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Summary
此 PR 包含
chore/ratewise-production-governance分支上的 105 個 ratewise 相關 commits,涵蓋以下主題:PWA 離線品質
NAVIGATION_NETWORK_TIMEOUT_MS+Promise.racecold-start timeout(iOS precache 驅逐時假離線根因)matchPrecache('index.html')即時回傳,背景 revalidateensureOfflineHtmlCached同步備份index.html至html-cache,增加 iOS 驅逐防禦resolveOfflineDocumentFallback新增matchIndexHtmlInAnyCache第二層 fallbackCDN 請求優化
exchangeRateService/moneyboxRateService移除 ETag / If-None-Match 條件式請求設計與 UI 治理
themes.ts primary↔index.css)SEO 與 OpenData
其他
Test plan
pnpm --filter @app/ratewise test(所有單元測試)🤖 Generated with Claude Code
via Happy