# FR-121 全庫無時區時間欄位改 timestamptz（資料庫統一存 UTC）

> 決策者 2026-09-27 裁：「資料庫應統一存 UTC」，並裁「這次就修」——進 b4。
> 來源：FR-114 第 9 棒 b3 總測回報 #16（CM-2231）、CM-2234 拆出、CM-2245 過渡不再需要。

## 問題

- DEV 實查：**63 張表、140 個欄位**是 `timestamp without time zone`（public 31 表 65 欄／compliance 20 表 51 欄／survey 10 表 21 欄／oscal 2 表 3 欄）。這種欄位存的數字沒有時區概念，寫入端用什麼時間就存什麼，讀取端只能「假設」它是某時區。
- 現況兩種寫法並存：絕大多數表以本地時間（Asia/Taipei）寫入；少數欄位以 UTC 寫入（實查：`remote_agents.last_seen_at`、`review_marks.reviewed_at`；jedi-iam 改密碼兩張待 .1 真機核。原判 flow-engine 三張為 UTC 是 DEV 舊開發庫特例，安裝版實為本地寫入）。
- 共用讀取 `jedi_common/session/database/db_mw.py:to_local()` 對 naive 一律當 UTC 轉 Asia/Taipei → 本地時間寫入的表畫面多 8 小時（b3 總測 #16）。
- 任何「當成某時區」的修法都綁死伺服器時區，客戶裝在別的時區就壞。

## 終局

所有時間欄位改 `timestamp with time zone`（timestamptz）。PostgreSQL 內部一律以 UTC 儲存，讀出依連線時區自動轉；Python 端一律用 aware datetime。之後不再有「naive 當什麼時區」的邏輯，`db_mw.to_local()` 的 naive 分支可刪。

## 做法（分四張子卡，按 schema 拆，每張獨立 migration 只加不破）

1. **public**（31 表 65 欄）— jedi-iam／jedi-system-core／jedi-bulletin／jedi-issue／jedi-log／jedi-file-upload／jedi-remote-agent 等套件表＋主線表。
2. **compliance**（20 表 51 欄）— jedi-flow-engine／jedi-task-platform／jedi-compliance-audit／主線 flow_control。
3. **survey＋oscal**（12 表 24 欄）— jedi-survey／jedi-oscal-v2。
4. **收尾** — `db_mw.to_local()` 刪 naive 分支；grep 全 monorepo `datetime.utcnow`／naive `datetime.now()` 寫入改 `datetime.now(timezone.utc)`；ORM `DateTime` 改 `DateTime(timezone=True)`；CM-2245 的過渡邏輯拆掉；守衛測試「全庫不得再有 timestamp without time zone」。

每張子卡固定步驟：
- 用 `information_schema` 列出該 schema 的欄位，逐欄判斷寫入端是本地還是 UTC（grep model default／server_default／寫入程式），列表進卡。
- migration：`ALTER TABLE t ALTER COLUMN c TYPE timestamptz USING c AT TIME ZONE '<Asia/Taipei 或 UTC>'`，依上一步逐欄選。**只套 DEV**。表屬套件的 migration 放該套件 `migrations/`，主線表放 `scripts/sql/`（先看既有放法）。
- 有 view／SQL 字串／`date_trunc`／`::date` 比較引用這些欄位的，`git grep` 欄位名確認型別改動後語意不變（view 與 SQL 字串是守衛盲區）。
- 驗證：套完再跑盤點 SQL 該 schema 歸零；抽三筆舊資料轉換前後讀出時間相同（本地 10:35 的那筆仍 10:35）；受影響套件既有測試帶 PYTHONPATH 全綠；**把 DB 時區改成 UTC 再讀一次仍正確**（這是終局要驗的點）。

## 驗收（決策者 190 手測）

- 升級後既有資料時間不變（角色／公告／流程執行的建立時間與升級前一致）。
- 190 容器 `TZ` 改成 `UTC` 重啟後畫面時間仍正確（驗不綁時區）。
- 忘記密碼 5 分鐘限制、24 小時內不能再改、密碼過期天數三條期限判斷正常。

## 出貨影響

- 四張都是主線／套件 active migration → **出貨基線要重產**（`scripts/init/02-schema.sql`）。
- 升級路徑：migration 只加不破，`--upgrade` 時自動套；舊資料依表逐欄正確轉換。
- 版號：jedi-common bump、各動到 models 的套件 bump。

## 沿革

見 `FR-114-LOG.md` 第 9 棒與 CM-2231 #16／CM-2234／CM-2245。

## 顯示時區原則（決策者 2026-09-27 補裁）

- **資料庫存 UTC；UI 顯示依使用者所在地轉換。** 這版先固定 Asia/Taipei，之後做個人 profile 時區設定。
- `db_mw.to_local()` 不刪，改成「aware UTC → 顯示時區」；顯示時區單一來源（設定／`TZ`，預設 Asia/Taipei），留 `get_display_tz(user_context)` 介面給 profile 接。只刪 naive 分支。
- API 回傳時間一律帶時區偏移（ISO 8601），前端不自行猜；前端把時間當本地字串解析的地方要一併查。
- CM-2245 作廢，不進 b4；b4 的時間問題全由本 FR 解。
- 子卡 .5（前端顯示收斂）：與 .4 同派；**業界做法是前端依使用者時區轉、後端回 UTC**，這版先寫死台北、前端不轉，只把顯示點收成一支函式留 profile 接口。
