C5 — 專案規劃頁加 SSP tab(FE)

級別:中 依賴:C2.1(party / ssp-resources / system-characteristic / current-ssp-uid endpoints)+ C1(角色 dropdown)+ C8(role label) 被依賴:C6(FE 詳情頁 deep link)


1. 目標

在「專案規劃」頁加上「SSP」tab,讓 manager 能在專案內直接編輯該專案 current AP 對應的 SSP 內容(基本資料 / 受評範圍 / parties / leveraged)。

控制項實作 + 程序書 已有既有 UI(A4/A5 階段),C5 不重做,但要從 SSP tab 內加 link 跳該頁。

2. 範圍

In scope(全 FE

  • 新 tab:「SSP」在專案規劃頁
  • AP 切換 dropdown
  • 五個子區塊(accordion 折疊):
    1. 基本資料(system_characteristic)
    2. 受評範圍 — 設備 / 資訊系統(ssp-resources)
    3. 外部利用服務(leveraged)
    4. 參與人員(parties,OSCAL 角色)
    5. (link)控制項實作 + 程序書(既有頁面)
  • Lazy load:tab 切到 SSP 才 fetch;accordion 展開才 fetch 子區塊
  • Deep link:?tab=ssp&section=scope|parties|... 自動展開對應 accordion
  • 權限:non-manager 進來看 readonly + 編輯按鈕 disabled
  • AP closed 狀態:所有 section 切 readonly + 顯示「此 AP 已關閉,無法編輯」hint

Out of scope

  • 控制項實作 UI(既有,A4/A5 已 ship)
  • 程序書 UI(既有)
  • Excel 匯入 UI(C2.2 後補,可後續加進 SSP tab)

3. UX 設計

3.1 整體 Tab 結構

專案規劃頁
├── Tab: 概覽
├── Tab: AP 列表
├── Tab: 參與人員  (project_participants — 既有)
└── Tab: SSP     ← C5 新加
    ├── [AP 選擇器: ▼ 第 1 期 (2026 Q1) - active]
    ├── [warning if AP closed: "此 AP 已關閉,僅可檢視"]
    │
    ├── Accordion: 基本資料            [展開]
    │   └── name / system_identifier / sensitivity / scope_description / status / system_owner
    │
    ├── Accordion: 受評範圍 — 設備      [折疊]
    │   └── DataTable: name / device picker / description
    │
    ├── Accordion: 受評範圍 — 資訊系統   [折疊]
    │   └── DataTable: name / info_system picker / description
    │
    ├── Accordion: 外部利用服務          [折疊]
    │   └── DataTable: service_name / provider / purpose
    │
    ├── Accordion: 參與人員 (SSP)        [折疊]
    │   └── DataTable: name / email / role(OSCAL dropdown) / user picker
    │
    └── Section: 控制項與程序書          [link]
        └── "→ 前往控制項實作頁" / "→ 前往程序書管理頁"

3.2 AP 切換邏輯

  • 預設選 latest active AP(從 GET /projects/<uid>/current-ssp-uid 取)
  • Dropdown 顯示所有 AP(API:GET /projects/<uid>/assessment-plans/menu,既有)
  • 切 AP → 觸發 SSP detail re-fetch
  • 切到非 current 的 AP(歷史 AP)→ 強制 readonly + hint「此為歷史 AP,僅可檢視」

3.3 Lazy load 策略

觸發點 動作
user click「SSP」tab call GET /projects/<uid>/current-ssp-uid 取 ssp_uid + ap 狀態
預設展開「基本資料」accordion call GET /ssp/<ssp_uid>/system-characteristic
user 展開其他 accordion call 對應 endpoint:GET /ssp/<ssp_uid>/parties / /ssp-resources / /leveraged
切換 AP re-fetch system-characteristic(其他 accordion 折疊重置)

3.4 權限狀態圖

角色 AP 狀態 UI 行為
manager active 全 editable
manager closed readonly + hint「此 AP 已關閉」
reviewer / auditor / viewer active readonly + 編輯按鈕 disabled
reviewer / auditor / viewer closed readonly

判斷:FE 從 current-ssp-uid response 拿 is_editable + project_participants role 雙條件決定。

URL pattern:/projects/<project_uid>/planning?tab=ssp&section=<section>

// router.beforeEach 或 component mounted
const { tab, section } = route.query
if (tab === 'ssp') {
  activeTab.value = 'ssp'
  if (section) {
    // 自動展開對應 accordion + scroll into view
    expandedAccordions.value = [section]
    nextTick(() => {
      document.querySelector(`#section-${section}`)?.scrollIntoView({ behavior: 'smooth' })
    })
  }
}

支援的 section value:

  • scope → 「受評範圍 — 設備」+「受評範圍 — 資訊系統」(同時展開)
  • parties → 參與人員
  • basic → 基本資料
  • leveraged → 外部利用服務

4. 元件設計

4.1 新元件清單

Container

  • SspTabPanel.vue(root,整個 SSP tab 內容)

Section components

  • SspBasicSection.vue — 基本資料(system_characteristic)
  • SspScopeDevicesSection.vue — 設備清單
  • SspScopeInfoSystemsSection.vue — 資訊系統清單
  • SspLeveragedSection.vue — 外部利用服務(C2.2 後補可後續加)
  • SspPartiesSection.vue — 參與人員(OSCAL 角色)

Sub-components(可考慮複用既有 MF 元件):

  • SspDevicePickerCell.vue — 設備 picker (仿 MF version)
  • SspInfoSystemPickerCell.vue — 資訊系統 picker
  • SspPartyEditDialog.vue — 編輯 party 的 dialog

4.2 元件複用(從 MF 版本搬)

MF 元件 SSP 版本作法
ModuleFramePartiesPanel.vue 複製 → 改 props 接 ssp_uid 而非 mf_uid,API 改吃 /ssp/<uid>/parties
ModuleFrameSspDevicesPanel.vue 同上
ModuleFrameSspInfoSystemsPanel.vue 同上
useModuleFrameParties composable 抽 base useSspParties(ssp_uid),MF wrapper 複用

4.3 Composable 設計

// src/composables/useSspContext.js (新)
export function useSspContext(projectUid) {
  const sspUid = ref(null)
  const apUid = ref(null)
  const apStatus = ref(null)
  const isEditable = ref(false)

  async function loadCurrent() {
    const res = await axios.get(`/projects/${projectUid.value}/current-ssp-uid`)
    sspUid.value = res.data.ssp_uid
    apUid.value = res.data.ap_uid
    apStatus.value = res.data.ap_status
    isEditable.value = res.data.is_editable
  }

  return { sspUid, apUid, apStatus, isEditable, loadCurrent }
}

// src/composables/useSspParties.js (新)
export function useSspParties(sspUid) { ... }

// src/composables/useSspResources.js (新)
export function useSspResources(sspUid) { ... }

5. 資料流

[User click SSP tab]
   ↓
useSspContext.loadCurrent(projectUid)
   ↓ ┌─ GET /projects/<uid>/current-ssp-uid → { ssp_uid, ap_uid, ap_status, is_editable }
   ↓ └─ 設定 reactive state
   ↓
[SspTabPanel 顯示 AP selector + sections]
   ↓
[Default expand: 基本資料 accordion]
   ↓
useSystemCharacteristic.fetch(ssp_uid) → GET /ssp/<uid>/system-characteristic
   ↓
[User click expand: 受評範圍-設備]
   ↓
useSspResources.fetchDevices(ssp_uid) → GET /ssp/<uid>/ssp-resources
   ↓
[User click edit on a row]
   ↓
useSspResources.updateItem(ssp_uid, item_uid, payload)
   ↓ ┌─ PUT /ssp/<uid>/ssp-resources/items/<item_uid>
   ↓ └─ if 200: refresh local cache
   ↓
[Display toast: "更新成功"]

6. 邊界條件

情境 UI 行為
專案還沒啟動 AP current-ssp-uid 回 404 / null → 顯示「請先啟動 AP」+ 引導 button
切到歷史 AP readonly + 「歷史 AP,僅可檢視」hint
Manager 編輯時 AP 突然 closed API 回 412,FE 顯示 toast「AP 已關閉」+ refresh state
Network 斷線 section 內顯示 retry button + error state
Concurrent edit(兩個 manager 同時改) API 層 last-write-wins(A5 既有模式),不做樂觀鎖

7. 待 user 拍板的小決策

編號 問題 我建議
C5-D1 AP selector 預設選 latest active 還是 last viewed? latest active(直觀)
C5-D2 五個 accordion 預設展開哪個? 基本資料 + 受評範圍-設備(最常編輯的兩個)
C5-D3 控制項實作 / 程序書 link 跳新頁還是側邊面板? 跳新頁(既有頁面已是 fullpage 設計)
C5-D4 leveraged section 是否要等 C2.2 後再做? ,C5 first ship 不含 leveraged(accordion 內顯示「尚未開放」)
C5-D5 non-manager 看 readonly 時,編輯按鈕是 hide 還是 disable? disable + tooltip 顯示「需 manager 權限」(user 知道有此功能但無權)

8. 開發後狀態

  • 「專案規劃」頁多一個 SSP tab
  • 五個 section accordion lazy load
  • AP 切換 dropdown
  • 權限 + AP 狀態雙判斷的 readonly mode
  • Deep link 從專案詳情頁可跳直接展開特定 section
  • 複用 MF 元件節省開發時間

9. UI Mock(建議)

執行前先做 wireframe(PrimeVue 元件層級):

  • Tab + AP selector:PrimeVue TabView + Dropdown
  • Accordion:PrimeVue Accordion
  • 表格:PrimeVue DataTable
  • 編輯 dialog:PrimeVue Dialog

可用 figma-use 出 mockup(如果需要),或直接照既有 MF template-edit 的視覺風格做。


10. Implementation Reality / Reconciliation(2026-05-23 Phase D 修正)

C5 first ship (commit FE 57caca2 + dd28658, 2026-05-23 上午) 走原 design 的 Accordion 5-section 結構。User 測試後反饋「UI 不如預期,能不能參考合規資源庫編輯頁 不用手風琴一樣用 Tab」→ 同日下午整個 redesign 為 Phase D 結構。實際 ship 跟原 design 偏離點:

10.1 容器:Accordion → TabView

項目 原 design 實際
容器 Accordion 多 section 折疊 TabView 6 個 sub-tab 平行
進入體驗 折疊 → 點開 → fetch lazy 進入即 mount 全部 (:lazy="false"),counts 立即正確
AP selector 有 dropdown 拿掉,apUid 從 URL params.apUid 直接拿

理由:user 反饋 accordion UX 拖泥帶水(counts 滯後、層級感不對),TabView 對齊既有 MF template-edit 視覺風格。

原 design 5 個 section:

  1. 基本資料(system_characteristic)
  2. 受評範圍 — 設備 / 資訊系統
  3. 外部利用服務
  4. 參與人員
  5. (link) 控制項實作 + 程序書

實際 6 個 sub-tab:

  1. 受評標的(前「基本資料」/「範圍概要」兩輪改名)
  2. 責任單位(OSCAL organization parties)
  3. 責任人員(OSCAL person parties)
  4. 設備
  5. 資訊系統
  6. 外部利用服務

差異:

  • 拿掉「適用控制項」+「程序書文件池」兩個入口:user 反饋這兩個不該放專案規劃; 程序書管理改為專案規劃頂層 tab(同 Project Settings 整合)
  • parties 拆成「責任單位 / 責任人員」兩個 sub-tab:對齊 OSCAL responsible-parties 按 party_type 分離的 spec,視覺更清楚
  • 設備 / 資訊系統 從 section 升 sub-tab,跟 MF 編輯頁對齊

10.3 「受評標的」UX(前「基本資料」)

原 design 只列「system_characteristic」一句帶過。實際做了 UX 重塑:

  • Tab 名稱:基本資料 → 範圍概要 → 受評標的(user 兩輪迭代後拍板)
  • 加 intro block(4 條 bullet:是什麼 / 多敏感 / 誰負責 / 邊界在哪)
  • 各欄位加 placeholder / hint,且中性化系統 → 範圍(為後續多框架支援鋪路)
  • 預設值:FIPS 分類 = high / status = active(BE _empty_placeholder 同步)
  • 範圍負責人改 Dropdown picker(從 menuStore.userMenu 拉),不再手填 UID

10.4 Phase B — Generalize MF panels(C5 §2.3 共用 panel 落地)

原 design §2.3 寫「複用 MF 元件節省開發時間」目標達成路徑:

4 個 MF panel 加 apiBase ('module-frame' | 'ssp') + scopeUid props:

  • ModuleFramePartiesPanel
  • ModuleFrameSspDevicesPanel
  • ModuleFrameSspInfoSystemsPanel
  • ModuleFrameDocumentPoolPanel

同一份元件兩個 context(MF / SSP)共用,service 80% 可複用。

10.5 Project Settings 整合進專案規劃頁(超出原 C5 scope)

原 design 沒寫,user 後續要求把專案設定也走 SSP 模式整進專案規劃頁,避免兩個入口。

抽 3 個 panel(src/components/grc/project/{ProjectBasicInfoPanel, ProjectParticipantsPanel, ProjectCloudIntegrationsPanel}.vue),加入頂層 tab 結構:

專案規劃頁
├── 控制項實作(既有)
├── 系統安全計畫(C5 + Phase D, 6 sub-tabs)
├── 專案基本資訊(C5 後續)
├── 專案參與人員
├── 程序書管理
└── 雲端整合

各 panel card 70% 水平置中。拿掉右上角「專案設定」按鈕(重複入口)。 舊 /project/projects/<id>/settings route 保留可用(外部連結相容)。

10.6 i18n / 命名統一(超出原 C5 scope)

  • 全 zh-tw「AP」→「稽核計畫」(en 保留 AP)
  • 全 zh-tw「SSP」→「系統安全計畫」(en 保留 SSP)
  • 「單位 / 參與人員」→「責任單位 / 責任人員」(對齊 OSCAL responsible-parties)
  • 專案層 tab:「參與人員」→「專案參與人員」(區分 SSP 層 OSCAL parties)
  • MF template-edit 7 個 tab header i18n 化(hardcoded → t() call)

10.7 C5-D 系列小決策 final

編號 問題 原建議 實際
C5-D1 AP selector 預設 latest active 拿掉 selector(apUid 從 URL)
C5-D2 accordion 預設展開 基本資料 + 設備 TabView 預設第一個 sub-tab 受評標的
C5-D3 控制項實作 link 跳新頁還側邊面板 跳新頁 拿掉 link(控制項實作不放 SSP tab)
C5-D4 leveraged 等 C2.2 是 (first ship 不含) (C2.2 已 ship, 直接做 leveraged sub-tab + Phase C 結構化欄位)
C5-D5 non-manager readonly 按鈕 disable + tooltip 維持原建議

11. Track A + C Reconciliation(2026-05-23)

Track A (MF / SSP 結構對齊) + Track C (專案 SSP tab Excel round-trip) 完成後的 deviation 紀錄。

11.1 Track A — section component generalize(A3/A4)

SspBasicSection.vue + SspLeveragedSection.vueapiBase: 'ssp' | 'module-frame' + scopeUid 2 props。scopeUid 優先、sspUid fallback — 既有 SSP-tab caller 不破壞。

SspService 對應 method 加 apiBase 參數,自動切 /ssp ↔︎ /module-frame 路徑前綴。useSspSystemCharacteristic / useSspLeveraged composable cache key 改 ${apiBase}:${scopeUid} 隔離兩端 scope。

MF template-edit 新增「受評標的」+「外部利用服務」2 tab(後因 user 反饋順序調整,「受評標的」移到「責任單位」前)。

11.2 Track A — MF endpoint mirror SSP pattern(A1/A2)

SspSystemCharacteristicContextService(mirror C2.1 SspResourcesContextService 抽法)— SSP-side AppService 改 thin wrapper(perm + delegate),shape 不變。

新 MF endpoint:

  • GET / PUT /module-frame/<uid>/system-characteristic
  • GET / POST /module-frame/<uid>/leveraged
  • PUT / DELETE /module-frame/<uid>/leveraged/<itemUid>

兩端 endpoint 共用同一個 context service — 確保 response shape 一致。

11.3 Track A — 啟動專案 clone SC(A7)

oscal_project_service._setup_ssp_system_implementation_clone_system_characteristic_from_template:在 Step 2.5(找到 template_ssp 後、clone items 前)clone SC from template SSP 到 new SSP。MF template-edit 內 user 設的 SC「樣板預設值」隨樣板繼承到專案。

同時補 leveraged clone 漏的結構化欄位(provider / category)— Phase C 8de7991 加進 jedi-oscal entity 後 clone 流程沒同步補。

11.4 Track C — SSP-scoped Excel template download

SspImportTemplateAppService.generate_for_ssp(ssp_uid, mode, locale)

  • Resolve chain:SSP→profile→MF(MF lookup data / ref docs / controls 結構走 MF)
  • Filled mode 從 SSP 自身資料讀(context_type='ssp' parties / system_security_plan_id items / SC by ssp_id)
  • 缺 MF 時 graceful fallback

新 route /ssp/<uid>/excel-template?mode=blank|filled,註冊在 api/oscal/__init__.py (SSP 路徑與 C2.2 T9 既有 /ssp/<uid>/excel-import/* 並列)。

11.5 Track C — FE 接通 SSP-scoped excel-import upload

C2.2 T9 ship 時 BE endpoint 已有但 FE 沒接通;Track C 補上:

  • SspTabPanel.vue 加「下載」+「匯入」button (absolute 疊在 sub-tab nav row 右側)
  • 匯入 dialog 用 dropzone 樣式 mirror ImportExcelPage.vue
  • Upload via SSP-scoped endpoint (require_manager 權限 gate)
  • 預覽走既有 non-scoped /ssp-excel-import/<parse_uid> endpoint(parse_uid 全 unique)
  • Confirm 完成後判 ?return_to=ssp query 跳回 project-planning-ap route

11.6 Track C ship 時挖出來的 BE bug

  • session_scopessp_scoped_excel_import_route 4 個 method 直接 call permission_checker.require_manager,但 perm 內部 query DB 需要 session_scope。C2.2 T9 ship 時就有此 bug,FE 沒接通所以未觸發。Fix: 4 個 method 各包 with session_scope():
  • ControlWriteStrategy upsert key:原本 (ssp_id, catalog_control_id),但 DB unique 是 (ssp_id, control_identifier)。SSP-scoped flow collision — project 啟動已用 MF profile 的 catalog_control_id 建好 row,Excel reconciler 配到不同 catalog_control_id (different framework version 的 DB id)。Fix: 改用 control_identifier(OSCAL 標準 ID)。

11.7 Track A/C 跟原 design 的差異

議題 原 design Track A/C 實際
MF template-edit 是否有 leveraged tab 沒提 (mirror SSP tab 結構)
MF leveraged endpoint 沒提 新增(reuse SspLeveragedContextService)
啟動專案是否 clone SC 沒提(C3 PR1 拿掉 SC snapshot) clone(從 template SSP,A7 加)
SSP tab 下載匯入 Excel 沒提 新增(Track C)
Excel sheet 名前綴 01_基本資料 拿掉前綴(v2.3.0, parser 加 legacy alias)
Excel leveraged 欄位 category / service_name / provider / status / purpose 加 date_authorized + description(v2.4.0, 對齊 SSP UI dialog)