# SSP 匯出 Track B — 收尾 SUMMARY（2026-05-22）

## 一句話摘要

Track B（SSP 匯出 B1~B6）全部 shipped，含 6 種格式匯出（docx/pdf/odt/json/yaml/xml）、CMMC 正式 DOCX 格式、LibreOffice 跨平台支援、中文檔名修正。

---

## Commits 清單

### BE（compliance-manager-be）

| Commit | 說明 |
|--------|------|
| `062782d` | feat: Track B B1~B4 — docx generator + MF/SSP version loaders + LibreOffice converter + API routes |
| `25909f2` | feat: B5 OSCAL JSON/XML/YAML 匯出（SSP version route） |
| `4fde105` | fix: DOCX 匯出 control_name / ao_name 空白（catalog 關聯未映射） |
| `8df2525` | feat: B6 DOCX 格式升級 — 對齊 CMMC SSP 正式樣板（本次） |
| `8ee55f5` | fix: LibreOffice binary 自動偵測（macOS soffice / Linux libreoffice）（本次） |
| `43a9389` | fix: 中文系統名稱下載檔名顯示為底線（BE — RFC 5987）（本次） |

### FE（compliance-manager-fe）

| Commit | 說明 |
|--------|------|
| `7231b6a` | feat: B6 SSP 文件匯出按鈕（MF + 專案 SSP 版本，6 種格式） |
| `c21372c` | fix: 下載檔名中文被替換為底線（FE — filename regex）（本次） |

---

## 功能說明（改動範圍）

### B1~B4：核心匯出 pipeline（062782d）
- `SspDocxGenerator`：docxtpl + python-docx 混合渲染
- `MfSspContentLoader` / `SspVersionContentLoader`：從 MF 或 SSP 版本組裝 `SspExportDataModel`
- `SspLibreOfficeConverter`：subprocess 呼叫 LibreOffice 產 PDF/ODT
- API routes：`/module-frame/<uid>/ssp-export` + `/ssp/<uid>/export`

### B5：OSCAL 格式（25909f2）
- `/ssp/<uid>/export?format=json|yaml|xml`
- 同一 route，透過 `SystemSecurityPlanYamlMapper` 序列化 jedi-oscal entity

### B6 DOCX 格式升級（8df2525）
- 封面：標題居中、系統名稱、metadata 表（Version/Date/Prepared by）
- System Information：動態 loop，只顯示有值的列
- Responsible Organization：動態 kv rows
- Participants：合併人員表（Name / Role / Email），過濾 organization 類型
- Assessment Objectives：bullet list（去掉 statement_id 前綴重複問題）
- 標題、欄位名稱：全純英文，移除中英夾雜
- 修正 `docxtpl.get_docx()` 回傳未渲染原件的 bug（改用 `save → Document`）

### LibreOffice 跨平台（8ee55f5）
- 新增 `_resolve_libreoffice_cmd()` 自動偵測：env var → PATH libreoffice → PATH soffice → macOS app bundle
- 解決開發機 macOS 只有 `soffice` 而非 `libreoffice` 導致 PDF/ODT 500

### 中文檔名（43a9389 BE + c21372c FE）
- **BE**：`_UNSAFE_FILENAME_RE` 取代原 ASCII-only 過濾；Route 加 RFC 5987 `filename*=UTF-8''`
- **FE**：3 處 `.replace(/[^a-zA-Z0-9-_]/g, '_')` 改為 `.replace(/[\\/:*?"<>|\x00-\x1f]/g, '_')`
- 結果：`SSP_亞O航空專案_2026-05-22.docx`（不再是 `SSP__O____2026-05-22.docx`）

---

## Changelog 清單

| 檔案 | 類型 |
|------|------|
| `docs/changelog/2026-05-22-feat-ssp-export-b2-b3-b4-content-loader-api-converter.md` | feat |
| `docs/changelog/2026-05-22-feat-ssp-export-b5-oscal-json-yaml-xml.md` | feat |
| `docs/changelog/2026-05-22-feat-ssp-export-b6-fe-export-button.md` | feat |
| `docs/changelog/2026-05-22-fix-ssp-docx-export-missing-control-ao-names.md` | fix |
| `docs/changelog/2026-05-22-feat-ssp-docx-template-rebuild.md` | feat |
| `docs/changelog/2026-05-22-fix-ssp-export-libreoffice-binary-detect.md` | fix |
| `docs/changelog/2026-05-22-fix-ssp-export-unicode-filename.md` | fix |

---

## 已知 Follow-up（非阻塞）

| 項目 | 說明 |
|------|------|
| `_resolve_ssp_id` 多版本 SSP | MF 有 3 個 SSP 共用 profile_id，`get_one()` 隨機取一。實務沒差（3 版本都有 items），但未來若有版本差異會 flaky，應改為明確取 latest |
| AO 無中文翻譯 | catalog 尚無 ZH 翻譯，AO section 僅顯示英文 `ao_name` |
| network_architecture / data_flow 空白 | MF/SSP version loader 都回空字串，DB 尚無對應欄位 |
| E2E 測試 | Track B 無 cucumber E2E，後續可補 |

---

## Working Tree 狀態

```
pyproject.toml    M   dev-only jedi-oscal path dep — 不 commit
```

其餘工作樹乾淨。

---

## 部署 Handover

- **BE 需要 LibreOffice 安裝**（PDF/ODT）。macOS 開發機用 `soffice`（Homebrew），Linux production 用 `libreoffice`，均自動偵測。若 binary 在非標準路徑，設 `LIBREOFFICE_CMD` env var。
- **FE 不需重新 build**（Vite dev server hot reload），production 需 re-build。
- `pyproject.toml` 有 jedi-oscal path dep 待恢復 Nexus pin（待 jedi-oscal 正式發版後執行）。
