SSP Export 收尾 Handoff — 2026-05-22(libreoffice + 空 inventory 診斷)

§1

接手 Prompt

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


§2

本 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 尚未實作完整)。重啟即解決。


§3

異動檔案清單(本 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

§4

待辦事項(接手後從這裡開始)

立即要做

  1. 重啟 BE(最關鍵 — 兩個本 session 修的問題都需要重啟才生效,B6 改動也還沒生效)

    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 筆 rowWindows 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 重寫

    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

    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)。


§5

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_idget_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 沒重啟。


§6

已知限制 / Follow-up

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