# Bug 排查參考文件 — Template-Edit + 新建專案（2026-05-21）

> 涵蓋本日兩個 bugfix session 所有處理的 14 個問題。
> 供日後遇到同類問題快速定位用。

---

## BUG 1：參與人員 Picker 選帳號沒反應

**現象**：開啟「新增參與人員」Dialog，下拉選帳號後，姓名欄沒帶入，form 無任何變化。

**根因**：  
PrimeVue 3.53 的 Dropdown 有一個已知 quirk：**當 `options` 為空陣列時，`@update:modelValue` 不會觸發**。  
`pickerOptions` 在 Dialog 開啟後才開始 lazy load（`ensurePicker` 呼叫在 `startAdd` 內），所以 Dialog 剛開時 options = `[]`，此時 user 點選下拉雖然能看到選項（上次預載的 stale data），但不觸發 event。

**修正**：在 `onMounted` 就預載 picker options，確保 Dialog 開啟前 options 已有資料。

**教訓**：PrimeVue Dropdown 空 options 不觸發 event — 任何依賴 Dropdown event 的流程都要確保 options 在互動前已載入。

---

## BUG 2：Email 沒有自動帶入

**現象**：選了鉤稽帳號後，Email 欄仍為空。

**根因**：  
`GET /users/menu` 的 `UserMenuResponse` schema 沒有 `email` 欄位，API response 沒回傳 email，FE 拿到 `undefined`。

**修正**：  
BE `api/auth/serializers/user.py`：
```python
class UserMenuResponse(Schema):
    id = fields.Integer()         # ← 加
    uid = fields.String()
    login_name = fields.String()
    nickname = fields.String()
    email = fields.String(allow_none=True)  # ← 加
```

---

## BUG 3：單位 Dropdown 顯示 `#99` 而非名稱

**現象**：參與人員 Dialog 的「鉤稽單位」Dropdown 選項顯示 `#99`（index fallback）而非組織單位名稱。

**根因**：兩個疊加問題：
1. `OrgUnitMenuRoute` 的 `@marshal_with(OrgUnitResponse(only=["uid","name"]))` 排除了 `id` 欄位，FE 取 `option.value = u.id` 拿到 `undefined`
2. Schema 加了 `apply=False` 但沒有顯式呼叫 `.dump()`，導致序列化根本沒執行

**修正**：  
`api/auth/routes/org_unit_route.py`：
```python
@marshal_with(OrgUnitResponse(only=["id", "uid", "name"], many=True), apply=False)
def get(self, ...):
    res = org_unit_service.get_org_units_menu()
    return return_response(True, OrgUnitResponse(only=["id", "uid", "name"], many=True).dump(res))
```

**教訓**：`apply=False` 的 marshal_with 只當文件用，一定要手動 `.dump()`；`only` 列表要確認 FE 需要的所有欄位都在。

---

## BUG 4：新增參與人員 500 錯誤（`AttributeError: uid`）

**現象**：點「儲存」新增參與人員，BE 回 500，log 顯示 `AttributeError: 'ResponsiblePartyEntity' object has no attribute 'uid'`。

**根因**：  
`ResponsiblePartyEntity` 是 link record（`party_uuid + role_id + context_type + context_id` 即為 natural key），**沒有 `uid` 欄位**。  
`_link_to_module_frame` 原本呼叫 `self._resp_party.update(entity)`，`BaseRepoImpl.update()` 內部走 `if entity.uid: ...` 引發 `AttributeError`。

**修正**：改為 idempotent return pattern：
```python
existing = self._resp_party.get_all(ResponsiblePartyQueryEntity(...)) or []
if existing:
    return  # 已存在，skip
self._resp_party.add(ResponsiblePartyEntity(...))
```

**教訓**：CLAUDE.md 有標示「ResponsiblePartyEntity 是 link record，沒 uid 欄位，不能用 update()」。看到 `ResponsiblePartyEntity` 就要用 idempotent return，不要嘗試 update。

---

## BUG 5：資訊系統 Picker 值存 UUID 進整數 FK 欄位

**現象**：新增資訊系統時，picker 選了選項，儲存後 BE 報 400 或 `IntegrityError`（type mismatch）。

**根因**：  
`InformationSystemMenuResponseSchema` 的 `id` 欄位傳 UUID（為了 `SystemPicker` 元件向後相容），但 `system_characteristic_id` DB 欄位是整數型態。FE picker 設 `optionValue="id"` 拿到 UUID → 送給 BE 的是字串 UUID，不是 integer。

**修正**：Schema 加 `pk` 欄位：
```python
class InformationSystemMenuResponseSchema(Schema):
    id = fields.Method("get_id")   # UUID — 給 SystemPicker 用
    pk = fields.Integer(attribute="id")  # integer PK — 給 system_characteristic_id 用
```
FE picker 改 `optionValue="pk"`。

**教訓**：同一個 menu API 被兩個不同 consumer 用，需要回傳兩個 id（UUID 給 UI 辨識，integer PK 給 FK 欄位）。加 `pk` 欄位是標準解法，不要改掉 `id`（會破壞 SystemPicker）。

---

## BUG 6 / 7：Picker 無 loading 狀態 + 顯示已鉤稽選項

**現象 6**：Picker Dropdown 拉取選項期間 placeholder 沒有提示，用戶以為無資料。  
**現象 7**：Dropdown 顯示已被其他列鉤稽的選項，選後造成重複鉤稽。

**修正**：
- 各 panel 加 `*Loading` ref，Dropdown 加 `:placeholder="loading ? '選項載入中...' : '請選擇'"` 和 `:disabled="loading"`
- 加 `available*Options` computed：過濾已使用的 id，編輯時保留自身的 id：
```javascript
const availablePickerOptions = computed(() => {
    const usedIds = new Set(parties.value.map(p => p.party_uid).filter(Boolean))
    const currentId = editing.value?.party_uid
    return pickerOptions.value.filter(o => !usedIds.has(o.value) || o.value === currentId)
})
```

---

## BUG 8：設備 Panel Picker 空列表（`page_size` snake_case）

**現象**：template-edit 設備 Tab 點「新增設備」，picker 下拉清單為空。BE log：`{'pager': {'pageSize': ['Unknown field.']}}`。

**根因**：FE 送 `pager: { page: 1, pageSize: 1000 }` 但 BE schema 用 `page_size`（snake_case）。

**修正**：`pageSize` → `page_size`。

**教訓**：BE pager schema 一律 snake_case。FE 發 API 時要對照 BE schema 名稱。

---

## BUG 9 / 10：新建專案設備/資訊系統不帶入 + 參與人員角色全是 viewer

**現象 9**：新建專案選資源庫後，受評範圍「資訊系統」「設備」沒有自動帶入。  
**現象 10**：帶入的參與人員角色全是 `viewer`，不管資源庫設定的角色為何。

**根因 9**：
- `ssp-resources` API response 只回傳 `matched_device_name` / `matched_info_system_name`（顯示用），沒有 `matched_device_uid` / `matched_info_system_uid`（選取用）
- `ProjectCreateView` 沒有呼叫 `ssp-resources` API

**根因 10**：`loadModuleFrameParties` 硬寫 `role: 'viewer'`，沒有讀資源庫的 role。

**修正**：
- BE 加 `matched_device_uid` / `matched_info_system_uid` 欄位（batch 查詢 UUID）
- FE 加 `loadModuleFrameSspResources()` 函式，呼叫 API 後把 `matched_*_uid` merge 進 `selectedSystemIds` / `selectedDevices`
- 修正 `role: p.role || 'viewer'`

---

## BUG 11 / 12：設備/資訊系統/單位鉤稽名稱顯示「未鉤稽」或 `#id`

**現象 11**：template-edit 設備/資訊系統 Tab 鉤稽欄一律顯示「未鉤稽」（Tag severity=warning），即使 Excel 匯入時已設定 `device_id` / `system_characteristic_id`。  
**現象 12**：template-edit 單位 Tab 鉤稽欄顯示 `#id`（數字），不顯示組織名稱。

**根因 11**：  
`_batch_device_names` 呼叫 `self._device.get_by_id(did)` — 但方法名應是 `get_device_by_id`（AttributeError，被 except 靜默吞）。  
`_batch_info_system_names` 同理，`InformationSystemDomainService` 根本沒 `get_by_id`。

**修正 11**：
```python
# Before（錯）：
for did in device_ids:
    d = self._device.get_by_id(did)  # AttributeError!

# After（正）：
devices = self._device.get_devices_by_ids(unique_ids)  # 1 次 batch query
```

**根因 12**：  
`module_frame_party_service._enrich_user_org_names` 用 `OrgUnitQueryEntity(_in_id=list(org_ids))` — 但 `OrgUnitQueryEntity.__init__` 沒有 `_in_id` 參數，TypeError 被 `except Exception: pass` 靜默吃掉。

**修正 12**：改用 `self._org_unit.get_org_units_by_id_list(list(org_ids))`。

**教訓**：Domain service method 名稱要先 grep 確認（不要靠直覺猜），batch 查詢要用正確的 `get_*_by_ids` 方法。`except Exception: pass` 是 dangerous anti-pattern，只能在確定有 fallback 時用。

---

## BUG 13：新建專案 AO 實施狀態/現況說明不帶入

**現象**：新建專案選有 AO 資料的資源庫，AO 的「實施狀態」和「現況說明」在新專案中一律為空。

**根因**：  
`module_frame_template_copy_service.py` Step 2 的 AO UPDATE SQL：
```sql
JOIN oscal.catalog_control_assessments ccai
  ON ccai.uid::text = mfod.statement_identifier
```
此 JOIN 只能匹配 UUID 格式。但 SSP Excel import 和 docx import 透過 `module_frame_write_strategy` 寫入 AO defaults 時，`statement_identifier` 使用 `(a)/(b)` letter key（從 `ccai.description` 前綴 `[a]`/`[b]` 推導）。UUID JOIN 零命中 → `objectives_updated = 0`。

**修正**：改用 CTE `task_map` 同時支援兩種格式：
```sql
WITH task_map AS (
    SELECT apt.*, ccai.uid::text AS ccai_uid, apc.control_id AS ctrl_id,
        CASE WHEN apt.title ~ '^\s*\[[a-zA-Z]\]'
             THEN '(' || lower(substring(apt.title FROM '^\s*\[([a-zA-Z])\]')) || ')'
             ELSE NULL END AS derived_letter_key
    FROM oscal.assessment_plan_tasks apt
    JOIN oscal.catalog_control_assessments ccai ON ccai.id = apt.catalog_control_assessment_id
    JOIN oscal.assessment_task_controls atc ON atc.task_id = apt.id
    JOIN oscal.assessment_plan_controls apc ON apc.id = atc.control_id
    WHERE apt.assessment_plan_id = :ap_id
)
-- JOIN with OR: UUID format OR letter key format
JOIN task_map tm ON (
    tm.ctrl_id = mfod.control_identifier AND (
        tm.ccai_uid = mfod.statement_identifier
        OR (tm.derived_letter_key IS NOT NULL AND tm.derived_letter_key = mfod.statement_identifier)
    )
)
```

**教訓**：`module_frame_control_objective_defaults.statement_identifier` 有兩種格式（UUID from UI、letter key from import），任何 SQL 或查詢邏輯都需要雙格式支援。

---

## BUG 14：FE 手動存 AO 報驗證錯誤

**現象**：在 template-edit 控制項 Tab 編輯 AO 實施狀態後點儲存，BE 回 400 `MODULE_FRAME_OBJECTIVE_NOT_VALID`。

**根因**：  
FE `ModuleFrameTemplateEditView` 用 `aoLetterKey(a)` 把 AO statement_identifier 轉成 `(a)/(b)` letter key，儲存時送給 BE。BE `_validate_objective_in_profile` 只驗 `valid_assessment_uids`（UUID 集合），`(a)` 不在集合 → 拋 BadRequestError。

**修正**：validation 也接受從 `ccai.description` 前綴推導的 letter key：
```python
valid_letter_keys = set()
for a in assessments:
    m = re.compile(r'^\s*\[([a-z])\]', re.I).match(getattr(a, 'description', '') or '')
    if m:
        valid_letter_keys.add(f'({m.group(1).lower()})')

if statement_identifier not in valid_assessment_uids and statement_identifier not in valid_letter_keys:
    raise BadRequestError(ModuleFrameErrorCode.MODULE_FRAME_OBJECTIVE_NOT_VALID)
```

---

## 共通 Pattern 與教訓

| Pattern | 說明 |
|---------|------|
| **PrimeVue Dropdown 空 options 不觸發 event** | 所有依賴 Dropdown event 的流程，options 必須在互動前預載 |
| **`apply=False` marshal_with 需手動 `.dump()`** | Schema 裝飾器 apply=False 僅供文件，返回前要顯式 dump |
| **ResponsiblePartyEntity = link record，無 uid** | 不能 update()，只能 idempotent add（查存在 → skip） |
| **menu API 雙 id 格式** | UUID（UI 辨識）+ integer（FK 欄位）可分 `id` / `pk` 回傳 |
| **statement_identifier 雙格式** | UUID（UI save）or letter key（Excel/docx import），任何 JOIN/查詢要雙格式支援 |
| **Domain service method 名稱要 grep 確認** | 不要猜 `get_by_id`，先 grep 找到正確方法名 |
| **`except Exception: pass` 掩蓋 bug** | 靜默 catch 導致 silent failure，bug 表現為「沒資料」而非錯誤訊息 |
