級別:中 依賴:C2.1(party / ssp-resources / system-characteristic / current-ssp-uid endpoints)+ C1(角色 dropdown)+ C8(role label) 被依賴:C6(FE 詳情頁 deep link)
在「專案規劃」頁加上「SSP」tab,讓 manager 能在專案內直接編輯該專案 current AP 對應的 SSP 內容(基本資料 / 受評範圍 / parties / leveraged)。
控制項實作 + 程序書 已有既有 UI(A4/A5 階段),C5 不重做,但要從 SSP tab 內加 link 跳該頁。
?tab=ssp§ion=scope|parties|... 自動展開對應 accordion專案規劃頁
├── 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]
└── "→ 前往控制項實作頁" / "→ 前往程序書管理頁"
GET /projects/<uid>/current-ssp-uid 取)GET /projects/<uid>/assessment-plans/menu,既有)| 觸發點 | 動作 |
|---|---|
| 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 折疊重置) |
| 角色 | 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§ion=<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 → 外部利用服務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 — 資訊系統 pickerSspPartyEditDialog.vue — 編輯 party 的 dialog| 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 複用 |
// 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) { ... }[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: "更新成功"]
| 情境 | 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 既有模式),不做樂觀鎖 |
| 編號 | 問題 | 我建議 |
|---|---|---|
| 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 知道有此功能但無權) |
執行前先做 wireframe(PrimeVue 元件層級):
TabView + DropdownAccordionDataTableDialog可用 figma-use 出 mockup(如果需要),或直接照既有 MF template-edit 的視覺風格做。
C5 first ship (commit FE 57caca2 + dd28658, 2026-05-23 上午) 走原 design 的 Accordion 5-section 結構。User 測試後反饋「UI 不如預期,能不能參考合規資源庫編輯頁 不用手風琴一樣用 Tab」→ 同日下午整個 redesign 為 Phase D 結構。實際 ship 跟原 design 偏離點:
| 項目 | 原 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:
實際 6 個 sub-tab:
差異:
原 design 只列「system_characteristic」一句帶過。實際做了 UX 重塑:
high / status = active(BE _empty_placeholder 同步)menuStore.userMenu 拉),不再手填 UID原 design §2.3 寫「複用 MF 元件節省開發時間」目標達成路徑:
4 個 MF panel 加 apiBase ('module-frame' | 'ssp') + scopeUid props:
同一份元件兩個 context(MF / SSP)共用,service 80% 可複用。
原 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 保留可用(外部連結相容)。
| 編號 | 問題 | 原建議 | 實際 |
|---|---|---|---|
| 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 | 維持原建議 |
Track A (MF / SSP 結構對齊) + Track C (專案 SSP tab Excel round-trip) 完成後的 deviation 紀錄。
SspBasicSection.vue + SspLeveragedSection.vue 加 apiBase: '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 反饋順序調整,「受評標的」移到「責任單位」前)。
抽 SspSystemCharacteristicContextService(mirror C2.1 SspResourcesContextService 抽法)— SSP-side AppService 改 thin wrapper(perm + delegate),shape 不變。
新 MF endpoint:
GET / PUT /module-frame/<uid>/system-characteristicGET / POST /module-frame/<uid>/leveragedPUT / DELETE /module-frame/<uid>/leveraged/<itemUid>兩端 endpoint 共用同一個 context service — 確保 response shape 一致。
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 流程沒同步補。
新 SspImportTemplateAppService.generate_for_ssp(ssp_uid, mode, locale):
context_type='ssp' parties / system_security_plan_id items / SC by ssp_id)新 route /ssp/<uid>/excel-template?mode=blank|filled,註冊在 api/oscal/__init__.py (SSP 路徑與 C2.2 T9 既有 /ssp/<uid>/excel-import/* 並列)。
C2.2 T9 ship 時 BE endpoint 已有但 FE 沒接通;Track C 補上:
SspTabPanel.vue 加「下載」+「匯入」button (absolute 疊在 sub-tab nav row 右側)ImportExcelPage.vue/ssp-excel-import/<parse_uid> endpoint(parse_uid 全 unique)?return_to=ssp query 跳回 project-planning-ap routesession_scope 缺:ssp_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)。| 議題 | 原 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) |