# SSP Export 收尾 Handoff — 2026-05-22（libreoffice + 空 inventory 診斷）

## 接手 Prompt

接手這個 session 請直接從「待辦事項」開始。branch = `feature/ssp-import-export-phase2`。本次延續上一份 handoff（`2026-05-22-docx-format-handoff.md` — B6 docx template 重寫），不重複 B6 內容。

---

## 本 Session 完成內容

### 1. PDF / ODT 匯出 500 修復

#### 症狀
`GET /api/1.0/module-frame/<uid>/ssp-export?format=pdf`（或 `format=odt`）回 500，log：
```
FileNotFoundError: [Errno 2] No such file or directory: 'libreoffice'
  app/oscal/service/export/ssp_libreoffice_converter.py:61 subprocess.run(...)
```
DOCX 不受影響（不走 LibreOffice）。

#### 根本原因
`SspLibreOfficeConverter.__init__` 預設 `libreoffice_cmd="libreoffice"`，DI 也沒覆寫。但 LibreOffice 在不同平台 binary 名稱不同：

| 平台 | 執行檔 |
|------|--------|
| Linux | `libreoffice` |
| macOS Homebrew | `/opt/homebrew/bin/soffice` |
| macOS app bundle | `/Applications/LibreOffice.app/Contents/MacOS/soffice` |

開發機只有 `soffice`，因此 subprocess 找不到 `libreoffice` binary。

#### 修法
`app/oscal/service/export/ssp_libreoffice_converter.py`
- 加 `_resolve_libreoffice_cmd()` 自動偵測：env var `LIBREOFFICE_CMD` → PATH 內 `libreoffice` / `soffice` → macOS app bundle 絕對路徑 → fallback `"libreoffice"`
- Constructor 預設 `None`，會觸發自動偵測；顯式傳 string 可 override（測試或客製化部署用）
- DI wiring 不需動

在本機驗證 `_resolve_libreoffice_cmd()` 回傳 `soffice` ✓。

### 2. Hardware Inventory 顯示空白的診斷（**並非 code bug**）

#### 症狀
使用者反映 DOCX 匯出的 Hardware Inventory section 沒資料。

#### 診斷過程（完整 evidence trail）
1. **DB 確認**：MF `3699fc01-3398-40af-be20-ef7913451a27` 透過 `oscal_profile_uid=68882112-…` 對到 profile_id=122，**有 3 個 SSP**（id 250/251/252），三個 SSP 各自都有 hardware items（3/4/3 筆）
2. **`module_frame_ssp_resources_service.list_resources()`**：回 `{devices: 3, info_systems: 3}` ✓
3. **`MfSspContentLoader.load()`**：`model.inventory_items` 3 筆 ✓
4. **`SspDocxGenerator._build_context()`**：context dict 的 `inventory_items` 3 筆 ✓
5. **完整 `gen.generate(model)`**：產出 DOCX 的 Hardware Inventory table 正確含 3 rows（`Windows server 2025 ERP System` / `D365 DB Server` / `Windows Server AD Service`）

**HEAD code (4fde105) 與 working tree（B6 改版）兩個版本都跑過，都正確產 3 列**。

#### 結論
Code path 沒問題。空白是因為 **BE process 尚未重啟**，跑的是上一個 session 之前的舊版 code（可能更舊，舊到 inventory loader 尚未實作完整）。重啟即解決。

---

## 異動檔案清單（本 session）

| 檔案 | 類型 | 說明 |
|------|------|------|
| `app/oscal/service/export/ssp_libreoffice_converter.py` | 小改 | 加 `_resolve_libreoffice_cmd()` binary 自動偵測 |
| `docs/changelog/2026-05-22-fix-ssp-export-libreoffice-binary-detect.md` | 新增 | changelog |

**上一個 session（B6）的未 commit 改動仍在**，本 session 沒 commit 任何東西。完整未 commit 清單（含 B6 + 本 session）：

```
M  app/oscal/service/export/ssp_docx_generator.py          # B6
M  app/oscal/service/export/ssp_export_app_service.py      # B6
M  app/oscal/service/export/ssp_export_model.py            # B6
M  app/oscal/service/export/ssp_libreoffice_converter.py   # 本 session
M  app/oscal/service/export/ssp_mf_content_loader.py       # B6
M  app/oscal/service/export/ssp_version_content_loader.py  # B6
M  app/oscal/templates/ssp/ssp_cmmc_template.docx          # B6
M  di_containers/oscal/oscal_containers.py                 # B6
M  scripts/generate_ssp_docx_template.py                   # B6
?? docs/changelog/2026-05-22-feat-ssp-docx-template-rebuild.md         # B6
?? docs/changelog/2026-05-22-fix-ssp-export-libreoffice-binary-detect.md  # 本 session
?? docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-22-docx-format-handoff.md  # B6
?? docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-22-export-libreoffice-and-inventory-handoff.md  # 本 session
?? docs/features/FR-011.2-2605-ssp-import-export-phase2/implementation-plan-docx-template-rebuild.md  # B6

M  pyproject.toml   # dev-only path dep, NOT to commit
```

---

## 待辦事項（接手後從這裡開始）

### 立即要做

1. **重啟 BE**（最關鍵 — 兩個本 session 修的問題都需要重啟才生效，B6 改動也還沒生效）
   ```bash
   lsof -ti:8000 | xargs kill -9
   nohup python main_socketio.py > /dev/null 2>&1 &
   ```

2. **Smoke test 三種格式**（每種開啟確認）：
   - `GET /api/1.0/module-frame/3699fc01-3398-40af-be20-ef7913451a27/ssp-export?format=docx`
   - `GET /api/1.0/module-frame/3699fc01-3398-40af-be20-ef7913451a27/ssp-export?format=pdf`
   - `GET /api/1.0/module-frame/3699fc01-3398-40af-be20-ef7913451a27/ssp-export?format=odt`

   驗收點：
   - 封面有系統名稱、版本、日期
   - System Information 表只顯示有值的列
   - Participants 表（person 類型）有資料
   - **Hardware Inventory 表有 3 筆 row**（`Windows server 2025 ERP System` / `D365 DB Server` / `Windows Server AD Service`）
   - System Components 表有 3 筆 row（`ERP SYSTEM` / `Jira` / `AlienVault`）
   - 控制項章節有 Heading 3 + AO 表格 + 實作說明
   - PDF / ODT 開得開、內容跟 DOCX 一致

3. **Commit 所有異動**（smoke test 通過後）

   建議分兩個 commit（兩個獨立主題）：

   **Commit A — B6 docx template 重寫**：
   ```bash
   git add app/oscal/service/export/ssp_docx_generator.py \
           app/oscal/service/export/ssp_export_app_service.py \
           app/oscal/service/export/ssp_export_model.py \
           app/oscal/service/export/ssp_mf_content_loader.py \
           app/oscal/service/export/ssp_version_content_loader.py \
           app/oscal/templates/ssp/ssp_cmmc_template.docx \
           di_containers/oscal/oscal_containers.py \
           scripts/generate_ssp_docx_template.py \
           docs/changelog/2026-05-22-feat-ssp-docx-template-rebuild.md \
           docs/features/FR-011.2-2605-ssp-import-export-phase2/implementation-plan-docx-template-rebuild.md \
           docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-22-docx-format-handoff.md
   ```

   **Commit B — libreoffice binary detect**：
   ```bash
   git add app/oscal/service/export/ssp_libreoffice_converter.py \
           docs/changelog/2026-05-22-fix-ssp-export-libreoffice-binary-detect.md \
           docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-22-export-libreoffice-and-inventory-handoff.md
   ```

   注意：**`pyproject.toml` 的 path dep 不 commit**（dev-only）。

---

## Context 重要資訊

### LibreOffice converter 設計
- 自動偵測 ⇒ 在 macOS 開發機 / Linux production 都能跑，不用環境特殊配置
- Override 機制：`LIBREOFFICE_CMD` env var 可指定絕對路徑（部署環境若 binary 不在 PATH 內可用）
- DI wiring `providers.Singleton(SspLibreOfficeConverter)` 不變

### Loader → Generator data flow（已驗證）
```
MF uid
  → ModuleFrameService.get_module_frame()              [RLS 過後可讀]
  → ModuleFrameSspResourcesService.list_resources()    回 {devices, info_systems}
  → MfSspContentLoader._build_inventory_items(devices) 回 SspInventoryItemExportItem[]
  → SspDocxGenerator._build_context(model) 內            "inventory_items": [{name, device_type, ip_address, os, status}]
  → DocxTemplate.render(context)                        Jinja {%tr for item in inventory_items %}
  → Document(rendered_buf) + _append_control_sections + _append_reference_docs
  → BytesIO docx
```

注意：profile_id=122 對應 3 SSPs（250/251/252，SSP versioning 產生的版本鏈）。`_resolve_ssp_id` 用 `get_one(profile_id=122)` 取**任意一個 SSP**（PostgreSQL 沒 ORDER BY，順序不確定）。三個 SSP 都有 hw items 所以實務上看不出差異，但這是個 latent issue — 未來如果只有某個 SSP 版本有 items 而其他沒有，會 flaky。**Follow-up：考慮明確指定哪個 SSP 版本**（latest by updated_at？）。

### 標準偵錯 pattern（如果之後有類似資料流問題）
本次用 standalone script bootstrap 完整 DI 容器追資料流。pattern：
1. `load_dotenv(.env)` + 補 third-party stub env（`GITLAB_*`, `GITHUB_*`, `DRIVE_TOKEN_ENCRYPTION_KEY`）
2. `sys.setrecursionlimit(5000)`
3. `create_app(enable_socketio=False)` + 從 `app.extensions['di_container']` 拿已配置好的 container（**不要** `Containers()` 開新的，config 沒注入）
4. `set_user_context(UserContextDTO(...))` — `allowed_tenant_paths` 要傳 tenant `path`（如 `/1/102/`），不是 tenant id
5. `with session_scope():` 包住所有 service 呼叫

本次跑出的證據顯示 working tree + HEAD code 都正確產 inventory，問題純粹是 BE 沒重啟。

---

## 已知限制 / Follow-up

- AO 表格目前無翻譯（catalog 尚無 ZH 翻譯），只有英文 `ao_name`（B6 知曉）
- MF 來源的 Responsible Organization 通常只有 Name，Address/Phone 留空
- `network_architecture` 和 `data_flow` 欄位目前 MF / SSP version loader 都回傳空字串（尚無對應 DB 欄位）
- `_resolve_ssp_id` 三 SSP 撞 profile_id 時挑「任意一個」— 應改為明確挑 latest version
- `pyproject.toml` 有 jedi-oscal path dep（dev 用），**不要 commit**
