SRS.md
Rama main · réplica de lectura del repositorio original.
# 資產管理 系統規格說明書 (SSD)
**版本:** 4.139.0
**日期:** 2026-10-08
**狀態:** 已實作
---
## 目錄
1. [文件定位與術語](#1-文件定位與術語)
2. [模組規格](#2-模組規格)
- 2.1 [使用者與權限](#21-使用者與權限)
- 2.2 [交易與帳戶](#22-交易與帳戶)
- 2.3 [分類系統](#23-分類系統)
- 2.4 [預算與固定收支](#24-預算與固定收支)
- 2.5 [統計報表](#25-統計報表)
- 2.6 [股票投資](#26-股票投資)
- 2.7 [資料匯出匯入](#27-資料匯出匯入)
- 2.8 [前端路由與頁面](#28-前端路由與頁面)
3. [技術附錄](#3-技術附錄)
- 3.1 [非功能需求](#31-非功能需求)
- 3.2 [資料模型](#32-資料模型)
- 3.3 [API 端點](#33-api-端點)
4. [版本歷程](#4-版本歷程)
---
## 1. 文件定位與術語
### 1.1 文件定位
本文件為「資產管理」網頁應用程式的系統規格說明書(System Specification Document, SSD),採按模組分段的敘述式寫法(narrative SSD),替代舊版以 IEEE-830 逐條編號列舉的軟體需求規格書(SRS)。核心目的是讓讀者從「這個模組要解決什麼問題、怎麼運作、哪些事情刻意不做」的角度理解系統,而非羅列 FR 條款。
本文件與 `.specify/memory/constitution.md`(憲章)及 `CLAUDE.md`(開發指引)構成專案的三份主要治理文件:
- 憲章制定原則性不可破壞規則(例如:所有規格必須以繁體中文撰寫)
- 本文件描述每個功能模組的實際行為與邊界
- `CLAUDE.md` 提供給 AI 協作工具(Claude Code、Copilot)的上下文提示
### 1.2 系統概觀
本系統為獨立的網頁應用程式,使用者透過瀏覽器即可使用。採前後端一體化架構,前端使用 Next.js App Router、React、TypeScript 與 Tailwind CSS,後端以 Node.js + Next.js API Routes 提供頁面與 API,資料持久化使用 PostgreSQL。支援桌面與行動裝置瀏覽,單一部署單位即可自帶介面運作。
### 1.3 技術棧
| 層級 | 技術 |
| -------- | ------------------------------------------------------------------------------------------------- |
| 前端 | Next.js 16、React、TypeScript、Tailwind CSS、Chart.js |
| 後端 | Node.js 24+、Next.js API Routes |
| 資料庫 | PostgreSQL |
| 驗證 | JWT(httpOnly Cookie)、Google OAuth、LINE Login、Passkey(WebAuthn);不提供本機 Email/密碼登入註冊 |
| 外部 API | TWSE OpenAPI、exchangerate-api.com、ipinfo.io、Google Identity Services、SMTP(Nodemailer)、Zeabur Email(ZSend)、Resend、MEGA S4 Object Storage |
| 部署 | 原生 Node.js 或 Docker;CI 自動從 `changelog.json.currentVersion` 推導 Docker tag 與 git tag |
### 1.4 使用者角色
| 角色 | 說明 |
| ---------- | -------------------------------------------------------------------- |
| 管理員 | 系統首位完成外部登入者自動成為管理員。可管理註冊政策、白名單與全站使用者帳號 |
| 一般使用者 | 透過外部身份服務登入後使用所有記帳、股票與報表功能 |
| 訪客 | 未登入狀態,僅可瀏覽登入、隱私權政策、服務條款與公開首頁 |
#### 共享帳本角色(#262)
帳本角色獨立於站台管理員身分:`owner` 可編輯並管理成員、邀請、稽核及移交;`editor` 可讀寫記帳資料但不可管理成員;`viewer` 只可讀取及匯出,所有狀態變更方法皆回覆 403。站台管理員不會因其全站角色自動取得其他帳本的成員權限。
本階段共享帳本以空白資料建立,涵蓋帳戶、分類、交易/附件/轉帳/還款摘要、預算與固定收支;原個人資料維持私有。每個帳本的 `data_owner_id` 為穩定資料命名空間,沿用既有查詢的 `user_id` 欄位但不等同於建立者或操作者。所有記帳 API 經中央授權把登入者與資料命名空間分離,並保留 `actorUserId` 供個人偏好與稽核使用。沒有指定帳本的既有用戶端與整合保持個人帳本行為。
Email 邀請使用高熵單次 token,資料庫只存 SHA-256 雜湊,7 天到期,接受時驗證目前登入信箱、期限與撤銷狀態。成員變更及擁有權移交以帳本列鎖和交易序列化;owner 不可直接降級、移除或離開,須先移交給有效成員。成員移除/離開即撤銷新請求的權限與既有待接受邀請,資料及稽核仍屬帳本。刪除 former owner 的個人帳號不刪除共享記帳;仍持有共享帳本的 owner 由 API 及資料庫雙層阻擋刪除。
新增 `financial_ledgers`、`ledger_members`、`ledger_invitations`、`ledger_audit_log`,部署啟動冪等建立個人帳本與 owner 成員,不重寫既有財務列。寫入稽核記錄操作者 ID、Email、當時角色、資源與結果;owner 可從帳本管理頁查閱。投資、Flutter 共享 UI、MCP/第三方整合及通知排程的帳本選擇另行追蹤。
### 1.5 名詞定義
| 名詞 | 定義 |
| -------- | -------------------------------------------------------------------- |
| 交易記錄 | 一筆收入、支出或轉帳的資料 |
| 分類 | 交易記錄的類別(如餐飲、交通),支援一層子分類 |
| 子分類 | 隸屬於父分類的細項分類(如餐飲 > 早餐) |
| 帳戶 | 資金來源(如現金、銀行帳戶、信用卡、虛擬錢包) |
| 預算 | 使用者設定的月度支出上限,支援整月總預算或分類預算 |
| 固定收支 | 週期性自動產生的交易記錄 |
| 轉帳 | 帳戶間的資金移轉,產生一對 `transfer_out` + `transfer_in` 配對記錄 |
| 批次操作 | 一次對多筆交易進行刪除或變更分類/帳戶/日期 |
| 股票持倉 | 使用者持有的股票資產,數量與成本一律由交易紀錄動態計算 |
| 股票交易 | 股票買入或賣出的交易記錄 |
| 股票股利 | 股票的現金股利或股票股利,日期欄位為除息日期 |
| FIFO | 先進先出法,用於計算股票成本基礎與實現損益 |
| TWSE | 臺灣證券交易所(Taiwan Stock Exchange) |
### 1.6 作業環境
- **前端:** 現代瀏覽器(Chrome、Firefox、Safari、Edge 最新版本)
- **後端:** Node.js 24+
- **部署:** 本地伺服器或雲端容器;預設 port 3000;支援 Docker(AMD64 + ARM64)
### 1.7 系統邊界
- 系統預設貨幣為新台幣(TWD),但支援多幣別交易與帳戶
- 提供銀行/券商對帳檔案匯入(OFX 1.x/2.x、可設定欄位對應的 CSV)與差異比對;不串接銀行 API 自動抓取對帳單
- 單一使用者帳號不支援多人共用(共享帳本為未來規劃)
- 股票投資僅支援台股(上市 + 上櫃)
---
## 2. 模組規格
### 2.1 使用者與權限
我們的核心目標,是讓使用者以最低的摩擦取得一個安全且可追蹤的身分,並讓系統擁有者能清楚掌握「誰在什麼時候、從哪裡登入了」。
#### 註冊與首次登入
使用者不得以電子郵件與密碼建立帳號或登入;註冊與登入僅透過已設定的外部身份服務。Google SSO 採 OAuth Authorization Code Flow,LINE Login 採官方授權流程,前端先取得一次性 state,回呼時由後端驗證後才建立登入狀態。未設定或未啟用的外部服務按鈕會隱藏,不影響其他可用方式。
Google 或 LINE 首次登入時,若 Email 尚未存在,系統會自動建立帳號、預設分類與預設帳戶;若已存在則更新對應的外部身份綁定。Passkey 僅供既有帳號註冊與登入使用。資料庫保留舊密碼欄位作相容用途,但應用程式不再接受、設定或驗證本機密碼。
登入成功會發放 JWT Token,瀏覽器端存於 httpOnly Cookie(防 XSS 竊取),搭配 `SameSite=Strict` 屬性(防 CSRF),有效期由 `JWT_EXPIRES` 控制;行動 App 端 Token 存於裝置端加密儲存,有效期由 `APP_JWT_EXPIRES` 控制。後端依請求是否帶有 App 專屬裝置識別 header 判斷要套用哪一組效期。登出會清除目前裝置的 Cookie,不影響其他裝置的登入狀態;登入 API 回傳 `currentLogin`,讓前端立即顯示本次登入資訊。
#### Passkey(WebAuthn)
使用者可在帳號設定註冊 Passkey(指紋、Face ID、PIN 碼、硬體安全金鑰),之後可在登入頁用 Passkey 一鍵無密碼登入。同一個帳號支援多組 Passkey,每組可命名並獨立刪除。後端有 origin 白名單防 rebinding。WebAuthn 前端模組由伺服器本地提供(不走 CDN),以避開外部失效風險。
#### 管理員身分與註冊政策
系統建立的第一個使用者自動成為管理員;若資料庫升級時尚無任何管理員,最早建立的使用者會被追認升級。這條規則沒有例外,目的是確保系統**永遠至少有一位管理員**。
管理員可以控制外部服務註冊政策,包含:是否開放公開註冊、以及一份 Email 白名單。白名單非空時,只有白名單上的 Email 可以透過 Google 或 LINE 首次建立帳號;白名單為空且公開註冊關閉時,訪客無法自行註冊。管理員可直接建立沒有本機密碼的使用者資料,待該使用者以外部服務登入後完成綁定;不提供設定或重設本機密碼。刪除帳號會一併刪除該使用者的所有關聯資料(交易、帳戶、分類、預算、股票等)。**管理員不可刪除自己,也不可讓系統剩餘的管理員歸零** — 這兩條規則對自刪與被刪皆適用。
#### 登入稽核
每一次登入成功都會寫入稽核紀錄,欄位包含登入時間、IP 位址、登入方式(google / line / passkey)、以及本次登入是否以管理員身分進行;失敗登入(帳號不存在、外部憑證無效、缺少憑證、暫時鎖定)也會記錄,且註明失敗原因。IP 的國家代碼優先取自 Cloudflare 的 `CF-IPCountry` 標頭,退回時才查 ipinfo.io;內網位址標記為 `LOCAL`。
一般使用者只能在「帳號設定」看自己最近的 100 筆登入紀錄。管理員另可在管理介面看自己作為管理員身分登入的最近 200 筆、以及全站的最近 500 筆(含失敗嘗試)。管理員的兩種紀錄都支援單筆刪除、多選批次刪除、手動同步,並顯示上次同步時間;即使舊資料缺少主鍵,也必須能透過備援識別(例如時間戳)刪除單筆。
帳號設定頁會列出目前有效登入裝置,欄位包含裝置名稱、登入時間、登入 IP 與單一裝置「登出」操作。一般登出與單一裝置登出只撤銷該 session;帳號安全設定或管理員執行高風險帳號變更時,會遞增 `token_version` 並撤銷所有既有 session,確保變更後舊裝置失效。
#### 伺服器時間與 NTP 校正
管理員面板有「伺服器時間」區塊,用以解決部署環境時區與時鐘偏移問題(例如:Zeabur / Docker 預設 UTC,但排程應以台灣時間判斷)。此區塊顯示伺服器實際時間、時區、目前採用時間(含偏移)、啟動後偏移量與 uptime;可填目標時間或毫秒偏移量調整 `SERVER_TIME_OFFSET`(系統時鐘本身不動,僅影響排程檢查),偏移持久化於 `system_settings`,上限 ±10 年。
校正來源可選外部 NTP 伺服器(預設依序嘗試 `tw.pool.ntp.org`、`pool.ntp.org`、`time.google.com`、`time.cloudflare.com`)。實作為原生 dgram UDP 的 SNTP v3 client(RFC 4330),3 秒逾時 fallback;校正時扣除單趟網路延遲提升精準度,支援「查詢(不套用)」預覽。NTP host 參數僅允許 IPv4 或 FQDN(擋 IPv6、私有網段、IPv4-mapped、link-local 等 SSRF 風險目標)。
#### 安全基線
- Google/LINE/Passkey 外部登入與相容停用端點套用速率限制(每 IP 每 15 分鐘 20 次)
- 本機密碼登入、註冊、修改與重設流程均不建立或驗證密碼
- `/privacy`、`/terms` 亦套用速率限制
- 所有使用者輸入經 `escHtml()` 跳脫後插入 DOM;分類顏色僅允許 `#RRGGBB` 格式(雙端驗證)
- 啟用 CSP、HSTS、X-Content-Type-Options、Referrer-Policy;停用 `X-Powered-By`
- 外部 CDN 腳本加 SRI `integrity` 屬性
- `.env` 檔案權限 `0o600`;資料庫備份排除於 `.gitignore` 與 `.dockerignore`
#### 不做什麼
- 不提供本機電子郵件密碼登入、註冊、修改或重設;外部身份服務僅支援已設定的 Google、LINE 與 Passkey
- 不做雙因素認證(2FA);Passkey 已取代此需求
- 不做帳號鎖定策略(連續失敗 N 次鎖定 M 分鐘),僅靠速率限制
---
### 2.2 交易與帳戶
核心目標:讓使用者以最低的摩擦記錄每一筆資金流向,並清楚掌握每個帳戶的當下餘額。
#### 帳戶
帳戶是資金的容器。使用者可自由新增帳戶並指定名稱、初始餘額、圖示、帳戶類別(銀行/信用卡/現金/虛擬錢包)與幣別。新使用者註冊時系統會自動建立一個「現金」帳戶,讓他第一筆交易就能立刻記下來。帳戶管理頁支援以類別分頁(Tab)切換、分組顯示,並對信用卡帳戶提供銀行分組與一鍵還款捷徑。
每個帳戶還有一個「是否計入總資產」開關,排除的帳戶會顯示專屬標籤,並在總資產卡上明示已排除(例如:公司發薪帳戶、朋友代管帳戶)。
帳戶可編輯、可刪除;但若該帳戶已被某筆交易引用則不允許刪除,使用者必須先處理那些交易或把它們移到其他帳戶。帳戶餘額永遠即時由交易紀錄計算而來,公式為 `初始餘額 + 收入 - 支出 + 轉入 - 轉出`。我們不另外儲存一個「目前餘額」欄位,避免資料飄移。
#### 交易
交易有三種類型:收入、支出、轉帳。收入與支出需要金額、日期、分類、帳戶,選填備註(最多 200 字),分類下拉以 optgroup 顯示父子階層。金額禁止為 0 或負數,日期則允許未來日期以供預先規劃(顯示時會加上「未來」標籤與歷史交易區分)。編輯時僅記錄最後修改時間,不保留版本歷史。刪除需二次確認 Modal;轉帳交易刪除時自動同步刪除另一半。
轉帳是一組對稱的 `transfer_out` + `transfer_in` 記錄,透過 `linked_id` 互相關聯。典型情境:信用卡消費時記為該信用卡帳戶的「支出」讓餘額呈現負值,繳款時記為「從銀行帳戶轉帳到信用卡」的轉帳,轉入金額中和負值 — 支出只被認列一次,繳款不會重複計入統計。
支援外幣交易:選擇非 TWD 幣別時,系統會自動從匯率 API 取得當下即時匯率並填入欄位;若是信用卡支出,還會加上海外刷卡手續費(預設 1.5%,可手動調整費率與金額)。交易儲存後,當下採用的匯率會與交易綁定,確保歷史報表不因日後匯率變動而飄移。匯率清單為跨使用者共用(5 分鐘 in-flight deduplication + 30 分鐘 server-level 快取)。
單筆交易可標記為「不計入統計」,以排除特定一次性大額事件(如巨額退稅、帳戶整併)對儀表板、分類統計、預算進度的汙染。
電子發票支援:交易新增 Modal 可開啟相機或上傳圖片掃描財政部電子發票 QRCode,系統自動解析金額、日期、店家資訊並填入表單。
#### 行事曆
`/finance/calendar` 提供月檢視與週檢視切換,整合顯示交易、既有股票股利日期與啟用中的固定收支預定發生日。股利標記沿用 `stock_dividends.date`(除息日);目前資料模型未另存實際入帳日。同日項目在日期格以交易/股利/排程數量摘要,並另外加總顯示當日收入/支出金額(台幣,收入綠字 `+`、支出紅字 `−`);金額加總口徑與 Dashboard 一致,只計實際交易(`type IN ('income','expense')`,排除轉帳)且排除標記「不計入統計」者,股利與固定收支排程(僅為預計金額)不併入加總。選取日期後於明細區展開全部項目;股利與固定收支排程僅為唯讀標記,不提供行事曆內編輯或觸發排程的操作。使用者可從選取日期直接開啟交易新增表單,並預填該日期。
行事曆「今天」依登入使用者 `users.timezone` 計算;交易、股利與排程皆以資料庫的 `YYYY-MM-DD` 日期欄位比對,不將日期字串轉成瀏覽器本地時間戳。月/週視窗以使用者選取的當地日期做 UTC 日期元件運算,避免跨時區及夏令時間造成日期偏移。
#### 批次操作
當使用者需要一次處理很多筆交易時,表格支援 checkbox 多選(全選、半選、取消全選都支援)。勾選後出現紫色批次操作列,可執行:批次刪除(轉帳會連帶刪除另一半)、批次變更分類、批次變更帳戶、批次變更日期。每一種變更都是單次選單操作,不必一筆筆打開 Modal。批次變更分類的下拉選單採用自訂元件(含色點與分區結構),避免 `<optgroup>` 嵌套時部分瀏覽器渲染不全。
#### 篩選與分頁
交易列表預設按日期新到舊排序。頂端篩選列支援日期範圍、類型(收入/支出/轉帳/未來交易/全部)、分類、帳戶、關鍵字搜尋(匹配備註欄);每頁筆數可選 10 / 20 / 50 / 100 或自訂輸入任意數字。類型以色彩標籤顯示:支出紅、收入綠、轉帳藍。
#### 不做什麼
- 不串接銀行/券商 API 自動抓取對帳單。交易來源為使用者手動、固定收支排程、自家 CSV 匯入,或銀行/券商對帳檔匯入(僅比對差異,不會自動新增交易)
- 交易列表不提供基準幣別切換;統計報表的基準幣別切換另見 §2.5
- 不保留交易修改歷史。僅記「最後修改時間」
---
### 2.3 分類系統
核心目標:讓使用者用最符合自己直覺的方式組織收支,並讓報表能以有意義的維度彙整。
#### 階層
分類支援兩層:父分類與子分類。不提供三層以上的孫分類 — 這是刻意的限制,避免使用者陷入「我要建幾層才夠」的糾結。每個分類有名稱、類型(收入/支出,強制 CHECK 約束)、顏色、是否為預設分類、排序順序。子分類的類型必須跟父分類一致,同一個父分類下名稱不可重複。子分類僅能隸屬於一個父分類。**分類類型一經建立永久不可變更**,避免歷史交易語意混亂。**交易僅能指派至子分類**(leaf-only),父分類僅作為組織節點不直接承載交易(後端強制)。
#### 預設資料
新使用者註冊時會自動建立完整的預設分類樹(13 個父分類 + 56 個子分類):
- **支出父分類(8):** 餐飲、交通、購物、娛樂、居住、醫療、教育、其他
- **預設子分類:**
- 餐飲 → 早餐、午餐、晚餐、飲料、點心
- 交通 → 大眾運輸、計程車、加油、停車費、高鐵/火車
- 購物 → 日用品、服飾、3C 用品、家電、美妝保養
- 娛樂 → 電影/影音、遊戲、旅遊、運動健身、訂閱服務
- 居住 → 房租/房貸、水電費、瓦斯費、網路費、管理費
- 醫療 → 掛號費、藥品、保健食品、牙科、健檢
- 教育 → 學費、書籍、線上課程、補習費
- 其他 → 雜支、禮金/紅包、捐款、罰款
- **收入父分類(5):** 薪資、獎金、投資、兼職、其他
- 薪資 → 月薪、加班費
- 獎金 → 年終獎金、績效獎金、節日禮金
- 投資 → 股利、利息、資本利得
- 兼職 → 接案、家教、打工
- 其他 → 退稅、贈與/紅包、雜項
舊使用者在系統升級後若缺少任一預設項,會在登入時被自動補建(P95 < 200 ms);補建會跳過使用者主動刪除過的預設項(透過 `DeletedDefaultRegistry` 追蹤),不會復活已刪除的預設項。
#### 畫面
分類管理頁採雙區塊:上半段「支出」、下半段「收入」。父分類顯示完整寬度,帶 `+`(新增子分類)、編輯與刪除按鈕;子分類以 CSS Grid 網格縮排顯示,帶左側 4px 藍色邊框和箭頭圖示。整棵分類樹支援 **HTML5 原生拖曳排序**(同層):父列拖曳重排該 type 的所有父分類;子分類在同一父分類底下拖曳重排,跨父歸屬調整則透過編輯 modal 內的「上層分類」下拉。色彩配色與儀表板、報表的圖例完全一致。
頁首提供「補回過去刪除的預設分類」按鈕:點擊後清空 `DeletedDefaultRegistry` 並執行一次補建;這是非破壞性操作,**不修改任何現有分類**。
#### 約束
- 分類下若有交易記錄則不可刪除
- 刪除父分類時,若任一子分類下有交易,整棵樹都不可刪除;否則連帶刪除所有子分類;連帶刪除的預設子分類會對稱寫入 `DeletedDefaultRegistry`,避免下次登入又被自動補回
- 顏色僅允許嚴格 `#RRGGBB` 6 碼 hex,後端會驗證;這是為了防止 CSS 注入(不接受 `#RGB` 縮寫或 `#RRGGBBAA` alpha 形式)
- 分類類型(收入/支出)一經建立永久不可變更
- 編輯父分類時不可將其改為某分類的子分類(避免形成循環)
#### 智慧分類建議(#252)
新增交易 Modal 在輸入備註(至少 2 個字元)後,依同類型歷史交易顯示 Top-3 子分類建議,附信心度與支持筆數。建議僅為提示,不會自動套用;使用者點選建議或自行選擇分類後,才會寫入表單,未儲存前不改動資料。
建議採本地可解釋啟發式統計(`lib/smartCategorySuggestions.ts`),不引入外部 AI 服務:CJK、Hangul 文字以單字與相鄰二字切詞,拉丁字母/數字片段轉小寫;以 Dice 係數計算摘要重疊度,近期歷史依 180 天半衰期加權,並用支持筆數調節信心度。只採用同類型、有效 leaf 分類、非空備註的最近 500 筆交易。沒有詞彙交集或低於 0.2 信心度時不顯示建議,不猜測。
查詢以帳本資料擁有者的 `user_id` 範圍計算;共享帳本成員只會使用該帳本的歷史交易,不跨帳本洩漏。
#### 不做什麼
- 不做跨使用者的共用分類模板庫;使用者想要套用他人範本只能透過 CSV 匯入匯出
- 不做「是否隱藏」屬性:分類只能新增、編輯、刪除,沒有第四種狀態
- 不呼叫外部雲端 AI 服務;分類建議使用本地統計
---
### 2.4 預算與固定收支
核心目標:讓使用者能事先規劃消費上限與週期性的進出,並在當下快速看到自己離目標多遠。
#### 預算
使用者可設定月度預算,分兩種粒度:整月總支出預算(不選分類)或分類預算(選單支援子分類群組顯示)。每個月每個分類只能有一筆預算 — 一個月內不允許多組同分類的預算並存。設定後儀表板即時顯示進度條:已用金額/預算、百分比、剩餘可用金額,超支時進度條變紅。預算不強制使用者遵守,僅作為視覺提示。
#### 固定收支
固定收支是一份「配方」,描述某筆交易會以什麼週期重複發生。設定欄位:類型(收入/支出)、金額、分類、帳戶、備註、起始日期、週期(每日/每週/每月/每年)、幣別(可選外幣並設定匯率)。
系統每次使用者登入或背景排程觸發時,會掃描所有啟用中的固定收支,比對 `last_generated` 欄位推算出應該產生的交易日期,直到今日為止一次全部產出。若 `last_generated` 為空(首次執行),會以 `start_date` 當作首次產生日期而非「`start_date` 的下一個週期」 — 讓起始日設在今天能正確觸發當日交易。
外幣固定收支在自動產生交易時,會帶入設定當下的匯率;使用者也可以手動改。
固定收支列表每一筆卡片顯示:起始日、上次產生日、下次產生日。若啟用且下次產生日已經到期,會以警示色加上「(待執行)」提醒。備註也會直接顯示在卡片上。編輯現有固定收支時,若原分類或帳戶已被刪除,下拉選單會插入「(原分類已刪除)」或「(原帳戶已刪除)」佔位項目,避免 `select.value` 靜默被清空。
#### 固定收支智慧偵測(#252)
固定收支頁會掃描最近 2,000 筆交易,將同類型、分類、帳戶與金額(外幣使用幣別及原幣金額)相符的交易依日期分組,偵測每日/每週/每月/每年週期。每組至少 3 筆交易,要求相鄰日期間隔穩定;月份 29–31 日的模式不會提示(既有配方只以 `last_generated` 推進日期,無法保存月末錨點,避免短月後日期逐月漂移);轉帳、手續費、排除統計的交易、未來日期、已建立的固定收支,以及使用者已忽略的建議皆不提示。若最後活動超過週期容差,或下一個週期日已在今日之前,模式不再提示,避免已停止的舊模式造成歷史交易回補。每筆建議顯示週期、金額、分類、帳戶、出現筆數、最近日期與信心度。
提示**不會自動建立固定收支**。按「設為固定收支」只將資料帶入既有表單,並以最新已觀察交易之後的下一個週期作為起始日,避免重複回補舊交易;外幣表單預填最近一筆的原幣金額、幣別與匯率。使用者仍須確認並儲存。按「不再提示」會記錄忽略簽章,只影響提示,不改動交易或固定收支。
分類建議與固定收支偵測共用 `user_settings.ai_assist_enabled`(預設開啟),可在「設定 › 帳號 › 智慧輔助」關閉。建議 API 的驗證路徑跳過自動排程與固定收支產生,確保讀取建議不會隱式寫入財務資料。
#### 目標儲蓄與還款計畫
儲蓄目標(`/finance/goals`)以「名稱 + 目標金額 + 目標日期 + 綁定來源」描述一筆要存到的錢。綁定來源二選一(或皆不綁定):
- **綁定帳戶**:以該帳戶目前餘額為「已存」,非 TWD 帳戶換算為 TWD 等值,語意與儀表板銀行餘額一致。
- **綁定分類**:以**建立目標當日**起該分類(含一層子分類)的支出累計為「已存」,語意與預算「已用金額」一致;`exclude_from_stats` 的交易不計入。
每張目標卡片顯示:已存/目標、進度條、尚缺金額、預估達成日(以「已存 ÷ 已過天數」的日均速度外推;零進度時顯示「需有存款進度」)、剩餘期間所需日均金額,以及進度狀態徽章(進度正常/進度落後/已逾期/已達成)。
還款計畫以「名稱 + 本金 + 年利率 + 期數 + 首次應繳日」描述一筆債務,採**本息平均攤還(annuity)**自動產生攤還表:每期先以餘額計息(四捨五入至分)再沖本金,最後一期以當時餘額清償,因此「各期本金加總 = 原始本金」與「最後一期後餘額 = 0」恆成立。月付金經四捨五入後若不足支付首期利息(無法按期攤還),建立時會拒絕該本金/利率/期數組合。清單摘要顯示月付金、依首次應繳日推算的已到期期數、預定剩餘本金、利息總額與下期應繳日;已到期期數不代表實際付款(系統不記錄實際還款)。完整攤還表由單筆端點提供;Web 於建立對話框即時試算,並可開啟攤還表明細。
進度判定以**時間比例**為準(已過天數 ÷ 總天數 × 目標金額 = 此刻應達金額),未達者視為落後;儀表板「目標進度提醒」卡片列出落後目標(逾期者優先、缺口大者在前,最多 3 筆),全部正常時顯示「所有儲蓄目標進度正常」。此卡片可於儀表板個人化設定中排序/隱藏。
金額計算一律以 `decimal.js`(`lib/savingsGoal.ts`),避免浮點誤差;本檔同時被伺服器路由與用戶端元件 import,兩端數值不可能分歧。
#### 不做什麼
- 不做超支推播/email 通知,僅在儀表板視覺提示
- 不自動建立固定收支:偵測結果一律需使用者確認
- 不做目標的自動存入/自動轉帳,也不做還款的自動記帳(僅計算與追蹤)
- 不做進度落後的外部通知(推播/LINE/email),僅於儀表板顯示提醒卡片
---
### 2.5 統計報表
核心目標:讓使用者用多種維度檢視自己的收支狀況,找出隱藏的消費模式。
#### 儀表板
登入後的首頁。顯示本月收入、本月支出、淨收支三張 KPI 卡;下方有「支出分類圓餅圖」(支援雙圓餅圖切換:內圈父分類、外圈子分類)、「資產配置圓餅圖」(含股票市值與帳戶餘額,同樣支援雙圓餅圖)、當月預算進度條、最近 5 筆交易摘要。若使用者有持股,資產配置會新增「持股前 5 名」與「帳戶前 5 名」兩欄列表。
支出分類圓餅圖內圈顯示父分類,外圈顯示子分類,並列出前 5 名排行。圖例與 tooltip 皆顯示金額與佔比百分比。
#### 統計報表頁
統計報表頁專注在「深度分析」:分類統計圓餅圖(同樣可切父子雙圓餅)、每日趨勢折線圖、每日消費長條圖。所有圖表共享頂端的「期間選擇器」與「類型切換器」:
- **期間預設:** 本月、上月、近 3 個月、近 6 個月、今年
- **自訂期間:** 選擇「自訂時間」後出現起始/結束日期輸入;僅填起始日時結束日預設為今天,僅填結束日時起始日預設為當月 1 日
- **類型:** 支出或收入
- **基準幣別:** 以 TWD 為預設;可從既有匯率清單與系統預設匯率所支援的 ISO 4217 幣別中切換。未指定時保留既有 TWD 等值行為。跨幣別換算以 `decimal.js` 全精度計算,依匯率設定列/共用快取/系統預設值換算,並標示來源與實際更新時間;靜態系統預設值會明確標示未提供時間戳。
- 切換即時重繪,不需按「套用」;CSV 匯出會套用相同期間、類型、基準幣別與匯率來源/時間資訊。
排序規則:圖例與圓餅區塊的順序一律是「父分類總額由高到低」,同一父分類下「子分類總額由高到低」。這個規則不可由使用者調整 — 讓不同時間的同一份報表放在一起比對時能保持一致的視覺錨點。
#### 信件排程報表
管理員可在排程設定對指定使用者自動寄送「資產統計信件」。信件頻率有每日/每週/每月三種,寄送前會自動更新該使用者所有持股的最新股價(走與即時查價一致的三段策略)。信件本身是 HTML + table-based 排版(為了相容 Outlook Desktop 的 Word 渲染引擎),內含三色漸層英雄區、3 欄 KPI 含上月對比(▲▼ pill)、儲蓄率進度條、分類顏色長條、近 5 筆交易摘要、CTA 按鈕。
信件中「交易紀錄」區塊會依頻率切換:
- 每日:昨日交易明細
- 每週:Mon-Sun 每日彙總,多顯示「區間收入/支出/淨額」三欄總覽卡,週末日期紫色標示
- 每月:上月每天彙總
「股票投資」區塊顯示 4 列:成本、市值、未實現損益、報酬率(含彩色 ± 符號)。寄信通道(v4.30.0 起)一律由環境變數設定,支援三種:SMTP(Nodemailer)、Zeabur Email(ZSend HTTP API)、Resend;以 `EMAIL_PROVIDER_PRIMARY` 與 `EMAIL_PROVIDER_FALLBACK`(值:`smtp` / `zeabur` / `resend` / 留空)指定主要與備用通道,primary 執行期失敗時若 fallback 已設定才自動退回,不重試不補寄;兩者皆未設定則回 503(沿用 005 FR-021)。寄件人顯示名稱可透過 `EMAIL_SENDER_NAME`(v4.30.1 起)為三通道統一指定,採 RFC 5322 quoted-string 包裝為 `"<name>" <email>`;若各通道 FROM 變數已是 `Name <email>` 格式則不覆寫,保留 per-provider override 能力。排程檢查以台灣時間(UTC+8,無 DST)為基準,使用 `twParts()` 輔助函式確保部署在 UTC 主機上仍能正確觸發。
#### 不做什麼
- 不做使用者自訂儀表板版位排序/隱藏,留給未來版本
- 不做週/月報 PDF 匯出
---
### 2.6 股票投資
核心目標:讓使用者記錄台股買賣與股利,自動計算手續費/證交稅/FIFO 損益,並能隨時掌握持股市值與報酬率。
#### 持倉
每檔持股代表一個「股票代號」加一個「類型」(一般股票/ETF/權證)。類型影響賣出時的證交稅稅率(一般 0.3%、ETF/權證 0.1%)。持有股數、平均成本、損益等數值一律由系統從交易紀錄動態計算,不額外儲存 — 確保帳目永遠跟交易一致。
投資組合總覽卡顯示總市值、總成本、總損益、整體報酬率(金額加權公式:`totalPL / totalCost × 100`;`totalCost = 0` 時顯示「—」);個股卡片顯示代號與名稱、持有股數、平均成本、目前股價、市值、損益與報酬率,並補「最後查價時間」標記(> 24 小時以橘色 ⚠ 警示)、價格來源(即時/收盤/T+1/凍結)、已下市 badge。三段式損益顯色:獲利綠色(▲)/損益 0 灰色/虧損紅色(▼)。
`GET /api/stocks` response 為 `{ stocks: [...], portfolioSummary: { totalMarketValue, totalCost, totalPL, totalReturnRate } }`;FIFO 計算改用 `lib/moneyDecimal.js` 內共用 `calcFifoLots()` helper(decimal.js 全精度,僅最終 response 階段取整以滿足 SC-004 ≤ 1 元誤差)。
`stocks.delisted INTEGER DEFAULT 0` 欄位記錄使用者於批次更新股價 Modal 手動標記的下市旗標;下市股票凍結價格、跳過 TWSE 查價、priceSource 固定為 `'frozen'`。
#### TWSE 整合
新增股票(或在交易/股利 Modal 輸入代號)時,系統會自動呼叫 TWSE 代理 API 查名稱與股價。價格採三段策略,依「當下台灣時間」判斷:
1. **盤中**(週一至五 09:00–13:30):`mis.twse.com.tw` 即時成交價,快取 1 分鐘
2. **盤後**(週一至五 13:30 後):TWSE `STOCK_DAY` 當日收盤價,快取 5 分鐘
3. **非交易日/盤前**:`STOCK_DAY_ALL`(T+1 資料),快取 10 分鐘
支援上市(TSE)與上櫃(OTC)。查詢成功在輸入欄旁顯示「✓ 股票名稱 價格類型 $xxx(時間/日期)」綠色提示並自動填入欄位;失敗顯示「找不到此股票代號」紅色提示。前端輸入有 500ms 防抖以避免打爆 API。TWSE 代號有格式驗證(僅允許數字 + 少數字母),防止注入。
#### 交易與股利
股票交易記錄買入或賣出;欄位包括日期、股數(強制整數,`Number.isInteger` 檢查)、每股價格、手續費、交易稅、帳戶、備註。手續費預設自動計算 `Math.floor(成交金額 × 0.1425%)`,整股最低 20 元(零股最低 1 元);證交稅僅賣出時計算 `Math.floor(成交金額 × 稅率)`,最低 1 元。輸入股數或價格時即時顯示費用摘要(成交金額、手續費、交易稅、總成本/淨收入),讓使用者在送出前就看到確切金額。買入/賣出按鈕切換時顏色即時變化(買入綠、賣出紅)。
若輸入的股票代號尚未建立持倉,送出交易時自動新增股票(從 TWSE 取得名稱與股價)。股利同樣支援;現金股利或股票股利至少填一項,股票股利會增加持有股數。日期欄位統一叫「除息日期」(ex-dividend date),因為這是決定投資人是否有資格領股利的法定基準,而非實際入帳日(通常在除息日後 1–2 週才匯入現金)。
交易紀錄與股利紀錄頁支援:依股票代號或名稱的下拉式搜尋篩選、checkbox 多選批次刪除、每頁筆數可選(含自訂)、伺服器端分頁(`GET /api/stock-transactions?page=1&pageSize=20` 回傳 `{ data, total, page, totalPages }`)。
#### 定期定額
使用者可設定某檔股票的定期定額:每期預算、週期、起始日、證券帳戶、備註、啟用/停用。系統在每次登入時 server-side 非同步(`setImmediate` fire-and-forget)觸發 `processStockRecurring(userId)` 補產生流程,補產生迴圈每期使用該期應觸發日的 TWSE STOCK_DAY 歷史收盤價計算可買股數(無法買 1 股時略過)並產生買進交易。
`stock_transactions` 表新增 `recurring_plan_id` / `period_start_date` 兩欄並建立 partial unique index `idx_stock_tx_recurring_idem`(`(user_id, recurring_plan_id, period_start_date)` 三元組僅一筆),配合 `INSERT OR IGNORE` 達成多裝置同時登入觸發 race 下不重複扣款。
若排程日遇週末或 TWSE 休市日則自動順延到下一個交易日;排程日本身仍以原日期推算保持週期節奏,交易紀錄的日期寫入實際交易日,備註附「原排程 YYYY-MM-DD 順延」。休市日快取 24 小時,來源為 TWSE `/v1/holidaySchedule/holidaySchedule` OpenAPI,並過濾掉「開始交易/最後交易」等特別交易日。
#### 自動同步除權息
股利紀錄頁有「同步除權息」按鈕。按下後,後端依使用者最早交易日期到今日,按年分段查 TWSE `TWT49U` 除權息列表,僅處理使用者已持有的股票代號,計算每個除息日當下的持股數(若持股為 0 則跳過)。純現金股利直接取 `權值+息值`;含股票股利者查 `TWT49UDetail` 明細取每股現金與每千股配股數。最終:
- 現金股利 = 持股數 × 每股現金股利(四捨五入)
- 股票股利 = 持股數 × 每千股配股數 ÷ 1000
同日期同股票若已存在股利紀錄則不重複新增;自動新增的備註會標示「TWSE自動同步(每股$X.XX)」。除權息 API 快取 30 分鐘。`POST /api/stock-dividends/sync?year=YYYY` 支援單年同步;前端「同步除權息」按鈕觸發阻擋式 Modal + 進度條 + 取消按鈕,從最早交易年份逐年呼叫並累計 `新增 N / 跳過 M / 失敗 K` 顯示於完成 toast。
#### 股票股利合成交易
新增或同步含 `stockDividendShares > 0` 的股利時,系統於 `stock_transactions` 同步寫入一筆合成 `$0` buy 交易(`note` 以 `[SYNTH] 股票股利配發` 唯一前綴開頭),讓 FIFO 佇列自然納入這批 $0 cost lot。刪除股利紀錄時連動刪除對應合成交易(依 `stock_id + date + price=0 + note 前綴` 匹配,容差 0.001 股);股票交易批次刪除拒絕含合成股票股利交易的選取,必須透過刪除股利紀錄處理。
#### 股利再投資(DRIP)
股利紀錄可勾選「股利再投資(DRIP)」:以現金股利買回同一標的,需填寫「再投資股數」與「再投資每股價格」(兩者皆須為正數,且 `再投資股數 × 再投資價格 ≤ 現金股利`,以 decimal.js 比較避免浮點誤差);台股再投資股數必須為整數,美股允許零股。
成立時系統於同一 transaction 內同步寫入一筆合成 buy 交易(`note` 以 `[DRIP] 股利再投資` 唯一前綴開頭,`fee = 0`,`linked_dividend_id` 指向所屬股利),讓既有 `calcFifoLots()` 自然把這批股票納入 FIFO 成本基礎 — 未來賣出的成本基礎即包含再投資批次,已實現損益不需任何特殊分支。`stock_dividends` 新增 `reinvest` / `reinvest_shares` / `reinvest_price` 三欄;`stock_transactions` 新增 `linked_dividend_id` 以精確追蹤股利合成交易。
- **編輯**:更新股利時先刪除同一 (`stock_id`, `date`) 的既有 DRIP 合成交易再依新設定重建,避免改日期或股數後留下孤兒紀錄。
- **刪除**:刪除股利(單筆或批次)時連動刪除對應 DRIP 合成交易;`stock_transactions` 批次刪除拒絕含 DRIP 合成交易的選取,必須透過刪除股利紀錄處理。
- **回歸保證**:未標記 `reinvest` 的股利完全不進 FIFO 佇列,純現金股利行為與升級前一致。
- 股利 CSV 匯出/匯入同步支援「再投資 / 再投資股數 / 再投資價格」與合成買入的 FIFO 排序時間/識別碼欄,匯入驗證規則與 API 相同。股票交易 CSV 匯出會排除所有有 dividend link 或合成前綴的股利買入列,避免先匯入交易再匯入股利時重複計入;匯入股利 CSV 時會依股利列重建合成買入。普通交易 CSV 亦保留 `created_at` 與排序識別碼,讓同日 FIFO 順序在資料還原後維持一致。
#### 現股當沖證交稅減半
股票交易賣出可標記「現股當沖」。稅率與適用規則集中於 `lib/twStockTaxRules.ts`(單一真實來源,附法規依據註解):
| 規則 | 稅率 | 法規依據 |
| --- | --- | --- |
| 一般股票賣出 | 0.3%(千分之三) | 證券交易稅條例第 2 條第 1 款 |
| 現股當沖賣出 | 0.15%(千分之一點五) | 證券交易稅條例第 2 條之 2 |
| ETF/權證賣出 | 0.1%(使用者設定) | 不適用第 2 條之 2(條文限「上市或上櫃股票」) |
標記限制:僅台股一般股票(`stock_type = 'stock'`,非 ETF/權證、非美股)且僅賣出交易可標記;優惠期間為 2017-04-28 至 2027-12-31,逾期或違反條件時 API 回 400。當沖賣出必須指定類型為「證券帳戶」的帳戶,並由同一帳戶、同一交易日、同一標的的現款買入數量足額支應;一筆買入數量不可重複支應多筆賣出。仍有當沖賣出交易時不可將帳戶改為其他類型。無證券帳戶的交易仍使用一般稅率。`stock_transactions` 新增 `day_trade` 欄位。
`PUT /api/stock-transactions/{id}` 切換當沖標記時,若原稅額為自動計算(`tax_auto_calculated = 1`)會跟著重算(0.3% ↔ 0.15%),避免使用者切換標記後稅額不變;原為手動輸入且標記未變時維持手動值。CSV 匯出/匯入同步支援「現股當沖」欄位。
#### 實現損益
獨立 Tab 呈現每筆賣出交易的 FIFO 實現損益。`GET /api/stock-realized-pl` response shape 為 `{ entries: [...], summary: { totalRealizedPL, overallReturnRate, ytdRealizedPL, count } }`,整體報酬率採金額加權公式(`totalRealizedPL / totalCost × 100`,`totalCost = 0` 時為 `null`)。頂部彙總卡顯示總實現損益、整體報酬率、今年實現損益、已實現筆數。表格列出賣出日期、股票、股數、賣出均價、成本均價(FIFO)、手續費+稅、實現損益、報酬率,皆套用三段式顯色。
FIFO 邏輯:買入批次依時間序進入佇列(手續費分攤至成本);每筆賣出依序從最早批次扣除,算出該筆賣出的成本基礎;實現損益 = 賣出收入(股數 × 賣出價 - 手續費 - 證交稅)- FIFO 成本基礎;報酬率 = 實現損益 ÷ FIFO 成本基礎 × 100%。未賣出部位的平均成本以 FIFO 剩餘批次計算。
#### 投資組合績效分析(XIRR/TWR/歸因)
獨立 Tab(`/stocks/performance`)呈現投資組合績效與五因子收益歸因:
- **年化內部報酬率 (XIRR)**:金額加權複利回報,以 Newton-Raphson 演算法求解,基準日數 Actual/365。多重根時回傳 `multiple-roots`,無根或無法收斂時回傳 `no-root`,不回傳假數值。
- **時間加權報酬率 (TWR)**:消除外部現金流進出規模影響,依外部現金流時點將區間切割為子期間並以幾何連乘計算 `(1 + R1) * (1 + R2) * ... - 1`。缺歷史收盤價時標記不可用或警告。
- **大盤基準比較 (Benchmark)**:美股市場以 S&P 500 (`^GSPC`)、台股市場以元大台灣50 (`0050.TW`) 為比較標的,計算同期間價格報酬與超額報酬(Alpha = TWR - Benchmark)。
- **五因子收益歸因**:嚴格維持金額守恆:期末資產市值 = 期初資產市值 + 淨注入資金 + 總收益(已實現損益 + 未實現損益變動 + 股利收入 - 手續費與交易稅)。
- **走勢與明細**:視覺化顯示資產總市值與累計淨投入資金走勢,並提供完整現金流與資金進出明細表。
- **匯出與權限**:支援 CSV 與串流 Excel (.xlsx) 匯出,寫入稽核紀錄 `export_stock_performance`,納入共享帳本權限政策驗證。
#### 批次更新股價
Modal 列出所有持股與目前股價輸入框,每列附「標為已下市」checkbox(凍結價格 + 後續查價跳過),支援「從證交所取得最新股價」一鍵批次拉 TWSE 最新收盤價(透過 `POST /api/stocks/batch-fetch` 並發查價,受 `TWSE_MAX_CONCURRENCY` 環境變數控制,預設 5;失敗時指數退避重試 2 次,間隔 1s/2s)。`POST /api/stocks/batch-price` 接受 `{ updates: [{ stockId, currentPrice, delisted? }] }`;省略 `delisted` 時保留向後兼容(沿用 baseline 行為僅更新價格)。也允許手動調整個別股價;確認後一次寫回。
#### 不做什麼
- 不提供海外股票(美股、港股等),僅限台股(上市+上櫃)
- 不做歷史股價圖表
---
### 2.7 資料匯出匯入
核心目標:讓使用者能自由把自己的資料帶走/帶回來,不被系統綁架。
#### 交易記錄
匯出 CSV(UTF-8 BOM,欄位:日期、類型、分類「父分類 > 子分類」格式、金額、帳戶、備註),可選日期範圍。匯入同樣是 CSV,第一行為標題自動跳過;支援拖曳或點擊上傳;匯入前顯示前 10 筆預覽。類型支援支出、收入、轉出、轉入;分類比對支援「父分類 > 子分類」格式。轉出與轉入依日期+金額自動配對建立 `linked_id` 關聯。匯入時若偵測到缺少的分類或帳戶,會詢問是否自動建立。匯入完成顯示成功/略過/錯誤筆數。受限於 body size,CSV 匯入端點上限 25MB 與 20000 筆。
CSV 內容經過 Formula Injection 防護處理(以 `=`、`+`、`-`、`@` 開頭的儲存格值會被轉義),避免匯入後在 Excel 開啟觸發公式執行。
#### 銀行/券商對帳匯入與差異比對
支援匯入銀行或券商提供的對帳檔案,與帳本現有交易比對並列出差異,**不會**自動新增或修改帳本交易。
**支援格式**
- **OFX 1.x(SGML)與 2.x(XML)**:解析銀行帳戶對帳聲明(`STMTRS`)、信用卡帳單(`CCSTMTRS`)與券商投資對帳聲明(`INVSTMTRS`,含 `BUYSTOCK`/`SELLSTOCK`/`INCOME`/`INVEXPENSE`/`INVBANKTRAN` 等)。沒有帳本收支語意的投資交易型別(如 `TRANSFER`)會略過並於結果中計數。解析器為自寫實作,不新增任何相依套件。
- **銀行/券商 CSV(可設定欄位對應)**:以「mapping profile」描述來源檔每個語意欄位對應的標題名稱,支援自訂分隔符號、略過前導列、日期格式、金額正負號語意,以及「單一金額欄」或「借方/貸方兩欄」兩種金額表達方式。常用樣板可一鍵套用,設定可具名儲存並重複使用。
**差異比對(三類)**
1. **帳本有、對帳單無**:帳本已記錄但對帳單沒有對應交易。
2. **對帳單有、帳本無**:對帳單有交易但帳本未記錄(提示使用者補登)。
3. **金額不符**:雙方都有但金額不同。
配對策略為可解釋的固定順序:先以銀行/券商端識別碼(FITID)精確配對,再於日期窗口(預設 ±3 天,可用於吸收入帳日與消費日差異)內以同方向、金額完全相同配對;仍未被配對者才推定為金額不符,且推定僅限同日(避免把對帳單獨有的新消費誤判成既有交易的金額不符)。比對以帳本 `twd_amount`(台幣等值)為準,並排除外幣手續費子交易(`is_fx_fee = 1`)以避免假性差異。每次對帳結果都會保存,可回查歷史與差異明細。
**安全性與資料完整性**
- 匯入走 DB transaction 整批原子化:解析或寫入任何一步失敗即整批回滾,不留下半套資料。
- CSV 匯入去除 UTF-8 BOM,並比照匯出邏輯做 Formula Injection 防護(反向還原自家匯出的 `'` 前綴、移除第三方檔案的公式起始字元,但保留負數金額的正負號,避免收支方向被反轉)。
- 匯入與對帳行為寫入 `data_operation_audit_log`(`action='import_reconciliation'`),成功與失敗皆記錄來源檔案與差異統計,不記錄帳號或交易明細;欄位對應設定的刪除另記 `action='delete_reconciliation_profile'`。
- 套用既有帳本授權邊界:對帳 API 屬帳本資料路徑,viewer 唯讀(匯入回 403),跨帳本或跨使用者的對帳結果一律回 404。
- 與既有匯入共用互斥鎖:同一使用者已有匯入進行中時回 409。
**資料表**:`reconciliation_import_profiles`(欄位對應設定)、`reconciliation_sessions`(一次匯入/比對的摘要)、`reconciliation_items`(差異明細,隨 session 級聯刪除)。
#### Excel(.xlsx)匯出
匯出頁可選擇 CSV 或 Excel(.xlsx),兩者並存、欄位一致,適用於帳戶、交易、分類、股票交易與股利紀錄五個模組。XLSX 以純伺服器端串流方式產生(`lib/xlsxExport.ts`),資料庫結果使用穩定 keyset pagination 分頁讀取、每頁至多 1,000 列並逐列寫入 ZIP,不先把完整資料集或 XLSX 檔案累積於記憶體。支援 File System Access API 的瀏覽器將回應串流寫入使用者選定的檔案;其他瀏覽器保留既有 Blob 下載相容路徑。
XLSX 具備欄位標題列,日期寫入為 UTC 午夜對應的 Excel 日期序列值(`yyyy-mm-dd` 格式),金額、股數與匯率為真正的數值欄位(非字串),並套用千分位/小數格式。Formula Injection 防護沿用既有策略:以 `=`、`+`、`-`、`@` 開頭的文字儲存格前置 `'`,並將儲存格標記為文字格式(`@`),雙重確保 Excel 不會把內容當成公式執行。匯出操作於串流完整產生後寫入成功稽核;串流失敗或用戶取消會分別記錄失敗/取消結果。
#### 分類結構
匯出 CSV 欄位:類型、分類名稱、上層分類、顏色。父分類的上層欄位為空;子分類填入父分類名稱。匯入時先建父後建子,已存在分類自動略過。
#### 股票交易/股利
分別為獨立 CSV 檔。
- **交易欄位:** 日期、股票代號、股票名稱、類型(買進/賣出,亦接受英文 buy/sell)、股數、成交價、手續費、交易稅、帳戶、備註
- **股利欄位:** 日期、股票代號、股票名稱、現金股利、股票股利、備註
匯入的股票代號若未建立持倉則自動新增股票;若已存在但名稱為代號(不正確),會以 CSV 中的名稱自動更新 — 修正舊資料最省力的方式就是再匯入一次。
#### 資料庫備份(管理員專用)
管理員可匯出整個未加密的 SQLite 檔案作為完整備份。匯入時系統自動驗證檔案格式與必要資料表結構,並在覆寫前自動備份現有資料庫。這是災難復原用途,一般使用者不應接觸。資料庫備份檔案排除於 `.gitignore` 與 `.dockerignore`。
#### 全球匯率自動更新
匯率設定頁可串接全球即時匯率 API(exchangerate-api.com),基礎貨幣固定為 TWD。使用者可自訂 3 碼幣別代碼並存入個人匯率清單;可開關自動更新;可手動按「立即取得即時匯率」即時同步。前端顯示上次取得時間(精確到秒)。跨使用者的匯率快取 30 分鐘共用,有 `is_manual` 欄位區分手動/自動匯率。支援免費版 API(無需 key)或付費版(`EXCHANGE_RATE_API_KEY` 環境變數)。
#### API 使用與授權資訊頁
側邊欄獨立頁面,集中列出系統使用的所有外部 API、用途、來源連結與授權注意事項:
- 全球即時匯率:`https://www.exchangerate-api.com/`(支援免費版與付費版)
- IPinfo:`https://ipinfo.io/lite`(授權標示 `IP address data is powered by IPinfo`)
- TWSE:`https://openapi.twse.com.tw/`、`https://www.twse.com.tw/`
- Google Identity Services
- 寄信服務:SMTP(Nodemailer)、Zeabur Email(ZSend,`https://zeabur.com/docs/en-US/email/quick-start`)、Resend
- MEGA S4 Object Storage:`https://mega.io/zh-hant/objectstorage`(管理員手動上傳完整 SQLite 備份時使用;S3 相容端點預設 `https://s3.<region>.s4.mega.io`)
- 財政部電子發票整合服務平台:`https://www.einvoice.nat.gov.tw/`(雲端發票載具查詢;API endpoint 與憑證由環境變數設定)
#### 交易照片附件
新增交易時可附加最多 5 張照片,每張預設上限 10 MB。使用者可在新增 Modal 中選擇照片儲存位置:Server 本機儲存或 S3 相容物件儲存;S3 未設定時該選項停用。手機網頁需提供「拍照」與「選擇圖片」兩個入口,前者使用 `capture="environment"` 呼叫相機,後者允許從相簿或檔案上傳。LINE 新增記錄 wizard 中可於確認前傳送照片,系統先暫存 LINE image message id,確認新增後透過 LINE message content API 下載並附到該筆交易;LINE 端使用 `TRANSACTION_PHOTO_DEFAULT_STORAGE` 決定本機或 S3。系統新增 `transaction_attachments` metadata 表,實際檔案不放 public 目錄,讀取照片需經 `/api/transactions/{txId}/attachments/{attachmentId}/file` 驗證交易與附件皆屬於目前使用者。本機儲存預設位於 `uploads/transaction-photos`,S3 設定可使用 `TRANSACTION_PHOTO_S3_*`,未設定時可 fallback 使用 `MEGA_S4_*`。
#### 雲端發票載具匯入(#253)
於「系統設定 → 雲端發票」頁面綁定手機條碼載具(`/` + 7 碼英數字),最多 5 組;輸出的載具清單一律為遮蔽值(`/ABC••••`),驗證碼以 AES-256-GCM 加密後才落地,主金鑰為 `EINVOICE_ENCRYPTION_KEY`(未設定時自動產生,比照 `API_TOKEN_ENCRYPTION_KEY`)。
- **匯入為草稿、確認後才入帳**:使用者按「立即同步」或由登入時的排程(`lib/requestMaintenance.ts` 掛載、每載具至少間隔 1 小時,冷卻依 `last_sync_at` 存於資料庫並跨實例共用)拉取發票,寫入 `invoice_imports` 交易草稿(發票號碼/店家/金額/日期/時間);草稿本身不建立交易,須由使用者選定帳戶與分類並確認後才呼叫既有 `insertIncomeExpenseTransaction()` 入帳。查詢區間預設近 30 天、上限 90 天,並自該載具上次最新發票日續拉。
- **去重**:以發票號碼為唯一鍵(`UNIQUE(user_id, invoice_number)`),重複同步不產生第二筆草稿;確認入帳另以 `client_ref = sha256('einvoice:' + userId + ':' + invoiceNumber)[:32]` 冪等,重送不會重複記帳。
- **失敗不重試風暴**:比照 `monthly_report_send_log` 的保留錯誤狀態設計,載具列記錄 `last_sync_status`、`last_error`、`consecutive_failures`、`next_retry_at` 與 `last_sync_retryable`;退避為指數成長(30 秒起、上限 1 小時),退避期間不發出請求,同步回覆 `skipped` 與剩餘秒數。網路/408/429/5xx 暫時錯誤僅於下一次排程檢查且退避到期後重試;憑證失效(401/403)、無法解密與格式錯誤標記為不可排程重試,保留錯誤並提示重新綁定/處理。不可重試錯誤仍套用短退避以防手動連點,退避到期後可手動重試,重新綁定會立即重置。成功的同步重置退避。
- **並行保護**:`sync_lock_until` 為每載具原子租約,同一載具同時最多一個供應商請求;租約於程序中止後 60 秒到期。
- **供應商可設定與優雅降級**:端點與憑證全部來自環境變數 `EINVOICE_API_ENDPOINT`(必須 HTTPS)、`EINVOICE_API_APP_ID`、`EINVOICE_API_KEY`;三者未齊備時同步為 `skipped`(不呼叫網路、不計入連續失敗、不設退避),UI 仍可操作。憑證僅以 `X-App-Id`/`X-Api-Key` 標頭送出。測試全程以注入的 stub fetch 覆蓋,不連線真實財政部 API。
- **稽核與帳本邊界**:綁定、解除綁定、同步、入帳與略過皆寫入 `data_operation_audit_log`,metadata 不含任何憑證明文。載具憑證與發票匯入屬個人整合(比照 MCP/API Token 等既有慣例,見 `asset_openapi.yaml` 與 1.4 共享帳本角色),端點不採納 `X-Ledger-Id`;前端也固定讀取登入者的個人帳戶/分類,確認後只會建立個人交易,不讀寫共享帳本。
- **端點**:`GET/POST /api/imports/invoice-carriers`、`DELETE/POST/PATCH /api/imports/invoice-carriers/:id`、`GET /api/imports/invoices`、`POST/DELETE /api/imports/invoices/:id`。
- **資料表**:`invoice_carriers`(憑證密文、遮蔽條碼、退避狀態、`auto_sync`)、`invoice_imports`(發票欄位、狀態 draft/imported/dismissed、對應交易 ID)。
#### 不做什麼
- 不做 Excel (.xlsx) 匯出/匯入,僅提供 CSV
- 不做 QIF 格式匯入(OFX 1.x/2.x 已支援,見 §2.7.1)
- 不做自動雲端同步(Google Drive、Dropbox 等)
- 不內建財政部電子發票 API 憑證;未設定供應商時載具管理與草稿檢視仍可用,僅同步停用
- 不自動將發票入帳;不可重試錯誤不會由排程重試,可重試的暫時錯誤只於退避到期後、下一次已登入請求觸發的排程檢查重試(服務閒置時不建立背景計時器)
---
### 2.8 前端路由與頁面
核心目標:讓 URL 跟畫面一一對應,使用者可以直接分享連結、按上一頁/下一頁,或把瀏覽器書籤當捷徑用。
#### 路由
**公開(無需登入):**
- `/` — 網站介紹首頁(Public Home)
- `/login` — 登入/註冊頁
- `/privacy` — 隱私權政策
- `/terms` — 服務條款
**主應用程式(需登入):**
| URL | 頁面 |
| ----- | ------ |
| `/dashboard` | 儀表板 |
| `/finance/transactions` | 交易記錄 |
| `/finance/calendar` | 行事曆(交易、股利與固定收支) |
| `/finance/reports` | 統計報表 |
| `/finance/budget` | 預算管理 |
| `/finance/goals` | 目標儲蓄與還款計畫 |
| `/finance/accounts` | 帳戶管理(含匯率設定) |
| `/finance/categories` | 分類管理 |
| `/finance/recurring` | 固定收支 |
| `/finance/reconciliation` | 銀行/券商對帳匯入與差異比對 |
| `/stocks`、`/stocks/portfolio` | 持股總覽(預設) |
| `/stocks/transactions` | 股票交易紀錄 |
| `/stocks/dividends` | 股票股利紀錄 |
| `/stocks/realized` | 股票實現損益紀錄 |
| `/stocks/performance` | 投資組合績效分析(XIRR/TWR/歸因) |
| `/api-credits` | API 使用與授權 |
| `/settings/account` | 帳號設定(含個人登入稽核) |
| `/settings/mcp` | MCP 連線(OAuth 位址與個人存取權杖) |
| `/settings/mcp-connections` | 已連接 AI 工具 |
| `/settings/api-integration` | API 整合(API Token 與 Webhook 訂閱、投遞紀錄) |
| `/settings/admin` | 管理員面板(含全站登入稽核、寄信通道狀態、排程、伺服器時間) |
| `/settings/export` | 資料匯出匯入 |
#### 實作
- 前端以 `history.pushState` 實作 SPA 路由,支援瀏覽器上一頁/下一頁(`popstate` 事件)
- 伺服器端有 catch-all 路由,對任何未知路徑都回傳 `index.html`,讓使用者直接輸入 URL 也能正確導航
- 靜態檔僅允許白名單公開資源(`index.html`、`app.js`、`style.css`、`favicon.svg`、`logo.svg`、`changelog.json`、`privacy.html`、`terms.html` 等),避免專案根目錄敏感檔案外洩
- 登入後依當前 URL 自動導航至對應頁面(不是一律跳儀表板),確保頁面重整時使用者還在原本的地方
#### 介面設計原則
- 側邊欄為主要導航,桌面常駐、手機漢堡選單展開
- 金額統一格式 `NT$ 1,234`(千分位逗號)搭配 tabular-nums;日期統一 `YYYY-MM-DD`
- 色彩系統:收入綠、支出紅、轉帳藍、主色調紫(`#6366f1`)
- 所有 Toast 通知:成功綠色、錯誤紅色
- 刪除一律二次確認 Modal
- 動畫:spring 動畫、iOS 風格分段控件、focus-visible 無障礙焦點環
- 無障礙:prefers-reduced-motion 支援、深色模式下所有對比度通過 WCAG AA(≥ 4.5:1)
#### 右下角快速新增按鈕(情境式)
浮動 FAB(Floating Action Button)依頁面情境顯示:
- 收支管理相關頁(`transactions` / `reports` / `budget` / `accounts` / `categories` / `recurring`):顯示「新增交易」,點擊開 `modalTransaction`
- 股票相關頁(`stocks` 含子分頁):顯示「新增股票交易紀錄」,點擊開 `modalStockTx`
- 其他頁面(儀表板、設定、API 使用與授權):不顯示
#### 外觀模式
使用者可在設定切換三種外觀:跟隨系統、強制淺色、強制深色。偏好同步跨瀏覽器儲存於後端(JSON 欄位),登入後自動套用。
#### Modal 一覽
| Modal | 用途 |
| ---------------- | --------------------------------- |
| modalTransaction | 新增/編輯交易記錄 |
| modalTransfer | 新增轉帳記錄 |
| modalCategory | 新增/編輯分類(含上層分類選擇) |
| modalAccount | 新增/編輯帳戶 |
| modalBudget | 新增/編輯預算 |
| modalRecurring | 新增/編輯固定收支 |
| modalBatchChange | 批次變更(分類/帳戶/日期) |
| modalConfirm | 刪除確認 |
| modalStock | 新增/編輯股票持倉(含 TWSE 查詢)|
| modalStockTx | 新增/編輯股票交易(含費用試算) |
| modalStockDiv | 新增/編輯股票股利 |
| modalPriceUpdate | 批次更新股價(含 TWSE 批次取得) |
#### 不做什麼
- 不做使用者自訂主題色,主色調鎖定紫色
### 2.9 PWA 與離線記帳
核心目標:讓 Web 端在沒有網路或網路不穩時仍能開啟、且能先把交易記下來,恢復連線後自動送回伺服器,不會因斷線而遺失記帳。
#### Web App Manifest 與安裝
- `public/manifest.webmanifest` 定義 `name`、`short_name`、`theme_color`(`#b0521c`,與亮色主題一致)、`background_color`、`display: standalone`、`start_url: /dashboard`、192/512 一般圖示與 512 maskable 圖示,以及「新增交易」「儀表板」兩個快捷方式
- `app/layout.tsx` 輸出 `<link rel="manifest">` 與 `theme-color`(由 `generateViewport` 依 `prefers-color-scheme` 切換亮/暗色),並設定 `appleWebApp`
- 登入後畫面掛載 `components/features/pwa/PwaRegistrar.tsx`:production 下註冊 `/sw.js`,並監聽 `beforeinstallprompt` 提供 App 內一鍵安裝提示(可關閉,記錄於 `localStorage`)
#### Service Worker 快取策略
- `public/sw.js`(手寫,無建置相依)於 install 階段 pre-cache 公開資源:`/offline`、manifest、圖示、`logo.svg`、`favicon.svg`;固定路徑資源以 stale-while-revalidate 更新快取,每個 SW 啟用週期至少重驗證一次離線頁,cache version 須隨 `changelog.json.currentVersion` 更新
- 導覽請求採 network-first;離線時僅回退至公開的 `/offline` 說明頁
- **絕不快取 `/api/` 回應**:財務資料一律即時取得,避免顯示過期餘額
- **絕不快取已登入的 HTML 頁面**:本系統為財務工具,共用裝置上若把帳戶畫面寫入 Cache Storage,下一位使用者離線時可能看到前一位的資料
- 靜態資源(`/_next/static/`、圖示、manifest)採 cache-first;`sw.js` 本身以 `no-cache, no-store` 送出,確保新版立即生效
- `next.config.ts` 的 CSP 新增 `worker-src 'self'` 與 `manifest-src 'self'`,維持既有 `default-src 'self'` 政策不變
#### 離線記帳佇列
- 純邏輯模組 `lib/offlineQueueCore.ts`(不依賴瀏覽器或伺服器端 API,可獨立單元測試)定義佇列項目形狀、解析/序列化、去重、容量上限(200 筆;已滿時拒絕新項目並保留全部未同步交易,不淘汰舊項目)、重試與指數退避(5 秒起、上限 5 分鐘、最多自動重試 5 次)與 `isRetryableStatus`
- 瀏覽器 I/O 模組 `lib/clientOfflineQueue.ts` 以使用者分隔的 `localStorage` key(`assetpilot.offlineQueue.<userId>`)持久化;`OfflineSyncStatus` 會先綁定 user id 再啟動同步。登出會停用本頁並透過 `storage` event 通知其他分頁停止同步,同一使用者重新登入可恢復佇列,不同使用者登入會清除其他 user keys;舊版未分隔 key 會移除。寫入後回讀驗證,儲存空間不可用時回報失敗、前端不得顯示「已離線儲存」。佇列變更/恢復連線時自動送出,網路層錯誤即使 `navigator.onLine` 誤報在線也會立即重試。
- `components/features/transactions/TransactionsClient.tsx` 於新增收支/轉帳遇上連線層失敗(`isNetworkError`)時改存入佇列,並以 Toast 提示
- `components/features/offline/OfflineSyncStatus.tsx`(掛載於 `AppLayout`)顯示離線/待同步筆數,並對失敗項目提供「重試」與「捨棄」
#### 同步衝突處理策略
明確定義為兩層,而非留白:
1. **伺服器為最後寫入權威(去重即冪等)**:每筆離線項目帶一組 client 產生的 32 碼十六進位 `clientRef`;`transactions` 新增 `client_ref TEXT NOT NULL DEFAULT ''` 欄位與 `(user_id, client_ref)` 部分唯一索引(`client_ref != ''`)。重送同一筆不會產生第二筆,`insertIncomeExpenseTransaction`/`insertTransferPair` 會改讀已寫入的列,以與新建完全相同的回應形狀回傳,前端無法區分「新建」與「去重」。線上直接新增(`clientRef` 為空)維持既有行為不變。
2. **不可自動解決者交還使用者**:可重試失敗(網路層、408/425/429/5xx)留佇列指數退避重試;不可重試失敗(其餘 4xx,例如帳戶已刪除或信用卡已停用)標記為 `failed`,以提示讓使用者選擇重試或捨棄。
#### 端點
- `POST /api/transactions` 與 `POST /api/transactions/transfer` 新增選填 `clientRef`(`^[a-f0-9]{32}$`,格式不符時視為未提供,不影響既有請求)
#### 不做什麼
- 不做 background sync 背景送出(iOS Safari 尚未支援,改以前景 `online` 事件重送)
- 不做整頁 HTML pre-cache(清單變動快且會與伺服器渲染版本競態)
- 不快取任何 API 回應或已登入頁面(見上)
---
## 3. 技術附錄
### 3.1 非功能需求
#### 效能
| 項目 | 要求 |
| ------------ | ---------------------------- |
| 頁面載入時間 | 首頁 ≤ 3 秒(一般網路環境) |
| API 回應時間 | 單筆 CRUD 操作 ≤ 500ms |
| 大量匯入 | 支援 20000 筆 / 25MB 一次匯入 |
| JSON body | 預設上限 5MB;CSV 匯入端點 25MB |
| DB 寫入 | 非阻塞(in-flight + pending 合併 + tmp/rename 原子寫) |
#### 安全
- 密碼 bcryptjs 加密;強密碼規則統一(大寫、小寫、數字、特殊符號、≥ 8 字元)
- JWT Token 存 httpOnly + `SameSite=Strict` Cookie(瀏覽器)或裝置端加密儲存(行動 App),有效期分別由 `JWT_EXPIRES`(預設 7 天)/`APP_JWT_EXPIRES`(預設 90 天)控制
- `token_version` 欄位支援 Token 撤銷
- 所有 API 端點(除 auth 相關與 `/api/config`)套用 authMiddleware
- 系統更新與管理員 API 僅允許 admin 身分呼叫
- 輸入資料進行 HTML 跳脫(XSS);分類顏色格式雙端驗證;TWSE 代號格式驗證
- IDOR 防護:驗證 `accountId`、`categoryId`、`stockId` 擁有者
- CSV Formula Injection 防護
- CSP、HSTS、X-Content-Type-Options、Referrer-Policy;停用 `X-Powered-By`
- CSP 另含 `worker-src 'self'` 與 `manifest-src 'self'` 供 PWA 使用;Service Worker 僅快取公開靜態資源,不快取 API 回應與已登入頁面(v4.113.0 新增)
- 外部 CDN 腳本 SRI `integrity` 屬性
- `/api/auth/login`、`/api/auth/register`、`/api/auth/google` 套用較嚴格速率限制(每 IP 每 15 分鐘 20 次);`/privacy`、`/terms` 沿用既有靜態頁桶
- 全域 `/api` 速率限制:每 IP 每 15 分鐘 600 次(v4.31.0 新增;CodeQL `js/missing-rate-limiting`)
- CSRF 防護:對所有狀態變更請求(POST/PUT/PATCH/DELETE)若使用 Cookie 登入,必須附帶來源於 `ALLOWED_ORIGINS` 白名單的 `Origin` 或 `Referer`;Bearer Token 流程不受影響(v4.31.0 新增;CodeQL `js/missing-token-validation`)
- 路由轉址參數 `?next=` 在後端 `normalizeRoutePath` 內限制最大長度 2048 字元(v4.31.0 新增;CodeQL `js/polynomial-redos`)
- 資料庫匯入端點 `/api/database/import` 嚴格收斂 `req.body` 為 `Buffer`(CodeQL `js/type-confusion-through-parameter-tampering`)
- CORS 可透過 `ALLOWED_ORIGINS` 限制允許來源
- `.env` 檔案權限 `0o600`
- 靜態檔白名單(僅允許指定公開資源)
#### 可用性
- 響應式設計(RWD),支援手機、平板、桌面
- 支援繁體中文介面(本系統不支援多語系)
- 操作流程直覺化:新增一筆記錄不超過 3 個步驟
- 重要操作提供 Toast 通知回饋
- 刪除操作需二次確認(確認 Modal)
- 深色模式對比度通過 WCAG AA(≥ 4.5:1)
#### 可靠性
- SQLite 資料庫即時儲存(每次寫入後 `saveDB()`)
- 異常情況提供友善錯誤 Toast 訊息
- body 過大統一回 JSON 413
- 伺服器時間可透過 `SERVER_TIME_OFFSET` 調整,支援 NTP 校正
---
### 3.2 資料模型
#### 實體關係圖
```
使用者 (User)
├── 1:N → 交易記錄 (Transaction)
├── 1:N → 分類 (Category)
│ └── 自關聯 parent_id → 子分類
├── 1:N → 帳戶 (Account)
├── 1:N → 預算 (Budget)
├── 1:N → 固定收支 (RecurringTransaction)
├── 1:N → 登入稽核 (LoginAuditLog)
├── 1:N → 登入裝置 (LoginSession)
├── 1:N → 股票持倉 (Stock)
│ ├── 1:N → 股票交易 (StockTransaction)
│ └── 1:N → 股票股利 (StockDividend)
交易記錄 (Transaction)
└── 自關聯 linked_id → 轉帳配對記錄
```
#### User(使用者)
| 欄位 | 型態 | 說明 | 必填 |
| ------------ | ------- | ---------- | ---- |
| id | TEXT PK | 主鍵 | 是 |
| email | TEXT | 電子郵件 | 是 |
| password_hash | TEXT | 舊密碼欄位的相容性雜湊,應用程式不使用於登入驗證 | 是 |
| has_password | INTEGER | 是否允許本機密碼驗證;固定為 0 | 是 |
| display_name | TEXT | 顯示名稱 | 否 |
| created_at | TEXT | 建立時間 | 否 |
#### LoginAuditLog(登入稽核)
| 欄位 | 型態 | 說明 | 必填 |
| -------------- | ------- | ------------------------------------- | ---- |
| id | TEXT PK | 主鍵 | 是 |
| user_id | TEXT | 外鍵 → User | 是 |
| email | TEXT | 登入當下帳號 Email | 是 |
| login_at | INTEGER | 登入時間(timestamp) | 是 |
| ip_address | TEXT | 客戶端 IP 位址 | 是 |
| login_method | TEXT | 登入方式(google / line / passkey) | 是 |
| is_admin_login | INTEGER | 是否以管理員身份登入(1/0) | 是 |
#### LoginSession(登入裝置)
| 欄位 | 型態 | 說明 | 必填 |
| ----------- | ------- | ---------------------------- | ---- |
| id | TEXT PK | session 主鍵 | 是 |
| user_id | TEXT | 外鍵 → User | 是 |
| token_hash | TEXT | JWT 雜湊值 | 是 |
| device_name | TEXT | 由 User-Agent 推導的裝置名稱 | 是 |
| ip_address | TEXT | 登入 IP | 是 |
| user_agent | TEXT | 登入 User-Agent | 否 |
| login_at | INTEGER | 登入時間(timestamp) | 是 |
| revoked_at | INTEGER | 撤銷時間;0 表示有效 | 是 |
#### Transaction(交易記錄)
| 欄位 | 型態 | 說明 | 必填 |
| ----------- | ------- | --------------------------------------------- | ---- |
| id | TEXT PK | 主鍵 | 是 |
| user_id | TEXT | 外鍵 → User | 是 |
| type | TEXT | income / expense / transfer_out / transfer_in | 是 |
| amount | REAL | 金額 | 是 |
| date | TEXT | 交易日期(YYYY-MM-DD) | 是 |
| category_id | TEXT | 外鍵 → Category | 否 |
| account_id | TEXT | 外鍵 → Account | 是 |
| note | TEXT | 備註 | 否 |
| linked_id | TEXT | 轉帳配對的交易 ID | 否 |
| created_at | INTEGER | 建立時間(timestamp) | 是 |
| updated_at | INTEGER | 更新時間(timestamp) | 是 |
#### Category(分類)
| 欄位 | 型態 | 說明 | 必填 |
| ---------- | ------- | ------------------------------- | ---- |
| id | TEXT PK | 主鍵 | 是 |
| user_id | TEXT | 外鍵 → User | 是 |
| name | TEXT | 分類名稱 | 是 |
| type | TEXT | income / expense(CHECK 約束) | 是 |
| color | TEXT | 顏色色碼(預設 #6366f1) | 否 |
| is_default | INTEGER | 是否為預設分類(1/0) | 是 |
| is_hidden | INTEGER | 是否隱藏(1/0) | 是 |
| sort_order | INTEGER | 排序順序 | 是 |
| parent_id | TEXT | 父分類 ID(空字串表示頂層分類) | 否 |
#### Account(帳戶)
| 欄位 | 型態 | 說明 | 必填 |
| --------------- | ------- | -------------------------------------- | ---- |
| id | TEXT PK | 主鍵 | 是 |
| user_id | TEXT | 外鍵 → User | 是 |
| name | TEXT | 帳戶名稱 | 是 |
| initial_balance | REAL | 初始餘額(預設 0) | 是 |
| icon | TEXT | 圖示名稱(預設 fa-wallet) | 否 |
| category | TEXT | 帳戶類別(銀行/信用卡/現金/虛擬錢包)| 否 |
| excluded | INTEGER | 是否排除計入總資產(1/0) | 否 |
| created_at | TEXT | 建立時間 | 否 |
#### Budget(預算)
| 欄位 | 型態 | 說明 | 必填 |
| ----------- | ------- | -------------------------- | ---- |
| id | TEXT PK | 主鍵 | 是 |
| user_id | TEXT | 外鍵 → User | 是 |
| category_id | TEXT | 外鍵 → Category(可為空) | 否 |
| amount | REAL | 預算金額 | 是 |
| year_month | TEXT | 適用年月(如 2026-03) | 是 |
#### RecurringTransaction(固定收支)
| 欄位 | 型態 | 說明 | 必填 |
| -------------- | ------- | --------------------------------- | ---- |
| id | TEXT PK | 主鍵 | 是 |
| user_id | TEXT | 外鍵 → User | 是 |
| type | TEXT | income / expense | 是 |
| amount | REAL | 金額 | 是 |
| category_id | TEXT | 外鍵 → Category | 否 |
| account_id | TEXT | 外鍵 → Account | 是 |
| frequency | TEXT | daily / weekly / monthly / yearly | 是 |
| start_date | TEXT | 起始日期 | 是 |
| note | TEXT | 備註 | 否 |
| is_active | INTEGER | 是否啟用(1/0) | 是 |
| last_generated | TEXT | 最後產生日期 | 否 |
#### RecurringSuggestionDismissal(固定收支建議忽略清單,#252)
| 欄位 | 型態 | 說明 | 必填 |
| ------------ | --------------- | ---------------------------------------------- | ---- |
| user_id | TEXT PK(複合) | 建議所屬帳本資料擁有者 | 是 |
| signature | TEXT PK(複合) | 類型/分類/帳戶/幣別/金額的穩定簽章 | 是 |
| dismissed_at | INTEGER | 忽略時間(timestamp) | 是 |
智慧輔助總開關儲存於 `user_settings.ai_assist_enabled`(INTEGER,預設 1)。忽略清單僅隱藏提示,不會更改交易或固定收支。
#### RecurringSuggestionDismissal(固定收支建議忽略清單,#252)
| 欄位 | 型態 | 說明 | 必填 |
| ------------ | --------------- | ---------------------------------------------- | ---- |
| user_id | TEXT PK(複合) | 建議所屬帳本資料擁有者 | 是 |
| signature | TEXT PK(複合) | 類型/分類/帳戶/幣別/金額的穩定簽章 | 是 |
| dismissed_at | INTEGER | 忽略時間(timestamp) | 是 |
智慧輔助總開關儲存於 `user_settings.ai_assist_enabled`(INTEGER,預設 1)。忽略清單僅隱藏提示,不會更改交易或固定收支。
#### Stock(股票持倉)
| 欄位 | 型態 | 說明 | 必填 |
| ------------- | ------- | ----------------------------------------- | ---- |
| id | TEXT PK | 主鍵 | 是 |
| user_id | TEXT | 外鍵 → User | 是 |
| symbol | TEXT | 股票代號(如 2330) | 是 |
| name | TEXT | 股票名稱(如 台積電) | 是 |
| stock_type | TEXT | 類型:stock / etf / warrant(預設 stock) | 否 |
| current_price | REAL | 目前股價 | 否 |
| updated_at | TEXT | 更新時間 | 否 |
#### StockTransaction(股票交易)
| 欄位 | 型態 | 說明 | 必填 |
| ---------- | ------- | ---------------- | ---- |
| id | TEXT PK | 主鍵 | 是 |
| user_id | TEXT | 外鍵 → User | 是 |
| stock_id | TEXT | 外鍵 → Stock | 是 |
| type | TEXT | buy / sell | 是 |
| date | TEXT | 交易日期 | 是 |
| shares | REAL | 股數(整數) | 是 |
| price | REAL | 每股價格 | 是 |
| fee | REAL | 手續費(預設 0) | 否 |
| tax | REAL | 交易稅(預設 0) | 否 |
| note | TEXT | 備註 | 否 |
| created_at | TEXT | 建立時間 | 否 |
#### StockDividend(股票股利)
| 欄位 | 型態 | 說明 | 必填 |
| --------------------- | ------- | --------------------- | ---- |
| id | TEXT PK | 主鍵 | 是 |
| user_id | TEXT | 外鍵 → User | 是 |
| stock_id | TEXT | 外鍵 → Stock | 是 |
| date | TEXT | 除權息日期 | 是 |
| cash_dividend | REAL | 現金股利(元) | 否 |
| stock_dividend_shares | REAL | 股票股利(股) | 否 |
| account_id | TEXT | 外鍵 → Account | 否 |
| note | TEXT | 備註 | 否 |
| created_at | INTEGER | 建立時間(timestamp) | 否 |
---
### 3.3 API 端點
API 路徑統一以 `/api/` 為前綴。所有需認證的路由自動套用 authMiddleware(排除 `/api/auth/`、`/api/config`);管理員專用路由另套用 adminMiddleware。
#### 認證
| 方法 | 端點 | 說明 |
| ---- | ------------------------ | ------------------------------------------- |
| GET | /api/config | 取得前端設定(Google Client ID 等) |
| POST | /api/auth/register | 舊版密碼註冊相容端點(固定回 410,不建立帳號) |
| POST | /api/auth/login | 舊版密碼登入相容端點(固定回 410,不建立登入狀態) |
| POST | /api/auth/logout | 登出目前裝置(撤銷目前 session 並清除 Cookie) |
| GET | /api/auth/me | 取得當前使用者資訊 |
| POST | /api/auth/google | Google SSO 登入/註冊(驗證授權碼並簽發 JWT) |
| POST | /api/auth/line | LINE Login 登入/註冊(驗證授權碼並簽發 JWT) |
| GET | /api/account/login-logs | 取得目前使用者登入稽核紀錄(最近 100 筆) |
| GET | /api/account/sessions | 取得目前有效登入裝置 |
| DELETE | /api/account/sessions/:id | 登出指定登入裝置 |
| PUT | /api/account/password | 舊版密碼修改相容端點(固定回 410) |
#### Passkey(WebAuthn)
| 方法 | 端點 | 說明 |
| ------ | --------------------------------------------- | ------------------------ |
| POST | /api/account/passkeys/registration/options | 產生 Passkey 註冊 options |
| POST | /api/account/passkeys/registration/verify | 驗證 Passkey 註冊 |
| POST | /api/auth/passkeys/authentication/options | 產生 Passkey 登入 options |
| POST | /api/auth/passkeys/authentication/verify | 驗證 Passkey 登入 |
| GET | /api/account/passkeys | 取得使用者 Passkey 列表 |
| DELETE | /api/account/passkeys/:id | 刪除指定 Passkey |
#### 分類
| 方法 | 端點 | 說明 |
| ------ | ------------------- | --------------------------- |
| GET | /api/categories | 取得所有分類(含 parentId) |
| POST | /api/categories | 新增分類(支援 parentId) |
| PUT | /api/categories/:id | 更新分類名稱與顏色 |
| DELETE | /api/categories/:id | 刪除分類(連帶刪除子分類) |
#### 帳戶
| 方法 | 端點 | 說明 |
| ------ | ----------------- | -------------------------- |
| GET | /api/accounts | 取得所有帳戶(含計算餘額) |
| POST | /api/accounts | 新增帳戶 |
| PUT | /api/accounts/:id | 更新帳戶 |
| DELETE | /api/accounts/:id | 刪除帳戶 |
#### 行事曆
| 方法 | 端點 | 說明 |
| ---- | ---- | ---- |
| GET | /api/calendar?date=YYYY-MM-DD&view=month\|week | 依登入使用者時區回傳月/週視窗內的交易、股利及唯讀固定收支排程標記,並附 `dailyTotals`(依日期加總的實際交易收入/支出台幣金額,統計口徑同 Dashboard) |
#### 交易記錄
| 方法 | 端點 | 說明 |
| ------ | ------------------------------ | ---------------------------------- |
| GET | /api/transactions | 取得交易列表(分頁、篩選) |
| POST | /api/transactions | 新增交易記錄 |
| PUT | /api/transactions/:id | 更新交易記錄 |
| DELETE | /api/transactions/:id | 刪除交易記錄(含 linked 關聯刪除) |
| POST | /api/transactions/batch-delete | 批次刪除多筆交易 |
| POST | /api/transactions/batch-update | 批次更新多筆交易(分類/帳戶/日期) |
| POST | /api/transactions/import | 匯入 CSV 交易記錄 |
| POST | /api/transactions/transfer | 新增轉帳記錄(產生配對) |
| GET | /api/transactions/suggestions | 依摘要與歷史紀錄回傳 Top-3 分類建議(僅提示) |
#### 預算
| 方法 | 端點 | 說明 |
| ------ | ---------------- | -------- |
| GET | /api/budgets | 取得預算 |
| POST | /api/budgets | 新增預算 |
| PUT | /api/budgets/:id | 更新預算 |
| DELETE | /api/budgets/:id | 刪除預算 |
#### 固定收支
| 方法 | 端點 | 說明 |
| ------ | ------------------------- | ------------------ |
| GET | /api/recurring | 取得固定收支列表 |
| POST | /api/recurring | 新增固定收支 |
| PUT | /api/recurring/:id | 更新固定收支 |
| DELETE | /api/recurring/:id | 刪除固定收支 |
| PATCH | /api/recurring/:id/toggle | 切換啟用/停用 |
| POST | /api/recurring/process | 處理到期的固定收支 |
| GET | /api/recurring/suggestions | 掃描交易回傳週期性建議(僅提示) |
| POST | /api/recurring/suggestions/dismiss | 忽略某組週期性建議(僅影響提示) |
| GET/PUT | /api/user/settings/smart-assist | 讀寫智慧輔助總開關 |
#### 股票
| 方法 | 端點 | 說明 |
| ------ | ------------------------------------ | ------------------------------------------------ |
| GET | /api/stocks | 取得所有股票持倉(含 FIFO 損益計算) |
| POST | /api/stocks | 新增股票(含 stockType:stock / etf / warrant) |
| PUT | /api/stocks/:id | 更新股票資訊(含 stockType) |
| DELETE | /api/stocks/:id | 刪除股票(連帶刪除交易與股利記錄) |
| POST | /api/stocks/batch-price | 批次更新股價 |
| GET | /api/stock-realized | 取得實現損益紀錄(FIFO 逐筆,支援 stockId 篩選) |
| GET | /api/stock-transactions | 取得股票交易記錄(支援 stockId 篩選、分頁) |
| POST | /api/stock-transactions | 新增股票交易 |
| PUT | /api/stock-transactions/:id | 更新股票交易 |
| DELETE | /api/stock-transactions/:id | 刪除股票交易 |
| POST | /api/stock-transactions/batch-delete | 批次刪除多筆股票交易 |
| POST | /api/stock-transactions/import | 匯入股票交易記錄 CSV(自動建立不存在的股票) |
| GET | /api/stock-dividends | 取得股票股利記錄(支援 stockId 篩選、分頁) |
| POST | /api/stock-dividends | 新增股票股利 |
| PUT | /api/stock-dividends/:id | 更新股票股利 |
| DELETE | /api/stock-dividends/:id | 刪除股票股利 |
| POST | /api/stock-dividends/batch-delete | 批次刪除多筆股票股利 |
| POST | /api/stock-dividends/import | 匯入股票股利記錄 CSV(自動建立不存在的股票) |
| POST | /api/stock-dividends/sync | 從 TWSE 除權息公告自動同步股利紀錄 |
| GET | /api/stock-recurring | 取得股票定期定額清單 |
| POST | /api/stock-recurring | 新增股票定期定額 |
| PUT | /api/stock-recurring/:id | 更新股票定期定額 |
| DELETE | /api/stock-recurring/:id | 刪除股票定期定額 |
| PATCH | /api/stock-recurring/:id/toggle | 切換股票定期定額啟用狀態 |
| POST | /api/stock-recurring/process | 執行股票定期定額排程並產生買進交易 |
#### TWSE 證交所代理
| 方法 | 端點 | 說明 |
| ---- | ----------------------- | ----------------------------------------------------------------------------- |
| GET | /api/twse/stock/:symbol | 查詢股票名稱與股價(`?realtime=1` 即時 / `?date=` 盤後 / 無參數備援) |
| GET | /api/twse/search | 搜尋股票代號或名稱(query 參數,回傳前 10 筆) |
#### 報表
| 方法 | 端點 | 說明 |
| ---- | -------------- | -------------- |
| GET | /api/dashboard | 儀表板摘要資料 |
| GET | /api/reports | 統計報表資料;可選 `baseCurrency`(ISO 4217,未提供時維持 TWD) |
| GET | /api/reports/export | 匯出目前期間、類型與基準幣別的統計報表 CSV |
#### 匯率設定
| 方法 | 端點 | 說明 |
| ---- | ---------------------------- | --------------------------------- |
| GET | /api/exchange-rates | 取得匯率清單與自動更新設定 |
| PUT | /api/exchange-rates | 手動儲存匯率表 |
| PUT | /api/exchange-rates/settings | 更新是否啟用匯率自動更新 |
| POST | /api/exchange-rates/refresh | 從全球即時匯率 API 立即同步匯率 |
#### 管理員
| 方法 | 端點 | 說明 |
| ------ | ---------------------------------------- | --------------------------------------------------------- |
| GET | /api/admin/settings | 取得註冊策略(公開註冊、白名單) |
| PUT | /api/admin/settings | 更新註冊策略(公開註冊、白名單) |
| GET | /api/admin/users | 取得使用者清單 |
| POST | /api/admin/users | 建立使用者(可指定是否管理員) |
| DELETE | /api/admin/users/:id | 刪除指定使用者(不可刪除自己或最後管理員) |
| PUT | /api/admin/users/:id/password | 舊版密碼重設相容端點(固定回 410) |
| GET | /api/admin/login-logs | 取得管理員登入紀錄與全站使用者登入稽核(含失敗事件) |
| DELETE | /api/admin/login-logs/admin/:id | 刪除單筆管理員登入紀錄 |
| POST | /api/admin/login-logs/admin/batch-delete | 批次刪除管理員登入紀錄(`{ ids: [...] }`) |
| DELETE | /api/admin/login-logs/all/:id | 刪除單筆全站使用者登入紀錄 |
| POST | /api/admin/login-logs/all/batch-delete | 批次刪除全站使用者登入紀錄(`{ ids: [...] }`) |
| GET | /api/admin/email-providers | 取得寄信通道狀態(primary/fallback + 各通道是否設定) |
| POST | /api/admin/test-email | 寄送測試信(驗證寄信通道設定) |
| GET | /api/admin/report-schedule | 取得資產統計信件排程設定(**deprecated**,005 起改用 /api/admin/report-schedules 複數)|
| PUT | /api/admin/report-schedule | 更新資產統計信件排程設定(**deprecated**;同步寫入 report_schedules 表)|
| POST | /api/admin/report-schedule/run-now | 立即寄送一次(**deprecated**;改為迴圈所有 enabled=1 schedules)|
| GET | /api/admin/report-schedules | 列出多筆報表排程(005,FR-016 / Round 2 Q2) |
| POST | /api/admin/report-schedules | 新增單筆排程(不檢查 (user_id, freq) 唯一性) |
| PUT | /api/admin/report-schedules/{id} | 更新單筆排程(hour/weekday/day_of_month/enabled) |
| DELETE | /api/admin/report-schedules/{id} | 刪除單筆排程 |
| POST | /api/admin/report-schedules/{id}/run-now | 立即執行單筆排程(FR-017 / FR-021;503 時回 no_email_service)|
| GET | /api/admin/server-time | 取得伺服器時間與偏移量 |
| PUT | /api/admin/server-time | 設定伺服器時間偏移量 |
| POST | /api/admin/server-time/ntp-sync | 從 NTP 同步時間 |
| GET | /api/admin/db/export | 匯出資料庫備份(未加密 SQLite) |
| POST | /api/admin/db/import | 匯入資料庫備份(覆寫前自動備份) |
### 3.X 007-data-export-import 新增端點(v4.28.0)
| 方法 | 路徑 | 說明 |
| ------ | ------------------------------------------ | ---------------------------------------------------------- |
| GET | /api/transactions/export | 匯出交易記錄 CSV(伺服端 stream,UTF-8 BOM) |
| GET | /api/categories/export | 匯出分類結構 CSV(父分類在前 / 子分類在後) |
| POST | /api/categories/import | 匯入分類結構 CSV(原子化、互斥鎖、唯一鍵 type+name 略過) |
| GET | /api/stock-transactions/export | 匯出股票交易 CSV |
| GET | /api/stock-dividends/export | 匯出股票股利 CSV(帳戶欄位反查) |
| GET | /api/imports/progress | 查詢目前匯入進度(short polling,1s 一次) |
| GET | /api/admin/backups | 列出 backups/ 目錄內備份檔(管理員) |
| DELETE | /api/admin/backups/:filename | 手動刪除備份檔(管理員,path.basename + regex 防遍歷) |
| GET | /api/admin/data-audit | 列出全部稽核日誌(管理員,支援 user / action / 時間過濾) |
| GET | /api/user/data-audit | 列出我的操作紀錄(一般使用者,後端強制覆寫 user_id) |
| GET | /api/admin/data-audit/export | 匯出稽核日誌為 CSV(管理員) |
| POST | /api/admin/data-audit/purge | 清空稽核日誌(管理員) |
| GET | /api/admin/data-audit/retention | 讀取稽核日誌保留天數設定(管理員) |
| PUT | /api/admin/data-audit/retention | 更新保留天數(30/90/180/365/forever) |
| GET | /api/external-apis | API 使用與授權清單(公開端點,無需登入) |
**新增資料表**:
- `data_operation_audit_log`(10 欄 + 3 索引)— 資料操作稽核紀錄。
- `system_settings.audit_log_retention_days`(新增欄位)— 稽核日誌保留期。
- `backups/`(執行期子目錄)— 還原前自動備份檔,保留最近 5 份且 ≤ 90 天。
**端點行為強化**:
- `POST /api/transactions/import`、`/api/stock-transactions/import`、`/api/stock-dividends/import`:互斥鎖(重入回 409)+ 全 DB transaction 原子化 + 進度回饋每 500 筆 + ISO 8601 嚴格 + 重複偵測(六欄 / 四欄)+ CSV 額外欄位 silent drop + 寫入稽核日誌。
- `GET /api/database/export`:檔名格式改為 `assetpilot-backup-{YYYYMMDDHHmmss}.db`。
- `POST /api/database/import`:必要表加入 stocks;通過驗證後寫入 `backups/before-restore-{ts}.db`;替換失敗自動回滾(422 RESTORE_FAILED_ROLLED_BACK);雙重失敗回 500 RESTORE_FAILED_DB_UNKNOWN 含可用備份檔清單;保留 5 份 + 90 天清理。
- `PUT /api/exchange-rates`、`GET /api/exchange-rates/:currency`:新增 ISO 4217 白名單前置驗證。
---
## 4. 版本歷程
### 4.1 使用案例摘要
| 編號 | 使用案例 | 主要角色 |
| ------ | ------------------------- | -------- |
| UC-001 | 註冊帳號 | 訪客 |
| UC-002 | 登入系統 | 使用者 |
| UC-003 | 新增支出/收入記錄 | 使用者 |
| UC-004 | 新增轉帳記錄 | 使用者 |
| UC-005 | 批次刪除交易記錄 | 使用者 |
| UC-006 | 批次變更交易分類 | 使用者 |
| UC-007 | 檢視當月收支 | 使用者 |
| UC-008 | 設定月度/分類預算 | 使用者 |
| UC-009 | 檢視統計報表 | 使用者 |
| UC-010 | 管理分類與子分類 | 使用者 |
| UC-011 | 帳戶間轉帳 | 使用者 |
| UC-012 | 設定固定收支 | 使用者 |
| UC-013 | 匯出交易記錄 CSV | 使用者 |
| UC-014 | 匯入交易記錄 CSV | 使用者 |
| UC-015 | 匯出/匯入分類結構 | 使用者 |
| UC-016 | 新增股票持倉(TWSE 查詢) | 使用者 |
| UC-017 | 記錄股票買賣交易 | 使用者 |
| UC-018 | 記錄股票股利 | 使用者 |
| UC-019 | 批次更新股價(TWSE) | 使用者 |
| UC-020 | 檢視投資組合損益 | 使用者 |
| UC-021 | 匯出股票交易/股利紀錄 CSV | 使用者 |
| UC-022 | 匯入股票交易/股利紀錄 CSV | 使用者 |
| UC-023 | 檢視實現損益紀錄 | 使用者 |
| UC-024 | 自動同步除權息股利 | 使用者 |
| UC-025 | Google SSO 登入 | 訪客 |
| UC-026 | 批次刪除股票交易紀錄 | 使用者 |
| UC-027 | 批次刪除股利紀錄 | 使用者 |
| UC-028 | 調整公開註冊策略 | 管理員 |
| UC-029 | 管理 Email 註冊白名單 | 管理員 |
| UC-030 | 建立使用者帳號 | 管理員 |
| UC-031 | 刪除使用者帳號 | 管理員 |
| UC-032 | 查詢個人登入紀錄 | 使用者 |
| UC-033 | 查詢全站登入稽核 | 管理員 |
| UC-034 | 修改顯示名稱 | 使用者 |
| UC-035 | 設定股票定期定額 | 使用者 |
| UC-036 | 投資組合績效分析(XIRR/TWR/歸因) | 使用者 |
### 4.2 版本歷程
| 版本 | 日期 | 變更說明 |
| --- | --- | --- |
| 4.149.1 | 2026-10-15 | 修復投資組合績效「部分持股缺乏歷史收盤價」警告:`lib/stockHistory.ts` 將 TWSE/TPEx 月度請求並行數上限設為 4、失敗重試 3 次(線性退避)、非 2xx 回應拋錯而不寫入快取、容許尚無資料的當月,避免上游限流的空結果被快取為有效資料;`/api/stock-performance` 於價格抓取失敗時記錄警告。 |
| 4.149.0 | 2026-10-15 | 新增 #310 投資組合績效分析:新增 `/stocks/performance` 頁面與 `/api/stock-performance` 路由,提供金額加權報酬率 (XIRR,Newton-Raphson 求解,Actual/365)、時間加權報酬率 (TWR,分割期間連乘)、大盤基準比較 (S&P 500 / 0050) 與五因子收益歸因(期初/期末市值、已實現、未實現變動、股利、手續費與稅,維持金額守恆);支援區間快選、資產市值與累計淨投入走勢圖、現金流明細,以及 CSV / Excel 匯出與帳本權限政策整合。新增 10 種語言完整翻譯與自動化單元測試。 |
| 4.141.0 | 2026-10-08 | 新增 #250 收據 OCR 自動帶入:新增交易可上傳收據影像辨識金額/日期/店家,僅以草稿預填、由使用者確認後儲存;HTTP OCR 供應商可由環境變數設定,未設定時回報無供應商並保留手動輸入。OCR API 不觸發自動背景寫入,讀取既有附件沿用 photoCrypto 與 S3 授權路徑。 |
| 4.140.0 | 2026-10-08 | 行事曆日期格在既有交易/股利/排程筆數徽章之外,新增顯示當日收入/支出金額加總(台幣,收入綠字 `+`、支出紅字 `−`)。加總口徑與 Dashboard 一致:只計實際交易(`type IN ('income','expense')`,排除轉帳)且排除「不計入統計」交易;股利與固定收支排程僅為預計金額,維持現有筆數徽章、不併入加總。`app/api/calendar/route.ts` 新增 `GROUP BY date, type` 的 `SUM(amount)` 查詢並以 `lib/calendarEvents.ts` 新增的零相依 `buildCalendarDailyTotals()` 彙總為 `dailyTotals` 回傳;`components/features/calendar/CalendarClient.tsx` 於日期格與 `aria-label` 同步呈現。新增 `incomeAmount`/`expenseAmount` 2 個行事曆多語系鍵並重新產生 Web 與 Flutter 共用翻譯;`tests/lib/calendar.test.ts` 補上日期範圍與型別過濾的彙總測試。 |
| 4.139.0 | 2026-10-08 | 新增 #252 本地智慧分類建議與固定收支偵測:新增交易依歷史摘要推薦 Top-3 子分類及信心度;偵測疑似週期交易並提供確認後建立固定收支的表單預填,支援忽略提示與智慧輔助開關。建議使用可解釋本地統計,無外部 AI 依賴;提示 GET 不觸發排程寫入,歷史資料以帳本範圍隔離。 |
| 4.138.0 | 2026-10-08 | 新增 #263 台股進階功能:股利再投資(DRIP)將股利轉為買入交易;新增當沖證交稅減半計算設定,並於相關投資報表和交易摘要呈現。 |
| 4.134.0 | 2026-10-08 | 新增 #260 目標儲蓄與還款計畫:建立可綁定帳戶餘額或分類支出的儲蓄目標,追蹤進度、預估達成日與落後提醒;建立本息平均攤還計畫與逐期攤還表,金額以 decimal.js 計算。新增個人化儀表板提醒、帳本授權 API、PostgreSQL schema、10 語言介面與自動化測試。 |
| 4.133.0 | 2026-10-07 | 修補 #285 Webhook 重新導向 SSRF 弱點:投遞前驗證 URL 與 DNS 解析位址,以已驗證公開 IP 連線並拒絕 3xx,避免簽章 payload 被轉送至內部或特殊保留目的地。 |
| 4.131.0 | 2026-10-07 | #255 多幣別報表基準幣別切換:報表可切換 ISO 4217 基準幣別,採 decimal.js 全精度換算,顯示匯率來源與實際時間戳,並以相同幣別匯出 CSV。 |
| 4.119.0 | 2026-10-07 | 新增 #262 共享帳本:空白帳本、Email 單次邀請與 owner/editor/viewer 角色;記帳資料 API 驗證帳本成員與寫入權限,稽核實際操作者及結果;支援離開、移除、owner 移交與刪除帳號保護,資料仍屬帳本;Web 提供帳本切換與管理,離線佇列保留原帳本歸屬。既有個人資料不自動共享;投資及 Flutter/MCP/API Token/LINE/通知排程後續另案。新增 PostgreSQL 授權整合及 Web E2E/axe 測試。 |
| 4.117.0 | 2026-10-06 | 新增 `/settings/api-integration` 設定頁 UI,讓使用者以介面(而非 `curl`)管理 #258 提供的 API Token 與 Webhook。新增 `components/features/settings/ApiIntegrationSettingsClient.tsx` 與對應頁面 `app/settings/api-integration/page.tsx`:Token 區可勾選權限範圍(`transactions:read`/`transactions:write`/`webhooks:manage`)建立、列出(名稱/前綴/範圍/狀態/建立、最後使用、到期時間)並撤銷;Webhook 區可建立、行內編輯(網址/事件/啟用狀態)與刪除訂閱,並可依訂閱篩選投遞紀錄(事件類型/狀態/嘗試次數/狀態碼/錯誤訊息)。建立 Token 與 Webhook 後皆以一次性 Modal 顯示明文權杖/簽章密鑰,必須按「我已複製」才能關閉,離開頁面即不再顯示;簽鑰 Modal 另附 `X-AssetPilot-Signature: t=<unix 秒>,v1=<hex>` 格式與時間戳容忍度說明。新增零相依 `lib/apiIntegrationUi.ts`(scope/event 選項、預設值、勾選切換與回應值正規化),刻意不 import `lib/apiTokenCore.ts`,避免把 `node:crypto` 帶進 client bundle;改由 `tests/lib/apiIntegrationUi.test.ts` 斷言 UI 清單與 `API_TOKEN_SCOPES`/`WEBHOOK_EVENTS` 一致,並驗證所有 labelKey 都存在於 zh-TW 字典(防止動態鍵繞過 `check:i18n` 的靜態鍵掃描)。`components/layout/Sidebar.tsx` 與 `AppLayout` 新增 `nav.apiIntegration` 導覽項目與頁首標題。i18n:`shared/i18n/app_*.arb` 新增 94 鍵(`nav.apiIntegration`、`common.refresh`/`copy`/`copied`、`settings.apiIntegration.*`),經 `npm run i18n:generate` 重新產生 Web 字典與 Flutter l10n 產物,10 語言對齊。測試:新增 `tests/lib/apiIntegrationUi.test.ts`(9 項,零相依、無需 PostgreSQL)並納入 `npm test`。複製一次性憑證時只在 `navigator.clipboard.writeText()` 成功後才標記為已複製,失敗(例如非安全脈絡下自架的 `http://<區網 IP>:3000`)會顯示 `settings.apiIntegration.copyFailed` 並維持關閉鈕停用,避免使用者未實際複製即關閉視窗而永久遺失明文;載入投遞紀錄以遞增序號認領請求,只採用最後一次回應,避免切換篩選時舊回應覆蓋新結果。驗證:`npm run typecheck`、`npm test`(含 `check:iso`、`check:i18n` 1,517 鍵對齊)、`npm run build`(`/settings/api-integration` 已列入路由輸出)皆通過。 |
| 4.115.1 | 2026-10-06 | 將 `source-map-js` 更新至 1.2.2 以上、`proxy-addr` 更新至 2.0.8 以上、`fast-copy` 更新至 4.1.0 以上,修補 CVE-2026-93749(GHSA-68fv-2mgg-jv7q)、CVE-2026-90711(GHSA-jqcg-44mw-7w3h)及 GHSA-jggr-w7fw-pc2j;確認所有解析出的相依副本均達修補版本。 |
| 4.115.0 | 2026-10-06 | 新增 `/finance/calendar` 與登入限定 `/api/calendar`,以月/週切換檢視交易、既有股利日期和啟用中固定收支排程(排程與股利唯讀);日期格顯示同日摘要並在選取後展開細節,新增交易按鈕會將選取日期帶入既有交易表單。`lib/calendarDates.ts` 以 UTC 日期元件做純日期區間計算,初始「今天」及 API 今日值依 `users.timezone`;新增 `tests/lib/calendar.test.ts` 涵蓋週/月邊界、閏日、使用者時區跨日及三種事件聚合,並納入 `npm test`。新增 25 個行事曆/導覽多語系鍵並重新產生 Web 與 Flutter 共用翻譯。 |\n| 4.114.0 | 2026-10-05 | 新增 TWSE/TPEx 歷史日 OHLC 查詢與支援日/週/月切換的股價 K 線圖,並標示交易買入/賣出。 |
| 4.113.0 | 2026-10-05 | 新增 PWA 支援與離線記帳(007-pwa-offline-entry)。PWA:`public/manifest.webmanifest`(名稱、`theme_color` 與亮色主題一致、192/512 一般與 512 maskable 圖示、新增交易/儀表板快捷方式)、`public/icons/*.png`(由 `logo.svg` 以 sharp 產生)、`app/layout.tsx` 輸出 manifest 連結與 `generateViewport` 的 `theme-color`;`components/features/pwa/PwaRegistrar.tsx` 於 production 註冊 Service Worker 並監聽 `beforeinstallprompt` 提供一鍵安裝提示(可關閉)。Service Worker:`public/sw.js` install 階段 pre-cache 公開資源、導覽 network-first、離線回退公開的 `/offline` 說明頁;**不快取 `/api/` 回應、也不快取已登入 HTML 頁面**,避免顯示過期餘額或在共用裝置外洩帳戶畫面;`next.config.ts` 的 CSP 新增 `worker-src 'self'` 與 `manifest-src 'self'`(維持 `default-src 'self'` 不變),並為 manifest/圖示/`sw.js` 設定對應 `Cache-Control`(`sw.js` 為 `no-store`);`proxy.ts` 讓 `/sw.js`、`/manifest.webmanifest`、`/icons/*`、`/offline` 免於登入導向,否則瀏覽器無法註冊 SW。離線記帳:新增零相依純模組 `lib/offlineQueueCore.ts`(佇列形狀、解析容錯、去重、容量上限 200、指數退避 5 秒至 5 分鐘、最多自動重試 5 次、`isRetryableStatus`)與瀏覽器 I/O 模組 `lib/clientOfflineQueue.ts`(localStorage 持久化、`online` 事件自動送出、併發送出共用 in-flight promise);`components/features/transactions/TransactionsClient.tsx` 於新增收支/轉帳遇連線層失敗(新增 `lib/clientApi.ts` 的 `isNetworkError`)時改存佇列並以 Toast 提示;`components/features/offline/OfflineSyncStatus.tsx`(掛載於 `AppLayout`)顯示離線/待同步筆數並對失敗項目提供重試/捨棄。衝突策略:`lib/db.ts` 新增 `transactions.client_ref TEXT NOT NULL DEFAULT ''` 與 `(user_id, client_ref)` 部分唯一索引;`lib/transactionWriteCore.ts` 的 `insertIncomeExpenseTransaction`/`insertTransferPair` 接受 `clientRef`,重送時改讀已寫入的列並以與新建相同的回應形狀回傳(伺服器為最後寫入權威、冪等),並處理併發競態;`app/api/transactions/route.ts` 與 `.../transfer/route.ts` 接受選填 `clientRef`(`^[a-f0-9]{32}$`,格式不符視為未提供)。i18n:`shared/i18n/app_*.arb` 新增 `features.offline.*`(10 鍵)與 `features.pwa.*`(3 鍵),重新產生 Web 字典與 Flutter l10n。測試:新增 `tests/lib/offlineQueue.test.ts`(13 項純函式)、`tests/lib/clientApiOffline.test.ts`(1 項連線錯誤分類)、`tests/lib/offlineClientQueue.test.ts`(9 項瀏覽器佇列流程,含跨使用者隔離、持久化失敗、線上重試)、`tests/lib/offlineIdempotency.test.ts`(5 項需 PostgreSQL;轉帳案例在缺少歷史 `to_account_id` 欄位的新 schema 明確 skip),皆納入 `npm test`。驗證:`npm run typecheck`、`npm test`(含 `check:i18n` `check:iso`)、`npm run build`、以本機 PostgreSQL 執行兩支新測試全數通過;實測 `next start` 下 manifest/`sw.js`/圖示/`/offline` 回應標頭與內容正確,`/dashboard` 未登入仍 307 導向 `/login`、`/api/transactions` 未登入仍 401。已補正轉帳目的帳戶欄位的 PostgreSQL schema 相容性:以 `transfer_to_account_id` 作為 canonical 欄位,`lib/db.ts` 偵測舊 schema 的 `to_account_id` 並將既有值回填;轉帳寫入/讀取、信用卡還款與刪除帳戶引用查詢均改用 canonical 欄位,全新與既有 PostgreSQL schema 均可建立/讀取轉帳。 |
| 4.112.3 | 2026-09-01 | 修補第三方滲透測試報告列出的 16 項漏洞(injection/auth/authz/SSRF)。Injection:`lib/postgresBackup.ts` 新增 `assertRestorableBackupSql()`,還原前以 tokenizer 逐一驗證每個 top-level SQL 陳述式須符合 `createPostgresBackupSql()` 自身產生的固定文法(僅允許 BEGIN/COMMIT/DROP TABLE IF EXISTS/CREATE TABLE/ALTER TABLE ADD CONSTRAINT/CREATE INDEX/INSERT INTO),DDL 陳述式另以危險函式黑名單檢查,並加上檔案大小與陳述式數量上限;`lib/transactionAttachments.ts` 的 `restoreAttachmentFromBundle()` 要求 bundle 內 `transaction_id`/附件 `id` 須符合伺服器產生的 32 碼十六進位格式,阻斷路徑穿越。Authn/Authz:`lib/apiHelpers.ts` 的 `requireAuth()`、`lib/mcpAuth.ts` 的 `verifyMcpToken()`(改為 JOIN users)與 Google/LINE/Passkey/App ticket 四條既有使用者登入路徑新增 `users.is_active` fail-closed 檢查(判斷式抽至零相依的新檔 `lib/userActive.ts`,避免 `mcpAuth.ts` 意外引入 `next/headers` 相依鏈);`lib/sessionHelpers.ts` 的 `verifyLoginSession()` 新增閒置逾時(`SESSION_IDLE_TIMEOUT_DAYS`,預設 14 天,`last_seen_at` 節流寫入)。OAuth CSRF:`lib/lineOAuthState.ts`/`lib/googleOAuthState.ts` 的 state 記錄新增 `bindingToken`(web 走 httpOnly cookie、行動 App 走 JSON 欄位顯式回傳,`mobile/lib/api_client.dart`/`line_auth.dart`/`google_auth.dart` 同步調整),LINE 綁定流程另需在核發時綁定已登入 `userId`/`sessionId` 並於核銷時比對;`app/api/auth/line/route.ts`、`app/api/account/link-line/route.ts` 改為嚴格核對 state 的 `flow` 欄位,杜絕以連結流程繞過登入流程的 Turnstile 驗證。SSRF:`lib/s3Storage.ts` 新增 `assertSafeS3Endpoint()`(僅接受 HTTPS、DNS 解析後排除私有/迴環/連結本地/多播位址範圍,含 IPv4/IPv6),`putS3Object`/`getS3Object`/`deleteS3Object` 一律先驗證、`redirect:'manual'` 並限制逾時與回應大小;`app/api/database/mega-s4/route.ts` 的 PUT 端點存檔前先行驗證。App ticket:`lib/appAuthTicket.ts` 新增 `deviceNonce` 綁定(App 端 `mobile/lib/passkey_auth.dart` 產生並僅透過 outbound URL 傳遞,兌換時需原樣附上)。基礎設施:`proxy.ts`/`lib/loginHelpers.ts` 改用新增的零相依 `lib/requestIp.ts`(預設信任 XFF 最後一段而非可偽造的第一段)與 `lib/rateLimit.ts`(Map 容量上限保護);`docker-compose.yml` 的對外連接埠改綁 `127.0.0.1`;`next.config.ts` 正式環境新增 HSTS 標頭;同時修補 `app/api/account/passkey/register/route.ts`/`app/api/auth/passkey/login/route.ts` 的 WebAuthn origin 自我比對問題(改用新增的 `lib/originPolicy.ts` 允許清單,fail-closed)。回歸測試:`tests/lib/mcpAuth.test.ts` 修正測試 fixture(`createMcpCredential` 前需先建立對應 `users` 列)並新增停用帳號 PAT 立即失效的回歸測試。驗證:`npm run typecheck`(TypeScript 5.7.3,因 TS7 native-preview 於本機 Windows 缺對應平台二進位改用)、`dart analyze` 全數通過;以臨時 PostgreSQL 容器執行 `test:mcp-auth`(14/14)、`test:mcp-oauth`(17/17)、`test:db-migrations`(4/6,2 項失敗為既有問題與本次變更無關,已用 `git stash` 對照原始程式碼確認)。 |
| 4.112.1 | 2026-08-26 | 修正品牌入場動畫圓環未置中問題:`app/globals.css` 新增 `.splash-logo-wrap` 包裹容器並賦予 `.splash-logo` 定位語境,`.splash-ring` 改以 `top:50%; left:50%; margin:-84px 0 0 -84px` 精準置中於圖示,並移除與 `animation-name` 多值宣告衝突的 `animation` 簡寫及重複 `opacity:0`;`components/public/SplashIntro.tsx` 同步調整 DOM 結構以套用新容器。驗證:`npm run typecheck` 通過。 |
| 4.112.0 | 2026-08-26 | 新增品牌入場動畫:`components/public/SplashIntro.tsx` 於 `app/layout.tsx` 掛載,網頁載入時顯示一次性 CSS keyframes 動畫(logo 彈出、漸層光暈、進度條),以 `sessionStorage` 記錄同一工作階段只播放一次,並支援 `prefers-reduced-motion`;對應樣式定義於 `app/globals.css`。Flutter App 新增 `mobile/lib/screens/splash_screen.dart`,`app.dart` 新增 `RootGate` 於啟動時先播放品牌動畫(`AnimationController` 驅動 logo 彈跳、漸層背景、進度條),完成後才切換至原有 `AuthGate` 依登入狀態導向。另外 `mobile/analysis_options.yaml` 的 analyzer 排除清單新增 `build/`/`android/`/`ios/`/`web/`/`windows/`/`macos/`/`linux/` 平台產生目錄,避免這些自動產生檔案污染靜態分析結果。驗證:`npm run typecheck` 通過。 |
| 4.111.2 | 2026-08-26 | 新增 Google Play 商店與官網行銷截圖素材:`docs/screenshots/{app,web}/{light,dark}/{locale}` 存放真實 UI 截圖(含登入頁 Google SSO 選項),`docs/android-light`/`docs/android-dark`/`docs/web-marketing` 存放帶標題文案的正式行銷截圖(透過 app-store-screenshots skill 匯出);`docs/marketing-screenshots/{app-dark,app-light,web}` 為對應的可編輯截圖專案(Next.js,含 `app-store-screenshots.json` 版面設定)。`README.md`「螢幕截圖」章節改用上述真實截圖,移除從未實際產生內容的 `marketing-screenshots/{zh,en,ar}`、`web-marketing-screenshots/{zh,en,ar}` 失效連結與過時的登入流程截圖,並重寫專案優勢/目錄結構。`.gitignore` 新增 `docs/android-light`/`docs/android-dark`/`docs/web-marketing` 允許規則、`docs/marketing-screenshots/*/*.log` 忽略規則;`mobile/.gitignore` 新增 `/web/`,排除擷取截圖時暫時建立、不隨產品出貨的 Flutter Web 平台檔案。另外,`lib/db.ts` 新增 `alterIgnore` 補建 `stocks.shares`/`stock_transactions.shares`/`stock_dividends.shares`(`NUMERIC DEFAULT 0`),修正部分舊版 PostgreSQL 部署因欄位缺失導致 4.111.0 數值型別遷移失敗的問題;`app/globals.css` 修正 `--dashboard-ink`/`--dashboard-ink-soft` 淺色模式配色誤用暗色系數值,導致儀表板頂部卡片與訊號列文字在亮色模式下難以閱讀的問題。驗證:README 圖片路徑全數對應檔案系統中存在的檔案,`git status` 確認無殘留建置產物或未過濾的暫存記錄檔。 |
| 4.111.1 | 2026-08-24 | 修正 `lib/postgresMigration.ts` 的 SQLite 搬移檔案處理:`sql.js` WebAssembly 資產改由固定套件檔案定位,不再將 callback 提供的名稱直接組入路徑;寫入搬移 metadata 的來源路徑改僅保存檔名,避免靜態安全掃描將該路徑判定為可穿越的檔案存取。資料內容、PostgreSQL schema/constraint/index 搬移行為不變。驗證:`npm run typecheck`、`git diff --check` 通過。 |
| 4.111.0 | 2026-08-24 | PostgreSQL 資料完整性與寫入安全修正:`lib/db.ts` 新增 `monthly_report_send_log` 及其唯一索引/使用者外鍵,並將 `credit_card_repayment_summaries` 納入帳號刪除與資料匯出/還原;金額與投資數值欄位改用 `NUMERIC`,補上有限值、非負值、日期及核心股票關聯檢查。`lib/postgresRuntime.ts`/`lib/pgSyncWorker.cjs` 改為 transaction ID 對應 request-scoped client,禁止跨 async boundary 共用交易;`lib/transactionWriteCore.ts`、`lib/overseasFee.ts`、股票股利寫入與附件還原補償改為複合操作完整成功或回復。`lib/postgresBackup.ts`/`lib/postgresMigration.ts` 保留 UNIQUE/CHECK/FK constraints 與 indexes,SQLite REAL/INTEGER 轉 PostgreSQL 時依資料形狀保留 NUMERIC/BIGINT 精度。同步修正帳戶 PATCH 未提供欄位重設、無效轉帳日期靜默改為今天等問題。驗證:`npm run typecheck`、`npm test`、`npm run build`、`git diff --check` 通過;需 PostgreSQL 的 migration/transaction 整合案例因本機未設定 `DATABASE_URL`/`POSTGRES_URL` 略過。 |
| 4.110.1 | 2026-08-24 | 修正 Android App 登入頁移除本機密碼介面後多出一層版面結束符,導致 Dart 編譯器在正式版建置時於登入頁報語法錯誤。移除多餘結束符,Google、LINE、Passkey 登入流程與外部身份政策不變。驗證:CI Android/MobSF 建置流程應可繼續執行。 |
| 4.110.0 | 2026-08-24 | 移除本機密碼認證並限制為外部身份服務:新增 `lib/authPolicy.ts`,`/api/auth/login`、`/api/auth/register`、帳號密碼設定與管理員密碼操作固定回傳 HTTP 410 `password_auth_disabled`;Google/LINE OAuth 建立或更新使用者時強制 `has_password = 0`,既有 migration 同步停用本機密碼。Web 登入頁與行動 App 僅保留 Google、LINE、Passkey,刪除行動註冊畫面及密碼設定/管理 UI;帳號刪除改以 Email 確認,解除外部綁定時要求保留其他登入方式。同步更新 `asset_openapi.yaml`、README、隱私政策、環境變數註解,移除 `bcryptjs` 依賴。驗證:`npm run typecheck`、`npm run build`、`git diff --check` 通過;資料庫整合測試因本機未設定 `DATABASE_URL` 略過。 |
| 4.109.2 | 2026-08-23 | 修正行動 App 儀表板切換月份資料不變:`mobile/lib/api_client.dart` 的 `dashboard()` 以 `?ym=` 查詢參數呼叫,但 `app/api/dashboard/route.ts` 僅讀取 `yearMonth`,參數被忽略後一律回退使用者時區的當月,導致切換任何月份都顯示當月資料。改送 `?yearMonth=`(與 Web 端一致),並同步修正 `mobile/lib/models.dart` 的 API 文件註解。驗證:`flutter analyze lib/api_client.dart lib/models.dart` 無問題、grep 確認無殘留 `?ym=` 呼叫。 |
| 4.109.1 | 2026-08-23 | 修正 LINE Flex Message 送出失敗:`lib/lineMessaging.ts` 的水平明細列原使用 `alignItems: "start"`,LINE Box schema 僅接受 `flex-start`/`center`/`flex-end`,導致回覆 API 以 400 拒絕整張 Flex bubble。改用 `flex-start` 保留明細標籤和值的頂端對齊,並新增 `tests/lib/lineMessaging.test.ts` payload 回歸斷言;既有 postback data、權限檢查、照片附件與交易流程不變。 |
| 4.109.0 | 2026-08-23 | LINE 官方帳號記帳流程與 Flex Message 顯示優化:新增 `lib/lineRecordParser.ts` 集中處理今天/昨天、日期與直接記帳輸入,無效日期不再靜默改成今天;`app/api/line/webhook/route.ts` 直接記帳優先選擇相同幣別帳戶,並與 `insertIncomeExpenseTransaction()` 共用外幣信用卡手續費與交易欄位寫入規則。`lib/lineMessaging.ts` 抽出共用 header/body/footer 與標籤/值明細列,主選單、逐步記帳、查詢、完成摘要與舊版提示卡統一呈現;既有 postback data、資料權限與按鈕上限不變。新增 `tests/lib/lineRecordParser.test.ts`/`tests/lib/lineMessaging.test.ts` 與 `package.json` 測試腳本。驗證:`npm run typecheck`、LINE targeted tests、`npm test`(既有 `check:i18n` 產物過期而停止)、`npm run build` 162 頁、LSP 與 `git diff --check` 通過;PostgreSQL 整合測試因本機未設定 DB URL 略過。 |
| 4.108.0 | 2026-08-23 | 美股交易支援正數小數股數,台股維持整數股數契約。`lib/stockMarket.ts` 新增市場感知的股數驗證與定期定額股數計算;`app/api/stock-transactions/route.ts`、`[id]/route.ts` 與匯入路徑允許 US 小數股數並仍拒絕 TW 小數股數,`lib/stockRecurringHelpers.ts` 允許美股定期定額依金額/價格產生小於 1 股的正數數量。Web `StockTxClient` 與 Flutter 股票交易表單依市場切換 decimal/integer 輸入,既有 USD 計價、FIFO、持股與損益計算不變。新增 US 小數股數、台股整數限制與定期定額計算回歸測試;驗證 `npm test`、`npm run typecheck`、`npm run check:i18n`、隔離輸出 `npm run build`、Flutter targeted analyze 與 `git diff --check` 通過,PostgreSQL 整合測試因本機未設定 DB URL 略過。 |
| 4.107.0 | 2026-08-22 | 股票模組新增美股市場支援:`stocks` 及股票交易、股利、定期定額資料以 `market` 區分 `TW`/`US`,既有資料預設為 TW/TWD;美股以 USD 儲存並接入 Yahoo Finance 報價與股利同步。股票 CRUD、交易費用與交易稅、FIFO 已實現損益、混合幣別持股換算、批次/自動報價、CSV 匯入匯出與定期定額處理均改為市場感知,US 費用/稅額預設為 0 且保留手動覆寫。Web `PortfolioClient`、交易/股利/損益/設定頁與 Flutter `Stock` model/股票畫面同步新增市場、幣別及 USD 格式化;`asset_openapi.yaml`、MCP 股票讀取結果、外部 API attribution、shared i18n 產生輸出同步更新。新增 Yahoo quote/parser 與美股費率測試,驗證 `npm test`、`npm run typecheck`、`npm run check:i18n`、`npm run build`、Flutter targeted analyze、`git diff --check` 通過;PostgreSQL 整合測試因本機未設定 DB URL 略過。 |
| 4.106.0 | 2026-08-22 | Flutter Android App UI refresh:`mobile/lib/app.dart` 新增依窗口寬度切換的 Compact NavigationBar 與 Medium/Expanded NavigationRail;`theme.dart`/`widgets.dart` 建立 Calm Ledger 語意色、opaque surface、Material 3 controls、loading/error/empty shared surfaces。`dashboard_screen.dart` 重排 period context → net → cash flow → categories → recent transactions;`transactions_screen.dart` 新增 active filter count、explicit overflow actions、card rows;`stocks_screen.dart` 改以 view selector;`more_screen.dart` 分組 finance/settings/help;`transaction_form_screen.dart` 增加 SafeArea/IME-aware sticky footer。保留既有 REST、models、JWT/Cookie、OAuth/deep link、Turnstile、shared i18n、fl_chart、Sentry;`api_client.dart` 僅將已安裝的 `flutter_secure_storage` 11 API 由移除的 `encryptedSharedPreferences` 參數改為 AndroidOptions defaults。驗證:`flutter test` 22/22;`flutter analyze` 僅既有 Sentry experimental warning;`git diff --check` 通過。APK debug build 受現有 compileSdk 36 與 flutter_secure_storage 要求 37 不一致阻擋。 |
| 4.105.0 | 2026-08-22 | Dashboard 新增財務訊號列,集中顯示待處理提醒、現金展望期末餘額與投資組合未實現損益;提醒與兩項財務訊號皆可直接前往對應頁面,手機版改為直向排列並保留 RTL、鍵盤焦點與 reduced-motion 支援。使用既有 Dashboard 資料與翻譯,不變更 API contract、計算邏輯、schema 或 dependency。驗證:`npm run typecheck`、Dashboard targeted tests、`npm run build`、`git diff --check` 通過。 |
| 4.103.0 | 2026-08-16 | MCP 新增三個信用卡還款工具(見 `specs/007-mcp-credit-card-repayment/`)。`lib/creditCardRepayment.ts` 新增三個共用函式 `computeRepaymentAllocation()`/`executeRepayment()`/`evaluateRepaymentSummary()`,從既有兩支 HTTP 路由(`POST /api/accounts/credit-card-repayment`、`GET /api/credit-card-repayment-summaries/{id}`)原樣抽出,供既有路由與三個新 MCP 工具共用同一份計算與寫入邏輯,既有路由行為不變。`lib/mcpServer.ts` 的 `buildMcpServer()` 新增三個工具:`get_credit_card_repayment_preview`(唯讀試算,無條件註冊、不受 `allowCreate` 影響,驗證規則與錯誤訊息逐字重用寫入工具,但不呼叫 `executeRepayment()`,在協定層面不觸及任何寫入路徑)、`list_credit_card_repayments`(唯讀查詢,分頁+日期篩選,對每一列呼叫 `evaluateRepaymentSummary()` 標示各卡 `intact`/`modified`/`deleted` 與整體 `stale`)、`create_credit_card_repayment`(以一個總金額送出還款,僅在 `credential.allowCreate === true` 時才 `registerTool()`,與 `create_transaction` 綁同一旗標同進同出;驗證規則 V1–V5 逐字重用既有 `POST` 路由,以送出當下重新 `collectPayableCards()` 計算,呼叫 `computeRepaymentAllocation()` + `executeRepayment()` 寫入並標記 `aiCreated: true`,支援 `idempotencyKey` 冪等鍵沿用既有 `mcp_transaction_idempotency` 表 24 小時 TTL,成功後以 `withAudit()` 寫入 `action: 'mcp_create_credit_card_repayment'`、`metadata.repayment_summary_id`)。`lib/auditHelpers.ts` 的 `AUDIT_METADATA_ALLOWED_KEYS` 新增 `repayment_summary_id`(刻意不重用 `transaction_id`,避免稽核閱讀者誤判查詢對象);`components/features/admin/AdminClient.tsx` 的 `AUDIT_ACTION_LABELS` 新增 `mcp_create_credit_card_repayment` 中文標籤「MCP 信用卡還款」。新增 `tests/lib/mcpCreditCardRepayment.test.ts`(需 `DATABASE_URL`,涵蓋三個工具端到端、V1–V5 拒絕、`ai_created` 標記、冪等鍵、稽核),擴充 `tests/lib/mcpDescriptors.test.ts` 驗證 `allowCreate=true` 憑證 `tools/list` 額外出現 `create_credit_card_repayment` 且 annotations 標示可寫非破壞性、兩個唯讀工具在未開 `allowCreate` 時仍可見,並擴充 `tests/lib/creditCardRepayment.test.ts` 涵蓋三個共用函式;`package.json` 新增 `test:mcp-credit-card-repayment` 腳本並納入 `npm test` 主鏈。`asset_openapi.yaml` 無新增 HTTP 端點(MCP 工具非 REST 路徑),僅將 `info.version` 由 `4.102.0` 更新至 `4.103.0`。驗證:`npm run test:mcp-descriptors`、`npm run test:credit-card-repayment` 通過(DB 整合案例因本機無 `DATABASE_URL` 略過)。 |
| 4.102.0 | 2026-08-16 | 信用卡總金額還款:輸入一個總金額,系統依各卡欠款等比例自動分配到付款帳戶關聯的所有信用卡(見 `specs/006-credit-card-total-repayment/`)。`lib/db.ts` 的 `_runMigrations()` 新增冪等 `ALTER transactions ADD COLUMN repayment_summary_id TEXT DEFAULT ''`(非還款交易恆為空字串),並新增資料表 `credit_card_repayment_summaries`(`id`/`user_id`/`date`/`from_account_id`/`from_account_name`/`from_currency`/`total_amount`/`input_mode`/`allocations`(JSON 快照)/`created_at`/`updated_at`)與兩支索引(`idx_ccr_summaries_user`、`idx_transactions_repayment_summary` 帶 `WHERE repayment_summary_id != ''` 的部分索引);無任何資料回填或歷史重算,部署啟動自動套用。新增共用模組 `lib/creditCardRepayment.ts`(`collectPayableCards(userId, fromAccountId, fromCurrency)`:依納入規則 `account_type='信用卡' AND linked_bank_id=fromAccountId AND 餘額<0`,欠款換算同幣別 `round_half_up(-balance)`、異幣別經兩個匯率換算後 half-up、換算後 `<1` 設為 1,依 `created_at` 早→晚、相同再 `id` 升冪排序)與 `lib/creditCardRepaymentAllocation.ts`(`allocateRepayment(totalAmount, cards)` 純函式、純整數 BigInt 運算、無 DB/無 Next.js 相依;七步分配演算法含前置/後置條件 assert,殘差併入欠款最大卡、保底每卡 ≥1、超額依金額大→小扣減;因 tsconfig target ES2017 禁 BigInt 字面量,常數以 `BigInt(n)` 建構)。新增端點 `POST /api/accounts/credit-card-repayment`:以送出當下重新取得的欠款計算(不沿用前端快 snapshot),同一交易內寫入 2N 筆轉帳交易 + 1 筆摘要,任一例外一律 `ROLLBACK` 回 500;新增端點 `GET /api/accounts/{id}/repayment-cards`(付款帳戶快照,每卡只輸出 5 欄、不輸出 `created_at`,付款帳戶為信用卡回 400 `NotCreditCardPayer`)、`GET /api/credit-card-repayment-summaries/{id}`(事後讀取分配摘要,逐卡比對當下交易狀態標示 `intact`/`modified`/`deleted`,金額比對容差 0.005,`fromAccount.name`/`.currency` 取自摘要快照欄位不即時 JOIN)。前端新增 `components/features/accounts/CreditCardRepaymentDialog.tsx`(付款帳戶選擇、總金額輸入、本地 `allocateRepayment` 重算預覽不往返、送出後渲染實際分配結果含預繳標記),`AccountsClient.tsx`/`TransactionsClient.tsx` 整合還款入口與「檢視還款分配」。行動 App 對應實作:`mobile/lib/credit_card_repayment_allocation.dart`(Dart 對照實作,逐步驟對應 TS,以同一份 `shared/repayment-allocation/cases.json` 黃金測資釘住);`mobile/lib/api_client.dart`/`models.dart`/`screens/accounts_screen.dart`/`screens/transactions_screen.dart` 新增還款寫入、快照、分配摘要檢視與預繳標記。`lib/i18n/dictionaries/*.ts`(10 語言)/`shared/i18n/app_*.arb`/行動端 ARB 與產生檔新增 `repayment.totalAmount`/`totalAmountHint`/`totalDebt`/`allocationPreviewTitle`/`colCard`/`colAllocated`/`colBalanceAfter`/`prepaidBadge`/`prepaidAmount`/`resultTitle`/`resultDone`/`messages.repaymentTotalAmountInvalid`/`messages.repaymentTotalAmountTooSmall`/`transactions.viewRepaymentAllocation`/`repaymentSummaryTitle`/`repaymentSummaryTotal`/`repaymentSummaryStale`/`repaymentSummaryStatusIntact`/`repaymentSummaryStatusModified`/`repaymentSummaryStatusDeleted` 等鍵,經 `npm run i18n:generate` 同步(含 `flutter gen-l10n`),i18n 鍵數由 1,334 增至 1,354。`asset_openapi.yaml` 新增三組還款路徑契約,`info.version` 由 `4.101.0` 更新至 `4.102.0`。新增 `tests/lib/creditCardRepayment.test.ts`/`tests/lib/creditCardRepaymentAllocation.test.ts`(純函式含黃金測資與隨機案例)、`mobile/test/credit_card_repayment_allocation_test.dart`(Dart 端對照),並擴充 `tests/lib/db.test.ts` 涵蓋新資料表與欄位;`package.json` 新增 `test:repayment-allocation`/`test:credit-card-repayment` 兩支腳本並納入 `npm test` 主鏈。驗證:`npm run typecheck`、`npm test`(21 個檢查項目:19 支 `test:*` 腳本+`check:iso`+`check:i18n` 10 語言 1,354 鍵對齊)皆通過;`test:credit-card-repayment` 之 DB 整合案例因本機無 `DATABASE_URL` 略過。 |
| 4.101.0 | 2026-08-15 | 交易記錄顯示 AI/MCP 修改標記並支援使用者還原(見 `specs/005-mcp-transaction-restore/`)。`lib/db.ts` 的 `_runMigrations()` 新增三個冪等 `ALTER`:`transactions.ai_created`/`note_ai_modified` INTEGER NOT NULL DEFAULT 0、`pre_ai_note` TEXT NOT NULL DEFAULT '';並補兩段一次性回填 `UPDATE`:第一段依 `data_operation_audit_log` 中 `action = 'mcp_create_transaction'` 的 `metadata->>'transaction_id'` 回填既有交易的 `ai_created = 1`,第二段沿 `linked_id` 傳播一跳(涵蓋轉帳另一腳與自動產生的手續費子交易);兩段皆為冪等 `UPDATE`,重跑不影響已是 1 的列。`lib/transactionWriteCore.ts` 的 `insertIncomeExpenseTransaction()`/`insertTransferPair()` 新增 `aiCreated?` 輸入並寫入新欄位;新增匯出函式 `deleteTransactionCascade(userId, txId, linkedId)`,從原本內嵌在 `DELETE` handler 的級聯刪除邏輯抽出(含 `deleteTransactionAttachments()` 與 `linked_id` 配對列刪除),供既有 `DELETE app/api/transactions/[txId]/route.ts` 與新的還原端點共用,回傳實際被移除的 id 陣列;`lib/overseasFee.ts` 的 `insertFeeTransaction()` 同步新增 `aiCreated?`,讓 AI 建立轉帳觸發的自動海外手續費子交易一併標記。`lib/mcpServer.ts`:`create_transaction` 工具(一般收支與轉帳兩路徑)呼叫上述函式時固定帶入 `aiCreated: true`;`update_transaction_note` 工具原本「單一 `UPDATE` 只寫 `note`/`updated_at`」的陳述式擴充為同一陳述式內以 `pre_ai_note = note`(取異動前舊值)與 `note_ai_modified = 1` 一併寫入,維持不做讀改寫的既有保證。`app/api/transactions/[txId]/route.ts` 的 `PUT`/`PATCH` 共用 `updateHandler()`:因既有前端整份表單協定恆送出 `note`,新增「新舊 `note` 值是否真的不同」的純記憶體字串比對(`existing` 已由既有 `SELECT *` 取得,不新增 DB 往返)——不同則連同 `note` 一併清除 `note_ai_modified = 0`/`pre_ai_note = ''`,相同(同值重送)則原樣保留;`ai_created` 欄位不出現在這次 `UPDATE` 陳述式中,不受任何手動編輯影響(FR-002)。`DELETE` handler 改為呼叫 `deleteTransactionCascade()`,行為不變。`app/api/transactions/route.ts`(`GET` 列表)的 `TransactionListItem` 新增 `aiCreated`/`noteAiModified` 兩個布林欄位。新增端點 `app/api/transactions/[txId]/restore-ai-created/route.ts`(`POST`):`ownsResource()` 確認歸屬後,非 `ai_created = 1` 回 409 `NotRestorable`;否則呼叫 `deleteTransactionCascade()`,成功後 `auditSensitiveAction()` 寫入 `action: 'restore_ai_created_transaction'`,`metadata` 僅含 `transaction_id`/`linked_transaction_id`(新增進 `lib/auditHelpers.ts` 的 `AUDIT_METADATA_ALLOWED_KEYS` 白名單)。新增端點 `app/api/transactions/[txId]/restore-ai-note/route.ts`:`GET` 為純讀預覽(US3),回傳 `restorable`/`preAiNote`(僅 `restorable` 時給值,否則 `null`)/`currentNote`/`updatedAt`,不變更任何資料;`POST` 帶 `body.expectedUpdatedAt`,先呼叫既有 `assertOptimisticLock()` 重新確認當下狀態(FR-017),衝突時攔截為專屬訊息與 `code: 'NoteChanged'`(而非泛用鎖定錯誤),通過後再檢查 `note_ai_modified === 1`(否則 409 `NotRestorable`),單一 `UPDATE` 陳述式將 `note` 設回 `pre_ai_note`、`note_ai_modified = 0`、`pre_ai_note = ''`,成功後稽核 `action: 'restore_ai_modified_note'`,`metadata` 僅含 `transaction_id`(不含備註全文,比照既有 FR-012 保守揭露原則)。`components/features/admin/AdminClient.tsx` 的 `AUDIT_ACTION_LABELS` 新增 `restore_ai_created_transaction`/`restore_ai_modified_note` 中文標籤,未新增任何備註內容相關的 metadata 標籤。前端 `components/features/transactions/TransactionsClient.tsx`:卡片與表格兩種既有版面在 `note`/`isFxFee` 等既有條件旁新增 `tx.aiCreated`/`tx.noteAiModified` 兩個獨立徽章;操作列依條件新增「還原」(`Undo2`)與「還原備註」(`RotateCcw`)按鈕;新增兩個確認 Modal——還原建立為既有刪除類 danger 按鈕直接確認,還原備註的 Modal 先呼叫 `GET .../restore-ai-note` 取得預覽(US3)並排顯示 `currentNote`/`preAiNote`,確認後才 `POST` 並帶入預覽取得的 `updatedAt` 作為樂觀鎖依據。行動 App 對應實作:`mobile/lib/models.dart` 的 `Txn` 新增 `aiCreated`/`noteAiModified`/`updatedAt` 三個欄位(`fromJson` 相容 camelCase 與既有 snake_case 回應);`mobile/lib/api_client.dart` 新增 `restoreAiCreatedTransaction()`/`restoreAiNote()`/`getAiNoteSnapshot()`;`mobile/lib/screens/transactions_screen.dart` 的 `_TransactionsScreenState` 新增 `_restoreAiCreated()`/`_restoreAiNote()` 確認對話框處理函式,透過 `_TxnTile` 新增的 `onRestoreCreated`/`onRestoreNote` 回呼接上列表項目,呈現與網頁版一致的徽章與還原按鈕(`Icons.restore`/`Icons.undo`)。`lib/i18n/dictionaries/*.ts`(10 語言)/`shared/i18n/app_*.arb`/行動端對應 ARB 與產生檔新增 `aiCreated`、`noteAiModified`、`restoreCreated`、`restoreCreatedTitle`、`restoreCreatedMessage`、`restoreNote`、`restoreNoteTitle`、`restoreNoteConfirmMessage`、`restoreNoteCurrentLabel`、`restoreNotePreviewLabel` 共 10 鍵,經 `npm run i18n:generate` 同步(含 `flutter gen-l10n`)。`asset_openapi.yaml` 新增 `/api/transactions/{id}/restore-ai-created`(`POST`)與 `/api/transactions/{id}/restore-ai-note`(`GET`/`POST`)兩組路徑契約,`info.version` 由先前落後未跟上 4.100.0 的 `4.99.0` 追上 `4.101.0`。新增 `tests/lib/db.test.ts`(驗證兩段回填 `UPDATE` 陳述式的正確性與冪等性;因 `initDB()` 同行程內建「已初始化即直接 return」守門,無法真正重跑 migration,改為直接對 fixture 執行相同 SQL)、`tests/lib/transactionRestore.test.ts`(涵蓋兩個新端點成功/失敗路徑、轉帳整組還原、404/409 各類錯誤碼、稽核寫入與不含備註全文)、`tests/lib/transactionsManualEdit.test.ts`(本專案首次以已認證 Route Handler 方式測試,驗證手動編輯「備註值真的改變」才清除標記、同值重送保留標記、`ai_created` 標記不受手動編輯影響),並擴充既有 `tests/lib/mcpCreateTransaction.test.ts`/`tests/lib/mcpUpdateTransactionNote.test.ts`/`tests/lib/transactionWriteCore.test.ts` 涵蓋 `aiCreated`/`note_ai_modified` 相關斷言;`package.json` 新增 `test:db-migrations`/`test:transactions-manual-edit`/`test:transaction-restore` 三支腳本並納入 `npm test` 主鏈。驗證:`npm run typecheck`、`npm test`(20 個檢查項目:18 支 `test:*` 腳本+`check:iso`+`check:i18n` 10 語言 1,334 鍵對齊)皆通過;上述三個新測試檔與 `mcpCreateTransaction`/`mcpUpdateTransactionNote`/`transactionWriteCore` 之 DB 整合案例因本機無 `DATABASE_URL` 略過。 |
| 4.100.0 | 2026-08-15 | MCP 新增 `update_transaction_note` 工具,僅能更新既有交易的備註欄位,其餘欄位一律不可變更、無刪除能力(見 `specs/004-mcp-update-notes-only/`)。抽出共用模組 `lib/transactionEditRules.ts`:`findTransactionEditBlock()`(`is_fx_fee === 1` 之「列本身不可編輯」規則,供 `app/api/transactions/[txId]/route.ts` 的 `PATCH` 與新工具共用,錯誤訊息與狀態碼逐字相同)、`TRANSACTION_NOTE_MAX_LENGTH = 200`(供 `components/features/transactions/TransactionsClient.tsx` 手動編輯表單 `maxLength` 與新工具的伺服器端驗證共用同一常數,取代先前兩處硬編碼 200);此模組刻意不 import `lib/db.ts`,避免伺服器端相依污染前端 bundle。`lib/mcpServer.ts` 的 `buildMcpServer()` 新增 `update_transaction_note` 工具,僅在 `credential.allowUpdateNote === true` 時才 `registerTool()`(未開啟則 `tools/list` 不出現該工具),與 `create_transaction` 的 `credential.allowCreate` 判斷互相獨立、不巢狀;輸入以 `zod` schema 僅接受 `transactionId`/`note`(非 `.strict()`,夾帶其他欄位靜默剝除不報錯);處理順序為長度驗證(>200 拒絕)→ 依 `id`+`user_id` 查列 → `findTransactionEditBlock()` 擋不可編輯交易 → 單一 `UPDATE ... SET note = ?, updated_at = ?` 陳述式(不做讀改寫)→ 重新查列組回應(欄位形狀與 `list_transactions` 單筆項目一致)→ 成功時才寫入稽核。新增 `lib/mcpOpenAiCompatibility.ts` 的 `createUpdateOAuthToolDescriptor()`,`annotations` 採 `destructiveHint: true`/`idempotentHint: true`(與 `create_transaction` 的 `WRITE_TOOL_ANNOTATIONS` 刻意不同:覆寫既有備註值不符合「僅做加法」定義)。新增資料表欄位 `mcp_credentials.allow_update_note`/`mcp_oauth_connections.allow_update_note`(`lib/db.ts` 冪等 `ALTER`,預設 0,與 `allow_create` 各自獨立,不因既有 `allow_create = 1` 而預設開啟;關閉權限不回溯影響已更新的備註);`lib/mcpAuth.ts`/`lib/mcpOAuth.ts` 的 `verifyMcpToken()`/`verifyMcpOAuthAccessToken()`/`listMcpCredentials()`/`listMcpOAuthConnections()`/`serializeCredential()` 一併回傳 `allowUpdateNote`,新增 `setMcpCredentialAllowUpdateNote()`/`setMcpOAuthConnectionAllowUpdateNote()`(OAuth 連線比照既有 `allow_create` 行為:授權仍有效時重新授權不重置,全數撤銷後重新授權則重置為 0)。`PATCH app/api/user/mcp-credentials/[id]/route.ts`/`PATCH app/api/user/mcp-oauth-connections/[clientId]/route.ts` 改為同時接受 `allowCreate`/`allowUpdateNote`(body key 存在即視為要更新該欄位,兩者皆選填但至少擇一,皆缺回傳 400;未提供的欄位維持原狀不動)。前端 `components/features/settings/McpSettingsClient.tsx`/`McpConnectionsClient.tsx` 新增「允許更新備註」欄位與 checkbox;`components/features/admin/AdminClient.tsx` 的 `AUDIT_ACTION_LABELS` 新增 `mcp_update_transaction_note` 中文標籤。稽核 metadata 僅含 `transaction_id`,刻意不含 `transaction_summary`(本功能不變更金額)與備註全文;被拒絕的嘗試(長度超限、交易不存在、不可編輯、非本人)一律不寫入稽核。`asset_openapi.yaml`:`McpCredentialAllowCreateUpdate`/`McpOAuthConnectionAllowCreateUpdate` 更名為 `McpCredentialPermissionUpdate`/`McpOAuthConnectionPermissionUpdate`(屬性與端點路徑不變,`operationId` 改為 `setMcpCredentialPermissions`/`setMcpOAuthConnectionPermissions`),`McpCredential`/`McpOAuthConnection` 新增必填 `allowUpdateNote`,`info.version` 更新為 `4.100.0`。新增 `tests/lib/transactionEditRules.test.ts`(純函式、無需資料庫)、`tests/lib/mcpUpdateTransactionNote.test.ts`(需 `DATABASE_URL`,涵蓋成功更新、逐欄不變比對、回應形狀一致、夾帶欄位靜默忽略、不可編輯交易被拒、非本人交易被拒、刪除工具不存在、空字串清空、長度上限、轉帳單腳獨立、冪等、久遠交易、缺欄位被拒、稽核形狀與不含備註全文、被拒絕不留稽核),並擴充 `tests/lib/mcpAuth.test.ts`/`tests/lib/mcpOAuth.test.ts`/`tests/lib/mcpDescriptors.test.ts` 涵蓋 `allowUpdateNote` 預設值、開關獨立性、`tools/list` 可見性與 OAuth 重新授權重置情境。驗證:`npm run typecheck`、`npm test`(17 個檢查項目:15 支 `test:*` 腳本+`check:iso`+`check:i18n` 10 語言鍵值對齊)皆通過;`test:mcp-update-note` 之 DB 整合案例因本機無 `DATABASE_URL` 略過。 |
| 4.99.0 | 2026-08-14 | MCP AI 新增交易紀錄,僅限建立、任何情況下皆無刪除/修改既有資料的工具(見 `specs/003-mcp-write-no-delete/`)。`lib/mcpServer.ts` 的 `buildMcpServer()` 新增 `create_transaction` 工具,僅在 `credential.allowCreate === true` 時才 `registerTool()`(未開啟則 `tools/list` 完全不出現該工具,刪除/修改工具則不存在於任何程式碼路徑,不因權限而有例外);輸入以 `zod` schema 驗證 `type`(income/expense/transfer)、`amount`(>0)、`currency`/`fxRate`/`date`/`categoryId`/`accountId`/`fromAccountId`/`toAccountId`/`note`/`idempotencyKey`。新增 `lib/transactionWriteCore.ts` 的 `insertIncomeExpenseTransaction()`/`insertTransferPair()`,抽取自既有手動建立交易的計算邏輯(金額精度、幣別換算 `convertToTwd()`、跨幣別海外手續費 `resolveOverseasFee()`、轉帳雙腳寫入),`app/api/transactions/route.ts`(`POST`)與 `app/api/transactions/transfer/route.ts` 改為呼叫抽取後的共用函式,行為不變;MCP 新增交易與手動建立共用同一份計算邏輯,帳戶/分類受理政策維持兩者既有各自差異(MCP 缺帳戶或有多帳戶未指定則拒絕,分類不存在則歸未分類,比照既有 CSV 匯入路徑)。新增資料表欄位 `mcp_credentials.allow_create`(`lib/db.ts` 冪等 ALTER,預設 0)供 PAT 憑證逐憑證開關;新增資料表 `mcp_oauth_connections`(`user_id`+`client_id` 複合主鍵、`allow_create`、`first_connected_at`、`last_used_at`)代表「已連接 AI 工具」,獨立於既有 `mcp_oauth_tokens` 的 token/scope 輪替,`lib/mcpOAuth.ts` 的 `exchangeMcpAuthorizationCode()` 於核發 token 時以 `ON CONFLICT` upsert:既有有效 token 時僅更新 `last_used_at`/`client_name`,全數失效後重新授權則一併把 `allow_create` 重置為 0;新增 `listMcpOAuthConnections()`(僅回傳目前仍有未撤銷未過期 token 的連線)/`setMcpOAuthConnectionAllowCreate()`。新增資料表 `mcp_transaction_idempotency`(`credential_id`+`idempotency_key` 唯一索引、`expires_at`),`create_transaction` 呼叫時先查快取命中則直接回傳先前結果,否則建立後以 `ON CONFLICT DO NOTHING` 寫入(24 小時 TTL),處理併發下兩個相同 key 請求同時建立的極端情況;冪等鍵命中快取不視為新增操作,不重複寫入稽核。新增 API:`PATCH app/api/user/mcp-credentials/[id]/route.ts`(切換 PAT 的 `allowCreate`)、`app/api/user/mcp-oauth-connections/route.ts`(`GET` 列表)、`app/api/user/mcp-oauth-connections/[clientId]/route.ts`(`PATCH` 切換);`serializeCredential()` 由 `app/api/user/mcp-credentials/route.ts` 改移至 `lib/mcpAuth.ts` 供 GET/PATCH 共用(Next.js route.ts 只能匯出識別的 HTTP handler,原本額外具名匯出供同目錄 `[id]/route.ts` 匯入會被 typed routes 檢查判定為不合法的 route export,`npm run typecheck` 會失敗)。新增前端頁面 `app/settings/mcp-connections/page.tsx` + `components/features/settings/McpConnectionsClient.tsx`(「已連接 AI 工具」清單與開關),`components/features/settings/McpSettingsClient.tsx` 既有 PAT 清單新增「允許新增資料」欄位;`components/layout/Sidebar.tsx` 新增導覽項目。`lib/mcpServer.ts` 的 `withAudit()` 擴充 `action`/`extraMetadata` 參數,`create_transaction` 成功後寫入 `action: 'mcp_create_transaction'`、`metadata.transaction_summary`(僅金額與收支類型,不含備註全文);`components/features/admin/AdminClient.tsx` 的 `AUDIT_ACTION_LABELS`/`AUDIT_META_LABELS` 新增對應中文標籤(含補上既有 `mcp_query` 遺留的英文標籤落差)。速率限制沿用 `proxy.ts` 既有 `/api/mcp` 全域限流,未新增獨立機制。新增 `tests/lib/transactionWriteCore.test.ts`/`tests/lib/mcpCreateTransaction.test.ts`(DB 依賴部分於無 `DATABASE_URL` 時略過),`tests/lib/mcpDescriptors.test.ts` 新增案例驗證 `allowCreate=true` 憑證的 `tools/list` 額外出現 `create_transaction` 且 annotations 標示可寫非破壞性,並驗證刪除/修改語意的不存在工具名稱一律回傳工具不存在錯誤、既有交易欄位不受影響。`asset_openapi.yaml` 補上本次新增端點的契約並將 `info.version` 由既有落後的 `4.12.0` 追上 `4.98.2`(僅為文件契約,未被任何 CI 流程消費,不影響建置或測試)。驗證:`npm run typecheck`、`npm test`(15 支測試腳本+`check:iso`+`check:i18n` 10 語言 1,318 鍵對齊)皆通過;`test:mcp-create-transaction`/`test:transaction-write-core` 之 DB 整合案例因本機無 `DATABASE_URL` 略過;quickstart.md 七章節端對端手動驗收與 `asset_openapi.yaml` 升版後的 Cloudflare API Shield 契約相容性查核未執行(前者需真實 AI 工具連線環境,後者依 spec 標註不阻擋合併)。 |
| 4.98.2 | 2026-08-13 | MCP OAuth 新增 `private_key_jwt`(RS256/JWKS)client authentication,修正 ChatGPT connector 的 CIMD metadata(`token_endpoint_auth_method: private_key_jwt`、`jwks_uri`)在 4.98.1 僅支援 `none`/`client_secret_basic`/`client_secret_post` 之外仍會授權失敗的獨立成因。`lib/mcpOAuthCore.ts` 的 `normalizeMcpOAuthClientMetadata()` 新增 `allowPrivateKeyJwt` 選項(`registerMcpOAuthClient()`/`validateClientIdMetadataDocument()` 開啟,DCR 沿用既有 secret 方式),驗證 `jwks_uri` 必須為 HTTPS、`token_endpoint_auth_signing_alg` 僅接受 `RS256`;新增欄位 `mcp_oauth_clients.jwks_uri`/`token_endpoint_auth_signing_alg`(`lib/db.ts` 冪等 ALTER)。`lib/mcpOAuth.ts` 新增 `verifyMcpOAuthClientAssertion()`:解析 JWT 三段式結構、以 client 提供的 JWKS 找出對應 `kid` 的 RSA 公鑰(僅接受 `kty=RSA`、非 `enc` 用途、最小 2048-bit)驗證 `RS256` 簽章,檢查 `iss`/`sub` 等於 `client_id`、`aud` 等於 token/revocation endpoint、`exp`/`iat`/`nbf` 於允許誤差內、`jti` 格式合法;新增資料表 `mcp_oauth_client_assertions`(`client_id`+`jti` 複合主鍵)供一次性 `jti` 重放保護,`getClientJwks()` 以 SSRF 防護(HTTPS-only、僅接受公開 IP、無重導向、大小與逾時限制)擷取並快取 JWKS(`kid` 未命中時強制重新整理一次再判定失敗)。`parseMcpOAuthClientCredentials()`/`verifyMcpOAuthClientAuthentication()` 新增 `client_assertion_type`/`client_assertion` 參數解析與非同步驗證(需傳入 audience,`app/api/oauth/token`/`app/api/oauth/revoke` 對應改為 `await`);`createMcpAuthorizationServerMetadata()` 的 `token_endpoint_auth_methods_supported`/`revocation_endpoint_auth_methods_supported` 新增 `private_key_jwt`;`app/mcp/page.tsx` 補充 CIMD 支援 `private_key_jwt` 的說明。新增/擴充 `tests/lib/mcpOAuth.test.ts` 涵蓋以實際 RSA 金鑰產生的斷言簽章驗證、`jti` 重放拒絕、`kid` 找不到時的 JWKS 強制刷新。驗證:`npm run test:mcp-oauth` 15/15、`npm run typecheck`、`npm test`、`npm run build`、`npm audit`(0 vulnerabilities)、`git diff --check` 均通過。 |
| 4.98.1 | 2026-08-13 | MCP OAuth DCR(`lib/mcpOAuthCore.ts` normalizeMcpOAuthClientMetadata/`lib/mcpOAuth.ts`)新增支援 `client_secret_basic`/`client_secret_post` confidential client(除既有 `none` public client 外),註冊時簽發高熵 client secret 並以 SHA-256 hash 存於新增欄位 `mcp_oauth_clients.client_secret_hash`/`client_secret_expires_at`(`lib/db.ts` 冪等 ALTER),secret 僅於註冊回應回傳一次、`serializeRegisteredClient()` 不外洩 hash;`app/api/oauth/token`/`app/api/oauth/revoke` 新增 `parseMcpOAuthClientCredentials()`/`verifyMcpOAuthClientAuthentication()` 驗證 HTTP Basic 或 POST body 的 client 憑證,修正部分要求 confidential client 的 AI 工具連線失敗(CIMD 仍僅允許 `none`,並在 `validateClientIdMetadataDocument()` 明確拒絕 confidential document)。`app/api/mcp/route.ts` 401 回應改為結構化 `error`/`error_description`/`action: reauthorize`(區分 `invalid_token` 與 `authorization_required`);`app/oauth/authorize/page.tsx` 的 `ErrorCard` 新增保留 `returnTo` 的重新登入連結;`app/mcp/page.tsx` 補充「正常登出不會撤銷 MCP OAuth 授權」與 confidential client 需走 DCR 的說明。Next.js 16.3 慣例將 `middleware.ts` 遷移為 `proxy.ts`(邏輯不變,`nginx.conf` 註解同步更新)。升級 `next`/`react`/`react-dom`/`resend`/`lucide-react`/`@base-ui/react`/`tailwindcss` 等相依套件至最新相容版本並重新產生 `package-lock.json`(`npm audit` 0 vulnerabilities)。新增/擴充 `tests/lib/mcpOAuth.test.ts` 涵蓋 client credentials 解析、confidential client 驗證、secret 序列化與 401 結構化回應。驗證:`npm run test:mcp-oauth` 14/14、`npm run test:mcp-descriptors`、`npm run typecheck`、`npm test`、`npm run build`、`git diff --check` 均通過。 |
| 4.98.0 | 2026-08-11 | 行動 App 新增管理員設定(`mobile/lib/screens/admin_screen.dart`):系統設定(開放註冊/LINE 登入/允許註冊 Email/管理員 IP 白名單/稽核模式/交易照片儲存與上限/股價自動更新與間隔,對應 `app/api/admin/system-settings`)、使用者管理(列表/新增/重設密碼/切換管理員/刪除,對應 `app/api/admin/users`)、全站登入稽核(`/api/admin/login-audit?scope=all`),以及維運動作(股價立即更新、交易照片壓縮/加密、伺服器時間、NTP 同步)。設定頁在 `isAdmin` 時顯示管理員入口;`AppUser` 模型新增 `isSuperAdmin`。股票設定畫面(`mobile/lib/screens/stock_settings_screen.dart`)補齊股票狀態管理(下市/恢復追蹤,走 `POST /api/stocks/batch-price` 帶 `delisted`)與定期定額(`/api/stock-recurring` CRUD+啟用切換,`api_client.dart` `_send` 補 PATCH 支援;`models.dart` 新增 `StockRecurring`)。i18n:45 個 `admin.*` key 注入 10 個 shared ARB(含 10 語翻譯),`npm run i18n:generate` 重產生 web dicts/Flutter 翻譯。驗證:`flutter analyze` 僅既有 3 項 warning;`flutter test` 6/6;`npm test` 15/15;`npm run check:i18n` 通過;`npm run typecheck` 通過。 |
| 4.97.6 | 2026-08-10 | 行動 App 登入 Token 效期由與網頁共用的 `JWT_EXPIRES`(預設 7 天)改為獨立的 `APP_JWT_EXPIRES`(預設 90 天,可由環境變數覆蓋),`lib/sessionHelpers.ts` 的 `createLoginSession()` 依既有 `X-AssetPilot-Device-Id` header 判斷是否為 App 登入(App 端每次請求皆會帶入該 header)並傳入 `lib/auth.ts` 的 `signToken()` 決定簽發效期;瀏覽器登入行為不變。順手修正 `lib/apiHelpers.ts` 的 `setAuthCookie()` 原本把 Cookie `maxAge` 寫死 7 天、未跟隨 Token 實際效期的問題,改為依 Token 的 `exp` claim 動態計算(匯出 `sessionHelpers.ts` 的 `getTokenExpiresAt()` 供其重用),避免日後 `JWT_EXPIRES`/`APP_JWT_EXPIRES` 調整時 Cookie 效期與 Token 實際效期不一致。`.env.example`/`README.md`/`docker-compose.yml`/`Dockerfile` 同步新增 `APP_JWT_EXPIRES` 說明與預設值。驗證:`npx tsc --noEmit` 對本次變更檔案無新增錯誤;`npm test` 通過(`check:i18n` staleness 為既有本機問題,與本次無關)。 |
| 4.97.5 | 2026-08-04 | 安全性強化:修補 `npm audit` 回報的 2 個傳遞相依漏洞(`fast-uri` GHSA-7p8r-x3mc-p8w7 High、`hono` GHSA-8j4g-w8fx-2239 Moderate,皆經由 `@modelcontextprotocol/sdk@1.30.0` 引入),於 `package.json` `overrides` 釘 `fast-uri@^3.1.5`/`hono@^4.12.34`,`npm audit` 歸零。交易照片上傳改以 magic bytes 驗證實際內容為點陣圖(新增 `lib/transactionAttachments.ts` 的 `detectRasterImageMime()`,拒絕 SVG/無法辨識內容,`assertImageUpload` 明確拒絕 `image/svg+xml`),DB 儲存 MIME type 以偵測結果取代用戶端自報值;下載端 `app/api/transactions/[txId]/attachments/[attachmentId]/file/route.ts` 僅對白名單內點陣圖 MIME 使用 `inline`,其餘一律 `attachment`+`application/octet-stream`,並補 `X-Content-Type-Options: nosniff`,消除 stored XSS 前置條件。`nginx.conf` 兩個 location 新增 `proxy_set_header X-Forwarded-For $remote_addr`(覆寫防偽造)/`X-Real-IP $remote_addr`/`X-Forwarded-Proto $scheme`,使 `middleware.ts` 的每 IP 速率限制不再被用戶端自報 XFF 繞過或落入共用 `'unknown'` 桶。`docker-compose.yml` 的 `POSTGRES_PASSWORD` 與 `DATABASE_URL` 改為環境變數插值(保留本機預設值),移除固定資料庫憑證。驗證:`npm audit`(含 dev)0 vulnerabilities;`npm run typecheck` 通過;`npm test` 全數通過(`check:i18n` staleness 為 Windows 本機既有問題,與本次無關);`docker compose config` VALID;`detectRasterImageMime` 功能測試 JPEG/PNG/WebP 接受、SVG/HTML/亂碼拒絕。 |
| 4.97.4 | 2026-07-30 | 修正 MCP OAuth 授權端點(`app/api/oauth/authorize/route.ts`)在使用者點擊「允許連線」後,仍會被瀏覽器 CSP `form-action 'self'`(`next.config.ts` 全站設定)擋下重導向的獨立成因,與先前 3 個版本修正的 CIMD/Happy Eyeballs/`iss` 參數成因均不同。根因:`/oauth/authorize` 的同意畫面是一般 `<form method="post" action="/api/oauth/authorize">`,伺服器驗證通過並核發一次性 authorization code 後,原本以 HTTP 303 `Location` 導回用戶端跨網域的 `redirect_uri`(例如 ChatGPT connector 的 `https://chatgpt.com/connector/oauth/...`);Chrome/Firefox/Safari 對「表單送出後的重導向」仍會用原表單頁的 `form-action` 檢查目標網域,該網域未在允許清單中,瀏覽器因而靜默擋下該次導頁(僅在 DevTools console 顯示 CSP violation,畫面完全沒有反應),已核發的 code 從未送達用戶端即逾期作廢,使用者點擊同意後看不到任何回應也無法返回發起連線的 AI 工具。修法:`redirectWithOAuthResult()` 改為回傳 200 HTML(`<meta http-equiv="refresh">` + 備援連結)取代 HTTP 3xx,導頁改由已落地的同源頁面自行發起,不再落在原表單的 `form-action` 重導向鏈中,全站 CSP 設定不需變動;成功/拒絕/錯誤三種導頁路徑皆比照修正,並對插入的目標網址做 HTML escape。驗證:`npx tsc --noEmit` 無新增錯誤;`npm run test:mcp-oauth`(12/12)通過。 |
| 4.97.3 | 2026-07-30 | 修正 MCP OAuth `fetchPinnedClientMetadata()`(`lib/mcpOAuth.ts`)在 4.97.2 IPv4 優先修正之外仍會失敗的獨立成因。根因:傳給 `https.request()` 的自訂 `lookup` 只實作舊式單一位址 callback(`callback(null, address, family)`);Node.js(正式環境為 v24)的 Happy Eyeballs(`autoSelectFamily`,預設開啟)偵測到目標主機同時有 A/AAAA 記錄(dual-stack)時,會以 `{ all: true }` 呼叫 `lookup` 並預期回傳位址陣列,內部 `lookupAndConnectMultiple` 收到非陣列值會讀到 `undefined` 並丟出 `TypeError [ERR_INVALID_IP_ADDRESS]: Invalid IP address: undefined`,被 `fetchPinnedClientMetadata()` 的錯誤處理吞掉後仍顯示「無法授權 MCP 連線/Unable to retrieve client metadata document」。以使用者回報的 ChatGPT connector 實際 client_id(`https://chatgpt.com/oauth/{id}/client.json`,`chatgpt.com` 同時解析出 IPv4 與 IPv6)為樣本,用 `npx zeabur@latest service exec` 進入正式環境容器執行對照腳本重現:舊版 `lookup` 對該主機必定拋出上述 TypeError;即使已依 4.97.2 邏輯優先選出 IPv4 位址,只要主機是 dual-stack 就會觸發此問題,與用戶端(Claude/ChatGPT 等)無關。修法:`lookup` 依傳入的 `options.all` 決定回傳格式——為 `true` 時回傳 `[{ address, family }]` 陣列,否則沿用單一位址/family 的舊式 callback;已釘選的單一位址與 SSRF「僅接受公開 IP」驗證邏輯不變。修正後以同一支對照腳本對該 client_id 重測,成功取得 200 JSON。驗證:`npx tsc --noEmit` 無新增錯誤;`npm run test:mcp-oauth`(12/12)通過。 |
| 4.97.2 | 2026-07-30 | 修正 MCP OAuth 授權端點(`app/oauth/authorize/page.tsx` → `lib/mcpOAuth.ts` 的 `fetchPinnedClientMetadata()`)在擷取 Client ID Metadata Document(CIMD)時的連線位址選擇問題。根因:正式環境部署(Zeabur)容器只有 IPv4 對外路由,但 SSRF 防護用的 `dns.promises.lookup(hostname, { all: true, verbatim: true })` 回傳結果可能把 IPv6(AAAA)位址排在前面,原本直接取 `addresses[0]` 連線,若剛好選到 IPv6 位址會立即 `Network unreachable`,導致 client_id 為 CIMD URL 的 MCP 用戶端(如 Claude)在 `/oauth/authorize` 顯示「無法授權 MCP 連線/Unable to retrieve client metadata document」而無法完成授權。以 `npx zeabur@latest service exec` 進入正式環境容器以 `getent ahosts`/`nc` 實測驗證:目標網域確有 AAAA 記錄、對該 IPv6 位址的 TCP 連線確實 `Network unreachable`、IPv4 位址則正常連通。修法:`fetchPinnedClientMetadata()` 在解析出的公開位址中優先挑選 IPv4(`family` 由小到大排序後取第一筆)再建立連線,SSRF 的「僅接受公開 IP」驗證邏輯不變。另確認先前已排查的 `.well-known/oauth-protected-resource`/`.well-known/oauth-authorization-server` 500 錯誤,根因為正式環境未設定 `APP_URL` 環境變數,已由使用者於 Zeabur 後台補上並重啟服務解決,與本次程式碼修正為兩個獨立問題。驗證:`npm run typecheck` 通過;`npm run test:mcp-oauth`(12/12)通過。 |
| 4.97.1 | 2026-07-30 | MCP OAuth 授權端點(`app/api/oauth/authorize/route.ts`)的所有重導向回應(核發 code 成功、使用者拒絕、發生錯誤)新增 `iss` 查詢參數,帶入本服務 issuer(由 `APP_URL` 推導之 origin),符合 RFC 9207(OAuth 2.0 Authorization Server Issuer Identification)與 MCP 規格 `2026-07-28` 版 SEP-2468 建議,供串接用戶端比對預期 issuer 以降低多授權伺服器情境下的 mix-up attack 風險;純新增欄位、向下相容,不影響既有 PAT 或既有已核發之 OAuth token。驗證:`npx tsc --noEmit` 除既有且與本次無關的 e2e Playwright 型別錯誤外無新增錯誤;`npm run test:mcp-oauth`(12/12)與 `npm run test:mcp-auth` 全數通過。 |
| 4.97.0 | 2026-07-29 | MCP 整合新增 OAuth 2.1 授權登入,`/api/mcp` 從僅接受 PAT 擴充為同時接受 PAT(`ap_mcp_`)與 OAuth access token(`ap_mcp_oauth_access_` 前綴)。新增資料表 `mcp_oauth_clients`(`client_id`/`redirect_uris`/`token_endpoint_auth_method` 固定 `none`/`grant_types`/`response_types`/`client_name`/`client_uri`/`logo_uri`/`scope`)、`mcp_oauth_authorization_codes`(`code_hash` 為主鍵、`code_challenge`、`resource`、`expires_at`、`consumed_at` 供 5 分鐘單次使用)、`mcp_oauth_tokens`(`token_hash` 主鍵、`token_type`、`family_id`、`resource`、`expires_at`、`revoked_at`、`replaced_by_hash` 供 refresh token 輪替與重放偵測)。新增 `lib/mcpOAuthCore.ts`(純驗證函式:PKCE S256 驗證/推導、`redirect_uri`/`resource`/scope(固定 `mcp:read`)正規化、Client ID Metadata Document URL 格式檢查、`isPublicMetadataIp()` 阻擋私有/保留 IP 供 CIMD 擷取防護、`isMcpHttpOriginAllowed()` 同源+`ALLOWED_ORIGINS` allowlist、`resolveMcpOAuthOrigin()` 由 `APP_URL` 推導正式環境 HTTPS origin(本機限定 loopback 才允許 HTTP))與 `lib/mcpOAuth.ts`(`registerMcpOAuthClient()` 動態註冊 RFC 7591/CIMD 兩種來源、`issueMcpAuthorizationCode()`/`exchangeMcpAuthorizationCode()`/`exchangeMcpRefreshToken()`/`verifyMcpOAuthAccessToken()`/`revokeMcpOAuthToken()`,access token 1 小時、refresh token 30 天且每次使用皆輪替並綁定 `family_id`,重放同一 family 即整組撤銷)。新增路由:`app/.well-known/oauth-protected-resource/route.ts` 與 `app/.well-known/oauth-protected-resource/api/mcp/route.ts`(RFC 9728)、`app/.well-known/oauth-authorization-server/route.ts`(RFC 8414,經 `lib/mcpOAuthMetadata.ts` 組裝)、`app/api/oauth/register/route.ts`(RFC 7591 動態註冊)、`app/api/oauth/authorize/route.ts` + `app/oauth/authorize/page.tsx`(沿用既有登入 Cookie 驗證身份後顯示明確同意畫面,`sanitizeClientForConsent()` 僅回傳 client 名稱/圖示/redirect URI 供畫面呈現)、`app/api/oauth/token/route.ts`(`authorization_code`/`refresh_token` 兩種 grant)、`app/api/oauth/revoke/route.ts`。新增 `lib/loginReturn.ts` 的 `safeOAuthReturnTo()`:`returnTo` 參數僅接受同源 `/oauth/authorize` path,其餘一律清空以避免開放重導向;`app/login/page.tsx`、LINE 登入(`app/api/auth/line/authorize/route.ts`、`app/auth/line/callback/page.tsx`、`lib/lineOAuthState.ts`)密碼/Google/Passkey/LINE 四種登入方式皆改為登入成功後導向 `loginDestination()`(預設 `/dashboard`,OAuth 流程中回到授權同意頁)。`app/api/mcp/route.ts` 新增 Origin 驗證(非白名單回 403)、CORS 標頭、`GET`/`OPTIONS`/`DELETE` 方法(原僅 `POST`)、401 回應改為標準 `WWW-Authenticate: Bearer` challenge(含 `resource_metadata` 連結與 `mcp:read` scope,無效權杖另加 `error="invalid_token"`),並以 `lib/mcpOpenAiCompatibility.ts` 的 `OpenAiCompatibleMcpTransport` 包裝既有 transport,補回官方 SDK 1.30 `registerTool()` 會遺漏的 OpenAI 頂層 `securitySchemes` 擴充欄位;`lib/mcpServer.ts` 的 11 個唯讀工具新增 `title`、read-only annotations 與 `mcp:read` OAuth 需求(含 `_meta.securitySchemes` 舊版鏡像)。新增 `app/mcp/page.tsx` 公開服務說明頁,供 metadata 的 `service_documentation` 欄位指向。`middleware.ts` 的 `PUBLIC_PATHS` 新增 `/mcp`、`PUBLIC_PREFIXES` 新增 `/api/oauth/`(略過 Cookie 閘門),新增三組速率限制:`/api/oauth/{authorize,token,revoke}` 每 IP 每 15 分鐘 100 次、`/oauth/authorize` 頁面每 IP 每 15 分鐘 30 次(防止匿名觸發 CIMD 擷取)、`/api/oauth/register` 每 IP 每小時 20 次。`components/features/settings/McpSettingsClient.tsx` 新增 OAuth 連線網址(`/api/mcp`)複製區塊,與既有 PAT 建立/列表/撤銷區塊並存。`.env.example` 的 `APP_URL` 說明更新為「正式環境啟用 MCP OAuth 時必填」。新增 10 語言 i18n 鍵(OAuth 同意畫面、設定頁新區塊文案)。測試:新增 `tests/lib/mcpOAuth.test.ts`(12 項,涵蓋 PKCE、redirect_uri/scope/resource 驗證、Origin allowlist、正式環境未設定 `APP_URL` 時 PAT-only 401 fallback、token 重複參數拒絕、DB 生命週期案例於無 `DATABASE_URL` 時略過)與 `tests/lib/mcpDescriptors.test.ts`(1 項,驗證 `tools/list` 原始回應含頂層 `securitySchemes`/`_meta` 鏡像/read-only annotations),新增 `npm run test:mcp-oauth`/`test:mcp-descriptors` 並納入 `npm test`。驗證:`npm run typecheck` 通過;`npm test` 全數通過(`test:mcp-oauth` 12/12、`test:mcp-descriptors` 1/1、`check:i18n` 10 語言 1,257 鍵對齊);既有 PAT 連線行為與撤銷流程未變動。 |
| 4.96.0 | 2026-07-29 | 新增 MCP(Model Context Protocol)AI 財務紀錄查詢整合。新增相依套件 `@modelcontextprotocol/sdk@^1.30.0`;新增資料表 `mcp_credentials`(`id`/`user_id`/`name`/`token_hash`/`created_at`/`last_used_at`/`expires_at`/`revoked_at`,唯一索引於 `token_hash`),`lib/mcpAuth.ts` 實作 PAT 產生(`ap_mcp_` + `crypto.randomBytes(32)` base64url)、SHA-256 雜湊索引查找(比照 `lib/userDeletion.ts` 的 `createHashedEmail()` 手法,非 bcrypt,因需雜湊索引查找)、建立(20 組啟用中上限)、驗證、列表(衍生 `active`/`expired`/`revoked` 三態)、撤銷。`lib/mcpServer.ts` 的 `buildMcpServer(credential)` 以官方 SDK `McpServer` 註冊 11 個唯讀工具(`list_transactions`/`get_transactions_summary`/`list_accounts`/`list_categories`/`list_budgets`/`list_recurring`/`list_stock_holdings`/`list_stock_transactions`/`list_stock_dividends`/`list_stock_recurring`/`get_stock_realized_pl`),一律以 `credential.userId` 限定範圍,分頁類工具 `pageSize` 上限 200(超過回傳 `isError: true` 而非空結果)。`app/api/mcp/route.ts` 僅實作 `POST`(`GET`/`DELETE` 回 405),以 `WebStandardStreamableHTTPServerTransport`(`sessionIdGenerator: undefined`)處理無狀態 Streamable HTTP 請求,`Authorization: Bearer <PAT>` 驗證失敗回 401;`middleware.ts` 的 `PUBLIC_PATHS` 新增 `/api/mcp`(略過 Cookie 閘門,全域速率限制與 CSRF 判斷不受影響地自動涵蓋)。查詢邏輯重用既有計算:抽取 `app/api/stock-realized-pl/route.ts` 的 FIFO 已實現損益邏輯為 `lib/stockHelpers.ts` 的 `getStockRealizedPl(userId, { dateFrom?, dateTo?, stockId? })`(無篩選時輸出與抽取前完全一致,篩選僅套用於賣出明細不影響 FIFO 成本計算);抽取 `app/api/reports/route.ts` 的期間收支彙總為 `lib/dashboardHelpers.ts` 的 `getTransactionsSummary()`;抽取 `app/api/dashboard/route.ts` 內嵌的持股/未實現損益組裝邏輯為同檔的 `getStockPortfolioStatus()`(新增每檔持股股數/均價/現價/未實現損益/幣別明細),三個既有路由皆改為呼叫抽取後的函式,行為不變。新增使用者自助憑證管理:`app/api/user/mcp-credentials/route.ts`(`GET` 列表/`POST` 建立,回傳時間欄位一律轉為 ISO 8601 UTC 字串)、`app/api/user/mcp-credentials/[id]/route.ts`(`DELETE` 撤銷),沿用既有 `authToken` Cookie + `requireAuth()`。新增 `app/settings/mcp/page.tsx` + `components/features/settings/McpSettingsClient.tsx`(建立表單、一次性權杖顯示需確認已複製才可關閉、憑證清單含狀態徽章與撤銷按鈕),`components/layout/Sidebar.tsx` 新增導覽項目。`lib/auditHelpers.ts` 的 `AUDIT_METADATA_ALLOWED_KEYS` 新增 `mcp_credential_id`/`mcp_credential_name`,每次 `tools/call` 成功後寫入 `action: 'mcp_query'` 稽核紀錄(`metadata.scope` 為資料範圍摘要,不含實際金額/明細)。`lib/userDeletion.ts` 的 `USER_OWNED_TABLES` 加入 `mcp_credentials`,帳號刪除同步清除憑證。新增 10 語言 i18n 鍵 `navMcp` + 35 個 `settings.mcp.*` 鍵(`shared/i18n/app_*.arb`,經 `npm run i18n:generate` 產出 Web 字典與 Flutter `gen-l10n` 輸出);`mobile/` 未納入本次範圍。測試:新增 `tests/lib/mcpAuth.test.ts`/`tests/lib/stockHelpers.test.ts`/`tests/lib/dashboardHelpers.test.ts`(需 `DATABASE_URL`/`POSTGRES_URL`,未設定時自動略過以保持 `npm test` 在無 DB 環境仍可通過;新增 `tests/setup/register.mjs` 提供 Node 原生 ESM loader 解析 `@/*` 別名與省略副檔名的相對匯入,僅供測試執行使用)。驗證:`npm run build`(含 typecheck)通過;`npm test` 全數通過(含 3 組新增 DB 整合測試、`check:iso`、`check:i18n` 10 語言對齊);以本機 Postgres + dev server 實測完整流程(註冊 → 建立憑證 → 真實 MCP `initialize`/`tools/list`(11 個工具)/`tools/call` → 稽核紀錄寫入 → 撤銷 → 撤銷後立即 401);`/settings/mcp` 頁面 SSR 輸出以 `curl` 驗證渲染正確;受限於本機無法啟動瀏覽器自動化,前端互動(按鈕點擊、複製剪貼簿、彈窗開關)未做視覺化驗證。 |
| 4.95.2 | 2026-07-28 | 修補 GitHub Dependabot 回報的 10 個 npm 安全性告警(alert #36–#45)。`next` 從 `^16.2.10` 升級至 `^16.2.12`(修補 9 項 16.0.0–16.2.10 範圍內的漏洞,含 Server Actions SSRF、Middleware/Proxy bypass、快取污染、Image Optimization API DoS 等,均由 `16.2.11` 起修補);`postcss`(`overrides` 與 `devDependencies` 兩處)從 `^8.5.16` 升級至 `^8.5.18`,修補 Source Map 自動載入時的路徑穿越漏洞(可讀取任意 `.map` 檔)。重新產生 `package-lock.json`(`next`/`postcss` 解析至 `16.2.12`/`8.5.23`,5 個套件變動)。驗證:`npm run typecheck` 通過;`npm test` 中 `test:dashboard-forecast`(7 項)與 `check:iso`(15 項)通過;`check:i18n` 的 staleness 檢查在乾淨 main HEAD 上本就會失敗(Windows 本機既有問題,與本次改動無關,已確認回退變更後現象相同)。 |
| 4.95.1 | 2026-07-25 | CI 自動建立 GitHub Release,取代原本 `/update-docs` 收尾時手動 `gh release create` 的步驟(先前只自動打 tag、不建 Release,Releases 頁曾斷在 v4.76.3 與 v4.94.0)。新增 `.github/scripts/release-notes.mjs`:讀 `changelog.json` 指定版本(`VERSION` 未給時取 `currentVersion`),把 `changes` 依 `tag` 分組輸出 Markdown 至 `NOTES_FILE`,區塊順序固定 `warning`(升級需注意)→`new`(新增)→`improved`(改進)→`fixed`(修正)→`removed`(移除),未知 tag 依出現順序接在後面不丟內容,格式比照既有 v4.94.0 的 `### 新增` + bullet;標題以 `vX.Y.Z — <title>` 印到 stdout,changelog 查無該版時退回 `vX.Y.Z` + 一行通用內文。`.github/workflows/docker-publish.yml` 新增 `Publish GitHub Release` step,觸發條件 `github.ref == 'refs/heads/main' | | startsWith(github.ref, 'refs/tags/v')`,tag 觸發時以`GITHUB_REF_NAME` 為準(可補打舊版)、main 觸發時用 `changelog.currentVersion`。刻意排在 Docker Hub/ghcr 推送之後,Release 出現即代表映像檔可拉取,build 失敗則不發。冪等與競態處理:先`git ls-remote` 確認遠端有該 tag(無則只發 `::warning::` 並 `exit 0`)、再`gh release view`存在即略過不覆寫、`gh release create` 失敗但事後 `view` 查得到視為競態成功,其餘情況才 `::error::` + `exit 1`。沿用 job 既有`contents: write` 權限與 runner 內建的 `gh`/`node`,未新增 secret。驗證:抽出 step body 對 stub 過的`git`/`gh` 跑 6 情境(tag 不在遠端/Release 已存在/正常建立/建立撞競態/建立失敗/tag 觸發補打舊版)全數符合預期,腳本另以 4.95.0、4.84.0(含 `warning` tag)、不存在的 9.9.9、省略 `VERSION` 四種輸入實跑,YAML 解析與 `bash -n` 通過。 |
| 4.95.0 | 2026-07-25 | 交易記錄新增「排除轉帳」篩選。`app/api/transactions/route.ts` 的 `GET` 新增 `excludeTransfer` query 參數(接受 `1`/`true`),成立時於 where 條件追加 `AND t.type NOT IN ('transfer_in', 'transfer_out')`,與既有 `type`/`accountId`/`categoryId`/日期/關鍵字條件同層 AND;因 count 與 list 共用同一組 where 與 params,分頁 `total` 與翻頁結果一致。`components/features/transactions/TransactionsClient.tsx` 將 `excludeTransfer` 併入既有 `filters` 狀態(值為 `'1'`/`''`,沿用字串型別以相容 URL 同步與 `JSON.stringify` 比對),因此自動獲得網址同步(`?excludeTransfer=1`)、`activeFilterCount` 計數與 `clearFilters` 還原;`renderFilterFields()` 新增勾選框,桌機 xl grid template 由 `repeat(5,minmax(0,1fr))_auto` 改為 `repeat(5,minmax(0,1fr))_auto_auto` 讓勾選框與「清除」同列。互斥處理:新增 `toggleExcludeTransfer()`,勾選時若 `filters.type === 'transfer'` 則一併重設為空,類型下拉的 `transfer` option 於勾選狀態 `disabled`;`readFilters()` 同步做邊界正規化,手寫網址同時帶 `type=transfer&excludeTransfer=1` 時以排除轉帳優先,避免「已選但永遠空清單」的矛盾狀態。新增 10 語言翻譯鍵 `features.transactions.excludeTransfer`(來源 `shared/i18n/app_*.arb`,經 `npm run i18n:generate` 產出 Web 字典與 Flutter `gen-l10n` 輸出)。行動版 Flutter 交易頁篩選未納入本次範圍。驗證:`npx tsc -p tsconfig.typecheck.json --noEmit` exit 0、`npm test`(含 `check:i18n` 字典對齊與靜態 `t()` 鍵掃描)全過、i18n 產生器重跑輸出穩定(CI drift gate 不觸發);本機未啟動 dev server 做畫面實測、未跑 `npm run build`。 |
| 4.94.1 | 2026-07-22 | 修補 `Trivy (container scan)` CI 掃到的 HIGH 漏洞 `GHSA-f88m-g3jw-g9cj`(`sharp` 繼承自 libvips 的 CVE-2026-33327/33328/35590/35591)。根因:`next@16.2.10` 自帶 optional dependency `sharp: ^0.34.5`,因 0.x 版號的 caret range 語意(`^0.34.5` 僅涵蓋 `>=0.34.5 <0.35.0`)與專案自身的 `sharp: ^0.35.3` 無法合併,npm 另外裝了一份舊版巢狀套件於 `node_modules/next/node_modules/sharp@0.34.5`,並被 Next.js standalone build 的 tracing 一併打包進最終映像檔(掃描報告路徑為 `app/node_modules/next/node_modules/sharp/package.json`)。專案本身未使用 `next/image`,Next.js 自帶的這份 sharp 純屬未使用的 optional dependency。修法:在 `package.json` 既有的 `overrides` 區塊新增 `"sharp": "^0.35.3"`,強制 npm 全專案僅解析單一份 sharp,消除巢狀舊版拷貝;重新產生 `package-lock.json` 後該巢狀項目與其專屬平台的 `@img/sharp-*` 套件(約 500 行)自 lockfile 移除。驗證:`npm install --package-lock-only` 重生 lockfile 後 `npm ci` 於本機全新安裝成功、0 弱點;Docker 建置與 Trivy 掃描結果待 CI 驗證。 |
| 4.94.0 | 2026-07-22 | 持股總覽的 ETF 卡片新增「配息月份」資訊。`app/api/stocks/route.ts` 的 `GET` 端點在既有 FIFO 持股與市值計算後,新增 `dividendMonths` 衍生欄位:從該股票的 `stock_dividends` 紀錄中篩出 `cash_dividend > 0` 或 `stock_dividend_shares > 0` 的筆數,取 `date` 欄位的月份(`slice(5, 7)`)去重並排序後回傳,完全衍生自使用者既有的配息紀錄,不需另建 ETF 配息月份對照表、也不依賴外部資料源。`components/features/stocks/PortfolioClient.tsx` 新增 `formatDividendMonths()`:以 `Intl.DateTimeFormat`(`month: 'short'`)取得依語系在地化的月份名稱,再用 `Intl.ListFormat`(`style: 'short'`, `type: 'conjunction'`)串接,不支援 `Intl.ListFormat` 的環境 fallback 為逗號串接;僅在 `stockType === 'etf'` 時於卡片標題列下方顯示「配息月份:…」,尚無配息紀錄則顯示提示文字。新增 10 語言 Web 字典鍵 `features.stocks.portfolio.dividendMonths`/`dividendMonthsEmpty`,`node tools/check-i18n-parity.ts` 驗證全對齊、無缺鍵。驗證:`npm run typecheck` 通過;本機未設定 `DATABASE_URL`,無法啟動 dev server 做畫面實測,未跑 `npm run build`。 |
| 4.93.3 | 2026-07-20 | 修正 `/finance/info-board`(滿月資訊版)股票市值每個月都用 `stocks.current_price`(目前股價)重算,導致已結束月份的金額隨當下股價變動的問題(例如 6 月的股票市值會被 7 月最新股價蓋掉)。根因:`lib/fullMoonInfoBoard.ts` 的 `monthlyStockValues()` 對 12 個月一律套用同一個目前股價,沒有區分「已結束月份」與「進行中月份」。修法:新增資料表 `stock_month_close_prices`(`symbol` + `year_month` 為複合主鍵,依股票代號跨使用者共用,不綁定單一使用者)與 `lib/stockMonthClosePrice.ts` 的 `getMonthClosePrices()`:對已結束月份(`index < visibleThroughMonthIndex`)先查快取,未命中才呼叫既有的 `fetchTwseStockDay`/`fetchTpexStockDay`(帶入該月最後一天日期,TWSE/TPEx STOCK_DAY 端點會回傳整月資料,取陣列最後一筆即為當月最後交易日收盤價)並以 `ON CONFLICT(symbol, year_month) DO UPDATE` 回寫快取;`monthlyStockValues()` 已結束月份改用鎖定收盤價,當月(`index === visibleThroughMonthIndex`,尚未結束)仍用 `stocks.current_price` 即時股價,抓不到歷史收盤價時 fallback 回目前股價。`buildStockRows()`/`getFullMoonInfoBoardData()` 改為 async 以支援必要時的即時抓價,`app/finance/info-board/page.tsx` 對應加上 `await`。順手修正 `lib/twseFetchNext.ts` 的 `fetchAllWithLimit<T>` 泛型:原本回傳結果的 `value` 型別誤標成跟輸入 `item` 同型(實際是 fetcher 的回傳值),改為 `fetchAllWithLimit<TItem, TValue>` 兩個獨立型別參數,使新模組不需 `@ts-nocheck` 即可正確通過型別檢查。另補上先前 PR #193/#194 新增的排程現金展望與儲蓄情境試算 shared ARB 翻譯鍵所缺漏的 Flutter `gen-l10n` 產生輸出(`mobile/lib/generated/l10n/app_localizations*.dart`),修正 main 上持續失敗的 i18n 一致性檢查。 |
| 4.93.2 | 2026-07-19 | 修正 `components/features/recurring/RecurringClient.tsx` 的 `openEdit()` 未將 `recurring.amount`(DB 內一律以 TWD 儲存)還原成原幣別金額即填入編輯表單的問題。根因:`app/api/recurring/route.ts` 的 `GET` 回傳的 `amount` 為已換算的 TWD 整數,但 `openEdit()` 直接把該值塞進表單的 `amount` 欄位,同時 `currency`/`fxRate` 仍保留原始外幣設定;使用者若未察覺並直接儲存,`PUT /api/recurring/[id]` 會把畫面上的 TWD 金額當成原幣別金額,再次乘上 `fxRate` 換算,導致金額被壓縮,尤其是 TWD 匯率偏低的幣別(如 JPY、KRW、VND)在反覆編輯幾次後換算結果會 `Math.round()` 為 0,觸發既有的「金額必須為正整數(本幣)」400 驗證錯誤(`app/api/recurring/route.ts:87`、`app/api/recurring/[id]/route.ts:62`)。行動版 `mobile/lib/screens/recurring_screen.dart:247` 已透過 `shown = e.amount / rate` 正確處理此還原邏輯,僅網頁版遺漏。修法:`openEdit()` 比照行動版,當 `currency !== 'TWD'` 且匯率有效時以 `amount / fxRate` 還原原幣別金額並用 `fmtAmountForEdit()` 格式化(整數不留小數,否則取到小數點後兩位)後再填入表單;純 TWD 的固定收支不受影響。 |
| 4.93.1 | 2026-07-19 | 修正固定收支 `exclude_from_stats`(及其他布林旗標欄位如 `is_active`/`needs_attention`)讀取時被誤判的問題。根因:`lib/db.ts` 的 `translateDdlTypes()` 在 `CREATE TABLE`/`ALTER TABLE` 時會把 schema 中所有 `INTEGER` 欄位(含布林旗標)改建為 PostgreSQL `BIGINT`(原意是避免毫秒時間戳欄位溢位 `int4`),但 `node-postgres` 對 `BIGINT`(OID 20)預設以字串回傳查詢結果;`app/api/recurring/route.ts` 的 `GET` 端點以 `!!r.exclude_from_stats` 轉布林,字串 `"0"` 為 truthy,導致無論實際存的是 0 或 1,回傳一律為 `true`(不計入統計)。PR #192(commit `02691038`)修正的是請求 body 欄位命名不一致(`includeInStats`/`excludeFromStats`)的另一個問題,未觸及此讀取端 bug,故該次修正後問題仍未解決。修法:在 `lib/pgSyncWorker.cjs` 建立 `Pool` 前呼叫 `types.setTypeParser(20, (value) => parseInt(value, 10))`,將 BIGINT 欄位統一解析回 JS number(本專案所有 BIGINT 欄位皆為 0/1 旗標或毫秒時間戳,均在安全整數範圍內),一次性修正所有透過 `!!row.someIntColumn` 讀取布林旗標的既有程式碼路徑,無需逐一修改各 API 路由。 |
| 4.93.0 | 2026-07-19 | 新增「排程現金展望」與「儲蓄情境試算」兩個 Dashboard 模組。新增 `lib/recurringSchedule.ts`:從 `lib/recurringHelpers.ts` 抽出 `getNextRecurringDate()`(新增嚴格日曆驗證,輸入日期若非真實存在的西曆日期則回傳 `null`,避免如 2 月 30 日等不存在日期被 `Date.UTC` 自動進位成錯誤的下個月日期)、新增 `addDaysToIsoDate()`、`listRecurringDatesInWindow()`(依 `startDate`/`lastGenerated`/`frequency` 列舉指定日期區間內的所有發生日,處理 daily/weekly/monthly(含月底夾附)/yearly(含閏年 2/29)、逾期排程與 `maxOccurrences` 上限防護);`lib/recurringHelpers.ts` 改為 re-export 以維持既有呼叫相容。新增 `lib/dashboardForecast.ts`:`getScheduledAccountImpactTwd()` 以 Decimal 計算單筆固定收支對連結帳戶幣別的影響金額(處理台幣/外幣、既有匯率快照與目前匯率、手續費換算,匯率非正數或缺漏回傳 `null` 視為不可估算);`buildScheduledCashOutlook()` 為純函式,輸入今日日期、銀行帳戶餘額、帳戶數與已標準化的排程清單,輸出 `ScheduledCashOutlook`(`available`/`unavailableReason`(`invalidDate`/`noBankAccounts`/`noSchedules`/`noCoveredSchedules`)/期間起訖/起始餘額/收支總額/期末估算餘額/期間最低估算餘額與日期/首次現金可能低於零的日期與金額/已涵蓋與未涵蓋排程數/`upcomingEvents`(最多 100 筆,依日期/類型/備註/ID 穩定排序))。`app/api/dashboard/route.ts` 的 `GET` 新增 `cashOutlook` 可加性欄位:彙總目前使用者已納入(`included=true`)的銀行帳戶餘額作為起始餘額,篩選 `is_active=1` 且 `needs_attention=0`、已連結帳戶且該帳戶為銀行類型並納入計算的固定收支,逐筆以 `getScheduledAccountImpactTwd()` 換算為 TWD 後交給 `buildScheduledCashOutlook()`;未連結帳戶、非銀行帳戶、未納入、`needs_attention=1`、找不到匯率的排程計入 `uncoveredScheduleCount` 而非靜默排除,維持既有 `dataStatus`/`assets`/`comparison`/`portfolioHealth` 欄位與 `user_id` 範圍查詢不變。`lib/dashboardPreferences.ts` 的 `DASHBOARD_MODULE_IDS` 新增 `cashOutlook`/`savingsScenario` 兩個模組 ID,`DashboardLayoutPreference`/`DEFAULT_DASHBOARD_LAYOUT`/`normalizeDashboardLayout()` 的 `version` 由 `1` 提升為 `2`(既有已儲存排版仍可正規化、缺漏的新模組 ID 會補齊到清單尾端)。`app/dashboard/page.tsx` 新增「排程現金展望」區塊(顯示起始餘額、預計收支、期末估算、最低估算與日期、已涵蓋/未涵蓋排程數、即將發生的排程列表、可能現金短缺的提醒與導向固定收支/帳戶設定的連結)與新增用戶端元件 `app/dashboard/components/SavingsScenario.tsx`(「儲蓄情境試算」:使用者輸入每月調整金額並以按鈕/鍵盤選擇 6/12/24 個月期間,累積差額 = 每月金額 × 月數,即時計算不呼叫 API,含 `aria-live` 精簡播報與清楚標示為簡易試算而非投資報酬預測的免責文字);`app/dashboard/components/DashboardPersonalization.tsx` 補上這兩個新模組的排序/顯示標籤鍵。新增 `tests/lib/dashboardForecast.test.ts`(純 Node 測試,覆蓋 daily/weekly/monthly 月底夾附/yearly 閏年排程、逾期排程、幣別換算、無帳戶/無排程/無涵蓋排程/日期無效的不可用狀態、現金低於零的判定與排序穩定性),新增 `npm run test:dashboard-forecast` 並納入 `npm test`。新增 10 語言 Web 字典(`dashboard.cashOutlook.*`、`dashboard.scenario.*`、`dashboard.personalize.modules.cashOutlook`/`savingsScenario`)與對應 shared/mobile ARB 並重新產生輸出。驗證:`npm run typecheck`、`npm test`(含新增 7 項排程現金測試)、`npm run check:i18n`(10 語言 1,217 鍵值一致)、`git diff --check`、`npm run build` 全數通過(Windows sandbox 外執行)。 |
| 4.92.0 | 2026-07-18 | 新增可個人化排序的 Dashboard、月度收支變化解讀與投資組合成本健診。新增 `lib/dashboardPreferences.ts`:固定 `DASHBOARD_MODULE_IDS` allowlist(`assets`/`attention`/`whyChanged`/`spending`/`portfolioHealth`/`incomeRecent`),`normalizeDashboardLayout()`/`parseDashboardLayout()` 過濾非法模組 ID 並補齊缺漏順序。`lib/db.ts` 對 `user_settings` 新增可加性欄位 `dashboard_layout TEXT DEFAULT '{}'`、`dashboard_layout_updated_at INTEGER DEFAULT 0`(既有表以 `alterIgnore` 相容升級)。新增 `app/api/user/settings/dashboard/route.ts`:`GET` 回傳目前排版與版本戳;`PUT` 以 `WHERE COALESCE(dashboard_layout_updated_at,0) = ?` 條件式 `UPSERT` 做樂觀鎖,版本不符時回傳 `409 OptimisticLockConflict`。新增 `app/dashboard/components/DashboardPersonalization.tsx`:鍵盤可操作的排序/隱藏對話框,含重設預設值。`lib/dashboardInsights.ts` 新增 `getDashboardComparisonWindow(selectedMonth, today)`:當月採「當月至今 vs 上月同天數」對齊比較(`monthToDate`),歷史月份採「完整月 vs 上一個完整月」(`fullMonth`),未來月份回傳 `null`;`buildDashboardChangeDrivers()` 依收入/支出分類彙總計算 Top 3 淨變化驅動因子(`netContribution` 依收入/支出反向計號、`isNew` 標示上期無資料的新分類)。`app/api/dashboard/route.ts` 的 `GET` 新增 `preferences`(讀取並正規化 `dashboard_layout`)、`comparison`(依 `getDashboardComparisonWindow()` 查詢當期/前期收支分類彙總並計算 `buildDashboardChangeDrivers()`,前期無交易時 `available=false`)、`portfolioHealth`(`getStockPortfolioStatus()` 擴充:以現有 FIFO 成本與目前市值比較,回傳 `unrealizedGrossPL`/`costReturnRate`/`largestHolding`/`unavailableReason`,`noHoldings`/`missingPrices`/`mixedCurrencies` 明確狀態取代虛構百分比,非追蹤大盤或年化報酬率指標)三組可加性欄位,維持 `user_id` 範圍查詢與既有欄位不變。`components/ui/dialog.tsx` 關閉按鈕定位由 `right-2` 改為邏輯屬性 `end-2`,修正 RTL 語言下位置錯誤。新增 `tests/lib/dashboardPreferences.test.ts` 與擴充 `tests/lib/dashboardInsights.test.ts`(比較期間跨年/跨月邊界、驅動因子正負號與穩定排序),`test:dashboard-preferences` 已納入 `npm test`;新增 10 語言 Web 字典與對應 mobile ARB 的個人化/比較/投資健診文案鍵值並重新產生輸出。驗證:`npm run typecheck`、`npm test`、`npm run check:i18n`、`git diff --check`、`npm run build` 全數通過。 |
| 4.91.0 | 2026-07-18 | 新增可執行的 Dashboard 待辦/驅動因子與交易頁行動裝置卡片化,附加測試與翻譯。新增 `lib/dashboardInsights.ts`(純函式,無 `@/lib/*` 別名依賴,供純 Node 測試載入):`getHoldingMarketContribution(totalShares, currentPrice)` 判斷持股是否「已持有但無正向現價」(`unpriced`),`buildDashboardDrivers()` 由既有收入/支出分類彙總算出本月金額最高的前三名驅動因子(含 `share` 佔比)。`app/api/dashboard/route.ts` 的 `GET` 回應新增可加性欄位:`dataStatus.generatedAt`(查詢時間戳)、`dataStatus.unpricedHoldingCount`(`getStockPortfolioStatus()` 取代原 `getStockMarketValue()`,回傳 `{ marketValue, unpricedHoldingCount }`)、`attention.recurringNeedsAttentionCount`(`recurring.needs_attention=1 AND is_active=1` 計數)、`attention.uncategorizedTransactionCount`/`uncategorizedAmount`(當月 `category_id IS NULL OR ''` 的收入/支出交易),皆維持 `user_id` 範圍查詢、無 schema 變更。`app/dashboard/page.tsx` 新增「待辦提醒」(最多 3 項,含週期扣款/未分類交易/持股缺現價,導向對應目的地,未分類項目走 `categoryId=__uncategorized__` 篩選)與「本月收支排行」(Top 3 驅動因子)兩個 section,並顯示 `dataStatus.generatedAt` 的在地化查詢時間。`components/features/transactions/TransactionsClient.tsx`:`md` 以下改用完整欄位卡片列表取代表格(狀態、備註、附件、轉帳/外幣/週期/排除旗標、編輯/刪除、多選皆與桌面版等價),篩選面板在行動裝置收合為可展開面板並顯示已套用篩選數(`activeFilterCount`),桌面版篩選維持常駐;新增浮動快速新增按鈕與多選時的固定式批次操作列(行動裝置);搜尋輸入改為 300ms 防抖(`searchDraft` state),並以 `latestLoadId` ref 忽略過期的 `load()` 回應避免舊資料覆蓋新結果;新增/編輯表單優先呈現類型/金額/分類/帳戶,日期等欄位收合於 `advancedOpen` 控制的「更多欄位」,表單驗證失敗時自動展開對應區塊;`app/api/transactions/route.ts` 新增 `categoryId=__uncategorized__` 查詢條件支援。新增 `tests/lib/dashboardInsights.test.ts`(`npm run test:dashboard-insights`,已納入 `npm test`)覆蓋 Top 3 驅動因子與持股估值(含零/負現價)情境;新增 10 語言 Web 字典與 12 語言 mobile ARB 的 Dashboard 待辦/驅動因子文案鍵值並重新產生對應輸出。驗證:`npm run typecheck`、`npm test`、`npm run check:i18n`、`git diff --check`、`npm run build`(147/147 靜態頁、exit 0)全數通過。 |
| 4.90.0 | 2026-07-18 | 已登入 Web 殼層與 `/dashboard` 使用者導向重新設計,並同步強化 Flutter App 的無障礙與視覺一致性,未變更任何財務計算或 API 契約。Web:`/dashboard` 改為單一月度現金流 hero + 可展開的收入/支出/帳戶/投資組合明細卡片,新增 `?action=add` 一次性參數讓 CTA 直接開啟既有新增交易對話框(`TransactionsClient.tsx` 處理後即從 URL 移除);新增 `components/layout/MobileNav.tsx` 手機版底部四頁籤導覽(lg 以上隱藏,保留完整側邊欄);`Sidebar.tsx`/`StocksTabNav.tsx` 由 `<button onClick=router.push>` 改為語意化 `<Link>` 並加上 `aria-current`;側邊欄新增 focus trap/entry/restore、Escape 關閉、背景 `inert`、RTL(`start`/`end` 邏輯屬性取代 `left`/`right`);`AppLayout.tsx` 新增翻譯後的跳至主要內容連結與唯一 `main`/`h1` landmark;`Modal.tsx` 改為包裝 Base UI `Dialog` 以取得一致的焦點管理;`globals.css` 拆分深色模式文字強調色與純色主要按鈕色,並提高淺色模式語意色對比(primary/income/expense/success/danger 均加深),月份切換按鈕加大至 44px。Mobile:新增 `mobile/lib/theme.dart`(`AssetPilotTheme` ThemeExtension,income/expense/profit/loss/warning/stale 語意色,明暗主題各自定義)並在 `app.dart` 套用、統一按鈕最小 48x48 觸控範圍;`widgets.dart` 的 `AsyncView` 重新整理時若已有資料不再閃回 loading spinner、錯誤訊息改用 `_safeErrorMessage()` 依 `ApiException.statusCode`(401 / 連線失敗 / 一般失敗)分類且不洩漏底層例外內容、`EmptyState` 新增 title/action、`toast()` 新增 isError/isSuccess 語意配色,並全面加上 Semantics/liveRegion 供螢幕報讀器使用;`format.dart` 的 `money()` 修正貨幣代碼未 trim/uppercase 的顯示問題並改用 `NumberFormat.currency(name:)`、`signed()` 改用正確負號字元並以絕對值格式化。Web 與 Mobile 共用 i18n 新增 `shell.skipToContent` 翻譯鍵(9 語言 Web 字典 + 12 語言 mobile ARB 同步)。 |
| 4.89.3 | 2026-07-17 | 修正 App 端因後端瞬斷(504/521 閘道逾時、連線中斷、header 未接收完整)被誤判為使用者可見錯誤的問題:`ApiClient._send` 對 GET 請求的暫時性重試範圍從 502/503/504 擴大到含 521,並新增對連線層例外(`ClientException`/`TimeoutException`/`SocketException`)的單次重試(排除一次性 nonce/state 端點以免重放攻擊面問題);同時關閉 `SentryFlutterOptions.captureFailedRequests`,避免已由 API client 處理並記錄為結構化 warn log 的失敗又被 `SentryHttpClient` 重複建立 error event。對應 Sentry issue ASSETPILOT-APP-A/B/E/F/G/H/J/K/M/N。新增對應單元測試(`mobile/test/widget_test.dart`)驗證重試判斷邏輯與 Sentry 設定。 |
| 4.89.2 | 2026-07-15 | 修正 4.89.1 新增 WorkManager 後 Android release build(R8 開啟)開啟即閃退:R8 預設規則會移除 WorkManager 內部 Room 資料庫實作類別 `WorkDatabase_Impl` 的無參數建構子,導致 `androidx.startup.InitializationProvider` 初始化 `WorkManagerInitializer` 時丟出 `NoSuchMethodException`,在 `Application`/`MainActivity` 建立前就使整個 process crash(實機 logcat 確認:Samsung Android 16,`appErrorCount=10`)。新增 `mobile/android/app/proguard-rules.pro` 保留該建構子與 `RoomDatabase` 子類別,並在 `build.gradle.kts` release buildType 明確接上 `proguardFiles`;已在 emulator 以 release build 驗證全新安裝與覆蓋安裝皆可正常啟動。 |
| 4.89.1 | 2026-07-15 | 修正 Android 桌面小工具在 Flutter App 關閉後不再更新:新增 WorkManager 定期工作,以 `flutter_secure_storage` 原生端相同設定讀取 `authCookie`,在連網時重新取得 Dashboard 快照並更新月度總覽、今日支出、資產與近期交易小工具;加入登出 generation guard、401/403 清除,以及依已安裝小工具排程/取消。交易新增、編輯、刪除後亦立即重抓 Dashboard,並新增 Kotlin JSON 解析與格式化單元測試。 |
| 4.89.0 | 2026-07-14 | 交易表單幣別欄位由固定下拉選單改為自由輸入 + 建議清單(web: `TransactionsClient.tsx` 的 `<datalist>`;mobile: `transaction_form_screen.dart` 的 `Autocomplete<String>`),並新增送出前的 `^[A-Z]{3}$` 格式驗證,格式不符時擋下儲存並顯示錯誤訊息。 |
| 4.88.5 | 2026-07-09 | 相依套件例行升級:`next` `^16.2.9` → `^16.2.10`、`resend` `^6.16.0` → `^6.17.2`、`@types/node` `^26.1.0` → `^26.1.1`;`package-lock.json` 以 `npm install` 同步重生(105 個套件變動,0 個已知漏洞)。Node 24 環境下 `npm run typecheck`/`npm run build`/`npm test`(`test:tz`/`test:photo-crypto`/`test:info-board`/`check:iso`)驗證通過。 |
| 4.88.4 | 2026-07-09 | 將開發相依套件 `typescript` 從 `^6.0.3` 升級至 `^7.0.2`。TypeScript 7 不再提供 `typescript/lib/typescript.js`,但 Next.js 16.2.9 仍會檢查該路徑以載入 `tsconfig` 的 `paths` 並執行內建型別驗證,因此 `next.config.ts` 的 `typescript.ignoreBuildErrors` 改為 `true`(跳過 Next 內建型別驗證),改由明確的 `npm run typecheck`(新增 `tsconfig.typecheck.json`,`extends` 主設定並排除尚未安裝 `@playwright/test` 的 `tests/e2e`)在 `next build` 前把關;`package.json` 的 `build` script 改為 `npm run typecheck && next build --webpack`。因 Next 16 的 webpack 設定無法透過 TS7 的新版 JS API 讀取 `tsconfig.paths`,`next.config.ts` 的 webpack `config.resolve` 新增明確的 `@` alias 指向 `PROJECT_ROOT` 以維持既有 `@/*` 路徑別名運作。另新增 `@typescript/native-preview@7.0.0-dev.20260707.2` 作為 Next 16 相容性訊號,避免建置時嘗試自動安裝舊版 `typescript`;實際型別檢查仍由 `typescript@7.0.2` 執行。過程中曾誤將 `compilerOptions.baseUrl` 加入 tsconfig 作為別名修法,但 TypeScript 7 已移除 `baseUrl`(`TS5102`),已改回改用 webpack alias,教訓記錄於 `tasks/lessons.md`「2026-07-09 TypeScript 7 Removed baseUrl」。 |
| 4.88.3 | 2026-07-07 | 修正 `/finance/info-board`(滿月資訊版)在目前月份之後的月份仍顯示帳戶餘額、股票市值、收支或現金股利數字的問題。根因:`lib/fullMoonInfoBoard.ts` 的帳戶/股票餘額查詢一律以 `monthEnd(year, 11)`(12 月)為上限彙總,收支與股利查詢則以 `${year}-12-31` 為上限,與「今天」無關,導致當年度 8-12 月(尚未到達)仍被計入交易餘額與統計;頁面渲染也把未來月份的合計、淨資產、月增率顯示為 0 而非留白。修法:新增 `lib/fullMoonInfoBoardCutoff.ts` 提供 `visibleThroughMonthIndexForToday()`(依使用者時區今天算出可見月份索引)、`blankMonthsAfter()`、`isFutureMonthIndex()`;`getFullMoonInfoBoardData()` 改為 `currentPeriodForUser()` 同時回傳 `year` 與 `visibleThroughMonthIndex`,所有帳戶/股票/收支/股利彙總函式改以此索引為查詢與陣列上限;`FullMoonInfoBoardData` 新增 `visibleThroughMonthIndex` 欄位供頁面使用;`app/finance/info-board/page.tsx` 的 `change()`/`growth()`/`rowTotal()`/`sectionTotal()`/`sectionPercent()`/`rowPercent()` 改接受 `visibleThroughMonthIndex` 上限,未來月份的合計、淨資產、月增率欄位改為留白(非 0)。新增 `tests/lib/fullMoonInfoBoard.test.ts` 並納入 `npm run test:info-board`/`npm test`。 |
| 4.88.2 | 2026-07-07 | 修正 `/api/dashboard` 與 `/finance/info-board` 在既有 PostgreSQL 環境下因 `accounts`/`transactions`/`recurring`/`stocks` 資料表缺少 `currency` 欄位而 500 的問題(`column "currency" does not exist"`,Zeabur runtime log 顯示 `/api/dashboard` 連續多次報錯,對應 `getBankBalanceTwd()` 逐帳戶呼叫 `calcBalance()` 時對每個帳戶各報一次錯)。根因:這四張表的 `CREATE TABLE` 定義皆含 `currency TEXT DEFAULT 'TWD'`,但既有表 ALTER 相容清單從未補上這個欄位,導致既有資料庫(非全新建表)持續缺欄位,與前一版 `avg_cost` 同一類缺漏。修法:`lib/db.ts` `_runMigrations()` 新增 `accounts`/`transactions`/`recurring`/`stocks` 四張表的 `alterIgnore("ALTER TABLE ... ADD COLUMN currency TEXT DEFAULT 'TWD'")` 相容遷移,補齊既有表。 |
| 4.88.1 | 2026-07-07 | 修正 `/finance/info-board` 在既有 PostgreSQL 環境下因 `stocks` 資料表缺少 `avg_cost` 欄位而 500 的問題(`column "avg_cost" does not exist`,Zeabur runtime log 2026-07-07T04:18:18Z)。根因:`lib/db.ts` 的 `CREATE TABLE stocks` 已含 `avg_cost`,但既有表 ALTER 相容清單只補了 `current_price`/`stock_type`/`delisted`,漏了 `avg_cost`,導致既有資料庫(非全新建表)缺這個欄位。修法:`lib/db.ts` `_runMigrations()` 新增 `alterIgnore("ALTER TABLE stocks ADD COLUMN avg_cost REAL DEFAULT 0")` 補齊既有表;同時 `lib/fullMoonInfoBoard.ts` 的 `buildStockRows()` 移除未使用的 `avg_cost` 欄位查詢(股票市值運算本就只用 `current_price`/`stock_transactions`/`currency`,不依賴 `avg_cost`),改用最小欄位集合避免同類欄位缺漏再次觸發整頁 500。 |
| 4.88.0 | 2026-07-07 | 新增 `/finance/info-board`「滿月資訊版」頁面:以電子表格式版面(月份為欄、分類為列)呈現全年資產市值、收入、負債、支出,並算出個人淨值與月成長率,版面設計參考 `docs/跟著柴鼠減加乘-滿月記帳法.xlsx` 的欄位配置。新增 `lib/fullMoonInfoBoard.ts` 的 `getFullMoonInfoBoardData(userId)` 聚合當前使用者本年度資料:資產=非信用卡帳戶月底餘額(依 `accounts`/`transactions` 逐月重算並套匯率換算)+ 持股市值(`stocks` 現價 × `stock_transactions` 累積股數);收入=依分類彙總的收入交易 + `stock_dividends` 現金股利;負債=信用卡帳戶月底應繳餘額;支出=依分類彙總的支出交易,比對分類/項目名稱含「投資、股票、基金、ETF」關鍵字標記 `investment` tone。年度依使用者時區(`todayInUserTz`)當前年份決定。原頁面(前次提交)為固定示範數字,本次移除示範陣列改為即時查詢,`page.tsx` 收斂為純呈現層。導覽選單新增 `infoBoard`(顯示文字「資訊版」)多語系鍵值。 |
| 4.87.0 | 2026-07-02 | Android 桌面小工具第二批 + 尺寸規格統一。新增三個 `AppWidgetProvider`(`mobile/android/.../AssetPilotWidgets.kt`):`TodayExpenseLargeWidgetProvider`(今日花費大版,4×4,共用 `readDashboard()` 的 `todayExpense`/`period`/`updatedAt`)、`RecentTransactionsWidgetProvider`(近期交易,4×4,最多 5 列 title/subtitle/amount,tone 上色綠/紅/灰)、`RecurringBillReminderWidgetProvider`(週期帳單提醒,4×4,最多 5 列 title/detail/amount/date,status 上色逾期/三天內/之後),皆加入 `updateAllWidgets()`;對應新增 layout、xml provider-info、`strings.xml` 與 AndroidManifest `<receiver>`。資料流:近期交易沿用既有 `updateDashboard` 通道,`AppWidgetSync.updateDashboard()` 追加 `recentCount`/`recentTitle$i` 等欄位(取 `Dashboard.recent` 前 5 筆);週期帳單提醒新增 MethodChannel method `updateRecurringReminders`(`MainActivity` handler + `writeRecurringReminders`/`readRecurringReminders`),Dart 端 `AppWidgetSync.updateRecurringReminders()` 由 `Recurring`(active+expense,`_nextRecurringDate` 依 daily/weekly/monthly/yearly 推算下次扣款)與 `Account`(credit_card 未繳金額 `_creditCardPaymentReminder`)合併排序後寫入,於 `DashboardScreen._load()` 與 `RecurringScreen._load()` 觸發。尺寸規格統一:8 個 provider-info 一律規範為 2×2(`minWidth/minHeight=110dp`)或 4×4(`250dp`),並補 `targetCellWidth/Height`(API 31+ 格線吸附);`widget_today_expense_info.xml` 由不規則 2×1(`minHeight=56dp`、`resizeMode=horizontal`)改正規 2×2,`widget_today_expense.xml` 還原被壓縮的字級/內距與副標可見性。 |
| 4.86.0 | 2026-07-02 | Android 桌面小工具(App Widget)第一批:新增 `MonthlyOverviewWidgetProvider`(本月收支總覽,2×2,含 income/expense/net 與支出占收入比例 ProgressBar)、`QuickTransactionWidgetProvider`(快速記帳,「+記一筆」+ 餐飲/交通/購物捷徑)、`TodayExpenseWidgetProvider`(今日花費,2×1,顯示 Dashboard.todayExpense)三個 `AppWidgetProvider`(`mobile/android/.../AssetPilotWidgets.kt`),對應 layout/drawable/xml provider-info 與 `strings.xml`、AndroidManifest `<receiver>`。資料流:Dart 端 `mobile/lib/app_widget_sync.dart` 的 `AppWidgetSync` 透過 MethodChannel `assetpilot/widgets` 將 `Dashboard` 快照(含 twd 格式化字串、支出進度、更新時間)寫入原生 SharedPreferences(`assetpilot_widgets`),由 `MainActivity` handler 寫入並刷新所有 widget;`DashboardScreen._load()`、`TransactionFormScreen._refreshDashboardWidgets()` 儲存交易後同步,`ApiClient._clearAuth()` 登出時 `clearDashboard()` 清快照。快速記帳走 `assetpilot://transaction/new?category=<food\|transport\|shopping>&source=widget` deep link(Manifest 新增 host=transaction pathPrefix=/new intent-filter),`app.dart` `_handleIncomingLink` 帶入 `TransactionFormScreen(initialType:'expense', initialCategoryShortcut:...)`,`_applyInitialCategoryShortcut()` 依捷徑對應父/子分類。Review 修正:`transaction_form_screen.dart` `_type = initialType!`(Dart 不因 `==` 字面值比較收窄 nullability 的 invalid_assignment 編譯錯誤);`app.dart` 深連結去重由長存 SharedPreferences 改為 `await` 前同步設定的記憶體旗標 `_openingDeepLink`,修正同分類捷徑取消後於同一 widget 更新週期內無法重複點擊記帳的問題。 |
| 4.85.0 | 2026-07-01 | Turnstile 人機驗證擴大覆蓋範圍:`app/api/auth/google/route.ts`、`app/api/auth/line/authorize/route.ts`、`app/api/auth/line/state/route.ts`、`app/api/auth/passkey/login/route.ts` 皆新增 `getTurnstileSiteKey()` + `verifyTurnstileToken()` 檢查(原僅 `app/api/auth/login/route.ts` 密碼登入有此防護)。`lib/lineOAuthState.ts` 的 `issueLineOAuthState()`/`consumeLineOAuthState()` 改為 `consumeLineOAuthStateEntry()`,state entry 附帶 `flow` 與 `turnstileVerified` 欄位,於 `/api/auth/line` callback 消費 state 時強制檢查 `turnstileVerified`(僅 login flow,避免帳號連結 link flow 誤擋),防止繞過 authorize/state 端點的驗證直接呼叫 callback。前端 `app/login/page.tsx`、`app/app/passkey-login/page.tsx` 與行動端 `mobile/lib/{api_client,google_auth,line_auth,passkey_auth}.dart`、`mobile/lib/screens/login_screen.dart` 同步傳遞 turnstile token。另升級相依套件:`lucide-react` `1.22.0→1.23.0`、`@types/node` `24.13.2→26.1.0`(大版本更新,`tsc --noEmit` 驗證無新增錯誤;`engines.node` 仍鎖定 `>=24.0.0 <25`,型別與 runtime 版本不完全一致但無相容性問題)。`mobile/pubspec.yaml` 的 `intl` 因 `flutter_localizations` SDK 鎖定 `0.20.2`,維持不變。 |
| 4.84.3 | 2026-07-01 | 行動版 `api_client.dart` 的 `_send()` 對 GET 請求新增一次性重試:遇 502/503/504 閘道逾時時延遲 2 秒重試一次(見 Sentry ASSETPILOT-APP-A/B/C/D:`/api/config`、`/api/dashboard`、`/api/auth/google\|line/state` 等端點瞬斷),非 GET 或重試後仍失敗則維持原行為。同時升級 npm(`adm-zip` `0.5.17→0.5.18`、`lucide-react` `1.21.0→1.22.0`、`nodemailer` `9.0.1→9.0.3`、`sharp` `0.35.2→0.35.3`、`tailwindcss`/`@tailwindcss/postcss` `4.3.1→4.3.2`、`postcss` `8.5.15→8.5.16`)與 Flutter(`fl_chart` `0.69.2→1.2.0`、`app_links` `6.3.2→7.2.0`、`flutter_secure_storage` `9.2.2→10.3.1`、`package_info_plus` `8.3.1→10.2.0`)相依套件至最新相容版本。 |
| 4.84.2 | 2026-06-28 | 行動 App 套件相容性維護:`mobile/pubspec.lock` 於既有 constraints 內升級 app_links_platform_interface 2.0.2 → 2.0.3、path_provider_linux 2.2.1 → 2.2.2、path_provider_platform_interface 2.1.2 → 2.1.3、shared_preferences_android 2.4.25 → 2.4.26、webview_flutter 4.13.1 → 4.14.0;未調整 direct dependency constraints,major candidates 保留人工評估。 |
| 4.84.1 | 2026-06-28 | 登入頁 Turnstile 多語化與 UX 修正:`app/login/page.tsx` 新增 `turnstileLanguage`(從 `useT().locale` 衍生)並傳入 `window.turnstile.render()` 的 `language` 選項;useEffect 補 cleanup 函式(`turnstile.remove` + reset token),防止頁籤切換後 widget 殘留;提交時驗證失敗改以 `scrollIntoView` 提示使用者;登入按鈕 `disabled` 移除 `turnstileEnabled && !turnstileToken` 條件,改在 submit handler 做驗證。`app/api/auth/login/route.ts` 將 `isTurnstileConfigured()` 改為 `getTurnstileSiteKey()`;引入 `localeFromAcceptLanguage` + `getTranslator`,Turnstile 驗證失敗錯誤訊息改依 `Accept-Language` header 回傳本地化文字。 |
| 4.84.0 | 2026-06-28 | login_audit_logs / login_attempt_logs / login_sessions 三個資料表新增 device_id TEXT DEFAULT '' 欄位(alterIgnore migration);recordLoginAudit / recordLoginAttempt 從 X-AssetPilot-Device-Id header 提取 device ID 寫入 device_id 欄;createLoginSession 同步寫入 device_id;listLoginSessions 回傳 deviceId 欄位;GET /api/user/login-audit 回傳 deviceId;data_safety_export.csv PSL_DEVICE_ID 的 PSL_APP_FUNCTIONALITY 更新為 true。 |
| 4.83.0 | 2026-06-28 | i18n 單一來源化(Shared i18n Source of Truth):新增 `shared/i18n/locales.json`(locale 元資料:id、arbLocale、tsFile、htmlLang、dir、label、androidTag、flutter、matchPrefixes)與 10 個 ARB 字典(`shared/i18n/app_*.arb`)為唯一來源;`tools/generate-shared-i18n.mjs` 負責產出:(1) `lib/i18n/generated/config.ts`(GENERATED_LOCALES、GENERATED_HTML_LANG、GENERATED_HTML_DIR、GENERATED_LOCALE_LABELS、GENERATED_LOCALE_PREFIX_ALIASES、GENERATED_DEFAULT_LOCALE)、(2) `mobile/lib/generated/app_locales.dart`(kSupportedAppLocales、kAppLocaleLabels、kDefaultAppLocale、kLocalePrefixAliases)、(3) `mobile/lib/generated/shared_translations.dart`(各 locale 翻譯常數)。`lib/i18n/config.ts` 改 import generated config;`mobile/lib/l10n.dart` 改 import generated constants,`normalizeAppLocale` 改為 alias table 驅動。新增 `mobile/l10n.yaml`,flutter gen-l10n 讀 shared/i18n/*.arb 產出 `mobile/lib/generated/`。`package.json` 新增 `i18n:generate` 指令(`generate-shared-i18n.mjs && flutter gen-l10n`)與 `check:i18n` 加 `--check` 旗標。CI `i18n-check.yml` 擴展:setup Flutter → `npm run i18n:generate`(重生) → `git diff --exit-code` drift check → 靜態 Web key check(`t('...')` in app/components/lib) → 靜態 Flutter key check(`trKey('...')` in mobile/lib) → flutter gen-l10n 驗證。 |
| 4.82.0 | 2026-06-27 | i18n 品質防護:新增 `tools/check-i18n-parity.ts` CI gate((A) 9 本字典 vs zh-TW leaf 鍵集對齊 —— 缺鍵/多鍵皆 fail;(B) 掃 app/components/lib 靜態 `t('字面值')` 驗證存在於 zh-TW,模板字串略過);串進 `npm test` 與新 workflow `.github/workflows/i18n-check.yml`(零 npm install)。`lib/i18n/translate.ts` 新增 `I18nKey = DotPath<Dictionary>`,`TranslateFn` 改成 `I18nKey | (string & {})` 給 IDE 靜態鍵補全同時保留動態鍵相容。 |
| 4.81.0 | 2026-06-27 | i18n 擴展至 10 種語言(zh-TW、zh-CN、en、es、ar、fr、hi、pt-BR、ru、ko)。新增 `lib/i18n/dictionaries/` 下 ar/es/fr/hi/ko/pt-BR/ru/zh-CN 八個字典檔;`lib/i18n/config.ts` 擴充 `LOCALES`、`HTML_LANG`、`HTML_DIR`(阿拉伯文 RTL)、`LOCALE_LABELS`、`normalizeLocale`;新增 `lib/i18n/localeTag.ts`;新增 `tools/check-i18n-coverage.mjs`。行動端:`mobile/lib/l10n.dart` 新增 `supportedAppLocales`、`appLocaleLabels`、`normalizeAppLocale`、`flutterLocaleFor`、`appIntlLocaleTag`、MethodChannel `assetpilot/locale` 橋接 `AppCompatDelegate.setApplicationLocales`;`pubspec.yaml` 加入 `flutter_localizations`;`app.dart` 加入 `supportedLocales`、`localizationsDelegates`;`format.dart` 改用 `appIntlLocaleTag()` 動態 locale;`settings_screen.dart` 語言選單改用 `appLocaleLabels`;`AndroidManifest.xml` 加入 `AppLocalesMetadataHolderService`(兼容 API < 33);`locale_config.xml` 補 10 種 locale 宣告;`MainActivity.kt` 實作 `assetpilot/locale` channel。 |
| 4.80.1 | 2026-06-27 | i18n 補強:`middleware.ts` 將 `/api/i18n/locale` 加入 `PUBLIC_PATHS`,使語言偏好 API 在未登入狀態下可存取,登入頁與公開頁即可正確套用語言;`mobile/android/app/src/main/AndroidManifest.xml` 新增 `android:localeConfig="@xml/locale_config"`,並新增 `res/xml/locale_config.xml` 宣告支援 `zh-Hant-TW` 與 `en`,Android 13+ 每個應用程式語言設定生效。 |
| 4.80.0 | 2026-06-27 | 網頁版全面 i18n 支援(繁體中文 + 英文)。新增 `components/i18n/PublicLanguageSwitcher.tsx`(公開頁語言切換器,寫 `/api/i18n/locale` Cookie)、`lib/i18n/publicLegalContent.ts`(隱私政策/服務條款多語內容)、`components/public/LegalDocument.tsx`(共用法律文件渲染元件)。`lib/i18n/dictionaries/en.ts` 與 `zh-TW.ts` 大幅擴充,涵蓋全部功能頁(accounts/budget/categories/data-transfer/recurring/reports/settings/stocks/transactions)、儀表板、公開頁(home/privacy/terms)及 auth 區塊的所有介面字串。所有 Client 元件改以 `useT()` hook 取翻譯;`app/layout.tsx` 改用 `generateMetadata()` 動態輸出 i18n 標題與描述;儀表板月份改以 `Intl.DateTimeFormat` 依語系格式化;`Toast.tsx` aria-label 改為 `"Close"` 以符合多語環境。`mobile/android/settings.gradle.kts` 補 gradle-daemon-jvm.properties 設定。 |
| 4.79.1 | 2026-06-27 | 行動 App 支援 Android 15(targetSdk 35)強制 edge-to-edge:main.dart 啟動時 `SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge)` 並將狀態列/導覽列設透明、關閉 systemNavigationBarContrastEnforced(取代已棄用的 setStatusBarColor/setNavigationBarColor);各編輯用 modal bottom sheet(accounts/budgets/categories/recurring/report_schedule/stocks(含股票/股利/交易)/transactions 篩選/security 變更密碼)加入 `useSafeArea: true`,使表單底部按鈕避開系統導覽列,既有 viewInsets 鍵盤內距不重複計算。主畫面 Scaffold/AppBar/NavigationBar 與 transaction_form(已用 padding.bottom)原即正確處理 insets。 |
| 4.79.0 | 2026-06-27 | 多項強化:(1) 儀表板支出/收入分類新增父分類佔總額百分比(小數一位),WEB(app/dashboard/page.tsx 的 percentLabel)與 APP(dashboard_screen.dart 圖例)皆實作;(2) 統計報表 ReportsClient.tsx 圓餅圖加入資料指紋比對,10 秒輪詢資料未變時跳過 Chart 重建,消除閃爍;(3) 登入裝置記錄:login_audit_logs/login_attempt_logs 新增 user_agent 欄,loginHelpers.ts 新增 describeDevice() 解析 UA,admin/user login-audit API 回傳 device,AdminClient 與 AccountSettingsClient 與 mobile security_screens 顯示,mobile api_client 送出 AssetPilotApp UA;(4) 全部使用者登入紀錄前端新增 USER 與日期區間篩選;(5) 一般(唯讀)管理員:users 新增 admin_role 欄('super'/'readonly'),requireAdmin 依 HTTP 方法擋下唯讀管理員的變更操作、新增 requireSuperAdmin 用於匯出端點(data-audit/export、database/export),serverAuth/apiHelpers 回傳 isSuperAdmin,AdminClient 依 canWrite 隱藏/停用寫入與匯出控制並提供角色指派 UI;(6) 資料稽核詳記敏感操作:auditHelpers.ts 新增 auditSensitiveAction(),於建立/刪除帳號、角色變更、重設密碼、系統設定、稽核清空、登入紀錄刪除、憑證部署/刪除、伺服器時間、帳號自助刪除/改密碼等處寫入稽核,稽核表新增詳情與 IP 欄。 |
| 4.78.2 | 2026-06-27 | 修正當頁收支統計計算錯誤:TransactionsClient.tsx 的 pageTotals reducer 原本將 transfer_in 計入 income、transfer_out 計入 expense,導致轉帳交易被誤計為收支;改為僅 type==='income' 進 income、type==='expense' 進 expense,轉帳不納入統計。 |
| 4.78.1 | 2026-06-27 | 相依套件升級至最新版(皆為同主版本 minor/patch,無破壞性變更)。`typescript` `5.9.3 → 6.0.3`、`@types/node` `22.19.17 → 24.13.2`(對齊 Node 24 runtime,連帶 `undici-types` `6.21.0 → 7.18.2`)、`next` `16.2.6 → 16.2.9`、`react`/`react-dom` `19.2.5 → 19.2.7`、`@types/react` `19.2.14 → 19.2.17`、`sharp` `0.35.0 → 0.35.2`、`pg` `8.21.0 → 8.22.0`、`resend` `6.12.2 → 6.16.0`、`lucide-react` `1.14.0 → 1.21.0`、`@base-ui/react` `1.4.1 → 1.6.0`、`tailwindcss`/`@tailwindcss/postcss` `4.2.4 → 4.3.1`、`tailwind-merge` `3.5.0 → 3.6.0`、`decimal.js` `10.4.3 → 10.6.0`、`jsonwebtoken` `9.0.2 → 9.0.3`、`@passwordless-id/webauthn` `2.3.5 → 2.4.0`、`autoprefixer` `10.5.0 → 10.5.2`、override `postcss` `8.5.14 → 8.5.15`、`uuid` `14.0.0 → 14.0.1`。`tsconfig.json` 無須調整(未用到 TS 6.0 移除的舊選項)。`package-lock.json` 以 `npm install --package-lock-only` 重生,保留 sharp 全平台 optional 二進位(linux 條目完整)。本機 tsc/Docker 不可用,型別與 build 驗證靠 CI docker-publish。 |
| 4.78.0 | 2026-06-27 | 交易清單新增當頁收支合計統計。`TransactionsClient.tsx` 加入 `pageTotals` reducer,對當頁 `txs` 陣列累計 income(type=income/transfer_in)與 expense(type=expense/transfer_out)金額,並計算 `pageNet`。UI 以三欄 `.tx-page-summary` 格線顯示當頁收入、支出、淨收支,左邊框色分別對應 `--income`/`--expense`/`--net` CSS 變數;`pageNet < 0` 時淨收支加 `−` 前綴並套 `amount-expense` 樣式。`app/globals.css` 新增 `.tx-page-summary*` 規則,手機版(max-width: 640px)改為單欄直排。 |
| 4.77.1 | 2026-06-24 | 修正股票定期定額與股票交易費用自動計算。①新增 `lib/stockRecurringHelpers.ts`,將原本僅存在於 `app/api/stock-recurring/process/route.ts` 的處理流程抽成共用 helper,手動執行端點、登入後背景觸發與 `requireAuth()` 每日檢查皆呼叫同一邏輯;`lib/apiHelpers.ts` 新增 per-user/day/version 快取與 in-flight 防重,若處理結果有 skipped 會清除快取,讓暫時查不到價格的排程可在同日下一次請求重試。②`app/api/auth/login/route.ts` 原本動態 import `lib/stockHelpers` 並呼叫不存在的 `processStockRecurring`,改為呼叫 `processStockRecurringForUser()`,補回登入後自動執行股票定期定額。③定期定額計算改用 `todayInUserTz()`,避免 UTC 日期落差;休市日維持順延至下一交易日;產生交易仍用 `recurring_plan_id + period_start_date` 唯一索引去重。④`lib/db.ts` 補齊 `stock_recurring` 現行 API 實際使用欄位(`amount`、`frequency`、`start_date`、`account_id`、`note`、`last_generated`、`updated_at`),保留舊欄位並遷移 `freq/next_date/shares*price` 至新欄位,讓新環境與舊資料庫都可建立與執行股票定期定額。⑤股票交易 POST/PUT 端點改為未提供 `fee` 時依 `stock_settings` 自動計算手續費,賣出且未提供 `tax` 時依股票類型自動計算證交稅;有提供數值則視為手動覆寫並驗證不可為負或非數字。⑥Web `StockTxClient` 空白手續費/交易稅欄位不再送成 0,placeholder 顯示「自動計算」,買進時不送隱藏的交易稅欄位。驗證:`npm test` 通過;`node node_modules/typescript/lib/tsc.js --noEmit` 仍受既有 e2e 測試缺 `@playwright/test` 型別阻擋,非本次改動造成。 |
| 4.77.0 | 2026-06-22 | 行動 App 新增完整英文在地化。新增 `mobile/lib/l10n.dart`:以 `ValueNotifier<String> appLocale`(預設 `zh-TW`)持有當前語系,`loadAppLocale()` 於啟動時讀 `SharedPreferences` 的 `appLocale` 鍵,無值時依 `PlatformDispatcher.instance.locale.languageCode` 自動判定(`en`→英文,其餘→`zh-TW`);`setAppLocale()` 寫回偏好並更新 notifier。翻譯採「中文原文當 key」策略以利逐畫面漸進遷移:`tr(source)` 中文時直接回傳原文、英文時查 `_en` 字典,未命中再經 `_translateDynamic()` 以一組 RegExp 處理含參數字串(如 `^(\d+) 號$`、`^上次寄送 (.+)$`、`^已更新語言:(.+)$` 等),最後回退原文;另有 `trPair(zh, en)` 供行內二選一。`mobile/lib/app.dart` 的 `MaterialApp` 外層加 `ValueListenableBuilder<String>(valueListenable: appLocale)`,依語系設定 `locale: Locale('en')` 並於切換時重建整棵樹。`settings_screen.dart` 的 `_pickLanguage()` 除呼叫既有 `ApiClient.setLanguage()`(影響通知與網頁版)外,新增 `setAppLocale(picked)` 使 App UI 本身同步切換;語言列說明由「語言(影響通知與網頁版)」改為「語言(APP、通知與網頁版)」。各畫面(accounts/budgets/categories/changelog/dashboard/login/more/onboarding/recurring/register/report_schedule/reports/security/settings/stock_settings/stocks/transaction_form/transactions 等)顯示字串改以 `tr()` 包裝;多處 `const` 因 `tr()` 在執行期求值而移除。中文維持來源語言,使用者既有資料不因語系切換被改寫。本機無法編譯,驗證靠 CI android-apk。 |
| 4.76.6 | 2026-06-22 | 行動版補強 Sentry 效能監控(先前 4.76.x 僅有錯誤/崩潰擷取與 `tracesSampleRate=0.2`,但缺導覽與 HTTP 自動埋點,實質收不到效能數據)。①`mobile/lib/app.dart` 的 `MaterialApp` 加上 `navigatorObservers: [SentryNavigatorObserver()]`,為每次畫面導覽建立交易並量測 TTID/slow-frozen frames。②`mobile/lib/api_client.dart` 的 `_send` 將 `http.Client()` 以 `SentryHttpClient(client: …)` 包裝,為每個 API 請求產生 `http.client` span 與麵包屑(方法/路徑/狀態碼/耗時);`captureFailedRequests` 預設擷取 5xx 為事件。③隱私防護:`SentryHttpClient` 預設會把 query string 記進 span(`http.query`)、麵包屑與失敗請求事件(`request.queryString`),對財務 App 屬機敏(搜尋關鍵字)。於 `mobile/lib/sentry_config.dart` 新增 `beforeBreadcrumb`(移除 http 麵包屑 `http.query`/`http.fragment`)、`beforeSendTransaction`(移除各 span `http.query`/`http.fragment`,因 span 已 finish 故直接操作 `data` 即時 map),並把既有 `beforeSend` 改為直接清 `request.queryString`/`fragment`(避免已 deprecated 的 `copyWith`);認證 Cookie 因 `sendDefaultPii=false` 不被記錄。API 簽名對照本機 pub cache `sentry(_flutter) 9.22.0` 原始碼確認;`flutter analyze` 三檔僅餘既有 `unmask` 實驗性警告、無新增問題;CI 無 analyze 步驟、build APK 驗證。 |
| 4.76.5 | 2026-06-22 | 修補容器映像 Trivy 排程掃描失敗(CVE-2026-12151,HIGH)。Trivy 在 `usr/local/lib/node_modules/npm/node_modules/undici@6.25.0` 偵測到 undici DoS(unbounded memory,fixed in 6.27.0/7.28.0/8.5.0);該 undici 並非本專案相依(`package-lock.json` 僅有 `undici-types`),而是 `node:24-alpine` 基底映像所附 npm 的 vendored 副本,上游 npm 尚未隨基底映像更新。Next.js standalone 入口 runner 階段僅執行 `node server.js`,執行期完全用不到 npm,故於 `Dockerfile` runner 階段 `apk upgrade` 後新增 `rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx`,徹底移除該套件與其漏洞元件、同時縮小攻擊面。`tools/*.ts` 內 `npm run` 字樣僅為 host 端入口註解,container 不依賴。本機無法 build,驗證靠 CI Trivy 重跑。 |
| 4.76.4 | 2026-06-22 | 修補 nodemailer 兩項 Dependabot 資安告警:GHSA-p6gq-j5cr-w38f(message-level `raw` 選項繞過 `disableFileAccess`/`disableUrlAccess`,可達任意本機檔案讀取與 full-response SSRF)與同源的 GHSA-wqvq-jvpq-h66f(`jsonTransport` 於訊息正規化階段同樣繞過該兩旗標)。上游於同一 commit 修復、釋出於 9.0.1。`package.json` 將 `nodemailer` 由 `^8.0.9` 升至 `^9.0.1`,`package-lock.json` 同步解析至 `9.0.1`(integrity 更新)。v9.0.0 唯一破壞性變更為「遠端內容抓取(attachment href/path)、OAuth2 token endpoint、HTTP/HTTPS proxy CONNECT」改為預設驗證 TLS 憑證;本專案唯一消費者 `lib/emailService.ts` 僅以 `createTransport({host,port,secure,auth})` + `sendMail({from,to,subject,html})` 寄信,未使用 `raw`、attachments、`jsonTransport`、OAuth2 或 proxy,故漏洞路徑原本即不可達、v9 破壞性變更亦不影響;屬預防性升級。本機 node_modules 不完整無法跑 build/tsc,驗證靠 Docker/CI。 |
| 4.76.3 | 2026-06-18 | 行動版修兩支 release-only 空值崩潰 + Session Replay 遮罩讓 App 自身圖示可見。①Sentry `ASSETPILOT-APP-2`(`TypeError: Null check operator used on a null value`,culprit `_WidgetsAppState._onUnknownRoute`):`mobile/lib/app.dart` 的 `MaterialApp` 僅設 `home:`、無具名路由/`onGenerateRoute`/`onUnknownRoute`,OS/深層連結在背景推送未知路由時,框架走到 `_onUnknownRoute` 執行 `widget.onUnknownRoute!(settings)`,release 模式 assert 被移除故 `!` 對 null 取值崩潰。補上 `onUnknownRoute: (settings) => MaterialPageRoute(builder: (_) => const AuthGate(), settings: const RouteSettings(name: '/'))`,未知路由安全導回主畫面。②Sentry `ASSETPILOT-APP-4`(同型別錯誤,culprit `login_screen.dart:184` `_passkeySignIn` → `State.setState`):堆疊崩潰幀為 `State.setState`(`_element!.markNeedsBuild()`,State 已 dispose 時 `_element` 為 null、release assert 被移除)。`_passkeySignIn`/`_googleSignIn`/`_lineSignIn`/`_submit` 的 `on ApiException`/`catch` 區塊呼叫 `setState` 缺 `mounted` 防護(Passkey 走外部瀏覽器 await 最長 5 分鐘,期間 LoginScreen 易被 dispose),catch 內拋出的新錯誤不被下層 `catch` 接住而逸出成 `PlatformDispatcher.onError`。於 `mobile/lib/screens/login_screen.dart` 四方法與 `mobile/lib/screens/register_screen.dart` `_register` 的各 catch 開頭加 `if (!mounted) return;`(`finally` 本就有 `if (mounted)`)。③`mobile/lib/sentry_config.dart` 遮罩顯式化:`options.privacy.maskAllText=true`、`maskAllImages=true`、`maskAssetImages=false`、`unmask<Icon>()`(新增 `import 'package:flutter/widgets.dart'`)。Flutter `Icon` 內部以 `RichText` 渲染、被 `maskAllText` 的 RichText 規則連帶打碼,導致 App 自身圖示在 replay 被黑掉;`unmask<Icon>()` 解除(Icon 為固定字型字符、無使用者資料)。release 未開 `--obfuscate`,型別名稱保留、泛型遮罩規則可靠生效。`options.privacy` 為 `SentryFlutterOptions` 公開欄位、`unmask<T>` 公開方法,皆對照 `sentry_flutter 9.22.0` 原始碼確認;本機無法編譯,靠 CI android-apk 驗證。 |
| 4.76.2 | 2026-06-17 | 行動版補上實際 Sentry Logs 埋點(先前 4.75.0 僅 `enableLogs=true` + `more_screen` debug 測試,正式版幾乎不送 log)。`mobile/lib/api_client.dart` 新增 `import sentry_flutter`、`_logPath()`(記錄端點時去除 query string,避免上傳 `?keyword=` 等搜尋字)與 `_asCount()`(容忍 num/List 兩形態)。中央 `_send()` 三個失敗點各埋 log:連線層例外 `Sentry.logger.error`(method/path/error.type)、401 `info`(path)、非 2xx `warn`(method/path/status_code);`login()` 加連線失敗 `error`、非 200 `warn`、成功 `info`(attr `auth.method=password`,絕不記 email/密碼);`logout()` 加 `info`;`syncStockDividends()` 改 async、成功後 `info` 記 synced/skipped/errors 筆數。隱私:僅記 method、去 query 路徑、狀態碼、例外型別與筆數,不含 body/cookie/金額/個資。API 簽名 `Sentry.logger.warn(String,{attributes:Map<String,SentryAttribute>})` 經查 `sentry 9.22.0` 原始碼確認;本機無法編譯,靠 CI android-apk 驗證。 |
| 4.76.1 | 2026-06-17 | 行動版 AAB 改為僅打包 ARM ABI,消除 Google Play 審查 x86_64 模擬器的 SIGABRT 誤報。`.github/workflows/android-apk.yml` 的 `flutter build appbundle --release` 加入 `--target-platform android-arm,android-arm64`,不再產生 x86_64 split。Sentry issue `ASSETPILOT-APP-1`(native `SIGABRT: Abort`,thread `SentryExecutorServiceThreadFactory-0`)11 筆事件全來自同一台偽報 OnePlus8Pro 的 x86_64 模擬器(288×448、2 核、archs 以 x86_64 為首),分三叢集對應 4.74.0/4.75.1/4.76.0 三次發版上傳後幾分鐘內、每 user 崩一次,無任何真實 arm64 裝置受影響;根因為 Sentry NDK 崩潰處理/Session Replay 的 MediaCodec 在 x86 模擬器上 abort。排除 x86_64 後審查改用 ARM 模擬器即不再誤報,AAB 體積亦縮小。`mobsf.yml` 的 `build apk`(僅供 MobSF 靜態掃描、不分發)維持全 ABI 不動。`sentry_config.dart`/`main.dart` 未改(Replay/NDK 設定保留)。 |
| 4.76.0 | 2026-06-17 | 行動版交易表單分類選擇改為父→子兩段式。`mobile/lib/screens/transaction_form_screen.dart`:新增狀態 `_parentCatId`(僅 UI,存後端仍為子分類 `_categoryId`);新增 `_selectableParents`(依 `_selectableCats` 的 `parentId` 反查、保留 `_categories` 排序的父分類)與 `_childCatsOf(parentId)`;移除原 `_parentName()` 與單一「父 › 子」扁平 `DropdownButtonFormField`,改為父分類+子分類兩個下拉(子分類於 `_parentCatId==null` 時 `onChanged:null` 停用並顯示 hint)。`_buildForm` 開頭同步:編輯時由子分類回推父分類、切換收支類型時清父子選擇、selected 值不在清單即清空(避免 Dropdown value 斷言)。收支類型 SegmentedButton 的 onChanged 一併重置 `_parentCatId`。儲存 payload(`categoryId`)與後端不變。web/`recurring` 未動(web 已用 optgroup 分組)。 |
| 4.75.1 | 2026-06-17 | 修正 4.75.0 行動版建置失敗(`bundleRelease` 編譯錯誤)。`sentry_flutter 9.x` 已移除 `SentryReplayOptions.maskAllText`/`maskAllImages`(8.x API),`mobile/lib/sentry_config.dart` 的 `options.replay.maskAllText/maskAllImages = true` 觸發 `The setter ... isn't defined for the type 'SentryReplayOptions'`,使 `:app:compileFlutterBuildRelease` 失敗。移除該兩行——9.x 預設即遮罩所有 Text/EditableText/RichText/Image(正是所需行為);若日後要調整改用 `options.privacy.*`。保留 `replay.sessionSampleRate = kReleaseMode ? 0.0 : 1.0`、`onErrorSampleRate = 1.0`(此兩 setter 在 9.x 仍有效,原錯誤僅出在 mask 兩行)。web 4.75.0 不受影響(Docker image 已成功建置)。 |
| 4.75.0 | 2026-06-17 | 行動版 Sentry 監控擴充:Logs、Metrics、Session Replay。`mobile/lib/sentry_config.dart`:新增 `options.enableLogs=true`(結構化日誌,可用 `Sentry.logger`);註記 Metrics 於 SDK >= 9.11.0 自動啟用、無需旗標(`Sentry.metrics.count/gauge/distribution`);新增 Session Replay:`options.replay.sessionSampleRate = kReleaseMode ? 0.0 : 1.0`、`options.replay.onErrorSampleRate = 1.0`、`maskAllText/maskAllImages = true`(全程遮罩,正式版平時不錄、僅錯誤時回溯)。`mobile/lib/main.dart`:`runApp` 改包 `SentryWidget(child: const AssetPilotApp())`(Session Replay 擷取所需根節點)。`mobile/lib/screens/more_screen.dart`:debug 驗證項擴充為依序 `Sentry.metrics.count('verify_button_tapped',1)` + `distribution('verify_latency',187,ms)` → `Sentry.logger.fmt.info` → 丟 `StateError`,一鍵驗證 metrics/logs/error 三路徑。合規:`app/privacy/page.tsx` 第 2/7 節補上「發生錯誤時擷取去識別化(遮罩)畫面回溯、僅行動版」說明。AI Agent Monitoring 經評估不導入(全 repo 無任何 LLM/AI 呼叫,無可埋點對象)。無 `pubspec.yaml` 變更(`sentry_flutter ^9.22.0` 已滿足 logs>=9.0.0/metrics>=9.11.0/replay>=8.9.0)。 |
| 4.74.0 | 2026-06-17 | 行動版(Flutter)導入 Sentry 當機/效能監控。`mobile/pubspec.yaml` 新增 `sentry_flutter: ^9.22.0`;新增 `mobile/lib/sentry_config.dart`(DSN 以 `String.fromEnvironment('SENTRY_DSN')` 內嵌、可 `--dart-define` 覆寫,`environment` 依 `kReleaseMode` 標 production/development,`tracesSampleRate=0.2`,`sendDefaultPii=false`,`beforeSend` 清除 `authorization`/`cookie`/`set-cookie` 標頭避免上傳 JWT);`mobile/lib/main.dart` 改用 `SentryFlutter.init(configureSentry, appRunner: …)` 包住 `ApiClient.init()`/`loadThemeMode()`/`runApp`;`mobile/lib/screens/more_screen.dart` 新增僅 `kDebugMode` 顯示的「驗證 Sentry 設定」項(丟 `StateError` 測試上報)。`release`/`dist` 由 `package_info` 自動帶入。合規同步:`data/data_safety_export.csv` 將「當機記錄」「診斷資料」標為收集(非分享、非暫時性、required,用途 App functionality + Analytics);`app/privacy/page.tsx` 第 2/5/7 節補上 Sentry(僅行動版)說明並更新最後更新日期。CI symbol 上傳未納入(release 未開 `--obfuscate`,stack trace 可讀)。 |
| 4.73.2 | 2026-06-16 | 修補 nodemailer 三項 Dependabot 告警(#29/#30/#31):OAuth2 token fetch 的 improper TLS certificate validation、List-* header comment 的 CRLF injection、jsonTransport 於 message normalization 繞過 `disableFileAccess`/`disableUrlAccess`。皆於 nodemailer 8.0.9 修補(#29 8.0.8、#30/#31 8.0.9)。`package.json` 由 `^8.0.6` 提升至 `^8.0.9`,`npm install nodemailer@8.0.9 --package-lock-only` 將 lockfile 鎖至 8.0.9,`npm audit` 0 vulnerabilities。寄信邏輯(`lib/`)未動。 |
| 4.73.1 | 2026-06-16 | 修復 GitHub Actions「Workflow does not contain permissions」(#60)。`.github/workflows/android-apk.yml` 先前未宣告任何 `permissions`,被 Semgrep `missing-workflow-permissions` 規則標記。於 `on:` 與 `jobs:` 之間補上 top-level `permissions: { contents: read }`(最小權限):該 workflow 僅 checkout 程式碼,上傳 .aab/Google Play/S3 皆走專用 secrets 而非 `GITHUB_TOKEN`,無需任何 write scope。其餘 workflow 已有 top-level 或 job-level permissions(`docker-publish.yml` 為 job-level),不受影響。 |
| 4.73.0 | 2026-06-15 | 信用卡繳款改對應回所清償帳單 + 卡片改顯示上期帳單(WEB + App)。`lib/accountHelpers.ts` 新增 `creditCardPaymentWindow(closingDay, cycleEnd)`:回傳該帳單(結帳日=cycleEnd)的繳款窗口=結帳日隔天~下一個結帳日(即 `creditCardStatementCycle(closingDay, cycleEnd+1天)`)。`GET /api/accounts/[id]/cycles` 每期 payment 改為 `transfer_in` 於該期 `creditCardPaymentWindow` 內加總(對應回清償的帳單),非落在繳款當下所在區間。`GET /api/accounts` 移除 `cyclePayment`,改回傳上期帳單欄位:`lastCycleStart/lastCycleEnd/lastCycleSpending`(最近一張已結帳帳單,以本期起日前一天推算)與 `lastCyclePayment`(其繳款窗口=本期區間內 transfer_in 加總)。WEB `AccountsClient.tsx`:卡片「本期已繳」改為「上期帳單:消費 X / 已繳 Y(M/D–M/D)」,明細 Dialog 加說明字。App `models.dart` `Account` 以 `lastCycleSpending/lastCyclePayment/lastCycleStart/lastCycleEnd` 取代 `cyclePayment`;`accounts_screen.dart` 副標題「本期已繳」改「上期帳單 消費/已繳」、明細 Sheet 加說明。`StatementCycle.payment` 語意改為對應帳單(後端調整、model 不變)。`flutter analyze` 無新增問題;繳款窗口歸屬以腳本驗證通過。 |
| 4.72.1 | 2026-06-15 | 修正匯率管理更新匯率失敗:`exchange_rates` 缺 `is_manual` 欄位的 migration。`app/api/exchange-rates/route.ts`、`app/api/exchange-rates/[currency]/route.ts`、`lib/exchangeRateHelpers.ts` 的 INSERT/UPDATE/SELECT 皆引用 `is_manual`,但 `lib/db.ts` schema 從未加入該欄,Postgres 下報 `column "is_manual" of relation "exchange_rates" does not exist`。於 `_runMigrations()` alterIgnore 區塊補 `ALTER TABLE exchange_rates ADD COLUMN is_manual INTEGER DEFAULT 0`(PG runtime 翻譯為 BIGINT、冪等),啟動時自動補欄。 |
| 4.72.0 | 2026-06-15 | 信用卡新增「本期已繳」與「每期帳單明細」(WEB + App)。`lib/accountHelpers.ts` 新增 `creditCardStatementCycles(closingDay, todayStr, count)`:以「本期起日的前一天」逐輪往回推,回傳最近 count 期(最新在前、邊界連續不重疊,clamp ≤36)。`GET /api/accounts` 對每張信用卡加算 `cyclePayment`=本期區間內 `type='transfer_in'` 金額加總(`CASE WHEN original_amount>0 THEN original_amount ELSE amount END`)。新增 `GET /api/accounts/[id]/cycles?count=N`(信用卡且已設結帳日,否則 400/404):回傳 `{ id, name, currency, statementClosingDay, cycles:[{start,end,current,spending,payment}] }`,spending=expense、payment=transfer_in 各期加總。WEB `AccountsClient.tsx`:卡片加「本期已繳」一行(emerald)與「查看每期明細 ›」按鈕,開 Dialog 以表格列出各期消費/繳款(本期標記)。App `mobile/lib/models.dart` `Account` 加 `cyclePayment`、新增 `StatementCycle` model;`api_client.dart` 加 `accountStatementCycles(id,{count})`;`accounts_screen.dart` 副標題加「本期已繳」、卡片 trailing 加帳單明細 IconButton 開 `_StatementCyclesSheet`(FutureBuilder 列各期消費/已繳)。`flutter analyze` 無新增問題;帳單週期多期連續性以腳本驗證通過。 |
| 4.71.0 | 2026-06-15 | 信用卡結帳日 + 本期帳單消費補齊手機 App(WEB + App 一致)。`mobile/lib/models.dart` `Account` 新增 `statementClosingDay`(int?)、`cycleSpending`(num?)、`cycleStart`/`cycleEnd`(String?),新增 `_asNumOrNull`/`_asIntOrNull`/`_asStrOrNull` 解析並相容 snake_case key。`mobile/lib/screens/accounts_screen.dart` `_AccountForm` 信用卡區塊新增「結帳日(1~31)」`TextFormField`(驗證 1~31,空字串送 null),`_save()` 僅信用卡帶 `statementClosingDay`;帳戶列表 `ListTile` 改用 `_accountSubtitle()`:信用卡若有 `statementClosingDay` 與 `cycleSpending` 則於副標題加「本期消費 <money>(M/D–M/D)」一行(error 色、`isThreeLine`),`_md()` 將 ISO 日期轉 M/D。沿用既有 `createAccount`/`updateAccount` body 直送,後端 4.70.0 已支援、App 無需改 API。`flutter analyze` 無新增問題。 |
| 4.70.0 | 2026-06-15 | 信用卡新增每月結帳日 + 本期帳單消費統計(WEB)。`lib/db.ts` accounts 表新增 `statement_closing_day INTEGER DEFAULT NULL`(alterIgnore migration)。`lib/accountHelpers.ts` 新增 `normalizeStatementClosingDay()`(驗 1~31,否則 null)與 `creditCardStatementCycle(closingDay, todayStr)`:依結帳日與使用者當地今天算出「當期未出帳」區間=上一個結帳日隔天~下一個(含今天)結帳日,端點皆含,遇月底天數不足(如 31 號遇 2 月)以 `Date.UTC(y, mo, 0)` clamp 到當月最後一天,跨月起點用 UTC date +1 天避免進位錯誤。`GET /api/accounts` 以 `auth.userTimezone` + `todayInUserTz` 對每張 `credit_card` 算區間並 `SELECT SUM(CASE WHEN original_amount>0 THEN original_amount ELSE amount END)`(type='expense' 且 date BETWEEN start AND end),回傳 `statementClosingDay/cycleSpending/cycleStart/cycleEnd`;`POST /api/accounts` 與 `PUT | PATCH /api/accounts/[id]`接收驗證並寫入(非信用卡清空、空字串視為清除),`[id]` GET 一併回傳。前端 `components/features/accounts/AccountsClient.tsx`:新增/編輯表單於信用卡區塊加「結帳日(1~31)」欄位,卡片顯示「每月結帳日 X 號」與「本期消費 NT$X(M/D–M/D)」。本期消費含「不計入統計」交易(對齊帳單實際請款)。CSV 匯出/匯入暫未含此欄。 |
| 4.69.0 | 2026-06-15 | 手機 App 新增第一次使用引導教學。新增 `mobile/lib/screens/onboarding_screen.dart`:全螢幕 `PageView` 5 頁圖文輪播(歡迎/記帳/首頁/股票/更多),對齊底部導覽說明各功能位置;以 `shared_preferences` 旗標 `onboarding_seen_v1` 記錄是否看過,`OnboardingScreen.showIfFirstTime()` 第一次顯示後寫旗標、`show()` 供手動重看。`app.dart` `HomeShell` 於 `initState` 以 post-frame callback 呼叫 `showIfFirstTime`;`more_screen.dart` 新增「使用教學」入口呼叫 `show()`。純 Flutter 內建元件、Material 3 配色、無新增套件。另:README 美化(新增 App 螢幕截圖區塊與目錄項、修正亂碼行、標語改 Web + Android App)、`docs/screenshots/app/` 收錄 8 張手機截圖、`.gitignore` 改為 `docs/*` + `!docs/screenshots/` 使截圖可入庫 |
| 4.68.0 | 2026-06-14 | 交易記錄分類篩選支援父分類(WEB + App)。後端 `GET /api/transactions` 既有邏輯已支援:傳入父分類 `categoryId` 時會查 `categories.parent_id IN (...)` 展開為其所有子分類 id 再 `t.category_id IN (...)`,故本次僅改前端。WEB `components/features/transactions/TransactionsClient.tsx`:分類篩選下拉於每個父分類 `<optgroup>` 內新增可選的「<父分類>(全部)」`<option value={parent.id}>`。App `mobile/lib/screens/transactions_screen.dart`:`_load()` 改建「父分類後接其子分類」的有序清單(略過無子分類的父分類)供篩選下拉;`_TxnFilterSheet` 下拉父分類顯示粗體「<名稱>(全部)」並可選、子分類縮排顯示,選父分類即送出父分類 id;移除 `_TxnFilterSheet` 不再使用的 `catName` 參數 |
| 4.67.2 | 2026-06-14 | 修正 Flutter App 首頁「支出分類」圓餅圖統計邏輯,對齊 WEB。`/api/dashboard` 的 `catBreakdown` 回傳的是子分類(葉節點)層級節點、各自帶 `parentId`/`parentName`/`parentColor`(由 `lib/dashboardHelpers.ts` `buildCategoryAggregateNodes` 產生),WEB `app/dashboard/page.tsx` `groupCategoryRows()` 會依父分類彙總後再呈現;App 端 `mobile/lib/models.dart` 的 `CatNode` 先前未解析 parent 欄位,`dashboard_screen.dart` `_CategoryPie` 直接把每個子分類當成獨立扇區(取 top 8),與 WEB 不一致。修正:`CatNode` 新增 `parentId`/`parentName`/`parentColor`(fallback 規則對齊 `groupCategoryRows`),`_CategoryPie` 新增 `_groupByParent()` 依父分類彙總金額並依金額由大到小排序,扇區與圖例改為父分類層級,並移除原 top 8 截斷 |
| 4.67.1 | 2026-06-14 | 修正 Flutter App 兩項交易頁問題。**交易刪除可發現性**:原僅 `transactions_screen.dart` 的 `onLongPress` 可刪除,不易察覺;`transaction_form_screen.dart` 編輯模式 AppBar 新增刪除 `IconButton`(`_confirmDelete()` 呼叫 `deleteTransaction`,確認後 pop true),列表改以 `Dismissible`(`endToStart`,`confirmDismiss` 重用 `_delete()` 並改回傳 `Future<bool>`,刪除失敗不移除列項)支援左滑刪除,長按保留。**edge-to-edge 遮擋**:Android 15 預設 edge-to-edge,`transaction_form_screen.dart` 表單 `ListView` 底部未留安全區,致「儲存」鈕被系統導覽列遮住;底部 padding 改為 `16 + MediaQuery.padding.bottom`。`flutter analyze` 無錯誤 |
| 4.67.0 | 2026-06-14 | Flutter App 功能補齊至接近 WEB 對等,並新增使用者自助報表排程後端。**後端**:新增 `app/api/user/report-schedules/route.ts`(GET 列出本人、POST 建立)與 `[id]/route.ts`(PUT/DELETE,皆鎖 `auth.userId`),鏡像 `admin/report-schedules` 但改 `requireAuth` 並驗證擁有權,欄位含 freq/hour/minute/weekday/day_of_month/notify_email/notify_line/enabled(共用既有 `report_schedules` 表,無 schema 變更)。**App `api_client.dart`**:新增 updateRecurring、updateStock、updateStockTransaction、stock-dividends CRUD、stockSettings GET/PUT、creditCardRepayment、default/pinned currency GET/PUT、changePassword(改 raw PUT 擷取輪替後 cookie 避免本機登出)、setLanguage、sessions/revokeSession、loginAudit、passkeys/rename/delete、unlinkGoogle/Line、report-schedules CRUD;`transactions()` 補 dateFrom/dateTo/accountId/categoryId/keyword 並改送 `limit`(原誤用 `pageSize` 致只載 20 筆)。**models.dart**:`Txn.originalAmount/fxRate`、`Recurring.fxRate`、`StockTxn.stockId/note`、`Dividend.stockId/accountId/note`、`Stock.stockType`、`AppUser.googleLinked/lineLinked`,新增 StockSettings/LoginSession/LoginAuditLog/Passkey/ReportSchedule。**畫面**:`transaction_form_screen.dart` 幣別+匯率選擇器(外幣送 originalAmount/fxRate);`recurring_screen.dart` 支援編輯+幣別(外幣編輯以 `amount/fxRate` 還原原幣,避免 WEB 既有雙重換算);`transactions_screen.dart` 進階篩選 sheet+外幣顯示改 originalAmount;`stocks_screen.dart` 持股編輯/刪除(stockType 僅變更才送,免誤觸稅額重算)、交易編輯+選填 fee/tax(留空=後端自動)、股利 CRUD、設定入口;新增 `stock_settings_screen.dart`、`security_screens.dart`(幣別/密碼/Passkey/綁定/裝置/登入紀錄;新增 Passkey 與帳號綁定的 ceremony 導向瀏覽器)、`report_schedule_screen.dart`;`accounts_screen.dart` 信用卡還款 sheet;`settings_screen.dart` 串接上述入口+語言(zh-TW/en);`dashboard_screen.dart` 外幣顯示改 originalAmount。未納入:CSV 匯入匯出、管理員後台、API Credits |
| 4.66.2 | 2026-06-13 | 修正金額輸入欄位的 `step` 限制。`TransactionsClient.tsx`(交易金額、轉帳金額)、`RecurringClient.tsx`(固定收支金額,原無 step=預設整數)、`AccountsClient.tsx`(初始餘額)的 `<input type="number">` 由 `step="0.01"` 改為 `step="any"`,避免瀏覽器 HTML5 validity 擋下非 step 倍數的小數(如 14.115 跳「請輸入有效值,最接近的兩個有效值…」)。伺服器端依幣別進位邏輯不變;Flutter 端用十進位鍵盤無此限制 |
| 4.66.1 | 2026-06-13 | 海外手續費率單位由千分點(‰)改為百分比(%)。`lib/accountHelpers.ts` `resolveOverseasFee` 公式 `twdBase×rate/1000`→`/100`;`app/api/accounts/route.ts`、`[id]/route.ts` 驗證 `0~1000`→`0~100`、改保留 2 位小數(`Math.round(v*100)/100`)、訊息改「0~100(百分比)」。前端費用試算(`TransactionsClient.tsx`、`RecurringClient.tsx`)`/1000`→`/100`、提示 `‰`→`%`;`AccountsClient.tsx` 標籤「(千分點)」→「(%)」、卡片顯示加 `%`、輸入 `step=0.01`。App `accounts_screen.dart`/`transaction_form_screen.dart`/`recurring_screen.dart`/`models.dart` 標籤與 helper 改百分比。註:既有以千分點輸入的費率值語意改變,未自動轉換,需重新確認 |
| 4.66.0 | 2026-06-13 | 信用卡國外刷卡手續費「獨立交易」自動計算與紀錄 + 修正 App 無法設定「不計入統計」。手續費不併入原交易,而是另存為一筆 TWD 支出交易並雙向 `linked_id` 關聯。`lib/db.ts` migration:`transactions.is_fx_fee INTEGER DEFAULT 0`、`recurring.fx_fee REAL`、`recurring.exclude_from_stats INTEGER`。`lib/accountHelpers.ts` `resolveOverseasFee({userId,accountId,currency,twdBase,clientFxFee})`:client 帶 `fxFee`(含 0)→ 手動覆寫;否則「`credit_card`/`account_type='信用卡'` + 幣別≠TWD + `overseas_fee_rate>0`」時 `round(twdBase×rate/1000)`(伺服器為單一真實來源)。`lib/overseasFee.ts` `insertFeeTransaction(db,{...})`:寫入 `is_fx_fee=1`、幣別 TWD、分類同原交易、`note='國外刷卡手續費'`。`app/api/transactions/route.ts`(POST):原交易 `fx_fee=0`/`twd_amount=base`,`type==='expense' && fxFee>0` 時建手續費列並回填 `linked_id`。`[txId]/route.ts`(PUT):`is_fx_fee` 列回 422 `FxFeeImmutable` 禁止直接編輯;編輯原交易自動同步(建立/更新/刪除)配對手續費列。DELETE 既有 `linked_id` 連動刪除涵蓋成對刪除。list/GET 回 `isFxFee`。固定收支:`recurring` GET/POST/`[id]`PUT 收送 `fxFee`/`excludeFromStats`;`lib/recurringHelpers.ts` `processOneRecurring` 每期產生主交易 + 手續費列(同 `source_recurring_id`,無 transactions 唯一索引故可並存)。WEB `TransactionsClient.tsx`:外幣「支出」信用卡顯示可編輯「海外手續費(TWD)」(自動帶入、可覆寫 + 自動計算鈕)、手續費列標記且禁止編輯;`RecurringClient.tsx` 加「不計入統計」。App(Flutter)`models.dart`:`Account.overseasFeeRate`、`Txn.excludeFromStats`/`fxFee`/`isFxFee`、`Recurring.*`;`transaction_form_screen.dart` 加「不計入統計」開關(修復主因)+ 外幣信用卡選填手續費;`transactions_screen.dart` 手續費列點擊提示不可編輯;`recurring_screen.dart`、`accounts_screen.dart`(信用卡可設費率)同步 |
| 4.65.0 | 2026-06-12 | 多語言(i18n)基礎建設與英文介面。新增 `lib/i18n/`:`config.ts`(locale 註冊表 `['zh-TW','en']`、`normalizeLocale`、`localeFromAcceptLanguage`、`LOCALE_COOKIE`)、`dictionaries/{zh-TW,en}.ts`(zh-TW 為來源語言,型別 `Dictionary = typeof zhTW`;en 用 `DeepPartialDict<Dictionary>` 部分翻譯,缺漏鍵回退)、`translate.ts`(`createTranslator` dot-path 查詢 + `{var}` 插值,找不到回傳 key)、`getDictionary.ts`(deepMerge en 覆蓋 zh-TW 基底並快取)、`resolveLocale.ts`(cookie→Accept-Language→預設)、`userLanguage.ts`(讀寫 `user_settings.language`)。`components/i18n/I18nProvider.tsx`(client context + `useT()`)由 root `app/layout.tsx`(改 async:`resolveLocale`→`getDictionary`→設定 `<html lang>` 並注入 Provider,全站轉 dynamic render)提供;server component 可直接 `getTranslator(await resolveLocale())`。`lib/db.ts` migration `alterIgnore("ALTER TABLE user_settings ADD COLUMN language TEXT DEFAULT 'zh-TW'")`;`app/api/account/settings/language` POST(`requireAuth` → `setUserLanguage` + 寫 `locale` cookie),`components/i18n/LanguageSwitcher.tsx`(POST 後 `router.refresh()` 讓 server 重新解析)置於帳號設定頁。已英文化:登入頁 `app/login/page.tsx`(`auth.*` 含 `auth.errors.*`,全部 label/placeholder/aria/錯誤訊息 `t()`)、App 殼 `components/layout/{Sidebar,AppLayout,TopNav}.tsx`(`nav.*`/`shell.*`,模組級 `NAV_SECTIONS`/`THEME_OPTIONS`/`PAGE_TITLE_KEYS` 改用 labelKey 於 render 解析)、伺服器通知 `lib/statsEmailReport.ts`(`buildUserStatsReport`/`renderStatsEmailHtml` 新增 `locale` 參數)+ `lib/lineMessaging.ts`(`buildStatsReportFlex`/`buildExpenseReminderFlex` 新增 `locale` 參數,`notifications.*` 命名空間),由 `lib/scheduler.ts` 以 `getUserLanguage(u.id)` 帶入(排程無 request context)。純資料(金額、日期、分類名、星期字)不譯;其餘頁面內文待後續逐頁搬遷 |
| 4.64.0 | 2026-06-12 | 行動版 LINE 登入啟用 LINE auto-login + 修正 App 版本資訊頁空白,並改用單一 edit 雙軌發佈。`mobile/lib/line_auth.dart`:授權 URL 移除 `disable_auto_login=true`(改用 LINE 預設值 false),裝置已安裝/登入 LINE App 時 access.line.me 授權頁直接喚起 LINE App 一鍵授權,未安裝則 fallback 回網頁登入;`redirect_uri`(`{baseUrl}/app/line-callback`)與回呼流程不變,仍須於 LINE Developers 後台註冊 Callback URL。`Dockerfile`:runtime stage 新增 `COPY /app/changelog.json`,修正 `app/api/changelog/route.ts` 以 `readFileSync(process.cwd()/changelog.json)` 動態讀檔在 standalone 輸出不被 trace 而缺檔、加上私有 repo 令 raw.githubusercontent.com fallback 404,導致 `/api/changelog` 回 `{currentVersion:'0.0', releases:[]}`、行動端 `ChangelogScreen` 拿到空清單。`.github/workflows/android-apk.yml`:`Publish to Google Play` 由 `r0adkll/upload-google-play`(單軌)改為 `.github/scripts/play-publish.mjs`(googleapis Edits API:單一 edit 上傳 .aab 一次→指派 versionCode 至 `internal`+`alpha` 兩軌→commit 送審),`TRACKS=internal,alpha` |
| 4.63.0 | 2026-06-12 | 統計報表(Email + LINE)依週期重新設計並修正帳號刪除不完整。`lib/statsEmailReport.ts`:`periodFor('weekly')` 由「上一完整 Mon–Sun 週」改為滾動 T-7~T-1(`end=addDays(today,-1)`、`start=addDays(end,-6)`,label `過去 7 日`);`buildUserStatsReport` 新增 `periodIncome/periodExpense/periodNet`(period 區間實際收支)、對應 `period*ChangePct`(對比 comparePeriod:日=T-2、週=T-14~T-8、月=M-2)、`sendDate`/`reportDate`/`reportWeekday`/`reportMonth`/`subject`、`periodTopCategories`(period 區間支出 Top5,月報用)。`renderStatsEmailHtml` 依 `period.kind` 分日/週/月版型:各自表頭+說明條標示涵蓋區間與寄送日,KPI 一律用 period 實際收支(lead 昨日/本週/上月);週報加每日明細表(補滿 7 列)、日週報附本月累計、月報 Top5/交易改用報表月(M-1)資料。`lib/lineMessaging.ts` `buildStatsReportFlex` 比照(altText/表頭/banner/KPI/週每日明細/月 Top5 來源/catHeading),新增 `weekdayZh`/`addDaysYmd`。`lib/scheduler.ts` 寄信主旨改用 `stats.subject`。帳號刪除:新增 `lib/userDeletion.ts` `deleteUserCompletely(userId)`(單一可信賴流程)+ `lib/transactionAttachments.ts` `purgeUserPhotoFiles(userId)`(刪 S3 物件/本機檔 + `rm -rf uploads/transaction-photos/{userId}`);`app/api/account/delete` 與 `app/api/admin/users/[id]` DELETE 統一改呼叫之,補齊先前兩路徑各漏的 `transaction_attachments`(含實體檔)、`login_sessions`、`line_bot_states`、`data_operation_audit_log`、`report_schedules`、`deleted_defaults`、`user_settings` 等表,並移除 `system_settings.report_schedule_user_ids` 中該使用者、`login_attempt_logs` 失敗列去識別化。後台報表排程列表新增「寄送結果」欄並於立即執行後回報成功/部分/失敗(`AdminClient.tsx`、`run-now` route 錯誤訊息強化) |
| 4.62.0 | 2026-06-12 | 交易照片靜態加密(encryption at rest,envelope/KMS 模型):新增 `lib/photoCryptoCore.ts`(純密碼學原語,無 DB 相依、可單測)與 `lib/photoCrypto.ts`(DEK 持久化層)。主金鑰 `PHOTO_MASTER_KEY`(env,base64 32B)以 AES-256-GCM 包覆每使用者隨機 DEK,存於新表 `user_photo_keys(user_id, wrapped_dek, iv, tag, created_at)`;DEK 加密照片,密文格式 `[MAGIC "APX1" 4B][iv 12B][tag 16B][ciphertext]`。`lib/transactionAttachments.ts`:`saveLocalPhoto`/`saveS3Photo` 寫入前 `encryptPhoto`、`readTransactionAttachment` 讀出後 `decryptPhoto`(以 MAGIC 辨識、舊明文自動 fallback)、`compressExistingS3Photos` 改為 解密→`sharp` 重壓→重加密。opt-in:未設 `PHOTO_MASTER_KEY` 維持明文;備份匯出走 read(解密成明文入 ZIP)、還原走 save(重加密)天然相容;LINE webhook 上傳與帳號刪除(新增刪 `user_photo_keys`,crypto-shred)一併涵蓋。新增批次加密 `encryptExistingPhotos()` + `POST /api/admin/transaction-photos/encrypt`(`requireAdmin`、稽核),`AdminClient.tsx` 系統設定頁新增加密狀態徽章(來源 `GET /api/transactions/attachments/storage` 的 `encryptionEnabled`)與「加密既有照片」按鈕。主金鑰輪替工具 `tools/rotate-photo-master-key.ts`(npm `rotate:photo-key`,`pg` 直連、dry-run 預設、逐列驗證 unwrap 成功才寫、`BEGIN/COMMIT` 包覆、可安全重跑)。單元測試 `tests/lib/photoCryptoCore.test.ts`(12 項,含金鑰隔離與竄改偵測)併入 `npm test`。`.env.example` 登記 `PHOTO_MASTER_KEY` |
| 4.61.2 | 2026-06-11 | 行動版 OAuth 登入網頁自動關閉:`google_auth.dart`/`line_auth.dart` 的 `signIn()` 將 `launchUrl` 由 `LaunchMode.externalApplication` 改為 `LaunchMode.inAppBrowserView`(Android Custom Tab),並於 `finally` 收到回呼導回 App 後呼叫 `closeInAppWebView()` 主動關閉授權分頁(外部瀏覽器無法被程式關閉故會殘留)。另把先前只在 `google_auth.dart` 套用的 `getInitialLink()` 殘留舊回呼修正同步套到 `line_auth.dart`(啟動前記錄 `staleInitialUri`,僅當 initial link 不同時才採用),避免 LINE 登出後再登入觸發「狀態不符」。無後端變更 |
| 4.61.1 | 2026-06-11 | 修正 production build 失敗:`sharp@0.35` 的 package.json `exports` 在 `moduleResolution: bundler` 下未正確曝露型別,`lib/transactionAttachments.ts` 動態 `import('sharp')` 觸發 TS7016(找不到宣告檔)使 `npm run build` 型別檢查失敗。新增 `types/sharp.d.ts`(`declare module 'sharp';` ambient 後備,比照 `adm-zip.d.ts`/`sql.js.d.ts`)。無功能/執行期變更 |
| 4.61.0 | 2026-06-11 | 行動版隱私/安全強化(對應 Android privacy best practices):1)移除 `AndroidManifest.xml`(main)的全域 `android:usesCleartextTraffic="true"`,改加 `android:networkSecurityConfig="@xml/network_security_config"` — 新增 `res/xml/network_security_config.xml`,`base-config cleartextTrafficPermitted="false"`,僅 `10.0.2.2`/`localhost`/`127.0.0.1` 的 `domain-config` 開放 cleartext 供本機開發。2)`application` 加 `android:allowBackup="false"` 停用雲端備份/adb backup,避免憑證外流。3)認證 JWT cookie 由明文 `SharedPreferences`(key `authCookie`)改存 `flutter_secure_storage`(`AndroidOptions(encryptedSharedPreferences: true)`,Keystore 加密);`ApiClient.init()` 加一次性遷移(讀舊明文 → 寫入 secure storage → 移除明文,含解密失敗 try/catch 回退),`_persistCookie()` 改走 secure storage。`pubspec.yaml` 新增 `flutter_secure_storage: ^9.2.2`。無後端變更 |
| 4.60.0 | 2026-06-11 | 交易照片壓縮:行動版 `mobile/lib/screens/transaction_form_screen.dart` 的 `_pickPhotos()` 在相機 `pickImage()` 與相簿 `pickMultiImage()` 都加上 `maxWidth: 1600, maxHeight: 1600`(沿用既有 `imageQuality: 82`),由 `image_picker` 在裝置端縮圖/重編碼,行為對齊網頁版 Canvas 壓縮(最長邊 1600、JPEG 0.8)。後端新增既有 S3 照片回填壓縮:`package.json` 加入 `sharp@^0.35.0`(`next.config.ts` 設 `serverExternalPackages: ['sharp']`,`Dockerfile` runner 階段手動複製 `sharp`/`@img`/`detect-libc`/`semver`,比照 `pg`)。`lib/transactionAttachments.ts` 新增 `compressExistingS3Photos()`:掃描 `transaction_attachments` 中 `storage='s3'` 列,逐筆 `getS3Object` 取回、以 `sharp().rotate().resize(1600,1600,{fit:'inside',withoutEnlargement:true}).jpeg({quality:82})` 重編碼,僅在結果更小時 `putS3Object` 原 key 覆寫並更新 `byte_size`/`mime_type`(sharp 以動態 `import()` 延遲載入,不影響一般上傳路徑)。新增 `app/api/admin/transaction-photos/compress` POST(`requireAdmin`、`maxDuration=300`、寫操作稽核)。`AdminClient.tsx` 系統設定頁新增「壓縮 S3 既有照片」按鈕(confirm 後呼叫,回報 scanned/recompressed/skipped/failed 與節省位元組) |
| 4.59.0 | 2026-06-11 | 修正行動版 Google 登入登出後再登入失敗:`mobile/lib/google_auth.dart` 的 `signIn()` 先前在啟動瀏覽器後無條件採用 `appLinks.getInitialLink()`,而該值具持久性會回傳上一次登入殘留的回呼 URI(夾帶舊 `state`),導致第二次登入立即被舊回呼以不符的 state 完成而拋「Google 登入狀態不符,請重試」。修正為啟動前先記錄 `staleInitialUri`,啟動後僅當 initial link 與其不同(真正冷啟動回呼)才採用,否則只信任 `uriLinkStream` 的本次回呼 |
| 4.58.0 | 2026-06-11 | 行動版記帳列表標示照片張數:`mobile/lib/models.dart` 的 `Txn` 新增 `attachmentCount`(解析 API 既有的 `attachmentCount`/`attachment_count`)。`mobile/lib/screens/transactions_screen.dart` 的 `_TxnTile` 在有附件時於日期列尾顯示相片圖示與張數(主題色),與網頁版一致。後端 `/api/transactions` 本即回傳 `attachmentCount`,無後端變更 |
| 4.57.0 | 2026-06-11 | 行動版版本資訊頁:新增 `package_info_plus` 依賴讀取 App 自身 `versionName`(CI 以 `--build-name=<changelog.currentVersion>` 打包,故正式版顯示真實版號;本機 debug 顯示 pubspec 預設 1.0.0)。`mobile/lib/api_client.dart` 新增 `changelog({refresh})`(GET `/api/changelog`,`refresh:true` 帶 `?refresh=1` 強制刷新遠端分支快取)。新增 `mobile/lib/screens/changelog_screen.dart`:同時讀 `PackageInfo.fromPlatform()` 與 `/api/changelog`,以語意化版號比較(自實作 `compareVersions`)判斷 `updateAvailable`,頂部卡片顯示目前版本/可更新狀態,清單在有新版時只列高於 App 版本的 release、否則列最近 10 筆,每條變更帶中文標籤(new/improved/fixed/removed/warning),支援右上重新整理與下拉刷新(走 `refresh=1`)。`mobile/lib/screens/settings_screen.dart` 於登出上方新增「版本資訊」ListTile 導向該頁。無後端變更(沿用既有 `/api/changelog`) |
| 4.56.0 | 2026-06-11 | 行動版檢視/刪除已上傳交易照片:`mobile/lib/api_client.dart` 新增 `listTransactionAttachments(txId)`(GET `/api/transactions/{txId}/attachments`,回傳附件清單)、`deleteTransactionAttachment(txId, attachmentId)`(DELETE 同路徑下 `/{attachmentId}`),以及供 `Image.network` 載入受 Cookie 認證原圖的 `attachmentFileUrl(txId, attachmentId)` 與 `mediaHeaders()`(沿用既有 `_headers()`)。`mobile/lib/screens/transaction_form_screen.dart` 在編輯模式 `_loadRefs()` 時載入既有附件(失敗靜默略過不擋表單),於「新增照片」按鈕上方新增「已上傳照片」縮圖區:縮圖以 `Image.network`+`mediaHeaders()` 載入,點按 `_viewExistingPhoto()` 開全螢幕 `InteractiveViewer` 檢視並可縮放、右上叉叉 `_removeExistingPhoto()` 經確認對話框呼叫 DELETE 後移除。修正先前行動版只能上傳、無法查看已上傳照片的問題。後端 list/file/delete 端點皆已存在,無後端變更 |
| 4.55.0 | 2026-06-11 | 行動版手動同步股利:`mobile/lib/api_client.dart` 新增 `syncStockDividends()`(POST `/api/stock-dividends/sync`,因後端逐年查詢含節流延遲,將逾時放寬至 120 秒,`_send()`/`_getMapFromSend()` 新增選用 `timeout` 參數)。`mobile/lib/screens/stocks_screen.dart` 的 `StocksScreen` 於「股利」分頁(index 2)的 AppBar 新增「同步股利」action(`Icons.sync`,執行中顯示共用 spinner),透過新增的 `GlobalKey<_DividendTabState>` 呼叫 `_DividendTabState.syncDividends()`:呼叫同步 API → 以 toast 回報 `synced`/`skipped` 筆數(`synced==0` 顯示「沒有新的股利可同步」)→ 有同步成功才 reload。無後端變更(沿用既有 `/api/stock-dividends/sync`,依各檔持有期間自 TWSE 抓除權息寫入股利紀錄) |
| 4.54.0 | 2026-06-11 | 行動版交易照片支援直接拍照:`mobile/lib/screens/transaction_form_screen.dart` 的 `_pickPhotos()` 改為先以 `showModalBottomSheet` 讓使用者選擇來源——「拍照」走 `ImagePicker.pickImage(source: ImageSource.camera, imageQuality: 82)` 拍單張加入;「從相簿選擇」維持原 `pickMultiImage(imageQuality: 82, limit: remaining)` 多選。仍受單筆最多 5 張上限(`remaining` 計算)約束。`image_picker` 相機在 Android 透過系統相機 intent 取像,免額外 `CAMERA` 權限,`AndroidManifest.xml` 無需變更;上傳沿用既有 `uploadTransactionPhotos()`(multipart `POST /api/transactions/{id}/attachments`),無後端變更 |
| 4.53.0 | 2026-06-11 | 行動版手動更新股價:`mobile/lib/api_client.dart` 新增 `batchFetchStockPrices()`(POST `/api/stocks/batch-fetch`)與 `batchUpdateStockPrices()`(POST `/api/stocks/batch-price`)。`mobile/lib/screens/stocks_screen.dart` 的 `StocksScreen` 改用顯式 `TabController`,於「持股」分頁的 AppBar 新增「更新股價」action(執行中顯示 spinner),透過既有 `GlobalKey<_HoldingsTabState>` 呼叫新增的 `_HoldingsTabState.updatePrices()`:批次抓 TWSE/TPEx 最新價 → 篩 `status=='ok'` 組 `{stockId,currentPrice}` 寫回 → reload,並以 toast 回報更新/失敗檔數。無後端變更(沿用既有批次查價/寫價 API) |
| 4.52.1 | 2026-06-11 | 容器安全修補:`Dockerfile` runner 階段加入 `RUN apk upgrade --no-cache`,升級 base image(`node:24-alpine`)落後的 OS 套件至 alpine repo 最新修補版,修補 Trivy 掃描回報的 OpenSSL `libcrypto3`/`libssl3` 高風險弱點(PKCS7_verify use-after-free,CVE-2026-45447),讓 `trivy.yml`(`ignore-unfixed: true`、HIGH/CRITICAL `exit-code: 1`)的容器掃描通過。另修正 `secret-scan.yml`(gitleaks)`permissions` 補上 `pull-requests: read`,解決 PR 事件下 gitleaks-action 取 PR commits 清單時 GitHub API 回 403 導致 CI 失敗。無應用程式碼變更 |
| 4.52.0 | 2026-06-11 | 股價自動更新(伺服器排程):新增 `lib/stockPriceUpdater.ts`,由 `instrumentation.ts` 排程心跳(每 `SCHEDULER_TICK_MS` 一次)呼叫 `checkAndRunStockPriceUpdate()`;交易時段閘門限台北時間週一~五 09:00–14:00,並依 `stock_auto_update_interval_min` 節流(預設 10 分,由 `stock_auto_update_last_run` 記錄)。`runStockPriceUpdate()` 以 `SELECT DISTINCT symbol` 跨使用者去重,沿用 `twseFetchNext` 三段策略(realtime → STOCK_DAY → TPEx)經 `fetchAllWithLimit`(並發走 `TWSE_MAX_CONCURRENCY`)抓價,批次 `UPDATE stocks SET current_price WHERE symbol = ? AND COALESCE(delisted,0)=0`,僅覆寫現價不存歷史,結果寫入 `system_settings.stock_auto_update_last_summary`。`system_settings` 新增 `stock_auto_update_enabled`/`interval_min`/`last_run`/`last_summary` 四欄;`getSystemSettings()` 與 `app/api/admin/system-settings` PUT 曝露 enabled/interval;新增 `app/api/admin/stock-price-update/run-now`(管理員手動觸發,略過時段與節流);`AdminClient.tsx` 系統設定頁加開關、間隔、狀態與「立即更新股價」按鈕。環境變數 `STOCK_AUTO_UPDATE_ENABLED`/`STOCK_AUTO_UPDATE_INTERVAL_MIN` 可覆寫 DB 設定 |
| 4.51.0 | 2026-06-11 | 行動版第三方登入擴充:新增 LINE 登入與 Passkey 登入。Flutter 新增 `mobile/lib/line_auth.dart`(開啟 LINE 授權頁 → `/app/line-callback` 回呼 → 取得一次性 ticket)與 `mobile/lib/passkey_auth.dart`(透過 `/app/passkey-login` 進行 WebAuthn);`api_client.dart` 新增 `lineState()`/`lineLogin()`/`exchangeAppAuthTicket()`;`login_screen.dart` 依 `/api/config` 的 `lineCodeFlow`/`lineChannelId` 顯示「使用 LINE 登入」「使用 Passkey 登入」按鈕。Web 新增 `app/api/app/auth-ticket/route.ts` 與 `app/api/app/auth-ticket/exchange/route.ts`(一次性票券換認證 cookie,`exchange` 列入 `middleware.ts` PUBLIC_PATHS)、`lib/appAuthTicket.ts`、`app/app/line-callback/page.tsx`、`app/app/passkey-login/page.tsx`;`lib/lineOAuth.ts` 內建 redirect allowlist 附加 `/app/line-callback`(改為「附加」而非「覆蓋」自訂白名單);`public/.well-known/assetlinks.json` 填入正式 App 簽章 SHA-256 指紋以啟用 App Links。mobile 各畫面套用 `dart format` 重排(無語意變更)。另新增 Google Play 上架截圖(手機 1080×1920、7" 平板 1440×2560、10" 平板 1800×3200,各 8 張)與產生器於 `mobile/assets/play-store/` |
| 4.50.0 | 2026-06-11 | 行動版交易收據附件 + 登入狀態反應式化 + Google redirect 白名單修正:Flutter 新增 `image_picker`/`http_parser` 依賴,`transaction_form_screen.dart` 以 `pickMultiImage` 選取至多 5 張照片,`api_client.dart` 新增 `uploadTransactionPhotos()`(multipart POST `/api/transactions/{id}/attachments`,依副檔名推算 `Content-Type`)、`createTransaction` 改回傳交易 JSON 以取得新建 id;`ApiClient.authState`(`ValueNotifier<bool>`)於 login/register/googleLogin/logout/deleteAccount/401 統一更新,`app.dart` 改監聽 authState 切換登入畫面(登入階段過期自動導回登入)。移除 App 內更新檢查:刪除 `mobile/lib/screens/app_update_screen.dart`、移除 `package_info_plus` 依賴與 `appVersion()`/`/api/app/version` 呼叫,並清理 `more_screen.dart`/`settings_screen.dart` 相關進入點。修正 `app/api/auth/google/route.ts` `buildGoogleRedirectAllowlist()`:`GOOGLE_OAUTH_REDIRECT_URIS` 改為「附加」而非「覆蓋」內建 fallback,避免自訂 redirect URIs 時行動版固定回呼路徑 `/app/google-callback` 被擋 |
| 4.49.1 | 2026-06-11 | 法規/合規頁面補強:`app/privacy/page.tsx` 新增「交易附件(收據相片/檔案)選配」段落、IPinfo 登入地理位置查詢揭露、處理者(SMTP/Nodemailer、Zeabur Email、Resend、MEGA S4 整檔備份)揭露,第三方服務清單補 IPinfo/郵件通道/MEGA/Google Play Integrity;`app/terms/page.tsx` 釐清官方版 vs self-hosted、新增 Android App 形式與「管理員」定義;`app/api-credits/page.tsx` 第三方登入補 LINE、身份驗證服務數 1→2、更新日期;`lib/external-apis.json` 新增 LINE Login 條目。另補齊 Play Console `data_safety_export.csv`(資料安全表單)以符實際蒐集情形 |
| 4.49.0 | 2026-06-11 | 新增 Play Integrity API(classic/nonce 流程,軟性上線):`lib/playIntegrityVerdict.ts`(純判定邏輯,可獨立單元測試)+ `lib/playIntegrity.ts`(service account JWT bearer grant 換 OAuth2 token → `decodeIntegrityToken` → `evaluateVerdict`,全程不拋例外、未設定憑證 fail-open)+ `lib/playIntegrityNonce.ts`(一次性 TTL nonce,比照 `googleOAuthState.ts`);新增 `GET /api/app/integrity/nonce`;`auth/login`、`auth/register`、`auth/google` route 讀 `integrityToken`/`integrityNonce`,軟模式僅記錄、`PLAY_INTEGRITY_ENFORCE=true` 時不通過回 403。Android:`build.gradle.kts` 加 `com.google.android.play:integrity:1.4.0`、`MainActivity.kt` 註冊 `assetpilot/play_integrity` MethodChannel;Flutter:`mobile/lib/play_integrity.dart` + `api_client.dart` 於 login/register/googleLogin 自動帶 token。`.env.example` 新增 `PLAY_INTEGRITY_SERVICE_ACCOUNT_JSON`/`PLAY_INTEGRITY_PACKAGE_NAME`/`PLAY_INTEGRITY_ENFORCE`。並修正 `app/api/admin/users/[id]/route.ts` 僅匯出 `DELETE`,導致前端 `apiPut('/api/admin/users/[id]', { isAdmin })` 切換管理員身分回 405(Next.js 誤報 Failed to find Server Action)的問題:新增 `PUT` handler(含最後一位管理員保護) |
| 4.48.0 | 2026-06-10 | 補齊使用者帳號自助刪除:`app/api/account/delete/route.ts` 改為密碼帳號驗密碼、OAuth-only(Google/LINE)帳號驗 `confirmEmail`(移除 Web 無法產生的 id_token 依賴與 LINE 無分支死路);`AccountSettingsClient.tsx` 以 `Modal` 取代 `window.prompt`,依 `hasPassword` 切換驗證欄位;mobile 新增原生刪除帳號流程(`AppUser.hasPassword`、`ApiClient.deleteMyAccount()`、`settings_screen.dart` 刪除帳號項目);`app/privacy/page.tsx` 補開發者身分、隱私聯絡信箱 `assetpilot@shao.one`、無法登入時的刪除管道、兒少聲明、HTTPS 強制與 Google 登入資料揭露以符合 Play 上架要求 |
| 4.47.4 | 2026-06-09 | `mobile/lib/screens/app_update_screen.dart` 移除硬編碼 `_currentVersion`/`_currentBuildNumber`,改用 `package_info_plus` `PackageInfo.fromPlatform()` 動態讀取,修正 App 更新畫面版本顯示恆為 1.0 的問題 |
| 4.47.3 | 2026-06-09 | `app/api/auth/google/route.ts` `buildGoogleRedirectAllowlist()` fallback 新增 `https://${APP_HOST}/app/google-callback`,修正 App Google SSO 回呼時後端 allowlist 驗證失敗(`invalid_redirect_uri`) |
| 4.47.2 | 2026-06-09 | 首頁 `app/page.tsx` 主標語改為兩行(插入 `<br />`),移除行尾逗號與句號 |
| 4.47.1 | 2026-06-09 | `android-apk.yml` CI workflow 改上傳至 MEGA S4(`MEGA_S4_BUCKET`/`MEGA_S4_ACCESS_KEY_ID`/`MEGA_S4_SECRET_ACCESS_KEY`/`MEGA_S4_REGION`/`MEGA_S4_ENDPOINT`),與 `app/api/app/version` 讀取來源對齊,修正 App 更新畫面永遠取不到版本資訊的問題 |
| 4.47.0 | 2026-06-09 | 安卓 App 新增 App 更新畫面(`AppUpdateScreen` 透過 `GET /api/app/version` 查 MEGA S4 版本資訊);`GoogleAuth` 新增 `callbackScheme`/`callbackHost` 常數 + 自訂 URI scheme `assetpilot://google-callback` 備援 intent-filter,修正 App Links 未驗證時 OAuth 回呼失效;移除 `ApiClient.setBaseUrl()` 與 `SharedPreferences _kBaseUrl`,連線位址固定為 `https://asset.shao.one`;對應移除設定頁「後端位址」編輯 UI;`GET /api/app/version` 改以 `getMegaS4ConfigStatus()` 驅動,讀取 MEGA S4 的 `downloads/app-version.json`;新增 `app/app/google-callback/page.tsx`(重導向至 `assetpilot://google-callback` custom scheme) |
| 4.46.0 | 2026-06-09 | 安卓 App 新增 Google SSO 登入(系統瀏覽器 + App Link 回呼,`google_auth.dart` + `POST /api/auth/google`);新增帳號註冊畫面(`POST /api/auth/register`,成功後自動登入);新增 Turnstile WebView widget,修正後台啟用 Turnstile 時 App 登入 403(並修正 CSRF:所有請求帶 `Origin=<baseUrl>`);App 圖示改為 AssetPilot 品牌 adaptive icon(`flutter_launcher_icons`);預設 base URL 改為 `https://asset.shao.one`;`middleware.ts` 提取 `isPublicPath` 變數讓公開路徑略過 CSRF 檢查;排程 `dayOfMonth` 預設值及有效範圍由 0-28 修正為 1-28 |
| 4.45.0 | 2026-06-09 | 新增 Flutter 安卓客戶端(`mobile/`):以現有 REST API 為後端,手動管理 httpOnly `authToken` Cookie(`http` 套件不自動帶 Cookie,登入時擷取 `Set-Cookie`、後續以 `Cookie` 標頭回帶,連同 base URL 持久化於 `shared_preferences`)。底部導覽四分頁(首頁/記帳/股票/更多)+ Material 3 三模式主題(`fl_chart` 圓餅圖)。涵蓋 `auth/login`、`auth/me`、`dashboard`、`transactions`(CRUD+`transactions/transfer`)、`accounts`、`categories`、`budgets`、`recurring`、`stocks`/`stock-transactions`/`stock-dividends`/`stock-realized`、`reports`、`account/settings/display-name`。`AndroidManifest.xml` 加 `INTERNET` 權限與開發用 `usesCleartextTraffic`。未涵蓋:OAuth/Passkey 登入、CSV 匯出入、備份還原、管理員系統設定、交易照片附件。`flutter analyze` 無警告、widget test 通過、debug APK 可建置 |
| 4.44.1 | 2026-06-04 | 修正全新 PostgreSQL 環境首次部署啟動失敗:`lib/postgresRuntime.ts` 的 `translateSql` 新增 `translateDdlTypes()`,在 `CREATE TABLE`/`ALTER TABLE` DDL 將 SQLite `INTEGER`(64-bit)改寫為 PostgreSQL `BIGINT`,避免 `*_at` Unix 毫秒時間戳寫入 int4(上限 ~21 億)時溢位(`value out of range for type integer`),與 `lib/postgresMigration.ts` 型別對應一致 |
| 4.44.0 | 2026-06-04 | 使用者個人資料完整打包匯出/上傳合併還原:①新增 `lib/userDataBundle.ts`(`exportUserBundle` 打包 14 張 user 資料表 + `transaction_attachments` metadata + 圖片實體成 ZIP,含 `manifest.json`;`restoreUserBundle` 合併式還原,依主鍵查重、僅 INSERT 缺漏、`BEGIN/COMMIT/ROLLBACK` 原子化;欄位名以識別字正則驗證防 SQL injection);②`lib/transactionAttachments.ts` 新增 `restoreAttachmentFromBundle()`,將圖片位元組寫入 `getDefaultTransactionPhotoStorage()` 對應儲存並保留原 attachment id、remap user_id;③新增 `GET`/`POST /api/account/data-bundle`(requireAuth、200MB 上限、writeOperationAudit 記錄 export/restore_data_bundle);④`DataTransferClient.tsx` 新增不分權限的「完整資料備份(含圖片)」區塊;⑤`openapi.yaml` 登記契約、新增 `types/adm-zip.d.ts`;⑥移除遷移後死相依 helmet / cookie-parser / cors / react-hook-form / @hookform/resolvers / recharts / dotenv,README 安全標頭描述改為 Next.js 回應標頭設定 |
| 4.43.0 | 2026-06-04 | 交易憑證照片前端壓縮:`TransactionsClient.tsx` 新增模組級 `compressPhoto()`,於 `uploadPhotos()` 送出前以 Canvas 將每張照片縮至最長邊 1600px、JPEG quality 0.8 後再 append 進 FormData,降低上傳頻寬與 S3/本機儲存佔用;`createImageBitmap(file, { imageOrientation: 'from-image' })` 處理 iPhone EXIF 旋轉;GIF/SVG、`createImageBitmap` 不可用、解碼失敗、或壓縮後體積未縮小等情況一律回退原檔,後端 `assertImageUpload` 上限與既有流程不變(純瀏覽器原生、無新增套件,符合 Brownfield 原則 V) |
| 4.41.0 | 2026-05-16 | 交易憑證照片管理強化:①TransactionsClient 新增編輯模式照片管理(查看現有附件、個別刪除、追加新照片,合計上限 5 張);②附件數 > 1 時改以含縮圖的多張選擇器 Dialog 顯示,先前只能直開第一張;③新增 DELETE /api/transactions/:txId/attachments/:attachmentId 端點(deleteTransactionAttachment() 支援 local/S3 兩種儲存);④admin UI 新增「交易憑證照片大小上限(MB)」輸入欄,對應 system_settings.transaction_photo_max_bytes INTEGER 欄位(0 表示使用環境變數預設值);maxPhotoBytes() 優先讀取 DB 設定,再 fallback 至 TRANSACTION_PHOTO_MAX_BYTES env var;PUT /api/admin/system-settings 支援 transactionPhotoMaxBytes 欄位;⑤上傳流程移除前端儲存位置 select,改由 getDefaultTransactionPhotoStorage() 伺服器端統一決定 |
| 4.40.0 | 2026-05-16 | 管理員可從 UI 強制設定交易照片儲存位置:system_settings 新增 transaction_photo_storage 欄位(空字串 / 'local' / 's3');getDefaultTransactionPhotoStorage() 優先讀取 DB 設定,再 fallback 至 TRANSACTION_PHOTO_DEFAULT_STORAGE env var;PUT /api/admin/system-settings 支援 transactionPhotoStorage 欄位;管理員「系統設定」新增儲存位置下拉選單(依環境設定/強制本機/強制 S3) |
| 4.37.9 | 2026-05-11 | 帳號設定新增目前登入裝置管理:新增 `login_sessions` 紀錄有效登入 session,JWT 內含 `sessionId` 並以 token 雜湊比對;`GET /api/account/sessions` 列出裝置名稱、登入時間與登入 IP,`DELETE /api/account/sessions/:id` 可登出單一裝置,登出目前裝置時同步清除 Cookie;修改密碼與管理員重設密碼仍撤銷所有既有 session 並遞增 `token_version` |
| 4.37.8 | 2026-05-11 | 登出範圍修正:`POST /api/auth/logout` 改為只清除目前裝置的 `authToken` Cookie,不再遞增 `users.token_version`,避免 A 裝置登出時讓其他裝置同步失效;修改密碼與管理員重設密碼仍保留 `token_version` 遞增,維持高風險帳號變更後全裝置 Token 撤銷 |
| 4.37.3 | 2026-05-08 | 安全驗證差異補齊:①Next.js `requireAuth` / `requireServerAuth` 補回 JWT `tokenVersion` 與 `users.token_version` 比對,修改密碼、管理員重設密碼後舊 Token 立即失效;②middleware 與 API auth helper 補回 Cookie 認證狀態變更請求的 Origin / Referer CSRF 檢查;③新增 `lib/envSecrets.ts`,啟動時載入 `ENV_PATH` 並在缺少 `JWT_SECRET` / `DB_ENCRYPTION_KEY` 時自動產生寫入,避免固定預設 JWT 密鑰與未加密資料庫;④`/api/account/settings/delete` 與 `/api/account/settings/password` 復用主要帳號端點,避免 Google-only 刪除帳號與修改密碼流程安全性分岔;⑤production CSP 移除 `unsafe-eval`,僅 dev mode 保留 |
| 4.37.2 | 2026-05-07 | CI/CD build 修復 + 生產部署崩潰修正:①`lib/db.ts` 移除 `Function('return require')()` hack,改用頂層 `import * as path/fs/crypto from 'path'/'fs'/'crypto'`;`sql.js` 由 runtime require 改為 `await import('sql.js')`;②`next.config.js` 新增 `serverExternalPackages: ['sql.js']` + webpack `resolve.fallback: {path/fs/crypto: false}` for `nextRuntime !== 'nodejs'`(修正 Edge runtime 編譯時 UnhandledSchemeError / Module not found);③新增 `types/sql.js.d.ts`(`declare module 'sql.js'`)修正 TS7016;④`lib/db.ts._runMigrations` 清除 merge 殘留的 `const path = getPathModule()` / `const fs = getFsModule()` 呼叫 |
| 4.37.1 | 2026-05-06 | Docker build 修復 + 遷移後架構規範收尾:①`package.json` 補上遷移後遺漏的 runtime/dev deps(`next@15`、`react@19`、`react-dom@19`、`@tailwindcss/postcss@4`、`tailwindcss@4`、`@base-ui/react@1`、`lucide-react`、`class-variance-authority`、`chart.js`、`pino`、`tailwind-merge` + `typescript@5`、`@types/{react,react-dom,node}`、`postcss`、`autoprefixer`),重生 `package-lock.json`;②刪除損毀的 `next.config.ts`(重複 import/export、rewrites 自迴圈),保留 `next.config.js` 並把 `outputFileTracingExcludes` 移出 `experimental` 區塊(Next.js 15 規範),暫時加上 `typescript.ignoreBuildErrors` + `eslint.ignoreDuringBuilds` 作為遷移期權宜開關;③Next.js App Router route file 違規 export 修正:`app/api/auth/google/state/route.js` 內的 `issueGoogleOAuthState` / `consumeGoogleOAuthState` 拆出至 `lib/googleOAuthState.js`、`app/api/auth/passkey/challenge/route.js` 內的 `issuePasskeyChallenge` / `consumePasskeyChallenge` / `passkeyChallenge` 拆出至 `lib/passkeyChallenge.js`、`app/api/transactions/import/route.js` 共用 `importLocks` / `importProgress` 拆出至 `lib/transactionImportState.js`(同時更新 `categories/import`、`imports/progress` importer),`stock-dividends/import` 與 `stock-transactions/import` 內同名 const 改為 module-private;④新增 `lib/utils.ts`(標準 `cn()` = `twMerge(clsx(...))`)、placeholder client `components/features/api-credits/ApiCreditsClient.tsx` + `components/features/data-transfer/DataTransferClient.tsx`;⑤10 個 finance/stocks `page.tsx` 的 `requireServerAuth` import path `@/lib/auth` → `@/lib/serverAuth`、`stockHelpers.js` → `stockHelpers`(檔實際為 `.ts`)、`app/finance/transactions/page.tsx` 半成品(含 `// ...` 佔位)重寫為標準 `AppLayout + TransactionsClient` 模式;⑥`components/ui/Input.tsx`、`Select.tsx` 補 named export;`components/ui/dialog.tsx` 的 `DialogTrigger` / `DialogClose` 加 `asChild` → Base UI `render` 相容 shim(影響 8 個 client 共 16 處 `asChild` 用法);12 個 client component signature 加 `_props: { user?: any } = {}` 讓 `page.tsx` 的 `user={user}` 通過 TS 檢查;⑦`Dockerfile` 改寫為三階段 Next.js standalone build(`deps` → `builder` → `runner`,非 root user `nextjs:1001`,HEALTHCHECK 改打 `/api/config`,`HOSTNAME=0.0.0.0` 使 standalone 對外 listen);`docker-compose.yml` 補 `HOSTNAME` / `NEXT_TELEMETRY_DISABLED` / `SSL_PATH`;`.dockerignore` 補 `.next` / `out` / `.next/types` |
| 4.37.0 | 2026-05-06 | 前端遷移收尾:①Phase 5 任務 T016–T020 完成,將 Accounts、Categories、Budget、Recurring、Reports 5 個記帳模組由舊 `app/finance/*/page.js` + `components/features/*/Client.js` 全面改寫為 Next.js App Router `page.tsx` + `*Client.tsx`(TypeScript + Tailwind v4 utility classes);②持股相關 5 頁(`app/stocks/{dividends,portfolio,realized,settings,transactions}/page.tsx` + 對應 `*Client.tsx`)同步遷移;③Login、Account Settings、Admin、AppLayout、Sidebar、TopNav 改 `.tsx`;④清理 `app-legacy/` 整個舊版目錄與孤兒 `.js` client;⑤`package.json` scripts 由 `node server.js` 切為 `next dev` / `next build` / `next start`,部署需先 build 再 start;⑥`stockHelpers.js` → `stockHelpers.ts`;⑦剩餘 T014(lighthouse 驗證 LCP < 2.5s / TBT < 200ms)尚未執行 |
| 4.36.3 | 2026-05-05 | 側邊選單項目順序調整:`components/layout/Sidebar.tsx` 內 `NAV_ITEMS` 將「資料匯出匯入」(`/settings/export`)由原最末位上移至「持股總覽」之後;「帳號設定」(`/settings/account`)、「管理員」(`/settings/admin`,requireAdmin)緊接其後集中為設定區;「API 使用與授權」(`/api-credits`)移至清單最末。僅調整陣列順序,未變動路徑、icon 或 requireAdmin 條件 |
| 4.36.2 | 2026-05-05 | 修復交易記錄編輯視窗交互問題,優化事件處理機制 |
| 4.36.1 | 2026-05-05 | 前端架構遷移至 Next.js + Tailwind CSS v4:完成 Dashboard 與 Transactions 頁面遷移;UI 元件庫(Button、Table、Dialog、Badge 等)以 TypeScript 重構並套用 Tailwind v4 工具類別;Hybrid 路由轉發 API 至 Express 後端 |
| 4.36.0 | 2026-05-03 | 儀表板新增「收入分類」圓餅圖(010-dashboard-income-pie):①`server.js` `/api/dashboard` 新增 `incomeCatBreakdown` 欄位(`buildCategoryAggregateNodes` + `type='income'` 查詢);②`index.html` 在支出分類與資產配置卡片之間插入「收入分類」卡片(含雙圓餅圖開關 `dashIncomeDualPie`、canvas `dashIncomePieChart`、排行容器 `dashIncomeTop5`);③`app.js` 新增 `DASH_DUAL_INCOME_KEY` 常數、`dashDualPie.income` 狀態、`renderDashIncomePie()`(單環 + useDualPie 雙環 + FR-009 點擊跳轉 type=income)、`renderDashIncomeTop5()`(前 5 名排行)、`drawDashboardIncomeDualPie()`(父+子雙環,鏡射 `drawDashboardExpenseDualPie`)、income toggle change 事件綁定;④`openapi.yaml` 新增 `incomeCatBreakdown` 欄位定義 |
| 4.35.2 | 2026-05-03 | 修正儀表板「資產配置(含股票市值)」與「帳戶前 5 名」中,信用卡等負值欠款帳戶錯誤顯示為負數且被列入資產總和計算的問題。改為排除負值帳戶,確保排行榜與圓餅圖符合資產語意。 |
| 4.35.1 | 2026-04-29 | 修正 `navigateToTransactions()`(`app.js:3062`)與 008-frontend-routing 不相容:原寫法用 `history.replaceState(null, '', '#/transactions?...')` 把 URL 參數塞到 hash,但 `navigateToPath()` 在 `location.pathname !== normalized` 時會執行 `history.pushState({...}, '', normalized)` 覆寫整個 URL(normalizePath 已剝除 search/hash),導致 hash 內的 `categoryId` / `dateFrom` / `dateTo` / `type` 全部遺失,`restoreFiltersFromHash()` 接著讀到空字串 → 無 filter 套用 → 4.34.0 backend 父→子展開邏輯永遠收不到 categoryId。本版改為先 `pushState({path,page,sub,scrollY}, '', '/finance/transactions?<params>')` 把 URL 寫成「目標 pathname + search params」,這樣 `navigateToPath` 進來時 `location.pathname === normalized` 條件為 true 不再覆寫,URL 與 search 全部保留 → `restoreFiltersFromHash` 正確讀取 → `applyFilters` 正確套用。同時受惠:dashboard 支出餅(line 2202)、dashboard 資產餅(line 2362)、報表分類餅(line 3363)三個 callsite |
| 4.35.0 | 2026-04-29 | 自動重啟服務(in-app update):①`server.js` 新增 `detectProcessSupervisor()` 偵測 `/.dockerenv` / `process.env.pm_id` / `PM2_HOME` / `INVOCATION_ID`,回傳 `{available, type: 'docker' \| 'pm2' \| 'systemd' \| 'none'}`;②新增 `AUTO_RESTART_AFTER_UPDATE` env var(`auto`(預設)/ `force` / `off`)+ `planAutoRestart()` 決策函式,僅在 supervisor 可用時才執行 exit;③`POST /api/system/update-app` 成功後先註冊 `res.on('finish', ...)` hook,回應 JSON 含 `autoRestartScheduled` / `autoRestartReason` / `autoRestartSupervisor` / `restartDelayMs`,response flush 完成後 setTimeout 1500ms → `flushOnExit()` + `process.exit(0)`,由 supervisor (Docker `restart: unless-stopped`) 重新拉起;④`index.html` 新增 `#restartOverlay`(role=alertdialog,含 spinner / title / msg / elapsed / 立即重新整理 fallback button);`style.css` 加 `.restart-overlay` 全螢幕遮罩 + backdrop-filter blur(4px)、`.restart-overlay-card` / `.restart-overlay-spinner` / `.restart-overlay-elapsed` 樣式;⑤`app.js` 新增 `showRestartOverlayAndWait()` 函式:開遮罩、每秒更新已等待秒數、`startDelayMs+500` 後開始輪詢 `/api/config`(公開端點,不需 auth)、每 2 秒 fetch 一次、200 即 `location.reload()`,90 秒未恢復顯示逾時 + 手動 reload 按鈕;⑥前端 `runAppUpdateBtn` click handler 改為依 `result.autoRestartScheduled` 分流:true → 開遮罩等待;false → 顯示 confirm 含失敗原因(如「未偵測到 supervisor」),由使用者決定是否手動 reload;⑦不變動 admin auth 中介層,仍維持 `adminMiddleware` 限制 |
| 4.34.0 | 2026-04-29 | 交易分類篩選多選 + 父分類展開修正:①`server.js` 內 `GET /api/transactions` 的 `categoryId` 參數改為接受逗號分隔清單,每個 ID 都先查 `categories.parent_id IN (...)` 展開為「自身 + 全部子分類」後合成單一 `IN (...)` 條件,沿用既有 `idx_cat_user_parent_sort` 索引;單一 ID 行為向後相容(視為 1 元素清單)。修正 dashboard `renderDashPie` 餅圖(`app.js:2202`)與報表 `drawCategoryReport` 餅圖(`app.js:3246`)`navigateToTransactions({ categoryId: seg.parentId })` 後因後端使用 `category_id = ?` 精確匹配導致結果為空(交易實際全掛在 leaf 分類)的 bug;②`index.html` 將 `<select id="filterCategory">` 替換為 `.multi-select` custom dropdown(trigger button + dropdown 含「全選 / 清除」action + 父子 checkbox 清單),加上 `aria-haspopup="listbox"` / `aria-multiselectable="true"` / `aria-expanded`;③`style.css` 新增 `.multi-select` / `.multi-select-trigger` / `.multi-select-dropdown` / `.multi-select-actions` / `.multi-select-action` / `.multi-select-item.parent | .child` / `.multi-select-group` 共 ~80 行樣式,沿用既有 `batch-cat-*`設計語彙(`var(--border)` / `var(--primary)` / `var(--shadow-md)`/ 淡入動畫);④`app.js` 新增 `filterCategoryIds: Set<string>`(空 = 「全部」不送參數)+`populateFilterCategoryItems()` / `refreshFilterCategoryUI()` / `setFilterCategoryIdsFromString()` / `bindFilterCategoryEvents()`函式族;父子聯動:勾父 → 全勾子;取消任一子 → 父也取消;全部子勾起 → 父自動勾起;觸發按鈕標籤動態顯示「全部」/ 單一分類名 /「已選 N 項」;⑤`applyFilters()` 改讀 `filterCategoryIds` 並寫入 URL `categoryId=id1,id2,...`;`restoreFiltersFromHash()` 用 `setFilterCategoryIdsFromString`還原;`bindFilters`重置按鈕清空 Set 並 refresh UI;⑥UX 一致性:勾選不立即送請求,與其他 filter 對齊由「篩選」按鈕統一觸發,避免 N 次 API 呼叫;⑦dropdown 點擊外部自動關閉(`document.addEventListener('click', ...)`outside-click handler);`bindFilterCategoryEvents` 用 `dataset.bound` guard 確保僅綁一次 |
| 4.33.0 | 2026-04-29 | 多時區支援(009-multi-timezone):①users 表新增 `timezone TEXT NOT NULL DEFAULT 'Asia/Taipei'` IANA 識別碼欄位 + 新增 `monthly_report_send_log` 表(`UNIQUE(user_id, year_month)` 防重寄);②新增 `lib/userTime.js`(7 純函式:`isValidIanaTimezone` / `todayInUserTz` / `monthInUserTz` / `isFutureDateForTz` / `partsInTz` / `toIsoUtc` / `isValidIsoDate` + `__nowMs/__setNowMs` 測試 hook);`lib/taipeiTime.js` 改為 thin wrapper(內部呼叫 userTime 並固定傳 'Asia/Taipei');③`authMiddleware` 擴成單次 SELECT 同取 `token_version, timezone` → 掛 `req.userTimezone`(零額外查詢);④餘額計算(`server.js:6586/6707`)、定期交易展開(`processOneRecurring/processRecurringForUser` 改傳 `userTimezone`)、月度報表 `buildUserStatsReport()` 改用 `userTime.monthInUserTz(tz)` 取代 `thisMonth()` process timezone 依賴、POST `/api/transactions` 未提供 `date` 時預設 `userTime.todayInUserTz(req.userTimezone)`;⑤新增 `GET /api/users/me`(snake_case 完整 user 物件,`*_at` 過 `toIsoUtc()`)+ `PATCH /api/users/me/timezone`(IANA 驗證、no-op 不寫 audit、寫 `data_operation_audit_log` action='user.timezone.update' metadata={from,to,source});⑥前端 `app.js` 加 `getUserTz()` / `getBrowserTz()` / `todayInUserTz()` / `formatLocalDateTime()` / `listAvailableTimezones()` 工具,保留 `todayInTaipei` alias;`enterApp()` 呼 `GET /api/users/me` 合併 `currentUser.timezone`;`maybePromptTimezoneChange()` FR-010(b) 三條件 AND(tz=='Asia/Taipei' ∧ browserTz≠'Asia/Taipei' ∧ localStorage.tzPromptDismissedUntil 不在 7 天靜默期)→ 一次性 confirm;⑦個人設定頁新增「時區」card:搜尋下拉(`Intl.supportedValuesOf('timeZone')` ≈ 418 區域 + UTC/Etc/UTC 別名 + 10 項白名單 fallback)、即時預覽、儲存(source='manual');⑧scheduler `shouldRunSchedule(scheduleRow, userTimezone, nowTs)` 改用 `userTime.partsInTz(tz)` 計算 local hour/day/weekday + `localDayStartMs(tz, ymd)` 反推 helper;`checkAndRunSchedule()` JOIN users 取 timezone 帶入;`runScheduledReportNow` monthly 分支:先 INSERT `monthly_report_send_log` 失敗即 dedup 跳過,INSERT 成功才寄信,寄送失敗 UPDATE send_status='failed' + error_message 不自動重試(FR-018);環境變數 `SCHEDULER_TICK_MS` 可注入;⑨TWSE 例外(FR-014):`getTaiwanTime()` / `isTwseTrading()` / `lib/twseFetch.js` header 加註解標明永久鎖 Asia/Taipei 不隨使用者偏好變動;⑩憲章升級 v1.2.0→v1.3.0:新增 Principle IV「Time & Timezone Discipline」(後端 UTC ISO 8601 Z 字串 + per-user IANA + 市場/法規時間例外條款),FR-007a 由「鎖 UTC+8」修訂為「per-user `users.timezone` 預設 Asia/Taipei」;⑪OpenAPI(gitignored generated):新增 `/api/users/me`、`/api/users/me/timezone` 路徑與 `UserMe` / `UpdateTimezoneRequest` / `ErrorResponse` / `ValidationError` schema,`info.version` 升 4.33.0,`openapi: 3.2.0` 不變;source of truth 為 [contracts/multi-timezone.openapi.yaml](specs/009-multi-timezone/contracts/multi-timezone.openapi.yaml)(已提交);⑫測試:新增 5 支自動化套件(`tests/lib/userTime.test.js` 50/50 + `tests/migration/migration-009.test.js` 11/11 + `tests/integration/us1-natural-day.test.js` 15/15 + `us2-users-me-timezone.test.js` 18/18 + `us3-monthly-report.test.js` 13/13 + `fr015-transactions-historical.test.js` 6/6 = **128/128 pass**),`tools/check-iso-utc-format.js`(1000 隨機樣本)、`tools/sla-monthly-report.js`(100 帳號 × 12 時區 × 31 天 → P95 = 0.00 分鐘 ≤ 30 分 SC-003);`npm test` / `npm run test:tz` / `npm run check:iso` / `npm run check:sla` 暴露;⑬T021 範圍縮減(implement 階段決議):既有 `*_at` 欄位型別不一致(`users.created_at` 是 `'YYYY-MM-DD'` / `accounts.created_at` 是 INTEGER ms / `passkey_credentials.created_at` 是 DATETIME 字串),全面套 `toIsoUtc()` 違反 SC-001 regression-free,本 PR 僅本功能新增端點輸出 `.sssZ`,既有欄位另案處理;⑭T035/T039 暫緩:自動偵測新舊使用者統一走同一提示流程,散落 `toLocaleString` 全面替換為 `formatLocalDateTime` 為後續迭代;⑮Breaking Change 影響面:既有 Asia/Taipei 使用者所有 API 回應與 UI 行為相對升級前 100% 一致(regression-free,已通過自動化驗證) |
| 4.32.2 | 2026-04-29 | 兩處 UX 修正:①法律連結改為直連靜態檔:`index.html` 公開首頁 footer(行 237、241)與 sidebar legal links(行 369、371)、`privacy.html`(行 510)、`terms.html`(行 463、504)共 7 處 `href="/privacy"` / `href="/terms"` 改為 `href="/privacy.html"` / `href="/terms.html"`,避免命中 SPA 路由 `/privacy` / `/terms`(`app.js:1604-1607` 的 `showLegalPage()`)後渲染只有「完整政策內容請見 /privacy.html」單行佔位的 `#page-privacy` / `#page-terms`(`index.html:1841-1854`);保留原 SPA 路由註冊(`app.js:1039-1040`)以維持書籤相容性;②`buildCategoryOptions()`(`app.js:8482-8497`)移除父分類「(全部)」選項:父分類存在子分類時,原 `<optgroup label>` 內第一個 `<option value="${p.id}">${p.name}(全部)</option>` 整行刪除,僅保留子分類 `<option>`;對齊 003-categories(v4.24.0)後端 leaf-only 強制(交易 `category_id` 必為子分類),消除前端可選但後端會拒絕的 mismatch;同時影響「新增交易」(`txCategory`)與「編輯固定收支」(`recCategory`)兩處 select;無子分類的父分類(例如預設「其他」)仍走 `else` 分支保留為可選;`buildBudgetCategoryOptions()` 不受影響(本就 leaf-only) |
| 4.32.1 | 2026-04-28 | 公開首頁視覺重新對齊內頁設計語彙:①移除 `.public-home-orb--a/b` 兩顆 80px blur orb 與 backdrop-filter blur(12px);②`.public-home-hero` / `.pillar-card` / `.public-deploy` / `.public-stack` 等所有區塊卡片從自訂 `border-radius: 18-24px` 改為 `var(--radius)` 14px,從手刻多層 box-shadow 改為 `var(--shadow)` / `var(--shadow-md)`,與內頁通用 `.card` 完全同基底;③硬編碼 hex(`#4f6ef7` / `#7c3aed` / `#f97316` / `#10b981` / `#0f172a` 等 17 處)替換為語意 token:`var(--primary)` / `var(--income)` / `var(--net)` / `var(--today)` / `var(--text)` / `var(--text-secondary)` / `var(--text-muted)` / `var(--bg)` / `var(--surface)` / `var(--border)` / `var(--primary-light-bg)` / `var(--income-bg)` / `var(--net-bg)` / `var(--today-bg)`;④`.public-home-stats` 從 4 欄擠在單一容器靠 `+ ::before` 假分隔線,改為 `.summary-cards` 風格的 4 張獨立 `.stat-item` 卡(grid auto-fit minmax(220px, 1fr)),各帶 `.stat-icon`(44×44 / 12px radius / pastel bg + semantic color,沿用 `.summary-card .card-icon` pattern)+ `.stat-text` 區塊;⑤`.pillar-card` 移除 `::before` 3px 頂部 accent bar、移除 `color-mix()` bullet 環形光暈、hover 從 `translateY(-4px)` 改為 `-2px` 與 `var(--shadow-md)`,與儀表板 `.summary-card` 同步律;⑥`.public-deploy` 從 `linear-gradient(135deg, #0f172a, #1e293b)` 滿版深色 + 內含 radial orb 改為白色 `var(--surface)` 卡片外殼 + 內含暗色 `.code-window`(`#0f172a`,code-window 屬於程式碼塊本就應深色,與全站 syntax-highlighted block 一致);⑦`.public-stack` 從飄在背景上改為包在 `.card` 內,badge hover 從自訂陰影改為 border-color 切換;⑧`body.dark-mode .public-*` 規則從 60+ 行手動 hex 重設縮減為 5 條例外(`.public-home` 漸層、`.public-home-bg` grid 透明度、`.public-home-kicker` alpha、4 組 pillar bg、icon i color),主要靠 token 自動切換;⑨`@media (max-width: 750px)` 響應式 overrides 簡化(移除 orb 與 18px radius 孤兒規則,新增 `.stat-icon` 縮為 40×40);⑩CSS 規則 brace 從 1147 條減至 1098 條(淨減 ~25 條規則),HTML class token 92 個全對應、CSS 0 個 orphan rule;⑪保留所有 4.32.0 既有 `#goLoginBtn` / `#goRegisterBtn` / `App.openChangelog()` / `#deployCopyBtn` / `/privacy` / `/terms` 綁定,零行為變動 |
| 4.32.0 | 2026-04-28 | 公開首頁(`<section class="public-home">`)視覺改版:①Hero 區塊重寫文案為「你的財務指揮中心,從擁有資料開始」+ 漸層強調 `.h1-accent`(#4f6ef7→#7c3aed→#f97316),加入 `.public-home-trust` 5 顆信任 chip(開源 AGPL/落地加密/無雲端/一行部署/OpenAPI 3.2);②數據條 `.public-home-stats` 從 hero 內抽出獨立 4 欄 grid 卡片,每格新增 `.stat-sub` 副說明文字(Poly1305 AEAD、三段策略、decimal.js);③以 `.public-pillar-grid` 4 張深度卡(`pillar-card--blue | green | purple | amber`,CSS`--pillar-accent` 變數頂端 3px accent bar)取代舊 6 張單句 `.public-feature-card`,每張含 4 條 bullet 列出對應模組具體子功能;④新增`.public-deploy`深色漸層區塊(`#0f172a → #1e293b`),內嵌`.code-window`終端機樣式(traffic-light dots、5 種 token color:`.code-prompt | cmd | flag | arg | img`)展示單行`docker run` 指令,附 `#deployCopyBtn` clipboard 複製按鈕(成功時 `.is-copied`切換 icon 與標籤 1.8s);⑤`.public-stack-badges` 從 2 顆擴充為 10 顆(Node.js ≥ 24、Express 5、SQLite (sql.js)、ChaCha20-Poly1305、decimal.js、JWT + WebAuthn、OpenAPI 3.2.0、Helmet + Rate Limit、Chart.js、Docker multi-arch),含 `fab fa-docker` / `fab fa-node-js` 品牌圖示;⑥CSS 加入 `.public-home-orb--a/b` 兩顆 blur(80px) gradient orb 製造深度,hero 卡 `backdrop-filter: blur(12px)` + 半透明白底;⑦完整 `body.dark-mode .public-home...` 覆蓋(hero 卡 `rgba(21,25,34,.78)`、stat dividers`rgba(255,255,255,.08)`、pillar bg/fg 改用低飽和透明色、deploy 漸層加深至`#060a14→#0f172a`);⑧`@media (prefers-reduced-motion: reduce)` 全面禁用 `.pillar-card` / `.pillar-icon` / `.public-stack-badges .badge` / `.btn-cta` 的 transition / transform;⑨響應式 ≤ 960px 支柱卡 1 欄、deploy 直排、stats 2x2 grid 改用 `border-left/border-top`;⑩`app.js` 新增 `el('deployCopyBtn')`點擊處理(`navigator.clipboard.writeText` + 失敗時改顯「請手動複製」+ 1.8s 後狀態回復);⑪移除舊版 `.public-feature-card` / `.feature-icon-wrap` / `.feature-icon--blue | green | teal | purple | amber | red` / `.public-home-grid` / `.public-footer-label` 共 10 個孤兒 class(HTML 與 CSS 同步清除);保留既有 `#goLoginBtn` / `#goRegisterBtn` / `App.openChangelog()` / `/privacy` / `/terms` 綁定;零新前端依賴 |
| 4.31.1 | 2026-04-27 | 月度報表信件 Android Gmail 版面修正:`renderStatsEmailHtml()` 內成對 label/value `<td>`(儲蓄率列、topCategories 行、daily transactions 行、stockBlock 4 列)原僅 `width="100%"` 在外層 `<table>`,內層 `<td>` 無顯式寬度,Android Gmail 渲染時會把右側 `text-align:right` 儲存格收縮成內容寬度貼齊左側,造成「購物TWD 14,205」「本月儲蓄率0% · 偏低」之類視覺擠在一起;修正為各 `<td>` 顯式 `width="50%"` / `60%/40%` / `65%/35%` / `55%/45%` HTML 屬性 + 外層 `style="table-layout:fixed"`;另每日彙總表(weekly/monthly dailyBreakdown)加入 `<colgroup>` 28%/24%/24%/24% 固定欄寬 + `.ap-bk-cell` class,配合 `<style>` 內 `@media (max-width:600px) .ap-bk-cell { padding:8px 6px; font-size:12px }` 縮小行動版內距字體;日期欄 `white-space:nowrap` 移至內層 `<span>` 允許「日期」與「(星期)」必要時受控分行 |
| 4.31.0 | 2026-04-27 | GitHub Code Scanning(CodeQL)20 筆未修復告警一次性修補:①新增全域 API rate limiter `apiGlobalLimiter`(每 IP 每 15 分鐘 600 次,掛於 `app.use('/api', ...)`),覆蓋 `js/missing-rate-limiting` 規則所列 15 個端點(`/api/auth/google/state`、`/api/changelog`、`POST /api/auth/google`、`/api/auth/passkey/login`、`/api/auth/logout`、`/api/auth/me`、`/api/account/passkey/register`、`/api/admin/login-audit/{logId}` DELETE、`/api/admin/certs` 系列、`/api/database/import`、`/api/admin/backups` GET / DELETE);既有 `authLimiter`(20/15min)對 login/register/google 仍套用,多層獨立計數;②新增 `/api` CSRF Origin/Referer 驗證中介層覆蓋 `js/missing-token-validation`:對 Cookie 登入的非 GET/HEAD/OPTIONS 請求,必須附帶屬於 `ALLOWED_ORIGINS` 的 `Origin` 或 `Referer`,未通過回 403;Bearer Token 流程(瀏覽器無法自動跨站送 `Authorization` header)跳過此檢查;③`isEncryptedDB(buffer)` 與 `/api/database/import` handler 內 `req.body` → `dbBuffer` 嚴格 `Buffer.isBuffer` 型別收斂,覆蓋 `js/type-confusion-through-parameter-tampering` 3 筆 critical 告警;④`normalizeRoutePath(rawPath)` 加入 `MAX_ROUTE_PATH_LENGTH = 2048` 上限提早 return,覆蓋 `js/polynomial-redos` 1 筆告警;⑤本次修復皆為 server.js 內部加固,不變動 OpenAPI 契約、不新增 npm 套件、不影響前端流程;現有 SPA 走同源 Cookie 認證,CSRF 中介層在 `ALLOWED_ORIGINS` 已正確設定的部署環境零影響 |
| 4.30.3 | 2026-04-27 | Modal 關閉時 history.back 重複觸發修正:`closeModal()` 與 `ModalBase.attachHashWatch` 內的 hashChangeHandler 原以 `while (modalStack.length > 0) ModalBase.closeTopmost()` 迴圈逐次呼叫 `history.back()`,但 `modalStack` 僅在 `popstate` 觸發後的 `handlePopstate` 中同步清空;於部分瀏覽器(特別是 mobile)`popstate` 為非同步派發、或 `hashchange` 在 `popstate` 之前同步觸發時,迴圈內 `modalStack` 不會即時清空,導致 `history.back()` 被連續呼叫多次(例:交易表單儲存 → `closeModal('modalTransaction')` 退兩頁),使用者被推回開啟 Modal 之前所在路徑。改為一次性 `history.go(-N)`(N = 從目標 modal id 到 stack 頂端的距離),由 `handlePopstate` 之「全部關閉」/「後退到較底層 Modal」分支處理 stack pop 與 DOM hide,行為對 modalConfirm 疊在其他 Modal 上的單層退出仍正確 |
| 4.30.2 | 2026-04-27 | 月度統計報表信件 hero 區塊渲染修正:`renderStatsEmailHtml()` 的 `<td class="ap-hero">` 原僅以 `background:linear-gradient(135deg,#4f46e5,#7c3aed,#a855f7)` 設背景搭配 `color:#ffffff` 白字,部分 Gmail mobile(特別是中國地區簡中 UI)會剝除 CSS gradient 屬性,使白字落在白底完全不可見、整封信只剩 hero 區塊的 `👋` emoji;改採三重 fallback:①新增 HTML4 `bgcolor="#4f46e5"` 屬性、②`background-color:#4f46e5` standalone 屬性宣告於 shorthand 之前、③`background:#4f46e5 linear-gradient(...)` shorthand 內含 solid 顏色;同步移除無實際用途的 `position:relative`(亦屬常被 email client 剝除的屬性) |
| 4.30.1 | 2026-04-27 | 寄信人顯示名稱統一設定:新增 `EMAIL_SENDER_NAME` 環境變數(三通道共用),由 `formatFromAddress()` helper 統一包裝為 RFC 5322 quoted-string `"<name>" <email>` 格式;若各通道 FROM 變數已含 `<` 字元(亦即既有 `Name <email>` 格式)則尊重 per-provider 設定不覆寫,達成「全域品牌名稱 + 例外可覆蓋」。helper 對名稱中的 `\` / `"` 進行 RFC 5322 雙引號逸出,避免特殊字元破壞郵件標頭 |
| 4.30.0 | 2026-04-27 | 寄信通道改用環境變數 + 新增 Zeabur Email 通道:①移除管理員「SMTP 寄信設定」UI 與 `GET / PUT /api/admin/smtp-settings` 端點;不再執行 `system_settings.smtp_*` 六欄 schema migration(既有 DB 欄位保留不主動破壞,但程式不再讀寫),原本透過管理員 UI 設定的 SMTP 必須改寫到環境變數;②新增三條寄信通道環境變數族 — SMTP(`SMTP_HOST` / `SMTP_PORT` / `SMTP_SECURE` / `SMTP_USER` / `SMTP_PASSWORD` / `SMTP_FROM`)、Zeabur Email / ZSend(`ZEABUR_API_KEY` / `ZEABUR_FROM_EMAIL`,HTTP REST API:`POST https://api.zeabur.com/api/v1/zsend/emails`,Bearer token 認證)、Resend(沿用既有 `RESEND_API_KEY` / `RESEND_FROM_EMAIL`);③新增 `EMAIL_PROVIDER_PRIMARY` / `EMAIL_PROVIDER_FALLBACK` 環境變數(值:`smtp` / `zeabur` / `resend` / 留空),管理員可指定 1 主 1 備、僅 1 主或全停用;primary 執行期失敗時若 fallback 已設定才自動退回,不重試不補寄(沿用 005 FR-021 / Round 1 Q3 語意);④新增 `GET /api/admin/email-providers`(唯讀)回傳目前 primary / fallback 與三通道是否設定;管理員設定頁原 SMTP 卡片改為「寄信通道狀態」唯讀顯示 + 寄測試信按鈕;⑤`POST /api/admin/test-email` 與排程器錯誤訊息更新:未設定通道時提示「請設定 EMAIL_PROVIDER_PRIMARY 環境變數」;⑥`lib/external-apis.json` 新增 Zeabur Email 條目;⑦零新依賴(沿用既有 `nodemailer` + `resend`,Zeabur 走原生 `fetch`) |
| 4.26.0 | 2026-04-26 | 005-stats-reports 落地:①schema migration 新增 `report_schedules` 表(10 欄位 + 2 索引:`idx_report_schedules_user`、`idx_report_schedules_enabled_freq`),不變動既有表;啟動時自動把既有 `system_settings.report_schedule_*` singleton 排程遷移為多筆 row(冪等,僅當新表為空時執行);②新增 5 個端點:`GET / POST /api/admin/report-schedules`、`PUT / DELETE /api/admin/report-schedules/{id}`、`POST /api/admin/report-schedules/{id}/run-now`(FR-016 / Round 2 Q2,皆斜線路徑、kebab-case 多字動詞);③`GET /api/dashboard?yearMonth=YYYY-MM` 補 yearMonth query 參數,KPI / 預算 / 圓餅 / 最近 5 筆交易隨切換器同步重算(FR-001 / Round 1 Q1);④`GET /api/accounts` response 新增 `twdAccumulated` 計算欄位(為 `SUM(transactions.twd_amount, signed by direction)`),外幣帳戶 initial_balance 不納入此累計(FR-004 / Round 4);⑤雙圓餅圖「(其他)」虛擬子分類節點(父分類本身有交易但無子分類細項時呈現於外圈,金額不重複計入內圈,FR-013 / Round 3 Q1);⑥圓餅圖點擊跳轉至預先篩選的交易列表(FR-015a / Round 3 Q2),採同 SPA 內 push state;⑦統計報表頁工具列狀態 Session 內保留(FR-011a / Round 1 Q5);⑧排程引擎重寫為多筆並存(per-schedule lock map 取代既有全域 isRunningSchedule flag),`(user_id, freq)` 不設唯一鍵;⑨寄信通道執行期 fallback:SMTP 失敗時自動退回 Resend,兩通道皆未設定回 503(FR-021 / Round 1 Q3,不重試、不補寄);⑩排程停用→啟用不補寄(FR-024a / Round 2 Q3,`shouldRunSchedule` 比對 `last_run < periodStart`);⑪信件對比 pill 改為「同型前一段」(每日 vs 前日、每週 vs 上週、每月 vs 上月,FR-018 / Round 1 Q4);⑫信件「股票投資」區塊若任一持股 priceAsOf 超過 12h 則顯示「最舊資料時間」小字註記(FR-023);⑬`GET /api/reports`:起始日 > 結束日回 400 拒絕(不靜默交換)、categoryBreakdown 改為 CategoryAggregateNode[]、補 periodStart / periodEnd 預設化(FR-010 / FR-013);⑭信件每週分支週末日期改用紫色 `#a855f7`(採 inline style 相容 Outlook Desktop Word 渲染引擎,FR-019 / Round 4);⑮零新依賴(無新增 npm 套件、無新前端 CDN、無新外部 API) |
| 4.25.0 | 2026-04-25 | 004-budgets-recurring 落地:①schema migration(`budgets.amount` / `recurring.amount` REAL→INTEGER 對齊 002 慣例、`recurring.fx_rate` REAL→TEXT decimal 字串、`recurring` 補 `needs_attention` / `updated_at` 欄位、`transactions` 表新增 `source_recurring_id` / `scheduled_date` 欄位 + partial unique index `idx_tx_source_scheduled` 並發冪等保護 + 普通 index `idx_tx_source` 反查加速、`budgets` 補 `created_at` / `updated_at` + 兩條 partial unique index 對「(user_id, year_month, category_id IS NOT NULL)」與「(user_id, year_month, category_id IS NULL)」分別保唯一;啟動時自動備份 `database.db.bak.<ts>.before-004` / `.before-004-rec`);②新增端點 `PATCH /api/budgets/{id}`(僅編輯金額;FR-008);③預算進度條四段配色(綠/中性/黃/紅,閾值 50% / 80% / 100% 為固定值);④預算月份切換器,可切到任意歷史/未來月份檢視(FR-007、FR-009a),「已用金額」永遠依當前資料庫即時重算(不快取/不凍結);⑤固定收支自動產生流程提煉為共用函式 `processRecurringForUser(userId)`,於三條登入 handler(password / Google SSO / Passkey)內 server-side 觸發(與 003 backfillDefaultsForUser pattern 對稱),30 筆軟上限 + setImmediate 背景續跑保 SC-003 / SC-004;條件式 `last_generated` 推進(only-if-newer)+ `(source_recurring_id, scheduled_date)` 唯一鍵保並發冪等(資料層去重,不依賴應用層鎖);⑥固定收支卡片色階分流:紅/橘「需處理」(原分類/帳戶已刪除自動標記)> 黃「(待執行)」逾期 > 灰停用 > 正常;備註直接顯示於卡片上;⑦編輯固定收支時對已刪除的分類/帳戶以 `__deleted_category__` / `__deleted_account__` 佔位下拉防呆(前後端共驗),改選有效項並儲存後 needs_attention 旗標立即清除;⑧衍生交易在交易列表與編輯頁顯示「來自配方」chip(`COALESCE(NULLIF(r.note,''),'(未命名配方)')` LEFT JOIN),來源配方刪除後 chip 退化為灰字「(來源配方已刪除)」;⑨修正每月 31 號遇 2 月正確回退至月底(取代既有 `Date.setMonth(+1)` overflow),每年 2/29 遇平年回退至 2/28;⑩編輯既有配方分支邏輯:起始日變動則重置 `last_generated`(FR-021a)、週期變動保留(FR-021b)、業務欄位(金額/分類/帳戶/備註/幣別/匯率)變動皆不溯及既往(FR-021c,PUT handler 嚴格不對 transactions 表觸發 UPDATE);⑪固定收支類型欄位(收入/支出)建立後永久不可變更(與 003 對 `categories.type` 對稱治理);⑫OUT-003:本期不做伺服器端 cron/worker 排程,FR-012 僅以登入觸發路徑兌現;⑬零新依賴(無新增 npm 套件、無新前端 CDN、無新外部 API) |
| 4.24.0 | 2026-04-25 | 003-categories 落地:①schema migration 移除 `categories.is_hidden` 欄位(rebuild 模式)+ 新增 `deleted_defaults(user_id, default_key, deleted_at)` 表 + 索引 `idx_cat_user_parent_sort` / `idx_cat_user_type`;啟動時自動備份 `database.db.bak.<ts>.before-003`;②預設樹重新設計(13 父 + 56 子)含支出「其他」與全部 5 個收入父分類,新增「美妝保養」「訂閱服務」「瓦斯費」「牙科」「健檢」等項;③新增端點 `PATCH /api/categories/{id}`(移動子分類至另一父分類)、`POST /api/categories/reorder`(批次重排同層)、`POST /api/categories/restore-defaults`(清空 registry + 補建);④登入時冪等補建(password / Google SSO / Passkey 三條登入路徑)P95 < 200 ms,跳過 deleted_defaults 與既有客製化;⑤分類 type 一經建立永久不可變更(PUT 拒絕 type 變更);⑥leaf-only:交易 `category_id` 必為子分類(後端強制);⑦顏色驗證收緊為嚴格 `^#[0-9A-Fa-f]{6}$`,不接受 `#RGB` 或 `#RRGGBBAA`;⑧分類管理頁雙區塊(支出在上、收入在下)+ HTML5 原生拖曳排序(同層)+ 子分類縮排網格(左側 4px 藍色邊框 + ▸ 箭頭)+ 「補回過去刪除的預設分類」按鈕;⑨完全移除「是否隱藏」屬性;⑩零新依賴(無新增 npm 套件、無新前端 CDN) |
| 4.23.0 | 2026-04-25 | 002-transactions-accounts 落地:①CT-1 schema migration(`accounts` 補 `category` / `overseas_fee_rate` / `updated_at`、`transactions` 補 `to_account_id` / `twd_amount`、`amount` / `initial_balance` REAL→INTEGER 幣別最小單位、`fx_rate` REAL→TEXT decimal、`exchange_rates` 拆 per-user + global 跨使用者 30 分鐘共用快取、新增 `user_settings.pinned_currencies` JSON);②新增 dependency `decimal.js ^10.4.3`(後端 + 前端 CDN 同版本)並抽出同構模組 `lib/moneyDecimal.js` / `lib/taipeiTime.js` / `lib/exchangeRateCache.js`;③9 群端點 — `/api/accounts` GET/POST、`/api/accounts/{id}` GET/PATCH/DELETE、`/api/transactions` GET/POST、`/api/transactions/{id}` GET/PATCH/DELETE、`/api/transactions:batch-update` / `:batch-delete`、`/api/transfers` POST、`/api/exchange-rates/{currency}` GET、`/api/user/settings/pinned-currencies` GET/PUT;④FR-014a 樂觀鎖(PATCH/DELETE 接受 `expected_updated_at` 不符 409)、FR-060 IDOR 防線(`ownsResource(table,idColumn,idValue,userId)` 統一介面 + `requireOwnedAccount` / `requireOwnedTransaction`,非自己資源一律 404 不洩漏)、FR-015 同幣別轉帳 transfer 對 + 跨幣別 422、FR-016 / FR-017 統計過濾 `type IN ('income','expense') AND exclude_from_stats = 0`、FR-021 信用卡海外手續費(千分點)、FR-042 / FR-044 / FR-045 批次操作上限 500 筆 + BEGIN/COMMIT/ROLLBACK + 樂觀鎖、FR-007a 全程 Asia/Taipei 時區、FR-030 / FR-031 / FR-032 BarcodeDetector + 貼上文字 fallback;⑤a11y:批次操作列 `aria-live="polite"` + checkbox `aria-checked="mixed"`;⑥啟動 log `[startup] AssetPilot v4.23.0 / feature 002-transactions-accounts ready` |
| 4.21.1 | 2026-04-24 | 升級 `resend` 6.1.3 → 6.12.2 對齊 npm latest(13 個直接相依全對齊);`emails.send()` 物件回傳 API 未變更,`sendStatsEmail()` 無須調整,`node --check server.js` 通過;`specs/001-user-permissions/research.md` §5 同步標記 ✅,並新增 §5.1 記錄 `resend → svix → uuid<14` 鏈上 GHSA-w5hq-g745-h8pq 3 筆 moderate 漏洞(本專案未以 `buf` 參數呼叫 `uuid`,CVSS 0,不受影響;`fixAvailable` 建議降級為誤判,決策維持 6.12.2) |
| 4.21.0 | 2026-04-24 | SRS 全面改寫為敘述式 SSD(System Specification Document):按模組(使用者與權限、交易與帳戶、分類、預算與固定收支、統計報表、股票投資、匯出匯入、前端路由)分段,每個模組含核心目標敘述與「不做什麼」邊界;舊 IEEE-830 逐條 FR 結構轉為技術附錄保留;新增 Spec-Kit 憲章 `.specify/memory/constitution.md` v1.0.0(Principle I:所有規格與使用者文件必須為繁體中文,NON-NEGOTIABLE) |
| 4.20.5 | 2026-04-21 | 深色模式對比度修正:①`--text-muted` 由 `#6b7280`(surface 上 3.77:1,未達 WCAG AA)提亮為 `#8b94a3`(5.75:1),影響股票卡片標籤、空白狀態文字、表格次要欄位等共 21 處;②側邊欄 `.sidebar-version` 文字 alpha .4 → .6(3.80→7.27:1);③`.sidebar-legal-link` alpha .3 → .6(2.61→7.27:1);④`.sidebar-legal-sep` 分隔點 alpha .2 → .45(1.80→4.52:1) |
| 4.20.4 | 2026-04-21 | 外幣固定收支編輯失敗修正:refreshRecFxUi() 內仍殘留 fmtNum(twd) 呼叫(v4.20.3 僅修了列表 render),外幣記錄開啟編輯 Modal 即 throw「fmtNum is not defined」;改用 Number(twd).toLocaleString('zh-TW') |
| 4.20.3 | 2026-04-21 | 固定收支頁載入失敗修正:v4.20.2 外幣顯示誤用不存在的 fmtNum(),整頁 throw「fmtNum is not defined」;改用 Number(...).toLocaleString('zh-TW') 直接格式化 |
| 4.20.2 | 2026-04-21 | 固定收支列表 UX:①新增詳細明細區塊(起始日、上次產生日、下次產生日,啟用且下次產生日 ≤ 今日時以警示色標記「待執行」),備註一併顯示;②外幣固定收支顯示原幣金額與 TWD 換算;③編輯時若原分類/帳戶已刪除,下拉插入「(原分類已刪除)/(原帳戶已刪除)」佔位選項避免 select.value 靜默清空;④編輯時 type 非 expense/income 時不再 crash,回退為 expense |
| 4.20.1 | 2026-04-21 | 排程寄送資產統計報表修正:先前 shouldRunSchedule() 與 formatTwTime() 直接用 Date#getHours() / #getDate() / #getDay(),伺服器跑在非 Asia/Taipei 時區(例:Zeabur / Docker 預設 UTC)時會以 UTC 判斷觸發時間,導致設定 0 時寄送實際在 UTC 0 點(台灣 08:00)才觸發。新增 twParts()(固定 UTC+8 無 DST)與 twStartOfDayMs(),排程觸發時/週/日比對與 lastRun periodStart 一律以台灣時間為準 |
| 4.20.0 | 2026-04-21 | 股票定期定額強化:①遇 TWSE 休市日或週末自動順延到下一個交易日(排程日仍以原日期推算維持週期節奏,交易紀錄日期為實際交易日,備註附「原排程 YYYY-MM-DD 順延」);②新增 TWSE 休市日快取(/v1/holidaySchedule/holidaySchedule OpenAPI,24h TTL),過濾「開始交易/最後交易」特別交易日;③/api/stock-recurring/process 回傳新增 postponed 欄位;④股票交易股數限整數:前端 input min/step 0.0001 → 1,後端 POST/PUT/CSV import 皆加 Number.isInteger 檢查 |
| 4.19.5 | 2026-04-21 | 固定收支修正:①/api/recurring/process 與 /api/stock-recurring/process 原先以 getNextDate(start_date) 當首次生成日期,導致起始日為今天時跳過當日;現 last_generated 為空時改以 start_date 本身作為首次日期;②編輯固定收支(或交易)時,若分類為帶有子分類的父層,下拉無對應 option 導致 recCategory 被清空、表單無法送出;buildCategoryOptions() 於父分類 optgroup 最前方加入「父分類名稱(全部)」可選項 |
| 4.19.4 | 2026-04-20 | Copilot Review v4.19.3 修正 + NTP 功能簡化:①NTP 同步限制僅支援 IPv4 與 FQDN 網域,IPv6 位址與 AAAA 紀錄一律拒絕;②queryNtp() 改寫為 async function,移除 async Promise executor anti-pattern;③resolveHostToPublicIpv4() 改用 dns.lookup({family:4, all:true}),dgram socket 固定 udp4;④移除 parseIPv6Groups() 與 IPv6 私有網段判斷分支(zone id / 6to4 / IPv4-mapped/compatible 展開邏輯一併簡化) |
| 4.19.3 | 2026-04-20 | Copilot Review v4.19.2 修正:①isPrivateOrReservedIp() 改用 net.isIP() + 自訂 IPv6 展開器:link-local 改判 fe80::/10、ULA fc00::/7、multicast ff00::/8、IPv4-mapped 完整展開形式、IPv4-compatible ::a.b.c.d、6to4 2002::/16 內嵌私有 IPv4 皆擋;②queryNtp() 送出前 dns.lookup({all:true}) 解析 FQDN 逐一檢查,防 DNS rebinding;送 UDP 用解析後字面 IP 避免 TOCTOU;③依解析結果切換 udp4/udp6(原固定 udp4 導致 IPv6 必敗);④IPv4 補判 CGNAT 100.64.0.0/10;NTP API 回應加上 resolvedIp 方便稽核 |
| 4.19.2 | 2026-04-20 | Copilot Review v4.19.1 修正:①runScheduledReportNow() 內 startedAt/finishedAt 改用 serverNow(),與 shouldRunSchedule() 同一時間基準,避免 offset ≠ 0 時每 5 分鐘重複觸發;②NTP host 參數新增嚴格驗證(擋 private/loopback/link-local/ULA/multicast/IPv4-mapped、localhost/.local/.internal、格式 + 長度 253),降低 SSRF 風險;③伺服器時間區塊新增 uptime 欄位;④loadAdminServerTime() 成功時無條件清空狀態訊息 |
| 4.19.1 | 2026-04-20 | 伺服器時間新增 NTP 自動校正:以原生 dgram/UDP 實作 SNTP v3 client(RFC 4330),3 秒逾時 fallback;預設依序嘗試 tw.pool.ntp.org / pool.ntp.org / time.google.com / time.cloudflare.com;支援「查詢(不套用)」預覽;校正時扣除單趟網路延遲提升精準度;新增 POST /api/admin/server-time/ntp-sync API |
| 4.19.0 | 2026-04-20 | 管理員頁面新增「伺服器時間」區塊:顯示伺服器實際時間、時區、目前採用時間(含偏移)、啟動後偏移量;可填目標時間或毫秒偏移量設定 SERVER_TIME_OFFSET,套用於 checkAndRunSchedule() 排程檢查(系統時鐘本身不動);偏移持久化於 system_settings.server_time_offset、上限 ±10 年;新增 GET/PUT /api/admin/server-time API |
| 4.18.4 | 2026-04-18 | Copilot Review v4.18.2 修正:①完成分支 report_schedule_last_run 改寫 startedAt 取代 finishedAt,避免長執行跨過下個 periodStart 時 shouldRunSchedule() 將下一期誤判為已執行而跳過(完成時間改放 summary);②PUT /api/admin/report-schedule 硬編 100 上限改用 REPORT_SCHEDULE_MAX_TARGETS 常數,錯誤訊息亦由常數衍生 |
| 4.18.3 | 2026-04-18 | Copilot Review v4.18.1 修正:信件 sectionTitle 改用外層 `<table>` + `<td padding>` 包裹內層標題 table 取代 table margin;Outlook(Word 引擎)會忽略 table 上的 margin,原本 26px/10px 上下間距在 Outlook Desktop 會消失 |
| 4.18.2 | 2026-04-18 | Copilot Review v4.17.0 修正:①runScheduledReportNow() 統一回傳結構,skipped 一律為數字、新增 status 欄位(already_running/no_targets/no_email_service/completed);②未指定對象/寄信服務未設定的略過分支補上 report_schedule_last_run 更新,避免同一 period 內背景每 5 分鐘重複觸發;③targetIds 統一去重(Set)+ 上限 100 筆;④管理員寄送對象標題列移除巢狀 `<label>`(改 `<div>`);⑤前端執行結果改依 status 判斷顯示 |
| 4.18.1 | 2026-04-18 | Copilot Review 修正:①getReportPeriod('monthly') 改用 `new Date(y, m, 0)` 推算上月最後一天,避免硬編 86400000ms 在 DST/時區轉換偏移;②todayStr() 改呼叫 ymd(new Date()),移除重複格式化邏輯;③信件 sectionTitle 區塊標題改用 table 佈局取代 flex/gap,提升 Outlook Desktop 等郵件用戶端相容性 |
| 4.18.0 | 2026-04-17 | 信件「交易紀錄」區塊改為依排程頻率切換:daily → 昨日交易明細、weekly → 上週 7 天每日收支彙總(Mon-Sun)、monthly → 上月每天收支彙總;週/月彙總多顯示「區間收入/支出/淨額」三欄總覽卡,週末日期紫色標示;信件視覺美化(三色漸層 hero、卡片陰影、節標題色塊、CTA 陰影、品牌標記) |
| 4.17.0 | 2026-04-17 | 合併「寄送資產統計報表」與「排程自動寄送」成單一卡片:管理員勾選使用者 + 頻率 → 排程指定對象自動寄送(或「立即寄送一次」);寄送前自動更新該使用者所有持股最新報價(盤中即時 → STOCK_DAY → TPEx 三段策略),信件「股票投資」區塊改顯示 4 列(成本/市值/未實現損益/報酬率含彩色 ±);system_settings 加入 report_schedule_user_ids 欄位;移除 POST /api/admin/send-stats-report(功能合併) |
| 4.16.2 | 2026-04-17 | 修正排程設定儲存後 reload 會失效的 bug:①admin 表單加上 action="javascript:void(0);" + method="post" 防止 listener race window 期間 submit 觸發預設 GET 導航導致資料丟失;②修正後端 hour=0(午夜)/ weekday=0(週日)被 `\|\|` 當 falsy 重設為 default 的 bug,改用 Number.isFinite + clampInt;③前端 form 改用 dataset.bound flag 確保 listener 只綁一次,儲存成功後立即 GET 一次並 re-render 表單確認 DB 真的持久化 |
| 4.16.1 | 2026-04-17 | 文件補齊 + 信件「近 5 筆交易」幣別顯示修正:先前誤把 transactions.currency 當前綴,導致 USD 標記的交易顯示成 USD(實際 amount 已是 TWD 等值);改為一律 TWD 顯示與 dashboard 一致。README.md「Docker 環境變數」與「環境變數完整清單」補上 RESEND_API_KEY / RESEND_FROM_EMAIL / APP_URL 並註明 SMTP 改走管理員 UI;.env.example 補上 APP_URL;SRS.md 版本歷程補齊 4.14.0 ~ 4.16.1 |
| 4.16.0 | 2026-04-17 | 排程自動寄送統計報表 + 信件大改版:system_settings 加入 report_schedule_freq/hour/weekday/day_of_month/last_run/last_summary 6 欄;背景 setInterval 5 分鐘檢查;GET/PUT /api/admin/report-schedule + POST /run-now;信件版面重新設計(漸層 hero、3 欄 KPI 含上月對比 ▲▼ pill、儲蓄率進度條、分類顏色長條、近 5 筆交易、CTA 按鈕需設 APP_URL);table-based 排版兼容 Outlook |
| 4.15.0 | 2026-04-17 | SMTP 寄信支援 + 失敗原因 UI 顯示:system_settings 加入 smtp_host/port/secure/user/password/from 欄位;GET/PUT /api/admin/smtp-settings(密碼遮蔽 hasPassword:bool,空字串視為保留);POST /api/admin/test-email;統一 sendStatsEmail() 入口(SMTP > Resend > 503);前端新增 SMTP 設定卡片 + 寄測試信按鈕;寄送失敗/略過原因改 UI 直接顯示 |
| 4.14.0 | 2026-04-17 | 管理員寄送個人資產統計報表(Resend):新增 RESEND_API_KEY/RESEND_FROM_EMAIL 環境變數;POST /api/admin/send-stats-report(自動計算各幣別餘額、本月收支、前 5 大支出分類、股票持倉成本,輸出 HTML 信件);前端新增「寄送資產統計報表」卡片(多選使用者一鍵寄送);單次最多 100 位、間隔 600ms 避開 Resend 速率限制 |
| 4.13.1 | 2026-04-17 | 資安強化(續):Google-only 刪帳號改要求 fresh id_token 驗證(audience + sub + exp);express.json 全局 limit 50MB→5MB,CSV 匯入端點 25MB + 20000 rows 上限;body 過大統一回 JSON 413;saveDB() 改非阻塞(in-flight + pending 合併 + tmp/rename 原子寫) |
| 4.13.0 | 2026-04-17 | 資安強化:修補儲存型 XSS(normalizeDate 嚴格驗證 + 前端 escHtml)、IDOR(驗證 accountId/categoryId/stockId 擁有者)、JWT 無法撤銷(新增 token_version)、Passkey origin 白名單、CSV Formula Injection 防護、股票正數驗證、TWSE symbol 格式驗證、統一強密碼規則、.env 權限 0o600、.gitignore/.dockerignore 排除 DB 備份 |
| 4.12.2 | 2026-04-14 | 所有密碼欄位新增顯示/隱藏切換按鈕(登入、註冊、修改密碼、刪除帳號確認、管理員建立使用者、管理員重設密碼) |
| 4.12.1 | 2026-04-13 | 修正 Passkey 登入紀錄顯示為「密碼」而非「Passkey」;補充 Passkey 失敗原因中文翻譯 |
| 4.12.0 | 2026-04-13 | 新增資料庫匯出匯入備份功能(僅管理員):匯出未加密 SQLite 檔案、匯入時自動驗證格式與資料表結構、自動備份現有資料庫 |
| 4.11.0 | 2026-04-13 | 新增海外刷卡手續費計算:外幣 + 信用卡 + 支出時自動計算手續費(預設 1.5%),支援手動調整金額與費率,顯示摘要合計;後端新增 fx_fee 欄位 |
| 4.10.3 | 2026-04-13 | 修正正式環境登入紀錄國家欄位空白:優先使用 Cloudflare CF-IPCountry header,fallback 至 ipinfo.io |
| 4.10.2 | 2026-04-13 | 修正 Passkey 驗證 counter 路徑錯誤;登入頁 Google/Passkey 按鈕移至帳密下方;登入紀錄新增分頁與每頁筆數選擇;移除登入頁版本更新按鈕 |
| 4.10.1 | 2026-04-13 | 修正 Passkey 前端模組從 CDN 載入失敗,改為伺服器本地提供;更新 OpenAPI 規格至 v4.10.0 |
| 4.10.0 | 2026-04-13 | 新增 Passkey (WebAuthn) 無密碼登入:登入頁支援指紋/Face ID/PIN 碼快速登入;帳號設定可管理多組 Passkey(註冊、命名、刪除);後端新增 passkey_credentials 資料表與 6 支 API 端點 |
| 4.9.3 | 2026-04-13 | 密碼強度規則升級:需含大寫、小寫、數字與特殊符號;管理員重設密碼新增新舊密碼相同檢查;註冊表單新增前端即時驗證 |
| 4.9.2 | 2026-04-13 | 修正批次變更分類嵌套 optgroup 導致分類顯示不全;改為自訂下拉選單含色點與分區結構;允許刪除預設分類;修正匯率輸入 step 驗證過嚴;更新 OpenAPI 規格 |
| 4.9.1 | 2026-04-11 | 新增使用者修改密碼功能:帳號設定卡片支援自助修改密碼(驗證目前密碼、防止新舊相同);Google-only 帳號可設定本機密碼;管理員 → 使用者列表新增「重設密碼」按鈕與 Modal,可直接為任一使用者設定新密碼;新增 PUT /api/account/password 與 PUT /api/admin/users/:id/password 端點,密碼強度需 8 字元含英數 |
| 4.9.0 | 2026-04-11 | 移除 Cloudflare-issued Client Certificates(mTLS)功能:完整刪除後端 mtlsMiddleware / /api/admin/certs/mtls*端點 / MTLS_* 環境變數 / SSL_MTLS_*路徑 / HTTPS+mTLS 直連啟動模式、前端 isMtlsError / 警示卡 / renderAdminCerts mTLS UI 綁定、index.html 三個 mTLS admin 區塊、style.css .mtls-notice* 樣式、Dockerfile SSL/mTLS 目錄、.env / README mTLS 設定範例;管理員面板簡化為僅 Origin Certificate;建議搭配 Tailscale 或 Cloudflare Access 取代裝置層級身份驗證 |
| 4.8.x | 2026-04-11 | mTLS 相關錯誤處理、救援路徑、藥丸分段控制器、Cloudflare Managed Transform 標頭相容、登入紀錄分頁 UI 等多輪迭代(4.8.1 ~ 4.8.9) |
| 4.7.x | 2026-04-11 | SSL/TLS 憑證管理介面、mTLS 支援、openapi.yaml(Cloudflare API Shield Schema,88 端點)、多幣別相關修正等(4.7.0 ~ 4.7.9) |
| 4.6.x | 2026-04-07 ~ 10 | 認證 Token 改存 httpOnly Cookie + SameSite=Strict、logout API、JWT_EXPIRES 統一控制、外幣自動匯率、隱私權/服務條款公開頁、幣別下拉改用 CURRENCY_LABELS 完整對照、全頁 RWD 優化(4.6 ~ 4.6.9) |
| 4.5 | 2026-04-05 | 登入紀錄 country 欄位持久化至 DB,重啟後不重查;登入時 fire-and-forget 回寫 |
| 4.4 | 2026-04-05 | 全球匯率 API 改為 server-level 快取(TTL 5分鐘)+ in-flight deduplication,跨使用者共用 |
| 4.3 | 2026-04-05 | 新增交易「不計入統計」功能,可標記單筆不影響報表、儀表板、預算計算 |
| 4.2 | 2026-04-02 | 首頁視覺翻新(Plus Jakarta Sans 字型、英雄數據列、彩色圖示、橙色 CTA) |
| 4.1 | 2026-04-02 | 信用卡銀行分組與一鍵還款 |
| 4.0.x | 2026-03-25 ~ 31 | 全站 UI 現代化重設計:Inter 字體、新色彩系統、柔和陰影、毛玻璃、spring 動畫、iOS 風格分段控件、focus-visible、prefers-reduced-motion;多幣別支援擴充至 150 種、帳戶類別欄位(銀行/信用卡/現金/虛擬錢包)、排除計入總資產開關、儀表板支出分類改父分類 + 前 5 名、統計報表雙圓餅預設(4.0 ~ 4.0.10) |
| 3.6x ~ 3.4x | 2026-03-25 ~ 23 | 信用卡繳費操作指引、匯率更新冷卻期、股票定期定額、股利日期欄位統一為除息日期、全球匯率更新移除冷卻期、全站配色系統一致化、圓餅圖配色與圖例順序統一、儀表板與統計報表雙圓餅圖、全站使用者登入紀錄手動同步、IP 國家查詢、修改顯示名稱、未來交易等 |
| 3.3x ~ 3.1x | 2026-03-21 ~ 22 | 分類管理/固定收支移至收支管理、匯率設定移至收支管理、登入稽核紀錄支援刪除與批次刪除、快速新增按鈕依頁面情境顯示、全站登入紀錄納入失敗事件、Google 登入轉跳卡住修復、OAuth state、XSS/防護策略強化、公開首頁與登入路徑調整、管理員模式與註冊控管、API 使用與授權頁、全球即時匯率自動更新、備份下載上傳與自訂排程、主題切換同步、多幣別支援與匯率換算、電子發票 QRCode 自動輸入、深色模式、儀表板資產配置圓餅圖、股票交易計算設定、Google SSO GIS Redirect、Docker 一鍵更新、版本資訊一鍵更新、收支管理子選單整合、版本資訊手動重新檢查、全面依賴升級、遠端版本更新檢查、股票紀錄子頁面路由、Google SSO 授權碼流程、股票紀錄排序、TWSE 股利同步、上櫃股票報價、Docker 密鑰持久化、股票管理優化 |
| 3.0x | 2026-03-19 ~ 20 | 股票投資管理模組上線;TWSE 除權息自動同步、實現損益紀錄、股票 CSV 匯出匯入與類型管理、股票自動建立與搜尋篩選、Google SSO 登入與批次操作、安全性強化、環境變數與資料庫加密、TWSE 除權息自動同步、刪除帳號介面美化、帳號管理與版本資訊優化 |
| 2.0 | 2026-03-19 | 完整記帳功能 |
| 1.0 | 2026-03-18 | 初版發布 |
### 4.3 未來擴充方向
- 共享帳本(家庭/情侶記帳)
- PWA 離線記帳功能
- 多語系支援
- Excel (.xlsx) 匯出格式
- 股票歷史股價圖表
- 自動預算建議與超支預警(依過去消費趨勢產生建議)
- 銀行/券商對帳匯入(CSV/OFX)與對帳差異提示
- 自訂儀表板小工具(可拖曳排序、顯示/隱藏卡片)
- 目標儲蓄與還款計畫(目標追蹤與進度提醒)
- 推播通知(帳單到期、預算超標、股利發放提醒)
- 權限分級共享(可編輯/唯讀)與操作稽核紀錄
- API Token 與 Webhook(第三方自動化整合)
- 交易附件上傳與收據 OCR 自動帶入已完成:辨識結果僅作草稿預填,由使用者確認後儲存;未設定 OCR 供應商或辨識失敗時可手動輸入(#250)。
- 測試與品質提升:E2E 測試、視覺回歸測試、無障礙檢測
---
## v4.33.0 — 009 Multi-Timezone Support(2026-04-29)
### 規範升級(FR-007a 修訂)
- **FR-007a 重新定義**:原本「所有『今天/未來』判定固定以 UTC+8 計算」改為「以該操作所針對使用者的 `users.timezone`(IANA 識別碼,預設 `Asia/Taipei`)計算」。
- **新增治理原則(憲章 v1.3.0 Principle IV — Time & Timezone Discipline,NON-NEGOTIABLE)**:後端 timestamp 一律以 Unix ms 或 ISO 8601 UTC `Z` 字串儲存/傳輸;API 出口統一 `YYYY-MM-DDTHH:mm:ss.sssZ` 毫秒精度;「使用者當地某日/某月/某時刻」一律以 `users.timezone` 計算。例外:市場/法規時間(如 TWSE 09:00–13:30 Asia/Taipei)需源碼註解標明。
- **既有 Asia/Taipei 使用者不受影響**:所有 API 回應與 UI 行為相對升級前 100% 一致(regression-free)。
### 資料模型變更
- `users` 新增 `timezone TEXT NOT NULL DEFAULT 'Asia/Taipei'`(IANA 識別碼)。既有列由 DEFAULT 自動填值;不需資料遷移。
- 新增 `monthly_report_send_log` 表:`id`、`user_id`、`year_month`、`schedule_id`、`sent_at_utc`、`send_status`(success / failed)、`error_message`,以 `UNIQUE(user_id, year_month)` 防重寄;scheduler 先 INSERT、UNIQUE 衝突即跳過。
- `transactions.date` 欄位形式不變(`YYYY-MM-DD`),語意改為「該交易所有人於其偏好時區的當地自然日」;歷史列被視為以 Asia/Taipei 寫入,不重算。
### 後端工具
- 新增 `lib/userTime.js`:`isValidIanaTimezone`、`todayInUserTz`、`monthInUserTz`、`isFutureDateForTz`、`partsInTz`、`toIsoUtc`、`isValidIsoDate`、`__nowMs / __setNowMs`(測試 hook)。
- `lib/taipeiTime.js` 改為 thin wrapper(內部呼叫 `userTime.*` 並固定傳 `'Asia/Taipei'`),維持向後相容;既有 callsite 不需改。
- `authMiddleware` 擴展為同 `SELECT` 取 `timezone`,掛 `req.userTimezone`(零額外查詢)。
### 新 API 端點
| Method | Path | 說明 |
| --- | --- | --- |
| GET | `/api/users/me` | 取當前使用者完整資料(含 `timezone`、`theme_mode`、`created_at` 為 `.sssZ`) |
| PATCH | `/api/users/me/timezone` | 更新偏好時區;非 IANA 回 400;no-op(同值)不寫 audit;成功變更寫 `data_operation_audit_log`(`action='user.timezone.update'`、metadata 含 `from`/`to`/`source ∈ {auto-detect, manual}`) |
### 月度報表郵件 per-user 觸發(FR-006 / FR-018)
- `checkAndRunSchedule()` JOIN `users` 取 `timezone` 帶入;`shouldRunSchedule(scheduleRow, userTimezone, nowTs)` 改以 `userTime.partsInTz(tz, nowTs)` 計算 local hour / day / weekday。
- 環境變數 `SCHEDULER_TICK_MS` 可注入測試心跳間隔(預設 5 分鐘)。
- 寄送失敗保留 `monthly_report_send_log.send_status='failed'` + `error_message`,scheduler 不自動重試(避免風暴)。
### 前端
- `app.js` 新增工具:`getUserTz()`、`getBrowserTz()`、`todayInUserTz()`、`formatLocalDateTime(iso, opts?)`、`listAvailableTimezones()`,保留 `todayInTaipei()` 為 alias。
- 登入後 `enterApp()` 呼叫 `GET /api/users/me`,合併 `currentUser.timezone`。
- 既有使用者 `maybePromptTimezoneChange()` FR-010 (b) 三條件 AND(`timezone === Asia/Taipei` ∧ 瀏覽器 tz ≠ Asia/Taipei ∧ `localStorage.tzPromptDismissedUntil` 不在 7 天靜默期)→ 一次性 confirm 對話框。
- 個人設定頁新增「時區」card:搜尋下拉(`Intl.supportedValuesOf('timeZone')` ≈ 418 區域 + UTC/Etc/UTC 別名 + 10 項 fallback 白名單)、即時預覽(每秒更新)、儲存(`source='manual'`)。
### TWSE 例外(FR-014)
`getTaiwanTime()` / `isTwseTrading()` 永久鎖 Asia/Taipei;源碼加 `// FR-014: TWSE 市場時間鎖 Asia/Taipei,與 users.timezone 無關` 註解。市場開盤判斷不隨使用者偏好變動。
### 測試
新增 5 支自動化測試(**107 / 107 pass**):
| 檔案 | 內容 | pass |
| --- | --- | --- |
| `tests/lib/userTime.test.js` | 7 個函式單元測試(含 5 組 DST 邊界) | 50 |
| `tests/migration/migration-009.test.js` | DDL + UNIQUE 約束 | 11 |
| `tests/integration/us1-natural-day.test.js` | PST 23:30 / 00:30 / 跨日 / regression-free | 15 |
| `tests/integration/us2-users-me-timezone.test.js` | PATCH IANA 驗證 / no-op / audit / source 白名單 | 18 |
| `tests/integration/us3-monthly-report.test.js` | PST 觸發 / UNIQUE 防重寄 / DST 秋重 / 失敗保留 | 13 |
`npm run test:tz` 一鍵跑全部。
### Breaking Change 影響面
- 既有 Asia/Taipei 使用者:**零行為變化**(regression-free)。
- 非 Asia/Taipei 使用者:原本看到的「今天/本月」邊界錯亂問題已修復;月度報表郵件改在當地 1 號 00:00 寄送。
- 新 API 與既有 `/api/auth/me` 並存;前端 `currentUser` 同時讀兩個(前者取 timezone、後者取 displayName 等 camelCase 欄位)。
- OpenAPI `info.version` 升至 `4.33.0`;`openapi: 3.2.0` 字串保持不變(憲章 II)。
### 後續迭代範圍(不在本 PR)
- 既有 `*_at` 欄位全面正規化為 `.sssZ`(型別不一致需逐欄分析,risk vs SC-001 trade-off)。
- 管理員手動清除 `monthly_report_send_log.send_status='failed'` 列以重觸發寄送的 UI / API。
- 前端散落的 `new Date(x).toLocaleString()` 全面替換為 `formatLocalDateTime(x)`(現提供工具未強制套用,視覺顯示在不同時區帳號下會跟隨瀏覽器系統 tz;資料正確性不受影響)。
## v4.29.0 — 008 Frontend Routing & Pages(2026-04-27)
### 路由架構(FR-001 ~ FR-005、FR-008、FR-014)
- 前端採 URL-first SPA 路由,`app.js` 內以模組級常數 `ROUTES` 維護 20 條路徑(4 公開 + 16 受保護,含 `/stocks` 與 `/stocks/portfolio` 雙別名)。
- 純函式 `parsePath` / `buildPath` / `normalizePath` / `validateNextParam` 為單一資料來源;不引入 router 套件、不引入 path-to-regexp。
- 路徑正規化:小寫 + 折疊連續斜線 + 去尾端斜線(除根 `/`);不一致時以 `replaceState` 改寫;超出能力(含 `..` / `%2e%2e`)走 FR-027 path traversal 攔截。
- 不存在路徑或非管理員命中管理員路徑 → 前端渲染 `#page-404` 訊息頁(URL 保留原樣不被改寫)。
### 後端 admin-only 路徑常數(FR-032a)
- `server.js` 維護模組級常數 `const ADMIN_ONLY_PATHS = ['/settings/admin'];`,與前端 `ROUTES` 表 `requireAdmin: true` 條目對應。
- 同步要求:新增管理員專屬路徑時 MUST 同時更新前端 ROUTES 表與後端 `ADMIN_ONLY_PATHS`,由 PR code review 把關。
### 路由稽核模式(FR-033)
`system_settings.route_audit_mode` 欄位(TEXT、預設 `'security'`)控制 `data_operation_audit_log.action` 之路由相關事件寫入範圍:
| 模式 | route_admin_path_blocked | route_open_redirect_blocked | static_path_traversal_blocked | session_expired (401) | 既有 007 action |
| --- | --- | --- | --- | --- | --- |
| `security`(預設) | ✅ | ✅ | ✅ | ❌ | ✅ |
| `extended` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `minimal` | ❌ | ❌ | ❌ | ❌ | ✅ |
切換立即生效,不需重啟(catch-all 與 authMiddleware 即時查詢;單行 SQL)。
### 路由稽核 action 列舉值(FR-032)
`data_operation_audit_log.action` 擴充列舉值:
- `route_admin_path_blocked`:catch-all 偵測非管理員命中 `ADMIN_ONLY_PATHS`;metadata `{ path, normalizedPath }`。
- `route_open_redirect_blocked`:catch-all 偵測 `/login?next=...` 之 next 不通過白名單;metadata `{ next, reason }`。
- `static_path_traversal_blocked`:catch-all 偵測 `..` / `%2e%2e` / `%252e%252e`;metadata `{ rawUrl, pattern }`。
- `session_expired`:`authMiddleware` 對受保護 API 回 401(僅 `extended` 模式寫入);metadata `{ path, reason ∈ {'token-missing', 'token-invalid', 'token-expired', 'token-version-mismatch'} }`。
### 靜態檔白名單擴充(FR-026 / FR-028)
`PUBLIC_FILES` 擴充至 9 條:`/app.js`、`/style.css`、`/logo.svg`、`/favicon.svg`、`/vendor/webauthn.min.js`、`/lib/moneyDecimal.js`、`/changelog.json`、`/privacy.html`、`/terms.html`。
Cache-Control 規則:
- `/changelog.json`、`/index.html`:`no-cache`
- 其他白名單條目:`public, max-age=300`
### Modal 共用基底元件(FR-022 ~ FR-024b)
`ModalBase`(`app.js` 內 IIFE 物件)統一接管 12 個 Modal 之 lifecycle:history 整合(pushState `#modal-<id>` + popstate 判別)、捲動鎖(含 iOS Safari 防穿透)、堆疊規則(僅 `modalConfirm` 可疊在其他 Modal 上)、焦點 trap、ESC 關閉、初始焦點。
### 外觀模式跨裝置同步快取(FR-019 ~ FR-021a)
新增前端 localStorage key `theme_pref`(值域 `system` / `light` / `dark`);三層 fallback:
1. localStorage `theme_pref`(樂觀渲染)
2. API response `themeMode`(覆寫 localStorage)
3. `prefers-color-scheme`(首次或非合法值)
登出清除:`localStorage.removeItem('theme_pref')`(FR-007b)。