# A.6 盤點：全站寫入類按鈕 → API → 權限

> 卡片 CM-2561（FR-132 T-6.2），子母卡 CM-2559。只讀盤點，不改程式。FE 讀的是 `feature/azure-iam` 當下的工作樹（含平行 session 未 commit 的 access／user-manage 改動）。

## 怎麼讀

- **範圍**：FE `src/views`、`src/components` 下有 `<Button>` 的 188 支，加上同目錄裡只用 `@click`／`<button>` 的 16 支、`src/layout/AppLayout.vue` 的 AI 小幫手浮鈕，以及順著 emit 追到的上層頁，共 239 支全部逐檔讀完（清單見文末「附錄：逐檔清單」，含 0 顆的檔與原因）。
- **列什麼**：會打寫入 API，或打 export／下載這類有獨立權限 API 的按鈕與選單項。純導覽、開 dialog（送出鈕在同檔時）、本地篩選、查詢鈕不列。開 dialog 的入口鈕若送出在**別支檔**，入口鈕照列，備註寫「入口鈕；送出在 X」——實作棒在入口隱藏即可。
- **權限欄**：只照 `a1-capability-tree.md` 附錄 A 查（API → 新名）；附錄 C 豁免的寫「免判斷」。**428 顆全部查得到，沒有附錄 A 查不到的 API。**
- **一顆按鈕多個權限**時，備註會寫清楚是哪一種，實作棒不要一律套 `hasAny`：
  - **需 hasAny**：多支寫入 API 打不同權限，有任一就讓它出現。
  - **兩者都要**：按一下會連打兩支，缺一支會做一半（例：退回任務＝流程 API＋留言 API）。這種實作要 `has(a) && has(b)`。
  - **依情境擇一**（通則 C）：同一元件服務專案 SSP 與資源庫兩條路，看 `apiBase`／`mode`／`source_type` 決定用哪個權限——判斷要由頁面傳 props 進來，元件內不可寫死。
- **專案層條件**：今天決定按鈕出不出現的既有條件照抄（專案角色、輪次狀態、BE 回的旗標）。D-A6-4：公司層權限＋這欄都過才顯示，實作棒保留這欄、外面再 AND 公司層權限。`writeDisabled()` 是授權唯讀反灰（FR-062），不是權限判斷，不算在這欄。
- **現況**五種：無判斷／已有且正確／已有但名不對／已有但粗（用較粗的權限或旗標遮寫入鈕）／免判斷。

## 統計

| 模組 | 顆數 | 檔數 | 無判斷 | 已有但名不對 | 已有但粗 | 已有且正確 | 免判斷 | 要改（扣死碼） | 有專案層條件 |
|---|---|---|---|---|---|---|---|---|---|
| grc | 96 | 27 | 84 | 4 | 0 | 0 | 8 | 88 | 82 |
| oscal | 114 | 40 | 111 | 0 | 0 | 0 | 3 | 97 | 58 |
| flow | 11 | 2 | 1 | 4 | 0 | 6 | 0 | 5 | 8 |
| detection | 40 | 11 | 24 | 3 | 0 | 13 | 0 | 27 | 28 |
| evidence | 17 | 6 | 12 | 0 | 0 | 5 | 0 | 12 | 16 |
| survey | 39 | 8 | 39 | 0 | 0 | 0 | 0 | 39 | 4 |
| system | 23 | 10 | 19 | 0 | 0 | 3 | 1 | 19 | 3 |
| iam | 40 | 18 | 15 | 0 | 0 | 14 | 11 | 15 | 11 |
| platform | 12 | 6 | 9 | 0 | 1 | 0 | 2 | 10 | 2 |
| asset | 11 | 4 | 7 | 1 | 1 | 2 | 0 | 9 | 2 |
| ai | 12 | 10 | 10 | 0 | 0 | 2 | 0 | 10 | 1 |
| workspace | 13 | 5 | 6 | 1 | 4 | 0 | 2 | 11 | 7 |
| **合計** | **428** | **145** | **337** | **13** | **6** | **45** | **27** | **342** | **222** |

- 「模組」依按鈕打的 API 的權限前綴分，不依檔案目錄。所以 `views/project/ProjectPlanningView.vue` 的 SSP 匯出鈕歸 oscal、`views/survey-v2/SurveyPreview.vue` 的填答鈕歸 grc（通則 A）。同一支檔可能分在兩個模組。
- 「要改」＝無判斷＋已有但名不對＋已有但粗，扣掉 14 顆死碼按鈕（見下方「死碼」）。
- 「有專案層條件」222 顆：實作時要和公司層權限 AND，不可取代。

## 已有判斷但名不對或太粗（19 顆）

| 檔案 | 按鈕 | 今天用的 | 該用的（附錄 A） |
|---|---|---|---|
| `views/project/ProjectListView.vue` :254 :259 :375 :414 | 批次刪除、新增、列內刪除、編輯儲存 | `getUserPagePermissions('/project/projects')` 比 CREATE／UPDATE／DELETE 字串（舊選單動作機制） | `grc.project.delete`／`.create`／`.update` |
| `views/flow-template/FlowTemplateEditorView.vue` :969 :979、`FlowTemplateManageView.vue` :450 :464 | 發布、撤銷發布 | `flow.template.update` | `flow.template.publish`（編輯器 dirty 時先存檔，「發布」要 publish＋update 兩者都有） |
| `views/scan-zone/ScanZoneManage.vue` :136 :139 :158 | 產生建議、新增、刪除 | `detection.scan-zone.update` | `.create`、`.create`、`.delete`（`src/config/scanZone.js` 常數只有 READ／UPDATE，要補） |
| `views/device/DeviceManage.vue` :362、`DeviceExportMenu.vue` :39 | 匯入、匯出 | `asset.device.create`、`asset.device.read` | `asset.device.import`、`asset.device.export` |
| `views/ai-quota/AiQuotaCeilingTable.vue` :126 | 儲存額度上限 | 父層 `isPlatformAdmin && isRoot` 旗標 | `platform.ai-quota-ceiling.update` |
| `views/feedback/system/SystemFeedbackForm.vue` :455、`SystemFeedbackManage.vue` :266 :279 :282 :303 | 儲存、新增、編輯、刪除 | `getUserPagePermissions` 字串比對；Manage 的唯讀模式由 `FeedbackManageTabView.vue:69` 的「選單能力點只有一個」推出 | `workspace.feedback.create`／`.update`／`.delete`（改時連 `FeedbackManageTabView.vue:69` 一起改） |

## 建議拆棒（T-6.3 起）

前提照母卡：T-6.1（單一來源 `has()`）已合、CM-2556 補權限 migration 已套 DEV。數字是「要改」顆數。

| 棒 | 範圍 | 顆數 | 檔數 | 說明 |
|---|---|---|---|---|
| T-6.3 | grc 專案內頁（views/project 11 支＋`components/grc/project`） | 38 | 12 | 輪次三頁（ApAuthoring／AuditReview／Poam）、規劃頁、稽核總覽、專案列表／建立。ProjectListView 換掉 `getUserPagePermissions`。專案層條件最多，逐顆 AND |
| T-6.4 | grc 任務與匯入（JobExecutionDrawer、FlowPhaseBanner、job-drawer、planning、JobImportDialog、AP／AR 匯入、SurveyPreview 填答、摘要報告三頁、資源庫列表的「啟動專案」） | 50 | 14 | 抽屜 19 顆集中一檔；FlowPhaseBanner 要和 BE 回的 `user_can_advance` AND；退回任務是「兩者都要」 |
| T-6.5 | oscal 專案 SSP 與共用 panel（components/grc 的 SSP／資源庫共用 panel、ssp-docx-import-v2、ssp-excel-import 活的那幾支、ProjectPlanningView 的 SSP 匯出） | 55 | 20 | 共用 panel 一律加 props 由頁面傳入，元件內不寫死名字；ComponentsLeveragedInventoryTab 16 顆集中 |
| T-6.6 | oscal 資源庫與合規框架（views/module_frame、views/compliance-framework、FrameworkArticleContent、WorkflowSetupEditor）＋ flow 範本 5 顆 | 47 | 12 | 合規框架今天靠 `isPlatformAdmin` 遮，要決定是否保留成第二層（見待決 2）；流程範本 update→publish |
| T-6.7 | detection＋evidence＋survey | 78 | 22 | 量大但都是管理頁、幾乎沒有專案層條件，機械性高；可再對半拆成 detection＋evidence（39）與 survey（39） |
| T-6.8 | system＋iam＋platform＋asset＋ai＋workspace | 74 | 37 | 設定頁多為一頁一兩顆，檔散但單純；ScanZone 以外的「名不對」大多在這棒（裝置、回饋、額度上限） |

合計 342 顆。每棒 38～78 顆；T-6.7、T-6.8 超過卡上「20～40 顆」的建議，建議各對半拆：T-6.7 拆 detection＋evidence（39）與 survey（39），T-6.8 拆 system＋platform＋ai（39）與 iam＋asset＋workspace（35），共 8 棒。死碼按鈕不必改，建議另開一張清理卡刪檔（見下方）。

## 實作棒要注意

1. **隱藏 vs 反灰**：掃描設定檔管理頁的寫入項今天是「disabled＋label 寫無權限」，AoTaskSection 新增任務鈕是「反灰＋原因提示」。D-A6-1 裁定隱藏，實作棒改這兩處時要知道是在推翻既有設計。
2. **同一份 SSP 兩套專案層判準**：SspTabPanel 的 `canEdit` 要 planning 且 `role==='manager'`；ProjectPlanningView 給 DocumentPoolPanel／ReferenceDocumentList／SspImportDialog 的 `canEditSsp` 只看 `apStatus==='planning'`，非 manager 的參與者也看得到上傳／刪除／匯入。照 D-A6-4 只照抄、不修，但實作棒要知道。
3. **入口有擋、目的頁沒擋**：ProjectCreateView 的送出鈕無判斷，只靠列表頁入口；直接打網址進得去。JobImportDialog、AssignmentDialog、RoleImportDialog 同理（入口已擋或將擋）。目的頁也要補。
4. **半套寫入**：退回任務、完成任務先打 flow-engine 再補打留言 API——有 `grc.job.update` 沒有 `grc.job-comment.create` 的角色，任務已退回但留言 403。摘要報告匯出鈕先 PUT 再匯出——只有 export 的角色會先跳一次 403。這些「兩者都要」的按鈕已在備註標出（共 21 顆標了 hasAny／兩者都要，逐顆看備註）。
5. **權限繞道**（記錄，不屬本系列修）：問卷設計頁與建立問卷 dialog 的「發布狀態」開關經 `PUT /survey/<uid>`（update）或 `POST /survey`（create）送出，等於不用 `survey.survey.publish` 就能發布；帳號列表「刪除」走 `POST /user/change-status`（`iam.user.update`），`iam.user.delete` 守的 `DELETE /user/<uid>` 前端沒人呼叫。
6. **連 writeDisabled 都沒掛的**：SystemMenuManage、TenantManage、SurveyManage 與問卷子元件、RemoteAgentManage 列內啟用開關、ToolPluginManage 測試連線、各設定頁的「測試」鈕。補 `has()` 時順手補授權唯讀（不補也不違反本系列，但同一行）。
7. **檢測執行全部歸 `grc.job.update`**（通則 A），無法單獨遮檢測操作；這是附錄 A 的設計，不是漏。
8. **SurveyPreview 一檔兩用**：preview 模式從問卷管理進來（survey），fill 模式從任務抽屜開新分頁（grc.job），判斷要依 `route.query.mode` 分流。逐題作答走 socket `/socket/fill-survey`，不在 REST 表，只靠 `canEdit`。

## 待首腦決定

1. **WorkflowSetupEditor 在專案情境的存檔**：`PUT /module-frame/item/xml/<uid>` 附錄 A 只對到 `oscal.resource-library.update`，但這支編輯器也會從 ProjectPlanningView（專案 SSP 情境）開啟。照通則 C 專案情境該看專案權限，附錄 A 沒有另列——表上照附錄 A 寫，未自行改判。
2. **合規框架頁的 `isPlatformAdmin`**：四頁今天只靠「作用租戶是否單層」遮寫入鈕；後端另有 `require_platform_admin()`。改成 `has()` 後要不要保留 `isPlatformAdmin` 當第二層？（附錄 A 原廠層通則是「兩道都過」，傾向保留，但這幾支 route 不在 platform 模組。）
3. **Excel 批次建立帳號**：`POST /user/batch-upload` 每列會帶角色，附錄 A 只標 `iam.user.create`；單筆新增帶角色則另要 `iam.user.assign`。兩者不對稱，要不要也要求 assign？
4. **AI 小幫手「清除對話」**：`DELETE /ai-chatbot` 在 `src/stores/aiChatStore.js:57` 呼叫，附錄 A 守 `ai.assistant.read`。觸發它的按鈕沒有在本次盤點的檔案裡找到，實作棒動 ChatBox 時順手確認。

## 死碼（不必改，建議另開清理卡）

全專案無引用或無路由，按鈕照列在表內（備註標「死碼」），不算進「要改」：

- `components/grc/ssp/SspLeveragedSection.vue`
- `components/grc/ssp-excel-import/` 的 SheetPreviewDevices、SheetPreviewInfoSystems、SheetPreviewLeveraged，以及只被它們用的 RowActionMenu → EntityPickerDialog、InlineCreateDialog
- `components/grc/` 的 DepartmentsPanel、RoundSwitcherBar、SspSystemForm、ModuleFramePartiesDialog、ModuleFrameSspResourcesDialog（連帶 ModuleFrameSspDevicesPanel、ModuleFrameSspInfoSystemsPanel 不會出現在畫面）
- `components/grc/ssp-docx-import-v2/sections/` 的 MetadataSection、UnmatchedSection
- `components/compliance-framework/import/` 三支 ListEditor
- `views/module_frame/ModuleFrameItems.vue`（路由 `module-frame-items` 指到 ModuleFrameDetail）
- `views/workflow/BpmnProcess/` 整包四支（外來樣板殘骸，依賴的 `@/api`、`@/designer` 目錄不存在）
- 檔內死函式（無 template 引用）：ProjectAuditorOverview 的 `handleCloseRound`、RoundApAuthoringView 的 `saveControls`／`saveSubjects`、RoundAuditReviewView 的 `linkFindings`、ModuleFrame 的 `onCloneClick`、ModuleFrameTemplateEditView 的 `editMenuItems`

## 按鈕明細（依模組）

欄位：檔案:行號｜按鈕（i18n key 或文字）｜打的 API｜權限（附錄 A 新名）｜專案層條件（今天的既有條件，照抄）｜現況｜備註。檔案路徑相對 FE repo 根目錄。

### grc（96 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/components/grc/AuditControlRef.vue:548 | 證據檔案下載（icon） | GET /file/access-token/<uid> → GET /file/download/<uid> | 免判斷（附錄 C） | — | 免判斷 | 僅 RoundAuditReviewView.vue 用 |
| src/components/grc/FlowPhaseBanner.vue:63 | 前往下一階段（主按鈕，非審核階段經 confirm 後送出；審核階段改開同檔 Dialog） | POST /project/<pid>/audit-round/<rid>/stage/advance | grc.audit.update | showAdvanceButton（stageInfo.current_stage 存在 && BE 回 user_can_advance===true）；precondition 未過時改顯示 disabled 版 | 無判斷 | 共用（被 RoundAuditReviewView.vue、RoundApAuthoringView.vue、ProjectAuditorOverview.vue、RoundPoamView.vue、ProjectPlanningView.vue 用），判斷放 props 由頁面傳入 |
| src/components/grc/FlowPhaseBanner.vue:117 | 審核 Dialog 確定（核准／退回共用） | POST /project/<pid>/audit-round/<rid>/stage/advance（帶 decision） | grc.audit.update | showAdvanceButton && isReviewStage（current_stage.code==='review'）；退回時 comment 必填 | 無判斷 | 入口為同檔 :43 退回鈕與 :63 主按鈕；共用（同上 5 頁），判斷放 props 由頁面傳入 |
| src/components/grc/FlowPhaseBanner.vue:160 | 返回上一階段 Dialog 確認 | POST /project/<pid>/audit-round/<rid>/stage/rollback | grc.audit.update | isManager && canRollback（isManager＝stageInfo.user_role==='manager'；canRollback＝current_stage.code ∈ ap_authoring/audit/poam） | 無判斷 | 入口為同檔 :36；共用（同上 5 頁），判斷放 props 由頁面傳入 |
| src/components/grc/JobExecutionDrawer.vue:2286 | 開始執行／重新執行（檢測工具） | POST /detection-tools/jobs/<j>/execute | grc.job.update | canEdit（= props.canExecute && status==='PROCESSING'） | 無判斷 | 有排程列時改開同檔 :2947 確認框；共用（被 MyTasksView.vue、ProjectAuditorOverview.vue 用），判斷放 props 由頁面傳入 |
| src/components/grc/JobExecutionDrawer.vue:2325 | 替換源碼壓縮包 | POST /file/upload | 免判斷（附錄 C） | isUploadScanMode && canEdit | 免判斷 | 檔案 uid 之後隨 execute 送出；共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2336 | 上傳源碼壓縮包 | POST /file/upload | 免判斷（附錄 C） | isUploadScanMode && canEdit | 免判斷 | 同上；共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2382 | 整組取消 | POST /detection-tools/execution-groups/<g>/cancel | grc.job.update | canEdit && canCancelGroup(group)（非 legacy、多列、有活著的筆） | 無判斷 | 共用（同上），判斷放 props 由頁面傳入 |
| src/components/grc/JobExecutionDrawer.vue:2392 | 整組刪除 | DELETE /detection-tools/execution-groups/<g> | grc.job.update | canEdit && canDeleteGroup(group)（非 legacy、每一筆都 failed） | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2516 | 立即開始（排程中的組） | POST /detection-tools/execution-groups/<g>/assignments/<a>/start-now | grc.job.update | canEdit && !history && canStartNowAssignment(group, exec)（exec.status==='scheduled'） | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2526 | 重跑此組 | POST /detection-tools/execution-groups/<g>/assignments/<a>/rerun | grc.job.update | canEdit && !history && canRerunAssignment(group, exec)（failed／cancelled） | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2539 | 取消執行（單組） | POST /detection-tools/execution-groups/<g>/assignments/<a>/cancel | grc.job.update | canEdit && !history && canCancelAssignment(group, exec)（running／scheduled） | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2546 | 取消執行（legacy 舊資料） | POST /detection-tools/jobs/<j>/cancel | grc.job.update | canEdit && !history && group.is_legacy && exec.status === 'running' | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2555 | 下載報告（單份） | GET /file/access-token/<uid> → GET /file/download/<uid> | 免判斷（附錄 C） | — | 免判斷 | 多份時改開詳情視窗；共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2567 | 刪除失敗執行紀錄（icon） | DELETE /detection-tools/executions/<uid> | grc.job.update | canEdit && canDeleteExecution(exec)（status==='failed'） | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2608 | 證據檔案上傳（瀏覽檔案；含 :2588 FileUpload 拖放區） | POST /job-evidences（multipart） | grc.job-evidence.create | canEdit | 無判斷 | 共用（同上），判斷放 props 由頁面傳入 |
| src/components/grc/JobExecutionDrawer.vue:2660 | 證據檔案下載（icon） | GET /file/access-token/<uid> → GET /file/download/<uid> | 免判斷（附錄 C） | — | 免判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2661 | 刪除證據檔案（icon） | DELETE /job-evidence/<uid> | grc.job-evidence.delete | canEdit | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2715 | 新增證據連結（inline 表單送出） | POST /job-evidence | grc.job-evidence.create | !evidenceLoading && canEdit && showAddLink | 無判斷 | 入口為同檔 :2680「新增連結」切換鈕（條件 !evidenceLoading && canEdit）；共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2762 | 刪除證據連結（icon） | DELETE /job-evidence/<uid> | grc.job-evidence.delete | canEdit | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2787 | 儲存證據連結（inline 編輯） | PUT /job-evidence/<uid> | grc.job-evidence.update | canEdit（進入編輯的 :2757 鈕條件） | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2842 | 刪除留言（icon） | DELETE /grc/job/comment/<c> | grc.job-comment.delete | canComment && comment.author.id === userInfo?.uid | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2862 | 儲存留言（inline 編輯） | PUT /grc/job/comment/<c> | grc.job-comment.update | canComment && comment.author.id === userInfo?.uid（canComment＝canEdit \|\| props.canReturn） | 無判斷 | 入口為同檔 :2837 編輯鈕；共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2877 | 送出留言 | POST /grc/job-executions/<je>/comments | grc.job-comment.create | canComment（canEdit \|\| props.canReturn） | 無判斷 | 共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2893 | 退回任務（footer） | POST /flow-engine/task/revert/<id>；接著 POST /grc/job-executions/<je>/comments | grc.job.update、grc.job-comment.create | canRevert（(props.canExecute \|\| props.canReturn) && BE 回 can_revert_job） | 無判斷 | 入口鈕；送出在 JobActionCommentDialog.vue；需 hasAny（實際兩支都會打，缺 comment.create 會只做一半）；共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2899 | 完成任務（footer） | POST /flow-engine/task/complete/<id>；有填留言時再 POST /grc/job-executions/<je>/comments | grc.job.update、grc.job-comment.create | canEdit；disabled 條件 !canCompleteJob（問卷全完成／至少一份附件） | 無判斷 | 入口鈕；送出在 JobActionCommentDialog.vue；需 hasAny（留言那支僅在有填時打）；共用（同上） |
| src/components/grc/JobExecutionDrawer.vue:2947 | 排程確認框「確認執行」 | POST /detection-tools/jobs/<j>/execute | grc.job.update | canEdit（同 :2286 入口） | 無判斷 | 共用（同上），判斷放 props 由頁面傳入 |
| src/components/grc/JobExecutionDrawer.vue:3058 | 執行詳情視窗逐份下載報告 | GET /file/access-token/<uid> → GET /file/download/<uid> | 免判斷（附錄 C） | — | 免判斷 | 共用（同上） |
| src/components/grc/JobImportDialog.vue:258 | 上傳任務匯入檔（步驟 1） | POST /grc/project/<pid>/ap/<ap>/jobs/import | grc.job.import | — | 無判斷 | 僅 ProjectPlanningView.vue 用；入口在 ProjectPlanningView.vue:242 批次維護選單（disabled: writeDisabled(!canEditSsp)，canEditSsp＝apStatus==='planning'） |
| src/components/grc/JobImportDialog.vue:387 | 重新驗證 | POST /grc/project/<pid>/ap/<ap>/jobs/import/validate | grc.job.import | — | 無判斷 | 僅 ProjectPlanningView.vue 用 |
| src/components/grc/JobImportDialog.vue:392 | 確認匯入 | POST /grc/project/<pid>/ap/<ap>/jobs/import/confirm | grc.job.import | — | 無判斷 | 僅 ProjectPlanningView.vue 用 |
| src/components/grc/ap-docx-import/ApDocxImportDialog.vue:565 | `lang.ap_docx_import.choose_file`（選擇檔案；選檔即上傳解析，含 :555 dropzone 點擊） | POST /ap/<ap>/docx-imports/parse | grc.audit.import | （宿主 RoundApAuthoringView 的 canImportDocx）roundStatus==='audit_planning' && (isAuditor \|\| isProjectManager) && apUid | 無判斷 | 僅 RoundApAuthoringView.vue 用；dialog 本身只有 writeDisabled（授權唯讀）；入口鈕在 RoundApAuthoringView.vue:664（不在本批） |
| src/components/grc/ap-docx-import/ApDocxImportDialog.vue:713 | `lang.ap_docx_import.confirm_import`（確認匯入） | POST /ap-docx-import/<parse>/confirm | grc.audit.import | （宿主 canImportDocx）roundStatus==='audit_planning' && (isAuditor \|\| isProjectManager) && apUid | 無判斷 | 僅 RoundApAuthoringView.vue 用；只有 writeDisabled；取消鈕會打 DELETE /ap-docx-import/<parse>（同權限）但屬取消不列 |
| src/components/grc/ar-import/ArImportDialog.vue:477 | `lang.ar_import.choose_file`（選擇檔案；選檔即上傳解析，含 :467 dropzone 點擊） | POST /audit-round/<rid>/ar-imports/parse | grc.audit.import | （宿主 RoundAuditReviewView 的 canImportAr）roundStatus==='auditing' && (isAuditor \|\| isProjectManager) && stats.total > 0 | 無判斷 | 僅 RoundAuditReviewView.vue 用；dialog 本身只有 writeDisabled；入口鈕在 RoundAuditReviewView.vue:721（不在本批） |
| src/components/grc/ar-import/ArImportDialog.vue:636 | `lang.ar_import.discard`（捨棄並重來） | DELETE /ar-import/<parse> | grc.audit.import | （宿主 canImportAr）roundStatus==='auditing' && (isAuditor \|\| isProjectManager) && stats.total > 0 | 無判斷 | 僅 RoundAuditReviewView.vue 用；刪暫存解析結果後回 Step 1，與確認匯入同權限，隨 dialog 一起隱藏即可 |
| src/components/grc/ar-import/ArImportDialog.vue:640 | `lang.ar_import.confirm_import`（確認匯入） | POST /ar-import/<parse>/confirm | grc.audit.import | （宿主 canImportAr）roundStatus==='auditing' && (isAuditor \|\| isProjectManager) && stats.total > 0 | 無判斷 | 僅 RoundAuditReviewView.vue 用；只有 writeDisabled |
| src/components/grc/job-drawer/JobActionCommentDialog.vue:56 | 退回 Dialog 送出 | emit `submit` → 由 JobExecutionDrawer.vue handleReturnTask 處理：POST /flow-engine/task/revert/<id> ＋ POST /grc/job-executions/<je>/comments | grc.job.update、grc.job-comment.create | mode==='return'（入口條件見 JobExecutionDrawer.vue:2893 canRevert） | 無判斷 | 僅 JobExecutionDrawer.vue 用；需 hasAny |
| src/components/grc/job-drawer/JobActionCommentDialog.vue:60 | 完成 Dialog 送出 | emit `submit` → 由 JobExecutionDrawer.vue handleCompleteTask 處理：POST /flow-engine/task/complete/<id>（有留言再 POST comments） | grc.job.update、grc.job-comment.create | mode==='complete'（入口條件見 JobExecutionDrawer.vue:2899 canEdit） | 無判斷 | 僅 JobExecutionDrawer.vue 用；需 hasAny |
| src/components/grc/job-drawer/JobBlockedNotice.vue:81 | 強制開始（卡在合流閘道的任務） | emit `force` → 由 JobExecutionDrawer.vue handleForceStart／AoTaskForm→AoTaskSection→ProjectPlanningView.vue handleForceStartTask 處理：POST /grc/project/<pid>/job/<job>/force-start（useJobForceStart） | grc.job.assign | blocked && canForce（canForce 由父傳：抽屜 = 父頁 isProjectManager；規劃頁 = ProjectPlanningView isProjectManager） | 無判斷 | 共用（被 JobExecutionDrawer.vue、AoTaskForm.vue 用），判斷放 props 由頁面傳入 |
| src/components/grc/planning/AoTaskForm.vue:654 | 刪除任務 | emit `delete` → AoTaskSection → ProjectPlanningView.vue deleteTask：DELETE /grc/project/<pid>/job/<job> | grc.job.delete | canEdit（父傳 ProjectPlanningView canEdit：AP 未關閉 && apStatus==='planning' && 專案／群組／控制項 manager）；disabled 條件 deleteBlockedReason（mutationBlockedReason 或只剩一筆） | 無判斷 | 僅 AoTaskSection.vue 用（AoTaskSection 僅 ProjectPlanningView.vue 用） |
| src/components/grc/planning/AoTaskForm.vue:658 | 儲存任務 | emit `save` → AoTaskSection → ProjectPlanningView.vue saveTask：PUT /grc/project/<pid>/job/<job> | grc.job.update | canEdit（同上） | 無判斷 | 僅 AoTaskSection.vue 用；分派列增刪／複製／排程皆為本地狀態，隨此鈕一併送出 |
| src/components/grc/planning/AoTaskSection.vue:176 | 新增任務 | emit `create` → ProjectPlanningView.vue createTask：POST /grc/project/<pid>/assessment-object/<ao>/jobs | grc.job.create | 無 v-if；disabled 條件 !!mutationBlockedReason（taskMutationBlockedReason：授權唯讀／AP 已關閉／apStatus!=='planning'／!canEdit） | 無判斷 | 僅 ProjectPlanningView.vue 用 |
| src/components/grc/planning/AoTaskSection.vue:205 | 新增任務（空狀態） | emit `create` → ProjectPlanningView.vue createTask：POST /grc/project/<pid>/assessment-object/<ao>/jobs | grc.job.create | 同 :176（disabled 條件 !!mutationBlockedReason） | 無判斷 | 僅 ProjectPlanningView.vue 用 |
| src/components/grc/project/ProjectBasicInfoPanel.vue:139 | 專案基本資訊「儲存」 | PUT /grc/project/<uid> | grc.project.update | `canEdit`（ProjectSettingsView：本人在 participants 且 role==='manager'；ProjectPlanningView 傳 isProjectManager，同義） | 無判斷 | 共用（被 ProjectSettingsView.vue、ProjectPlanningView.vue 用），判斷放 props 由頁面傳入 |
| src/components/grc/project/ProjectCloudIntegrationsPanel.vue:127 | Drive 資料夾「檢查並修復」確認鈕 | POST /integrations/google-drive/projects/<p>/verify-and-repair-folders | grc.project.update | 入口鈕 :98 disabled＝`!canEdit`（同上 manager 判斷） | 無判斷 | 共用（被 ProjectSettingsView.vue、ProjectPlanningView.vue〔v-if driveConnected〕用），判斷放 props 由頁面傳入；確認 dialog 內按鈕本身未再看 canEdit |
| src/components/grc/project/ProjectParticipantsPanel.vue:103 | 專案參與人員「儲存」 | PUT /grc/project/<uid> | grc.project.update | `canEdit`（同上） | 無判斷 | 共用（被 ProjectSettingsView.vue、ProjectPlanningView.vue 用），判斷放 props 由頁面傳入；ParticipantPicker 增刪改角色只改本地 v-model，靠此鈕送出 |
| src/views/module_frame/ModuleFrame.vue:199 | 列動作 › 啟動專案（Menu model，:1454 渲染） | POST /oscal-project/start | grc.project.create | `rowData.status === 'published'`；disabled 另看 `!rowData.enable` | 無判斷 | 入口鈕；送出在 ProjectCreateView.vue（router push project-create 帶 profile_uid） |
| src/views/project-summary-report/ProjectSummaryReportForm.vue:319 | `summary_report_manage.export_report`（匯出 PDF） | GET /project-summary-report/export/<uid>（之前先 PUT /project-summary-report/<uid> 存檔） | grc.summary-report.export、grc.summary-report.update | — | 無判斷 | 一鍵打兩支：先 onSaveClick(false) 再匯出；PUT 失敗被 catch 吞掉仍續匯出（interceptor 仍會跳錯），故只有 export 也匯得出；僅 ProjectSummaryReportTabView 用 |
| src/views/project-summary-report/ProjectSummaryReportForm.vue:337 | `common.save`（儲存報告） | PUT /project-summary-report/<uid> | grc.summary-report.update | — | 無判斷 | 只有 writeDisabled()；僅 ProjectSummaryReportTabView 用 |
| src/views/project-summary-report/ProjectSummaryReportHistory.vue:145 | `revert_this_version`（還原此版本） | POST /project-summary-report/history-revert | grc.summary-report.update | i != 0（非目前版本） | 無判斷 | 只有 writeDisabled()；走 confirm.require |
| src/views/project-summary-report/ProjectSummaryReportManage.vue:243 | 列內 `common.delete`（刪除報告） | DELETE /project-summary-report/<uid> | grc.summary-report.delete | — | 無判斷 | 只有 writeDisabled()；走 confirm.require |
| src/views/project-summary-report/ProjectSummaryReportManage.vue:324 | `common.save`（新增報告 dialog 儲存） | POST /project-summary-report | grc.summary-report.create | — | 無判斷 | 只有 writeDisabled()（授權唯讀）非能力點；入口鈕 :233 開同檔 dialog |
| src/views/project/MyTasksView.vue:649 | `lang.my_tasks.batch_complete_btn`（批次完成任務，dialog 確定） | POST /grc/project/<pid>/jobs/batch-complete | grc.job.update | — | 無判斷 | 入口鈕 :427（同檔，`selectedJobs.length > 0` 才出現）；可勾選列受 canComplete（PROCESSING 且有附件／問卷已完成）限制；依 project 分組逐支送；僅 MyTasksView 用 |
| src/views/project/ProjectApListView.vue:282 | `lang.project_ap_list.btn_launch_reverify`（發起覆核） | POST /audit-round/<rid>/launch-reverify | grc.audit.create | `data.status === 'pending_reverify' && isProjectManager && !reverifyLaunched(data)` | 無判斷 | isProjectManager＝當前使用者在 project.participants 的 role==='manager'；僅 ProjectApListView 用 |
| src/views/project/ProjectApListView.vue:331 | `lang.project_ap_list.btn_new_round`（建輪次 dialog 送出） | POST /projects/<pid>/audit-rounds | grc.audit.create | `isProjectManager && ALLOW_MANUAL_ROUND_CREATE`（入口 :210；入口另 disabled 於 hasActiveRound） | 無判斷 | 入口鈕 :215 同檔；`ALLOW_MANUAL_ROUND_CREATE = false` 常數寫死，目前整個入口隱藏 |
| src/views/project/ProjectAuditorOverview.vue:1152 | `lang.project_planning.btn_start_task_execution`（啟動證據蒐集任務） | POST /grc/project/<pid>/start-task-execution | grc.job.assign | isProjectManager && selectedApUid && selectedAp?.status === 'planning' | 無判斷 | 頁面（route view）；另有 writeDisabled() 授權唯讀反灰；isProjectManager＝當前使用者在 project.participants 中 role==='manager' |
| src/views/project/ProjectAuditorOverview.vue:1711 | `lang.auditor_overview.btn_mark_reviewed`／`btn_reviewed`（群組詳情內控制項列：標記已審閱／取消審閱） | POST（標記）／DELETE（取消）/grc/project/<pid>/ap/<ap>/control/<c>/review | grc.audit.approve | canReviewControl(selectedGroup.id, ctrl.id)（輪次未關閉、非純 auditor、且在專案／群組／控制項層為 manager 或 reviewer）；disabled：未審閱且控制項非 completed，或已審閱但非本人審閱 | 無判斷 | 頁面（route view）；同一顆鈕依狀態切 POST/DELETE，同權限 |
| src/views/project/ProjectAuditorOverview.vue:1817 | AO 審閱 icon 鈕（群組詳情內 AO 列：標記已審閱／取消審閱） | POST／DELETE /grc/project/<pid>/ap/<ap>/control/<c>/ao/<ao>/review | grc.audit.approve | canReviewControl(selectedGroup.id, ctrl.id)；disabled：未審閱且 AO 非 completed，或已審閱但非本人審閱 | 無判斷 | 頁面（route view）；icon-only 鈕無 label |
| src/views/project/ProjectAuditorOverview.vue:1894 | `lang.auditor_overview.btn_mark_reviewed`／`btn_reviewed`（控制項詳情頁首：標記已審閱／取消審閱） | POST／DELETE /grc/project/<pid>/ap/<ap>/control/<c>/review | grc.audit.approve | canReviewControl(selectionGroupId, selectedControl.id)；disabled 同 1711 | 無判斷 | 頁面（route view） |
| src/views/project/ProjectAuditorOverview.vue:2009 | AO 審閱 icon 鈕（控制項詳情內 AO 列） | POST／DELETE /grc/project/<pid>/ap/<ap>/control/<c>/ao/<ao>/review | grc.audit.approve | canReviewControl(selectionGroupId, selectedControl.id)；disabled 同 1817 | 無判斷 | 頁面（route view） |
| src/views/project/ProjectCreateView.vue:499 | `lang.project_create.btn_create`（建立專案） | POST /oscal-project/start | grc.project.create | — | 無判斷 | 頁面入口在 ProjectListView :259（已用 pagePermission CREATE 擋）；本頁送出鈕本身無判斷；僅 ProjectCreateView 用 |
| src/views/project/ProjectListView.vue:254 | `lang.project_list.btn_batch_delete`（批次刪除專案） | POST /grc/projects/batch-delete | grc.project.delete | `selectedProjects.length > 0` | 已有但名不對 | 用 `getUserPagePermissions('/project/projects').includes('DELETE')`（頁面節點 action 比對，對應舊名 project.delete） |
| src/views/project/ProjectListView.vue:259 | `lang.project_list.btn_new_project`（新增專案） | POST /oscal-project/start | grc.project.create | — | 已有但名不對 | 入口鈕；送出在 ProjectCreateView.vue（路由跳頁）。用 `pagePermission.includes('CREATE')`（舊名 project.create） |
| src/views/project/ProjectListView.vue:364 | icon pi-google（同步到 Google Drive） | POST /integrations/google-drive/projects/<p>/init-folders | grc.project.update | `driveConnected && !data.drive_synced && (data.owner.id === userInfo.uid \|\| isProjectManager(data))` | 無判斷 | isProjectManager(data)＝該列 participants 有自己且 role==='manager'；僅 ProjectListView 用 |
| src/views/project/ProjectListView.vue:375 | icon pi-trash（刪除專案，列內） | DELETE /grc/project/<uid> | grc.project.delete | `data.owner.id === userInfo.uid \|\| isProjectManager(data)` | 已有但名不對 | 用 `pagePermission.includes('DELETE')`（舊名 project.delete） |
| src/views/project/ProjectListView.vue:414 | `lang.common.save`（編輯專案 dialog 儲存） | PUT /grc/project/<uid> | grc.project.update | `data.owner.id === userInfo.uid \|\| isProjectManager(data)`（掛在入口鉛筆 :370） | 已有但名不對 | 判斷掛在入口鉛筆鈕 :370 `pagePermission.includes('UPDATE')`（舊名 project.update），dialog 儲存鈕本身無判斷 |
| src/views/project/ProjectPlanningView.vue:241 | `lang.project_planning.btn_import_jobs`（批次維護選單：匯入任務 Excel） | POST /grc/project/<pid>/ap/<ap>/jobs/import（＋/import/validate、/import/confirm） | grc.job.import | 選單鈕 v-if="sspUid"；item disabled：writeDisabled(!canEditSsp) | 無判斷 | 入口鈕；送出在 JobImportDialog.vue；Menu model item |
| src/views/project/ProjectPlanningView.vue:247 | `lang.project_planning.btn_export_jobs`（批次維護選單：匯出任務 Excel） | GET /grc/project/<pid>/ap/<ap>/jobs/export | grc.job.export | 選單鈕 v-if="sspUid" | 無判斷 | Menu model item；舊名 project.export（BE 今天有守、FE 沒判） |
| src/views/project/ProjectPlanningView.vue:1965 | `lang.project_planning.btn_start_task_execution`（啟動證據蒐集任務） | POST /grc/project/<pid>/start-task-execution | grc.job.assign | isProjectManager && canEditSsp（canEditSsp＝輪次 apStatus==='planning'） | 無判斷 | 頁面（route view）；另有 writeDisabled() |
| src/views/project/ProjectPlanningView.vue:2462 | `lang.task_setup.quick_config_confirm`（快速配置 Dialog 確定：批次加指派人） | PUT /grc/project/<pid>/job/<job>（逐筆） | grc.job.update | 入口鈕（1978）v-if="canEdit && canEditSsp"（canEdit＝輪次未關閉且 planning，且為專案／群組／控制項層 manager） | 無判斷 | Dialog 在同檔；入口鈕 1978 另有 writeDisabled()；語意是指派但打的是 job PUT |
| src/views/project/RoundApAuthoringView.vue:664 | `lang.ap_docx_import.entry_button`（匯入稽核計畫 docx） | POST /ap/<ap>/docx-imports/parse | grc.audit.import | `canImportDocx`（= !readonly && apUid；readonly = !(roundStatus==='audit_planning' && (isAuditor \|\| isProjectManager))） | 無判斷 | 入口鈕；送出在 ApDocxImportDialog.vue（後續 GET/DELETE /ap-docx-import/<parse>、POST /ap-docx-import/<parse>/confirm 同為 grc.audit.import）；ApDocxImportDialog 僅 RoundApAuthoringView 用 |
| src/views/project/RoundApAuthoringView.vue:685 | `lang.ap_authoring.btn_save_tasks`（儲存行程） | PUT /ap/<ap>/tasks | grc.audit.update | `!readonly`（readonly 定義同上） | 無判斷 | 同區「新增行程」:683、刪除行程 :745 只改本地陣列，靠此鈕存檔；僅 RoundApAuthoringView 用 |
| src/views/project/RoundApAuthoringView.vue:871 | `lang.common.confirm`（新增稽核人員 dialog 確定） | POST /ap/<ap>/parties | grc.audit.update | `!readonly`（入口 :793 MultiSelect footer 以 writeDisabled(readonly) 反灰） | 無判斷 | 入口鈕 :793 同檔 |
| src/views/project/RoundAuditReviewView.vue:721 | `lang.ar_import.entry_button`（匯入稽核紀錄 xlsx） | POST /audit-round/<rid>/ar-imports/parse | grc.audit.import | `canImportAr`（= roundStatus==='auditing' && (isAuditor \|\| isProjectManager) && stats.total > 0） | 無判斷 | 入口鈕；送出在 ArImportDialog.vue（後續 POST /ar-import/<parse>/confirm、DELETE /ar-import/<parse> 同為 grc.audit.import）；ArImportDialog 僅 RoundAuditReviewView 用 |
| src/views/project/RoundAuditReviewView.vue:779 | `lang.round_audit.save`（儲存控制項判定） | PUT /audit-round/<rid>/ar/finding/<f>；判定 not_met 時另打 PUT /audit-round/<rid>/ar/risk/<r> 或 POST /audit-round/<rid>/ar/risks ＋ PUT /audit-round/<rid>/ar/risk/<r>/findings | grc.audit.update、grc.audit.create | `canEdit`（= roundStatus==='auditing'） | 無判斷 | 需 hasAny（not_met 且尚無風險時會新建風險＝create）；僅 RoundAuditReviewView 用 |
| src/views/project/RoundAuditReviewView.vue:811 | `lang.round_audit.obs_update`（更新觀察） | PUT /audit-round/<rid>/ar/observation/<o> | grc.audit.update | `canEdit`（入口鉛筆 :819 掛 v-if="canEdit"） | 無判斷 | 入口鉛筆 :819 只切編輯模式 |
| src/views/project/RoundAuditReviewView.vue:822 | icon pi-trash（刪除觀察） | DELETE /audit-round/<rid>/ar/observation/<o> | grc.audit.delete | `canEdit` | 無判斷 |  |
| src/views/project/RoundAuditReviewView.vue:869 | `lang.round_audit.obs_save`（新增觀察） | POST /audit-round/<rid>/ar/observations ＋ PUT /audit-round/<rid>/ar/finding/<f> | grc.audit.create、grc.audit.update | `canEdit` | 無判斷 | 需 hasAny（建觀察後回寫 finding 掛 observation_uid）；入口「＋新增一筆觀察」:848 只展開表單 |
| src/views/project/RoundAuditReviewView.vue:890 | `lang.round_audit.add_risk`（新增風險） | POST /audit-round/<rid>/ar/risks ＋ PUT /audit-round/<rid>/ar/risk/<r>/findings | grc.audit.create、grc.audit.update | `canEdit` | 無判斷 | 需 hasAny（選了來源控制項才打 PUT findings） |
| src/views/project/RoundAuditReviewView.vue:904 | icon pi-trash（刪除風險） | DELETE /audit-round/<rid>/ar/risk/<r> | grc.audit.delete | `canEdit` | 無判斷 |  |
| src/views/project/RoundAuditReviewView.vue:922 | `lang.round_audit.save`（風險編輯儲存） | PUT /audit-round/<rid>/ar/risk/<r> ＋ PUT /audit-round/<rid>/ar/risk/<r>/findings | grc.audit.update | `canEdit`（入口鉛筆 :903） | 無判斷 | 入口鉛筆 :903 只切編輯模式 |
| src/views/project/RoundPoamView.vue:558 | icon pi-trash（刪除改善計畫） | DELETE /audit-round/<rid>/remediation/<m> | grc.audit.delete | `rem.lifecycle !== 'recommendation' && canEdit`（canEdit = roundStatus==='remediation'） | 無判斷 | 僅 RoundPoamView 用 |
| src/views/project/RoundPoamView.vue:577 | 里程碑狀態 chip（點擊循環切換狀態，即存） | PUT /audit-round/<rid>/poam-milestone/<ms> | grc.audit.update | `canEdit`（寫在 @click 守門與 class，非 v-if） | 無判斷 | chip 永遠顯示，canEdit 為 false 時點了不送 |
| src/views/project/RoundPoamView.vue:583 | icon pi-trash（刪除里程碑） | DELETE /audit-round/<rid>/poam-milestone/<ms> | grc.audit.delete | `canEdit` | 無判斷 |  |
| src/views/project/RoundPoamView.vue:604 | `lang.round_poam.btn_save_ms`（里程碑編輯儲存） | PUT /audit-round/<rid>/poam-milestone/<ms> | grc.audit.update | `canEdit`（入口鉛筆 :582） | 無判斷 | 入口鉛筆 :582 只切編輯模式 |
| src/views/project/RoundPoamView.vue:619 | `lang.round_poam.add_milestone`（新增里程碑，就地卡片送出） | POST /audit-round/<rid>/remediation/<m>/milestones | grc.audit.create | `canEdit`（入口 :569） | 無判斷 | 入口 :569 只長出草稿卡 |
| src/views/project/RoundPoamView.vue:702 | `lang.round_poam.add_plan`／`btn_save_ms`（改善計畫 dialog 新增／編輯送出） | POST /audit-round/<rid>/ar/risk/<r>/remediations | grc.audit.create | `canEdit`（入口 :628 新增 `canEdit`；:555 編輯 `rem.lifecycle !== 'recommendation' && canEdit`） | 無判斷 | 同一支 upsert API 兼新增與編輯（帶 uuid＝編輯），編輯也落在 grc.audit.create |
| src/views/survey-v2/SurveyPreview.vue:1281 | 儲存草稿 | POST /project-survey/answers/<uid>/checkpoint | grc.job.update | mode 為 fill/review 且有 taskSurveyUid && taskSurvey.status !== 9 && (canEdit \|\| canVerify) | 無判斷 | 填答頁（route survey-v2-fill，由 JobExecutionDrawer 開新分頁）；canEdit／canVerify 由 task-assignees 的 is_approver＋task_survey.status 推出；只有 writeDisabled |
| src/views/survey-v2/SurveyPreview.vue:1286 | 提交問卷 | POST /project-survey/answers/<uid>/checkpoint（status=2） | grc.job.update | mode 為 fill/review && [0,1].includes(taskSurvey.status) && canEdit && taskSurvey.approval_required | 無判斷 | 同上 |
| src/views/survey-v2/SurveyPreview.vue:1291 | 提交回饋（審核退回） | POST /project-survey/answers/<uid>/checkpoint（status=3） | grc.job.update | mode 為 fill/review && taskSurvey.status === 2 && canVerify | 無判斷 | canVerify＝本人是 approver 且狀態待審 |
| src/views/survey-v2/SurveyPreview.vue:1296 | 提交補充 | POST /project-survey/answers/<uid>/checkpoint（status=2） | grc.job.update | mode 為 fill/review && taskSurvey.status === 3 && canEdit | 無判斷 |  |
| src/views/survey-v2/SurveyPreview.vue:1301 | 結案 | POST /project-survey/answers/<uid>/checkpoint（status=9） | grc.job.update | mode 為 fill/review && ((taskSurvey.status === 2 && canVerify) \|\| (!taskSurvey.approval_required && canEdit)) | 無判斷 |  |
| src/views/survey-v2/SurveyPreview.vue:1706 | 檔案題上傳區（主題） | POST /file/upload | 免判斷（附錄 C） | !isDisabled(question.id)（fill 模式下＝canEdit 且非他人編輯中） | 免判斷 | 上傳後答案經 socket `update` 即時寫入，非 REST；拖放（:1713）同路 |
| src/views/survey-v2/SurveyPreview.vue:1938 | 檔案題上傳區（子題） | POST /file/upload | 免判斷（附錄 C） | !isDisabled(sub.id) | 免判斷 | 同上 |
| src/views/survey-v2/SurveyPreview.vue:2080 | 歷史紀錄「還原此版本」 | POST /project-survey/history/revert | grc.job.update | 側欄入口（:1309）isInteractiveMode && histories.length > 0 && userRole !== 'reviewer' && taskSurvey.status !== 2；本鈕 i !== 0 && taskSurvey.status !== 9 | 無判斷 |  |
| src/views/survey-v2/SurveyPreview.vue:2138 | 討論送出 | POST /survey-discussion | grc.job.update | 側欄入口（:1317）isInteractiveMode && taskSurveyUid | 無判斷 | InputText @keydown.enter 也會送出（:2137） |

### oscal（114 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/components/compliance-framework/article/FrameworkArticleContent.vue:140 | 群組名稱 inline「儲存」 | emit `update:group` → 由 FrameworkVersionEditView 處理：PUT /oscal-catalog-group/<uid>；FrameworkImportPage 處理：只寫本地草稿，最後由「確認匯入」POST /oscal-framework-parse-jobs/<uid>/confirm 送出 | oscal.framework.update（編輯頁）／oscal.framework.import（匯入頁） | `!readOnly && !isGroupDeleted(g.uid)`（入口鉛筆鈕條件） | 無判斷 | 共用（被 FrameworkImportPage.vue 直接用、FrameworkVersionEditView.vue 經 FrameworkArticlePane 用），判斷放 props（既有 readOnly）由頁面傳入；編輯頁 readOnly 今天＝!isPlatformAdmin \|\| 非 draft |
| src/components/compliance-framework/article/FrameworkArticleContent.vue:156 | 刪除群組（icon） | emit `delete:group` → FrameworkVersionEditView：DELETE /oscal-catalog-group/<uid>；FrameworkImportPage：本地標記刪除，送出走 confirm | oscal.framework.update（編輯頁）／oscal.framework.import（匯入頁） | `!readOnly && allowDelete && !isGroupDeleted(g.uid)`（編輯頁 allowDelete＝!readOnly && !tree.has_references） | 無判斷 | 共用，同上 |
| src/components/compliance-framework/article/FrameworkArticleContent.vue:186 | 控制項代碼／標題 inline「儲存」 | emit `update:control` → FrameworkVersionEditView：PUT /oscal-catalog-control/<uid>；FrameworkImportPage：本地草稿，送出走 confirm | oscal.framework.update（編輯頁）／oscal.framework.import（匯入頁） | `!readOnly` | 無判斷 | 共用，同上 |
| src/components/compliance-framework/article/FrameworkArticleContent.vue:197 | 刪除控制項（icon） | emit `delete:control` → FrameworkVersionEditView：DELETE /oscal-catalog-control/<uid>；FrameworkImportPage：本地標記刪除，送出走 confirm | oscal.framework.update（編輯頁）／oscal.framework.import（匯入頁） | `!readOnly && allowDelete && !isControlDeleted(c.uid)` | 無判斷 | 共用，同上 |
| src/components/compliance-framework/article/FrameworkArticleContent.vue:214 | 控制項描述 inline「儲存」 | emit `update:control` → FrameworkVersionEditView：PUT /oscal-catalog-control/<uid>；FrameworkImportPage：本地草稿，送出走 confirm | oscal.framework.update（編輯頁）／oscal.framework.import（匯入頁） | `!readOnly` | 無判斷 | 共用，同上 |
| src/components/compliance-framework/article/FrameworkArticleContent.vue:236 | 控制項 Guidance inline「儲存」 | emit `update:control` → FrameworkVersionEditView：PUT /oscal-catalog-control/<uid>；FrameworkImportPage：本地草稿，送出走 confirm | oscal.framework.update（編輯頁）／oscal.framework.import（匯入頁） | `!readOnly` | 無判斷 | 共用，同上 |
| src/components/compliance-framework/article/FrameworkArticleContent.vue:264 | 評估目標 inline「儲存」 | emit `update:assessment` → FrameworkVersionEditView：PUT /oscal-catalog-control-assessment/<uid>；FrameworkImportPage：本地草稿，送出走 confirm | oscal.framework.update（編輯頁）／oscal.framework.import（匯入頁） | `!readOnly` | 無判斷 | 共用，同上 |
| src/components/compliance-framework/article/FrameworkArticleContent.vue:276 | 刪除評估目標（icon） | emit `delete:assessment` → FrameworkVersionEditView：DELETE /oscal-catalog-control-assessment/<uid>；FrameworkImportPage：本地標記刪除，送出走 confirm | oscal.framework.update（編輯頁）／oscal.framework.import（匯入頁） | `!readOnly && allowDelete && !isAssessmentDeleted(ao.uid)` | 無判斷 | 共用，同上 |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:890 | 元件列「刪除」（垃圾桶） | DELETE /ssp/<ssp_uid>/components/<item_uid> | oscal.ssp.update | `!readonly`（SspTabPanel 傳 `!canEdit`；canEdit＝apStatus==='planning' && currentUserRole==='manager'） | 無判斷 | 共用（被 SspTabPanel.vue[apiBase=ssp]、ModuleFrameTemplateEditView.vue[apiBase=module-frame] 用），判斷放 props 由頁面傳入；此列為 SSP 情境 |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:890 | 元件列「刪除」（垃圾桶） | DELETE /module-frame/<uid>/components/<item_uid> | oscal.resource-library.update | — | 無判斷 | 同上；此列為資源庫（MF）情境，MF 端不傳 readonly |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:980 | 外部授權列「刪除」 | DELETE /ssp/<ssp_uid>/leveraged/<item_uid> | oscal.ssp.update | `!readonly`（同上 canEdit） | 無判斷 | 共用，SSP 情境 |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:980 | 外部授權列「刪除」 | DELETE /module-frame/<uid>/leveraged/<item_uid> | oscal.resource-library.update | — | 無判斷 | 共用，MF 情境 |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1101 | 設備資產列「刪除」 | DELETE /ssp/<ssp_uid>/inventory-items/<item_uid> | oscal.ssp.update | `!readonly`（同上 canEdit） | 無判斷 | 共用，SSP 情境 |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1101 | 設備資產列「刪除」 | DELETE /module-frame/<uid>/inventory/<item_uid> | oscal.resource-library.update | — | 無判斷 | 共用，MF 情境 |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1188 | 系統資產列「刪除」 | DELETE /ssp/<ssp_uid>/inventory-items/<item_uid> | oscal.ssp.update | `!readonly`（同上 canEdit） | 無判斷 | 共用，SSP 情境（系統資產與設備資產共用 inventory 端點） |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1188 | 系統資產列「刪除」 | DELETE /module-frame/<uid>/inventory/<item_uid> | oscal.resource-library.update | — | 無判斷 | 共用，MF 情境 |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1278 | 元件 dialog「儲存」（新增／編輯） | POST /ssp/<ssp_uid>/components、PUT /ssp/<ssp_uid>/components/<item_uid> | oscal.ssp.update | 入口鈕 `!readonly`（同上 canEdit） | 無判斷 | 共用，SSP 情境；新增鈕 :813／編輯鈕 :886 開同檔 dialog |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1278 | 元件 dialog「儲存」（新增／編輯） | POST /module-frame/<uid>/components、PUT /module-frame/<uid>/components/<item_uid> | oscal.resource-library.update | — | 無判斷 | 共用，MF 情境 |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1341 | 外部授權 dialog「儲存」 | POST /ssp/<ssp_uid>/leveraged、PUT /ssp/<ssp_uid>/leveraged/<item_uid> | oscal.ssp.update | 入口鈕 `!readonly`（同上 canEdit） | 無判斷 | 共用，SSP 情境；新增鈕 :907／編輯鈕 :976 開同檔 dialog |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1341 | 外部授權 dialog「儲存」 | POST /module-frame/<uid>/leveraged、PUT /module-frame/<uid>/leveraged/<item_uid> | oscal.resource-library.update | — | 無判斷 | 共用，MF 情境 |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1437 | 設備資產 dialog「儲存」 | POST /ssp/<ssp_uid>/inventory-items、PUT /ssp/<ssp_uid>/inventory-items/<item_uid> | oscal.ssp.update | 入口鈕 `!readonly`（同上 canEdit） | 無判斷 | 共用，SSP 情境；新增鈕 :997／編輯鈕 :1097 開同檔 dialog |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1437 | 設備資產 dialog「儲存」 | POST /module-frame/<uid>/inventory、PUT /module-frame/<uid>/inventory/<item_uid> | oscal.resource-library.update | — | 無判斷 | 共用，MF 情境 |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1533 | 系統資產 dialog「儲存」 | POST /ssp/<ssp_uid>/inventory-items、PUT /ssp/<ssp_uid>/inventory-items/<item_uid> | oscal.ssp.update | 入口鈕 `!readonly`（同上 canEdit） | 無判斷 | 共用，SSP 情境；新增鈕 :1118／編輯鈕 :1184 開同檔 dialog |
| src/components/grc/ComponentsLeveragedInventoryTab.vue:1533 | 系統資產 dialog「儲存」 | POST /module-frame/<uid>/inventory、PUT /module-frame/<uid>/inventory/<item_uid> | oscal.resource-library.update | — | 無判斷 | 共用，MF 情境 |
| src/components/grc/DocumentLinkDialog.vue:343 | 「確認關聯」（程序書勾選送出） | POST /ssp/<ssp_uid>/control-implementation/<ci>/document-mappings、DELETE /ssp/<ssp_uid>.../document-mapping/<doc_uid>（AO 層改打 .../objective/<si>/document-mappings、.../objective/<si>/document-mapping/<doc_uid>） | oscal.ssp.update | —（入口鈕在 ReferenceDocumentList，受 canEditSsp 控） | 無判斷 | 僅 ProjectPlanningView.vue 用 |
| src/components/grc/DocumentPoolPanel.vue:250 | 程序書池「上傳」 | POST /file/upload（豁免）＋ POST /ssp/<ssp_uid>/document-pool | oscal.ssp.update | `!readOnly`（父層傳 `!canEditSsp`；canEditSsp＝apStatus==='planning'） | 無判斷 | 僅 ProjectPlanningView.vue 用（Tab＋Dialog 兩個實例）；FileUpload 拖拉區 :263 同一路徑 |
| src/components/grc/DocumentPoolPanel.vue:351 | 程序書「下載」 | GET /file/access-token/<uid> → GET /file/download/<uid> | 免判斷（附錄 C） | — | 免判斷 | 僅 ProjectPlanningView.vue 用 |
| src/components/grc/DocumentPoolPanel.vue:359 | 程序書「刪除」 | DELETE /ssp/<ssp_uid>/document-pool/<doc_uid> | oscal.ssp.update | `!readOnly`（同上 canEditSsp） | 無判斷 | 僅 ProjectPlanningView.vue 用 |
| src/components/grc/DownloadSspBlankTemplateDialog.vue:131 | 「下載空白 SSP 匯入範本」 | GET /ssp-import-template?framework_version_uid=&mode=blank | oscal.resource-library.read | — | 無判斷 | 僅 ModuleFrame.vue 用（入口為列表頁 menu item :76） |
| src/components/grc/EditIncludeControlsDialog.vue:317 | 編輯納入控制項「儲存」 | PUT /module-frame/<uid> | oscal.resource-library.update | — | 無判斷 | 僅 ModuleFrameTemplateEditView.vue 用 |
| src/components/grc/ModuleFrameBasicInfoPanel.vue:198 | 「分享給子租戶」開關（InputSwitch） | PUT /module-frame/<uid> | oscal.resource-library.update | `canShare`（moduleFrame.tenant_id === currentTenant，資源所屬租戶才顯示） | 無判斷 | 共用（被 ModuleFrameBasicInfoDialog.vue〔ModuleFrame.vue 用〕、ModuleFrameTemplateEditView.vue 用），判斷放 props 由頁面傳入 |
| src/components/grc/ModuleFrameBasicInfoPanel.vue:211 | 基本資料「儲存」 | PUT /module-frame/<uid> | oscal.resource-library.update | — | 無判斷 | 共用（同上） |
| src/components/grc/ModuleFrameDocumentPoolPanel.vue:279 | 參考文件「上傳」 | POST /file/upload（豁免）＋ POST /module-frame/<uid>/reference-documents | oscal.resource-library.update | — | 無判斷 | 僅 ModuleFrameTemplateEditView.vue 用（api-base=module-frame；ssp 分支無人使用）；FileUpload 拖拉區 :291 同一路徑 |
| src/components/grc/ModuleFrameDocumentPoolPanel.vue:378 | 參考文件「下載」 | GET /file/access-token/<uid> → GET /file/download/<uid> | 免判斷（附錄 C） | — | 免判斷 | 僅 ModuleFrameTemplateEditView.vue 用 |
| src/components/grc/ModuleFrameDocumentPoolPanel.vue:386 | 參考文件「刪除」 | DELETE /module-frame/<uid>/reference-documents/<doc_uid> | oscal.resource-library.update | — | 無判斷 | 僅 ModuleFrameTemplateEditView.vue 用 |
| src/components/grc/ModuleFramePartiesPanel.vue:440 | 責任單位／人員列「刪除」 | DELETE /ssp/<ssp_uid>/parties/<party_uid> | oscal.ssp.update | `!readonly`（SspTabPanel 傳 `!canEdit`；canEdit＝apStatus==='planning' && currentUserRole==='manager'） | 無判斷 | 共用（被 SspTabPanel.vue[ssp]、ModuleFrameTemplateEditView.vue[module-frame]、ModuleFramePartiesDialog.vue〔無人引用〕用），判斷放 props 由頁面傳入；SSP 情境 |
| src/components/grc/ModuleFramePartiesPanel.vue:440 | 責任單位／人員列「刪除」 | DELETE /module-frame/<uid>/parties/<party_uid> | oscal.resource-library.update | — | 無判斷 | 共用，MF 情境 |
| src/components/grc/ModuleFramePartiesPanel.vue:524 | 責任單位／人員 dialog「儲存」 | POST /ssp/<ssp_uid>/parties、PUT /ssp/<ssp_uid>/parties/<party_uid> | oscal.ssp.update | 入口鈕 `!readonly`（同上 canEdit） | 無判斷 | 共用，SSP 情境；新增鈕 :380／編輯鈕 :439 開同檔 dialog |
| src/components/grc/ModuleFramePartiesPanel.vue:524 | 責任單位／人員 dialog「儲存」 | POST /module-frame/<uid>/parties、PUT /module-frame/<uid>/parties/<party_uid> | oscal.resource-library.update | — | 無判斷 | 共用，MF 情境 |
| src/components/grc/ModuleFrameSspDevicesPanel.vue:282 | 設備列「刪除」 | DELETE /module-frame/<uid>/ssp-resources/items/<item_uid> | oscal.resource-library.update | — | 無判斷 | 僅 ModuleFrameSspResourcesDialog.vue 用，而該 dialog 無人引用（死碼）；ssp 分支對應 DELETE /ssp/<ssp_uid>/ssp-resources/items/<item_uid>＝oscal.ssp.update，亦無人使用 |
| src/components/grc/ModuleFrameSspDevicesPanel.vue:328 | 設備 dialog「儲存」 | POST /module-frame/<uid>/ssp-resources/items、PUT /module-frame/<uid>/ssp-resources/items/<item_uid> | oscal.resource-library.update | — | 無判斷 | 同上（死碼） |
| src/components/grc/ModuleFrameSspInfoSystemsPanel.vue:278 | 資訊系統列「刪除」 | DELETE /module-frame/<uid>/ssp-resources/items/<item_uid> | oscal.resource-library.update | — | 無判斷 | 同 DevicesPanel，僅掛在無人引用的 ModuleFrameSspResourcesDialog.vue（死碼） |
| src/components/grc/ModuleFrameSspInfoSystemsPanel.vue:328 | 資訊系統 dialog「儲存」 | POST /module-frame/<uid>/ssp-resources/items、PUT /module-frame/<uid>/ssp-resources/items/<item_uid> | oscal.resource-library.update | — | 無判斷 | 同上（死碼） |
| src/components/grc/ModuleFrameTemplateImportDialog.vue:390 | `import_upload_btn`（上傳並驗證 Excel） | POST /file/upload → POST /module-frame/<uid>/control-defaults/import/template → POST /module-frame/<uid>/control-defaults/import/verify | oscal.resource-library.update | — | 無判斷 | /file/upload 屬附錄 C 豁免，取後兩支（同權限）；僅 ModuleFrameTemplateEditView.vue 用（入口在該檔 menu command:227） |
| src/components/grc/ModuleFrameTemplateImportDialog.vue:533 | `import_btn_revalidate`（重新驗證） | POST /module-frame/<uid>/control-defaults/import/validate | oscal.resource-library.update | `hasEdited`（有改過才可按） | 無判斷 | 只驗證不寫入，但 BE 以 update 守；僅 ModuleFrameTemplateEditView.vue 用 |
| src/components/grc/ModuleFrameTemplateImportDialog.vue:538 | `import_btn_confirm_all`（確認匯入） | POST /module-frame/<uid>/control-defaults/import | oscal.resource-library.update | `validItems.length > 0 && !hasEdited` | 無判斷 | 既有 writeDisabled 是授權唯讀（license），非能力點；僅 ModuleFrameTemplateEditView.vue 用 |
| src/components/grc/ReferenceDocumentList.vue:187 | 「關聯程序書」 | emit `link` → 由 ProjectPlanningView.vue handleLinkRefDoc 開 DocumentLinkDialog → POST /ssp/<ssp_uid>/control-implementation/<ci>/document-mappings（AO 層 .../objective/<si>/document-mappings） | oscal.ssp.update | `linkMode && !readonly`（ProjectPlanningView 傳 readonly＝`writeDisabled(!canEditSsp)`；canEditSsp＝apStatus==='planning'） | 無判斷 | 入口鈕；送出在 DocumentLinkDialog.vue。共用（被 ProjectPlanningView.vue、AuditControlRef.vue、ProjectAuditorOverview.vue 用；後兩者固定 readonly），判斷放 props 由頁面傳入 |
| src/components/grc/ReferenceDocumentList.vue:213 | 參考程序書「下載」 | GET /file/access-token/<uid> → GET /file/download/<uid> | 免判斷（附錄 C） | — | 免判斷 | 共用（同上） |
| src/components/grc/ReferenceDocumentList.vue:221 | 「取消關聯」 | emit `unlink` → 由 ProjectPlanningView.vue handleUnlinkRefDoc 處理 → DELETE /ssp/<ssp_uid>.../document-mapping/<doc_uid>（AO 層 .../objective/<si>/document-mapping/<doc_uid>） | oscal.ssp.update | `!readonly && linkMode`（同上 canEditSsp） | 無判斷 | 共用（同上） |
| src/components/grc/SspImportDialog.vue:324 | 匯入實作描述「上傳」 | POST /ssp/<ssp_uid>/control-implementations/import | oscal.ssp.import | —（入口 menu item 在 ProjectPlanningView :226，disabled＝`writeDisabled(!canEditSsp)`） | 無判斷 | 僅 ProjectPlanningView.vue 用 |
| src/components/grc/SspImportDialog.vue:450 | 「重新驗證」 | POST /ssp/<ssp_uid>/control-implementations/import/validate | oscal.ssp.import | —（同上） | 無判斷 | 僅 ProjectPlanningView.vue 用 |
| src/components/grc/SspImportDialog.vue:455 | 「確認匯入」 | POST /ssp/<ssp_uid>/control-implementations/import/confirm | oscal.ssp.import | —（同上） | 無判斷 | 僅 ProjectPlanningView.vue 用 |
| src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue:941 | `lang.ssp_docx_import.confirm`（確認匯入 (N)） | create：POST /oscal/resource-libraries ＋ POST /ssp-docx-import/<parse_uid>/confirm；update-mf／update-ssp：POST /ssp-docx-import/<parse_uid>/confirm | 依 props.mode（通則 C）：update-ssp→oscal.ssp.import；update-mf→oscal.resource-library.update；create→oscal.resource-library.create | step===3 | 無判斷 | 共用（同上兩宿主），判斷放 props 由頁面傳入；create 模式打兩支寫入但同為 oscal.resource-library.create；只有 writeDisabled；所有 diff／sections 子元件的決策與編輯最終都經此鈕送出 |
| src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue:1023 | dropzone（`lang.ssp_docx_import.dropzone_hint`，點擊／拖放即上傳解析） | POST /ssp-docx-imports/parse | 依 props.mode（通則 C）：update-ssp→oscal.ssp.import；update-mf→oscal.resource-library.update；create→oscal.resource-library.create | — | 無判斷 | 共用（被 views/project/ImportDocxPage.vue〔mode=update-ssp〕、views/module_frame/ImportDocxPage.vue〔mode=create／update-mf〕用），判斷放 props 由頁面傳入；本身只有 canUpload（選版本）條件 |
| src/components/grc/ssp-excel-import/EntityPickerDialog.vue:143 | dialog `common.confirm`（選現有實體） | emit `pick` → 由 RowActionMenu.vue onPick 寫 store → ImportExcelPreviewPage confirm 送出 | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 依匯入來源擇一；僅 RowActionMenu.vue 用（死碼鏈） |
| src/components/grc/ssp-excel-import/InlineCreateDialog.vue:171 | dialog `common.confirm`（就地新建設備／系統／單位／使用者） | emit `save` → 由 RowActionMenu.vue onCreate 寫 store.inlineCreates → ImportExcelPreviewPage confirm 送出（BE 確認時建實體） | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 依匯入來源擇一；僅 RowActionMenu.vue 用（死碼鏈）；若復活，確認時會順帶新建使用者／設備，可能需另判 iam／asset 權限 |
| src/components/grc/ssp-excel-import/RowActionMenu.vue:102 | Menu item `skip_as_text`／`reset`（script 55、61 行） | 本身不打 API：store.setDecision → 由 ImportExcelPreviewPage confirm 送 POST /ssp-excel-import/<parse_uid>/confirm | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 依匯入來源擇一；只被 SheetPreviewDevices／SheetPreviewInfoSystems 用（兩者皆死碼）→ 連帶死碼 |
| src/components/grc/ssp-excel-import/SheetPreviewComponents.vue:225 | 列內 `pi-trash` 移除／216 復原（元件清冊） | 本身不打 API：寫 store.removedRows → 由 ImportExcelPreviewPage.vue onConfirm 送 POST /ssp-excel-import/<parse_uid>/confirm | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 依匯入來源擇一（通則 C），非 hasAny；預覽暫存動作，整頁入口與確認鈕已需匯入權；僅 ImportExcelPreviewPage.vue 用 |
| src/components/grc/ssp-excel-import/SheetPreviewDevices.vue:137 | 列內 `pi-trash` 移除／114 復原（設備） | 同上（store → ImportExcelPreviewPage confirm） | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 死碼：ImportExcelPreviewPage 已不掛此元件，全專案無 import |
| src/components/grc/ssp-excel-import/SheetPreviewInfoSystems.vue:117 | 列內 `pi-trash` 移除／94 復原（資訊系統） | 同上（store → ImportExcelPreviewPage confirm） | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 死碼：全專案無 import |
| src/components/grc/ssp-excel-import/SheetPreviewInventoryItems.vue:226 | 列內 `pi-trash` 移除／217 復原（資產清冊） | 同上（store → ImportExcelPreviewPage confirm） | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 依匯入來源擇一；僅 ImportExcelPreviewPage.vue 用 |
| src/components/grc/ssp-excel-import/SheetPreviewLeveraged.vue:204 | 列內 `pi-trash` 移除／195 復原（外部利用服務） | 同上（store → ImportExcelPreviewPage confirm） | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 死碼：ImportExcelPreviewPage 已改用 SheetPreviewLeveragedAuth，全專案無 import |
| src/components/grc/ssp-excel-import/SheetPreviewLeveragedAuth.vue:205 | 列內 `pi-trash` 移除／196 復原（外部授權） | 同上（store → ImportExcelPreviewPage confirm） | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 依匯入來源擇一；僅 ImportExcelPreviewPage.vue 用 |
| src/components/grc/ssp-excel-import/SheetPreviewParties.vue:200 | 組織列 `pi-trash` 移除／191 復原 | 同上（store → ImportExcelPreviewPage confirm） | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 依匯入來源擇一；僅 ImportExcelPreviewPage.vue 用 |
| src/components/grc/ssp-excel-import/SheetPreviewParties.vue:324 | 人員列 `pi-trash` 移除／315 復原 | 同上（store → ImportExcelPreviewPage confirm） | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 依匯入來源擇一；僅 ImportExcelPreviewPage.vue 用 |
| src/components/grc/ssp-excel-import/SheetPreviewSystemAssets.vue:170 | 列內 `pi-trash` 移除／161 復原（系統資產） | 同上（store → ImportExcelPreviewPage confirm） | oscal.ssp.import、oscal.resource-library.update、oscal.resource-library.create | — | 無判斷 | 依匯入來源擇一；僅 ImportExcelPreviewPage.vue 用 |
| src/components/grc/ssp/SspBasicSection.vue:317 | `common.save`（儲存受評標的） | PUT /ssp/<uid>/system-characteristic 或 PUT /module-frame/<uid>/system-characteristic（依 apiBase） | oscal.ssp.update、oscal.resource-library.update | `!readonly`（SspTabPanel 傳 `!canEdit`＝apStatus==='planning' && currentUserRole==='manager'；MF 頁不傳＝永遠可見） | 無判斷 | 依 apiBase 擇一（ssp→oscal.ssp.update；module-frame→oscal.resource-library.update），非 hasAny；共用（被 SspTabPanel.vue、ModuleFrameTemplateEditView.vue 用），判斷放 props 由頁面傳入 |
| src/components/grc/ssp/SspLeveragedSection.vue:288 | 列內 `pi-trash`（刪除外部利用服務） | DELETE /ssp/<uid>/leveraged/<item_uid> 或 DELETE /module-frame/<uid>/leveraged/<item_uid>（依 apiBase） | oscal.ssp.update、oscal.resource-library.update | `!readonly`（操作欄 v-if） | 無判斷 | 依 apiBase 擇一；**全專案無任何檔 import 此元件（死碼）** |
| src/components/grc/ssp/SspLeveragedSection.vue:374 | dialog `common.save`（新增／編輯外部利用服務） | POST /ssp/<uid>/leveraged、PUT /ssp/<uid>/leveraged/<item_uid>（module-frame 版同形） | oscal.ssp.update、oscal.resource-library.update | 入口「新增外部服務」235／列內編輯 287 皆 `!readonly` | 無判斷 | 依 apiBase 擇一；入口鈕在同檔不另列；死碼（無引用者） |
| src/components/grc/ssp/SspTabPanel.vue:315 | `btn_download_ssp_import_template_filled`（下載已填 SSP Excel） | GET /ssp/<ssp_uid>/excel-template?mode=filled | oscal.ssp.export | —（僅 `v-if="sspUid"` 外層） | 無判斷 | 舊名 module-frame.read；非 manager 也看得到；僅 ProjectPlanningView.vue 用 |
| src/components/grc/ssp/SspTabPanel.vue:452 | dialog `parse_button`（解析檔案＝上傳 SSP Excel） | POST /ssp/<ssp_uid>/excel-import/upload | oscal.ssp.import | `canEdit`（入口鈕 322 v-if；canEdit＝apStatus==='planning' && currentUserRole==='manager'） | 無判斷 | 入口鈕 322 開同檔 dialog；成功後導 ImportExcelPreviewPage；僅 ProjectPlanningView.vue 用 |
| src/views/compliance-framework/ComplianceFrameworkManage.vue:412 | 批次刪除框架 | DELETE /oscal-framework/<uid>（逐筆） | oscal.framework.delete | `selectedFrameworks.length > 0`（published 自動剔除） | 無判斷 | 今用 isPlatformAdmin（租戶路徑判斷，非能力點）；勾選欄 :423 同條件；僅 ComplianceFrameworkManage 用 |
| src/views/compliance-framework/ComplianceFrameworkManage.vue:476 | 列選單「刪除」（model 定義 :353） | DELETE /oscal-framework/<uid> | oscal.framework.delete | `isDeletableFramework(data)`（publish_status !== 'published'） | 無判斷 | 今用 isPlatformAdmin；僅 ComplianceFrameworkManage 用 |
| src/views/compliance-framework/ComplianceFrameworkManage.vue:544 | 新增／編輯框架 Dialog「儲存」 | POST /oscal-framework（新增）／PUT /oscal-framework/<uid>（編輯） | oscal.framework.create（新增）／oscal.framework.update（編輯） | — | 無判斷 | 依模式二擇一；入口「新增」:418 與選單「編輯」:348 今用 isPlatformAdmin；僅 ComplianceFrameworkManage 用 |
| src/views/compliance-framework/ComplianceFrameworkVersionManage.vue:600 | 批次刪除版本 | DELETE /oscal-framework-version/<uid>（逐筆） | oscal.framework.update | `selectedVersions.length > 0`（published 自動剔除） | 無判斷 | 今用 isPlatformAdmin；勾選欄 :612 同條件；僅 ComplianceFrameworkVersionManage 用 |
| src/views/compliance-framework/ComplianceFrameworkVersionManage.vue:607 | 匯入版本（入口鈕） | POST /oscal-framework-parse-jobs/parse | oscal.framework.import | — | 無判斷 | 入口鈕；送出在 FrameworkImportPage.vue（route 跳轉）；今用 isPlatformAdmin |
| src/views/compliance-framework/ComplianceFrameworkVersionManage.vue:649 | 列選單「編輯內容」（model 定義 :465） | PUT／DELETE /oscal-catalog-group\|control\|control-assessment/<uid> | oscal.framework.update | — | 無判斷 | 入口鈕；送出在 FrameworkVersionEditView.vue（route 跳轉；該頁 readOnly 也看 isPlatformAdmin）；今選單項以 isPlatformAdmin 決定是否出現 |
| src/views/compliance-framework/ComplianceFrameworkVersionManage.vue:649 | 列選單「刪除」（model 定義 :473） | DELETE /oscal-framework-version/<uid> | oscal.framework.update | `isDeletableVersion(data)`（publish_status !== 'published'） | 無判斷 | 今用 isPlatformAdmin |
| src/views/compliance-framework/ComplianceFrameworkVersionManage.vue:649 | 列選單「OSCAL 下載」（model 定義 :488） | GET /oscal-framework-version/download/oscal/<uid>/token → GET /oscal-framework-version/download/oscal/<uid> | oscal.framework.read | `!data.children` 時才出現（有 catalog 或葉節點） | 無判斷 | 下載類；權限是 read，與看頁同一顆，所有租戶可見 |
| src/views/compliance-framework/ComplianceFrameworkVersionManage.vue:698 | 新增／編輯版本 Dialog「儲存」 | POST /oscal-framework-version（新增）／PUT /oscal-framework-version/<uid>（編輯） | oscal.framework.update | — | 無判斷 | 入口「新增版本」:606 與選單「編輯」:460 今用 isPlatformAdmin |
| src/views/compliance-framework/FrameworkImportPage.vue:477 | 捨棄草稿（Step 2） | DELETE /oscal-framework-parse-jobs/<uid> | oscal.framework.import | `step === 2` | 無判斷 | 同上；無 parseUid 時只導回不打 API |
| src/views/compliance-framework/FrameworkImportPage.vue:481 | 確認匯入 | POST /oscal-framework-parse-jobs/<uid>/confirm | oscal.framework.import | `step === 2` | 無判斷 | 同上 |
| src/views/compliance-framework/FrameworkImportPage.vue:533 | 上傳並解析文件 | POST /oscal-framework-parse-jobs/parse | oscal.framework.import | `step === 1` | 無判斷 | 整頁 onMounted 以 !isPlatformAdmin 導回列表（縱深防護，非能力點）；僅 FrameworkImportPage 用 |
| src/views/compliance-framework/FrameworkImportPage.vue:651 | 捨棄（解析失敗 Step 3） | DELETE /oscal-framework-parse-jobs/<uid> | oscal.framework.import | `step === 3` | 無判斷 | 同上 |
| src/views/compliance-framework/FrameworkImportPage.vue:713 | 草稿管理 Dialog 列內「捨棄」 | DELETE /oscal-framework-parse-jobs/<uid> | oscal.framework.import | — | 無判斷 | 同上 |
| src/views/compliance-framework/FrameworkVersionEditView.vue:337 | 透過 PDF 更新內容 Dialog「上傳並解析」 | POST /oscal-framework-parse-jobs/parse（帶 target_version_uid 覆蓋模式） | oscal.framework.import | `allowReupload`（!readOnly && !tree.has_references；readOnly＝!isPlatformAdmin \|\| publish_status!=='draft'） | 無判斷 | 入口 :269 同條件；僅 FrameworkVersionEditView 用 |
| src/views/module_frame/ImportExcelPage.vue:352 | 上傳區（點選／拖放 .xlsx 後自動上傳解析） | POST /ssp-excel-imports/parse | oscal.resource-library.create、oscal.resource-library.update | — | 無判斷 | 依 props.mode 擇一（create→`oscal.resource-library.create`；update-mf→`oscal.resource-library.update`），不是 hasAny；目前只有授權唯讀判斷 `canUpload`／無公司層判斷；頁面由 ModuleFrame.vue「從 Excel 建立」與 TemplateEditView「Excel 匯入」兩個入口進來 |
| src/views/module_frame/ImportExcelPreviewPage.vue:379 | 確認匯入 | POST /ssp-excel-import/<parse_uid>/confirm | oscal.resource-library.create、oscal.resource-library.update、oscal.ssp.import | `parseResult.source_type`（module_frame→覆蓋確認；ssp→更新確認；其他→直接匯入） | 無判斷 | 依 parseResult.source_type 擇一（framework_version→create；module_frame→update；ssp→`oscal.ssp.import`），不是 hasAny；本頁同時被專案 SSP 流程（SspTabPanel.vue 導入、return_to=ssp）使用；只有 writeDisabled 授權唯讀判斷 |
| src/views/module_frame/ModuleFrame.vue:61 | 新增選單 › 從 docx 建立（Menu model，:1069 渲染） | POST /ssp-docx-imports/parse、POST /ssp-docx-import/<parse_uid>/confirm | oscal.resource-library.create | — | 無判斷 | 入口鈕；送出在 SspDocxImportPage.vue（經 module_frame/ImportDocxPage.vue，mode=create） |
| src/views/module_frame/ModuleFrame.vue:66 | 新增選單 › 從 Excel 建立（Menu model，:1069 渲染） | POST /ssp-excel-imports/parse、POST /ssp-excel-import/<parse_uid>/confirm | oscal.resource-library.create | — | 無判斷 | 入口鈕；送出在 ImportExcelPage.vue / ImportExcelPreviewPage.vue（create 模式） |
| src/views/module_frame/ModuleFrame.vue:215 | 列動作 › 編輯（Menu model，:1454 渲染） | PUT /module-frame/<uid> | oscal.resource-library.update | — | 無判斷 | 入口鈕；送出在 ModuleFrameBasicInfoPanel.vue（經 ModuleFrameBasicInfoDialog.vue，該 Dialog 僅 ModuleFrame.vue 用） |
| src/views/module_frame/ModuleFrame.vue:230 | 列動作 › 刪除（Menu model，:1454 渲染） | DELETE /module-frame/<uid> | oscal.resource-library.delete | — | 無判斷 | 只有 writeDisabled 授權唯讀判斷 |
| src/views/module_frame/ModuleFrame.vue:1054 | 批次刪除（勾選後出現） | DELETE /module-frame/<uid>（逐筆） | oscal.resource-library.delete | `selectedRows.length > 0` | 無判斷 | 只有 writeDisabled 授權唯讀判斷 |
| src/views/module_frame/ModuleFrame.vue:1440 | 新增資源庫 dialog › 建立 | POST /oscal/resource-libraries | oscal.resource-library.create | — | 無判斷 | dialog 由「新增選單 › 手動建立」(:55) 開啟；handler 另有 `currFrame.uid` 分支走 PUT /module-frame/<uid>，但本頁開 dialog 一律清空 currFrame，實際只走新增 |
| src/views/module_frame/ModuleFrameItems.vue:354 | 列內 刪除 | DELETE /module-frame/item/<uid> | oscal.resource-library.update | — | 無判斷 | 本頁沒有任何路由指向（router 的 module-frame-items 指到 ModuleFrameDetail.vue），疑為孤兒頁；BE 守門今天寫的是 `oscal.resource-library.delete`，附錄 A 定 update |
| src/views/module_frame/ModuleFrameItems.vue:519 | 執行項目 dialog › 儲存 | PUT /module-frame/item/<uid>（編輯）、POST /module-frame/item/<workflowTemplateUid>（新增） | oscal.resource-library.update | — | 無判斷 | dialog 由頁首新增鈕(:344)／列內編輯鈕(:352)開啟；孤兒頁同上；POST 帶 uid 的路徑對到附錄 A `POST /module-frame/item`（同一 resource 兩條 URL 註冊） |
| src/views/module_frame/ModuleFrameTemplateEditView.vue:186 | 批次維護選單 › 從 docx 匯入（Menu model，:1401 渲染） | POST /ssp-docx-imports/parse、POST /ssp-docx-import/<parse_uid>/confirm | oscal.resource-library.update | — | 無判斷 | 入口鈕；送出在 SspDocxImportPage.vue（經 module_frame/ImportDocxPage.vue，mode=update-mf） |
| src/views/module_frame/ModuleFrameTemplateEditView.vue:200 | 批次維護選單 › Excel 匯入（Menu model，:1401 渲染） | POST /ssp-excel-imports/parse、POST /ssp-excel-import/<parse_uid>/confirm | oscal.resource-library.update | — | 無判斷 | 入口鈕；送出在 ImportExcelPage.vue / ImportExcelPreviewPage.vue（update 模式） |
| src/views/module_frame/ModuleFrameTemplateEditView.vue:224 | 批次維護選單 › 匯入現況資料（Menu model，:1401 渲染） | POST /module-frame/<uid>/control-defaults/import（另打 import/template、import/verify、import/validate） | oscal.resource-library.update | — | 無判斷 | 入口鈕；送出在 ModuleFrameTemplateImportDialog.vue（僅本頁用）；四支同權限 |
| src/views/module_frame/ModuleFrameTemplateEditView.vue:1424 | 編輯適用控制項 | PUT /module-frame/<uid> | oscal.resource-library.update | — | 無判斷 | 入口鈕；送出在 EditIncludeControlsDialog.vue（僅本頁用） |
| src/views/module_frame/ModuleFrameTemplateEditView.vue:1664 | 控制項 › 儲存 | PUT /module-frame/<uid>/control-defaults/<ci> | oscal.resource-library.update | `isControlDirty(selectedControl.code)` 才可按（disabled） | 無判斷 | 只有 writeDisabled 授權唯讀判斷 |
| src/views/module_frame/ModuleFrameTemplateEditView.vue:1693 | 控制項程序書 chip › 解除掛載（×） | DELETE /module-frame/<uid>/control-defaults/<ci>/reference-documents/<doc_uid> | oscal.resource-library.update | — | 無判斷 |  |
| src/views/module_frame/ModuleFrameTemplateEditView.vue:1877 | 檢查項目程序書 chip › 解除掛載（×） | DELETE /module-frame/<uid>/objective-defaults/<od_uid>/reference-documents/<doc_uid> | oscal.resource-library.update | — | 無判斷 |  |
| src/views/module_frame/ModuleFrameTemplateEditView.vue:1910 | 檢查項目 › 儲存 | PUT /module-frame/<uid>/objective-defaults | oscal.resource-library.update | `isAODirty(...)` 才可按（disabled） | 無判斷 |  |
| src/views/module_frame/ModuleFrameTemplateEditView.vue:2083 | 從程序書池選用 dialog › 確認 | POST /module-frame/<uid>/control-defaults/<ci>/reference-documents、DELETE .../control-defaults/<ci>/reference-documents/<doc_uid>、POST .../objective-defaults/<od_uid>/reference-documents、DELETE .../objective-defaults/<od_uid>/reference-documents/<doc_uid> | oscal.resource-library.update | — | 無判斷 | dialog 由控制項(:1713)／檢查項目(:1897)「+ 從程序書池選用」開啟；四支同權限 |
| src/views/project/ProjectPlanningView.vue:225 | `lang.project_planning.btn_import_impl`（批次維護選單：匯入現況說明 Excel） | POST /ssp/<ssp_uid>/control-implementations/import（＋/import/validate、/import/confirm） | oscal.ssp.import | 選單鈕 v-if="sspUid"（1973）；item disabled：writeDisabled(!canEditSsp) | 無判斷 | 入口鈕；送出在 SspImportDialog.vue；Menu model item（script 行，Menu 在 template 1977） |
| src/views/project/ProjectPlanningView.vue:231 | `lang.project_planning.btn_export_impl`（批次維護選單：匯出現況說明 Excel） | GET /ssp/<ssp_uid>/control-implementations/export | oscal.ssp.export | 選單鈕 v-if="sspUid" | 無判斷 | Menu model item；經 SspService.exportExcel |
| src/views/project/ProjectPlanningView.vue:258 | `lang.project_planning.ssp_document_export_docx`（匯出 SSP 文件 docx） | GET /ssp/<ssp_uid>/export?format=docx | oscal.ssp.export | 選單鈕 v-if="sspUid" | 無判斷 | Menu model item |
| src/views/project/ProjectPlanningView.vue:263 | `lang.project_planning.ssp_document_export_pdf`（匯出 SSP 文件 pdf） | GET /ssp/<ssp_uid>/export?format=pdf | oscal.ssp.export | 選單鈕 v-if="sspUid" | 無判斷 | Menu model item |
| src/views/project/ProjectPlanningView.vue:268 | `lang.project_planning.ssp_document_export_odt`（匯出 SSP 文件 odt） | GET /ssp/<ssp_uid>/export?format=odt | oscal.ssp.export | 選單鈕 v-if="sspUid" | 無判斷 | Menu model item |
| src/views/project/ProjectPlanningView.vue:2242 | `lang.project_planning.btn_save_ssp`（控制項面板：儲存現況說明） | PUT /ssp/<ssp_uid>/control-implementation/<ci> | oscal.ssp.update | canEditSsp（輪次 apStatus==='planning'） | 無判斷 | 經 SspService.updateControlImplementation；另有 writeDisabled() |
| src/views/project/ProjectPlanningView.vue:2349 | `lang.project_planning.btn_save_ssp`（AO 面板：儲存現況說明） | PUT /ssp/<ssp_uid>/control-implementation/<ci>/objective/<si> | oscal.ssp.update | canEditSsp | 無判斷 | 經 SspService.updateObjective；另有 writeDisabled() |
| src/views/workflow/WorkflowSetupEditor.vue:472 | 分享給子租戶開關（InputSwitch） | PUT /module-frame/item/xml/<uid>（帶 scope） | oscal.resource-library.update | `canShare`（有 processDefinitionId 且 processDefinition.tenant_id === currentTenant） | 無判斷 | 切換即打 API；只判資源所屬租戶 |
| src/views/workflow/WorkflowSetupEditor.vue:481 | 儲存流程圖 | PUT /module-frame/item/xml/<uid> | oscal.resource-library.update | `!isReadonly`（prop view 或 ?readonly=1） | 無判斷 | 只有授權唯讀 writeDisabled()；僅 WorkflowSetupForm.vue 用（路由 /workflow/workflow-setup，由 ProjectPlanningView／ModuleFrameItems／ModuleFrameTemplateEditView／ModuleFrameDetail 開啟） |

### flow（11 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/views/flow-template/FlowTemplateEditorView.vue:956 | 儲存範本 | POST /flow-engine/flow-templates（新增）／PUT /flow-engine/flow-templates/<uid>（編輯） | flow.template.create（新增）／flow.template.update（編輯） | `!isReadonly`（含 builtinWritable：非內建或平台管理員） | 已有且正確 | :disabled 用 isReadonly＝新增看 hasCap('flow.template.create')、編輯看 hasCap('flow.template.update')＋授權唯讀＋內建 |
| src/views/flow-template/FlowTemplateEditorView.vue:969 | 發布 | PUT /flow-engine/flow-templates/<uid>/publish（dirty 時先 PUT /flow-engine/flow-templates/<uid>） | flow.template.publish、flow.template.update | `builtinWritable && !isCreating && template.status !== 'published'` | 已有但名不對 | 實際用 flow.template.update（isReadonly）；dirty 時兩支都要打，需兩者皆有 |
| src/views/flow-template/FlowTemplateEditorView.vue:979 | 撤銷發布 | PUT /flow-engine/flow-templates/<uid>/unpublish | flow.template.publish | `builtinWritable && !isCreating && template.status === 'published'` | 已有但名不對 | 實際用 flow.template.update（isReadonly） |
| src/views/flow-template/FlowTemplateEditorView.vue:1024 | 分享給子租戶開關（InputSwitch） | PUT /flow-engine/flow-templates/<uid>（帶 scope） | flow.template.update | `canShare`（非新增且 template.tenant_id === currentTenant） | 無判斷 | 切換即打 API；只判資源所屬租戶，沒看能力點 |
| src/views/flow-template/FlowTemplateEditorView.vue:1239 | 另存為 Dialog「儲存」 | POST /flow-engine/flow-templates | flow.template.create | — | 已有且正確 | 判斷在入口 ⋮ 選單「另存為」（:681 disabled: !canCreate＝hasCap('flow.template.create')），dialog 鈕本身沒判 |
| src/views/flow-template/FlowTemplateManageView.vue:269 | 新增範本（入口鈕） | POST /flow-engine/flow-templates | flow.template.create | — | 已有且正確 | 入口鈕；本頁 dialog「儲存」:544 只帶 query 跳編輯頁不打 API，真正送出在 FlowTemplateEditorView.vue:956；disabled 用 canCreate |
| src/views/flow-template/FlowTemplateManageView.vue:423 | 列內編輯（入口鈕） | PUT /flow-engine/flow-templates/<uid> | flow.template.update | `canWriteRow(data)`（非內建或平台管理員） | 已有且正確 | 入口鈕；送出在 FlowTemplateEditorView.vue；disabled 用 canUpdate |
| src/views/flow-template/FlowTemplateManageView.vue:450 | 列內發布 | PUT /flow-engine/flow-templates/<uid>/publish | flow.template.publish | `canWriteRow(data) && data.status === 'draft'` | 已有但名不對 | 實際用 flow.template.update（canUpdate） |
| src/views/flow-template/FlowTemplateManageView.vue:464 | 列內撤銷發布 | PUT /flow-engine/flow-templates/<uid>/unpublish | flow.template.publish | `canWriteRow(data) && data.status === 'published'` | 已有但名不對 | 實際用 flow.template.update（canUpdate） |
| src/views/flow-template/FlowTemplateManageView.vue:478 | 列內刪除 | DELETE /flow-engine/flow-templates/<uid> | flow.template.delete | `canWriteRow(data)` | 已有且正確 | disabled 用 canDelete＝hasCap('flow.template.delete') |
| src/views/flow-template/FlowTemplateManageView.vue:595 | 複製範本 Dialog「複製」 | POST /flow-engine/flow-templates/<uid>/duplicate | flow.template.create | — | 已有且正確 | 判斷在入口 :437（disabled: !canCreate），dialog 鈕本身沒判 |

### detection（40 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/views/detection-profile/DetectionProfileManageView.vue:427 | 選單項 `btn_edit`（編輯基本資料） | PUT /detection-tool-profiles/<uid> | detection.profile.update | `!readonly`（rowReadonlyReason：授權唯讀／公版非 root／已停用） | 已有且正確 | Menu model item（template:895，kebab 鈕 :866）；入口；送出在 ProfileFormDialog.vue:448（edit 模式）。canUpdate，disabled 不隱藏 |
| src/views/detection-profile/DetectionProfileManageView.vue:436 | 選單項 `btn_new_version`（版更） | POST /detection-tool-profiles/<uid>/new-version | detection.profile.create | `row.current_version && !readonly` | 已有且正確 | Menu item；入口；送出在 ProfileNewVersionDialog.vue:169。canCreate |
| src/views/detection-profile/DetectionProfileManageView.vue:448 | 選單項 `btn_fix_source`（修正現行版來源） | PATCH /detection-tool-profile-versions/<uid>/source | detection.profile.create | `row.current_version && !frozen && !readonly`（frozen＝referencing-usage 判定已被使用） | 已有且正確 | Menu item；入口；送出在 ProfileSourceDialog.vue:201。canCreate |
| src/views/detection-profile/DetectionProfileManageView.vue:460 | 選單項 `btn_fork`（複製為自有） | POST /detection-tool-profiles/<uid>/fork | detection.profile.create | `row.scope === 'SYSTEM' && row.is_active` | 已有且正確 | Menu item；入口；送出在 ProfileCopyDialog.vue:143（fork 模式）。canCreate；刻意不受整列唯讀影響 |
| src/views/detection-profile/DetectionProfileManageView.vue:469 | 選單項 `btn_copy_to_tool`（複製到其他工具） | POST /detection-tool-profiles/<uid>/copy-to-tool | detection.profile.create | `!readonly` | 已有且正確 | Menu item；入口；送出在 ProfileCopyDialog.vue:143（copy 模式）。canCreate |
| src/views/detection-profile/DetectionProfileManageView.vue:486 | 選單項 `btn_delete`（刪除整支基準） | DELETE /detection-tool-profiles/<uid> | detection.profile.delete | `usage && !frozen && !readonly`（未被使用才出現，與停用互斥） | 已有且正確 | Menu item；confirm 後 handleDelete(:584)。canDelete |
| src/views/detection-profile/DetectionProfileManageView.vue:496 | 選單項 `btn_deactivate`（停用） | PUT /detection-tool-profiles/<uid>/deactivate | detection.profile.delete | `!(usage && !frozen) && !readonly`（已被使用或判定未回時出現） | 已有且正確 | Menu item；handleDeactivate(:552)。canDelete |
| src/views/detection-profile/DetectionProfileManageView.vue:630 | `btn_new`（新增掃描設定檔） | POST /detection-tool-profiles | detection.profile.create | — | 已有且正確 | 入口鈕；送出在 ProfileFormDialog.vue:448。用 canCreate=hasCap('detection.profile.create')，disabled 不隱藏 |
| src/views/detection-profile/components/ControlExtractionSummary.vue:178 | `controls_btn_retry`（抽取失敗後重新解析） | emit `retry` → 由 DetectionProfileControlsView.vue handleRetry(:329) → POST /detection-tool-profile-versions/<uid>/extraction | detection.profile.create | `isFailed && canRetry` | 無判斷 | 元件有 canRetry prop（預設 true）但父層沒傳；判斷放 props 由頁面傳入；僅 DetectionProfileControlsView 用 |
| src/views/detection-profile/components/ControlExtractionSummary.vue:200 | `controls_btn_extract`（尚未抽取→開始解析） | emit `retry` → 由 DetectionProfileControlsView.vue handleRetry(:329) → POST /detection-tool-profile-versions/<uid>/extraction | detection.profile.create | `isPending && canRetry` | 無判斷 | 同上 |
| src/views/detection-profile/components/ControlExtractionSummary.vue:241 | `controls_btn_retry`（url 來源過期→重新解析） | emit `retry` → 由 DetectionProfileControlsView.vue handleRetry(:329) → POST /detection-tool-profile-versions/<uid>/extraction | detection.profile.create | `isStaleSnapshot && canRetry` | 無判斷 | 同上 |
| src/views/detection-profile/components/ProfileCopyDialog.vue:143 | `btn_fork`／`btn_copy_to_tool`（複製送出） | POST /detection-tool-profiles/<uid>/fork（fork 模式）；POST /detection-tool-profiles/<uid>/copy-to-tool（copy 模式） | detection.profile.create | — | 無判斷 | 兩模式同權限；只有 writeDisabled；僅 DetectionProfileManageView 用 |
| src/views/detection-profile/components/ProfileFormDialog.vue:448 | `btn_save`（新增／編輯送出） | POST /detection-tool-profiles（create 模式）；PUT /detection-tool-profiles/<uid>（edit 模式） | detection.profile.create、detection.profile.update | — | 無判斷 | 依 mode 二擇一（非 hasAny）；只有 writeDisabled（授權唯讀）；入口在父層已判；僅 DetectionProfileManageView 用 |
| src/views/detection-profile/components/ProfileNewVersionDialog.vue:169 | `btn_new_version`（版更送出） | POST /detection-tool-profiles/<uid>/new-version | detection.profile.create | — | 無判斷 | 只有 writeDisabled；入口在父層已判；僅 DetectionProfileManageView 用 |
| src/views/detection-profile/components/ProfileSourceDialog.vue:201 | `btn_fix_source`（修正來源送出） | PATCH /detection-tool-profile-versions/<uid>/source | detection.profile.create | — | 無判斷 | 只有 writeDisabled；主列表與版本子表兩入口共用；僅 DetectionProfileManageView 用 |
| src/views/detection-profile/components/ProfileVersionTable.vue:328 | 選單項 `controls_btn_extract`（開始解析） | POST /detection-tool-profile-versions/<uid>/extraction | detection.profile.create | `['pending','failed'].includes(row.extraction_status) && !readonly` | 已有且正確 | Menu item（template:573，kebab :559）；canCreate=hasCap('detection.profile.create')；僅 DetectionProfileManageView 用 |
| src/views/detection-profile/components/ProfileVersionTable.vue:341 | 選單項 `btn_fix_source`（修正此版來源） | emit `fix-source` → 由 DetectionProfileManageView.vue openVersionFixSource 開 ProfileSourceDialog → PATCH /detection-tool-profile-versions/<uid>/source | detection.profile.create | `!frozen && !readonly` | 已有且正確 | Menu item；canCreate；僅 DetectionProfileManageView 用 |
| src/views/detection-profile/components/ProfileVersionTable.vue:352 | 選單項 `btn_delete_version`（刪除此版） | DELETE /detection-tool-profile-versions/<uid> | detection.profile.delete | `usage && !frozen && !readonly` | 已有且正確 | Menu item；canDelete；僅 DetectionProfileManageView 用 |
| src/views/plugin/ToolPluginManage.vue:510 | 選單項 `common.enable`（啟用工具） | PUT /detection-tools/configs/<uid> | detection.plugin.update | `tool.config?.status === 'disabled'` | 無判斷 | Menu model item（template:621，⋮ 鈕 :605）；只有 writeDisabled |
| src/views/plugin/ToolPluginManage.vue:517 | 選單項 `stop_use`（停止使用） | PUT /detection-tools/configs/<uid> | detection.plugin.update | `tool.config?.status === 'enabled'` | 無判斷 | Menu item；確認前另打 GET referencing-tasks-count（read）；只有 writeDisabled |
| src/views/plugin/ToolPluginManage.vue:525 | 選單項 `reset_key`（重置憑證） | POST /detection-tools/configs/<uid>/reset | detection.plugin.update | `tool.config` | 無判斷 | Menu item；只有 writeDisabled |
| src/views/plugin/ToolPluginManage.vue:595 | `test_connection`（卡片測試連線） | POST /detection-tools/configs/<uid>/test-connection | detection.plugin.update | `tool.config` | 無判斷 | 不需前置時直接打；需主機／憑證組／多 agent 時改開同檔 dialog（送出在 :781）。連 writeDisabled 也沒掛；僅本頁用 |
| src/views/plugin/ToolPluginManage.vue:670 | `common.save`（工具設定 dialog 儲存） | POST /detection-tools/configs（首次設定）；PUT /detection-tools/configs/<uid>（既有設定） | detection.plugin.create、detection.plugin.update | — | 無判斷 | 依是否已有 config 二擇一（非 hasAny）；開窗鈕 :586 同檔不另列；只有 writeDisabled |
| src/views/plugin/ToolPluginManage.vue:781 | `test_connection`（測試前置 dialog 送出） | POST /detection-tools/configs/<uid>/test-connection | detection.plugin.update | `canRunTest` | 無判斷 | 多台主機逐台並發各打一次 |
| src/views/remote-agent/RemoteAgentManage.vue:438 | `generate`／`regenerate`（產生註冊 Token） | POST /agents/enroll-token | detection.remote-agent.create | — | 無判斷 | 只有 writeDisabled；僅本頁用 |
| src/views/remote-agent/RemoteAgentManage.vue:448 | `revoke_token`（撤銷註冊 Token） | POST /agents/enroll-token/revoke | detection.remote-agent.delete | `enrollToken?.exists` | 無判斷 | 只有 writeDisabled |
| src/views/remote-agent/RemoteAgentManage.vue:490 | `approve_agent`（核准待核准機器） | POST /remote-agents/<uid>/approve | detection.remote-agent.update | `status === 'pending'`（待核准區） | 無判斷 | 只有 writeDisabled |
| src/views/remote-agent/RemoteAgentManage.vue:495 | 刪除（待核准區 icon 鈕） | DELETE /remote-agents/<uid> | detection.remote-agent.delete | `status === 'pending'`（待核准區） | 無判斷 | 只有 writeDisabled |
| src/views/remote-agent/RemoteAgentManage.vue:608 | InputSwitch 啟用／停用 | PUT /remote-agents/<uid> | detection.remote-agent.update | — | 無判斷 | 列內開關；連 writeDisabled 都沒掛 |
| src/views/remote-agent/RemoteAgentManage.vue:614 | `reconcile`（對帳 icon 鈕） | POST /remote-agents/<uid>/reconcile | detection.remote-agent.update | — | 無判斷 | 只有 writeDisabled |
| src/views/remote-agent/RemoteAgentManage.vue:624 | `unrevoke_agent`（復用 icon 鈕） | POST /remote-agents/<uid>/unrevoke | detection.remote-agent.update | `data.status === 'revoked'` | 無判斷 | 只有 writeDisabled |
| src/views/remote-agent/RemoteAgentManage.vue:628 | `revoke_agent`（撤銷 icon 鈕） | POST /remote-agents/<uid>/revoke | detection.remote-agent.update | `data.status !== 'revoked'`（v-else） | 無判斷 | 只有 writeDisabled |
| src/views/remote-agent/RemoteAgentManage.vue:632 | 刪除（已核准區 icon 鈕） | DELETE /remote-agents/<uid> | detection.remote-agent.delete | — | 無判斷 | 只有 writeDisabled |
| src/views/remote-agent/RemoteAgentManage.vue:677 | `common.save`（新增／編輯 agent dialog 儲存） | POST /remote-agents（新增）；PUT /remote-agents/<uid>（編輯） | detection.remote-agent.create、detection.remote-agent.update | — | 無判斷 | 依有無 uid 二擇一（非 hasAny）；編輯前先打 POST health-check（read）；開窗鈕 :513／:618 同檔不另列 |
| src/views/scan-zone/ScanZoneDialog.vue:68 | `common.save`（網段儲存） | POST /scan-zones（新增）；PUT /scan-zones/<uid>（編輯） | detection.scan-zone.create、detection.scan-zone.update | — | 無判斷 | 依 zone prop 二擇一（非 hasAny）；入口在父層以 canEdit 擋；僅 ScanZoneManage 用 |
| src/views/scan-zone/ScanZoneManage.vue:136 | `suggest`（依 Agent 產生建議網段） | POST /scan-zones/suggest-from-agents | detection.scan-zone.create | `canEdit`（hasCap(SCAN_ZONE_UPDATE)，v-if :135） | 已有但名不對 | 實際用 `detection.scan-zone.update`；僅本頁用 |
| src/views/scan-zone/ScanZoneManage.vue:139 | `create`（新增授權網段） | POST /scan-zones | detection.scan-zone.create | `canEdit`（v-if :135） | 已有但名不對 | 實際用 `detection.scan-zone.update`；入口鈕；送出在 ScanZoneDialog.vue:68 |
| src/views/scan-zone/ScanZoneManage.vue:153 | 確認授權（pending→active icon 鈕） | PUT /scan-zones/<uid> | detection.scan-zone.update | `canEdit`（欄 v-if :150）`&& data.status === 'pending'` | 已有且正確 |  |
| src/views/scan-zone/ScanZoneManage.vue:156 | 編輯（icon 鈕） | PUT /scan-zones/<uid> | detection.scan-zone.update | `canEdit`（欄 v-if :150） | 已有且正確 | 入口鈕；送出在 ScanZoneDialog.vue:68 |
| src/views/scan-zone/ScanZoneManage.vue:158 | 刪除（icon 鈕） | DELETE /scan-zones/<uid> | detection.scan-zone.delete | `canEdit`（欄 v-if :150） | 已有但名不對 | 實際用 `detection.scan-zone.update` |

### evidence（17 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/components/integrations/GoogleDriveIntegrationCard.vue:310 | ⋯選單 `gdrive.reauth_btn`（重新授權） | POST /integrations/google-drive/auth-url | evidence.cloud-integration.create | isConnected（業務狀態） | 已有且正確 | Menu model item 定義在 script:235，以 canCreate 過濾；僅 CloudIntegrationsView 用 |
| src/components/integrations/GoogleDriveIntegrationCard.vue:325 | `gdrive.connect_btn`（連線 Google Drive） | POST /integrations/google-drive/auth-url | evidence.cloud-integration.create | status.status==='DISCONNECTED' && !loading（業務狀態） | 已有且正確 | v-if="canCreate"（hasCap）；僅 CloudIntegrationsView 用 |
| src/components/integrations/GoogleDriveIntegrationCard.vue:341 | `gdrive.reauth_btn`（連線異常區的重新授權） | POST /integrations/google-drive/auth-url | evidence.cloud-integration.create | isError（status 為 REVOKED/EXPIRED，業務狀態） | 無判斷 | 同支 connect()；⋯選單裡的重新授權有 canCreate，這顆錯誤橫幅內的沒有，沒權限者點了必 403；僅 CloudIntegrationsView 用 |
| src/components/integrations/GoogleDriveIntegrationCard.vue:396 | `gdrive.disconnect_btn`（中斷連線） | DELETE /integrations/google-drive | evidence.cloud-integration.delete | isConnected（業務狀態） | 已有且正確 | v-if="canDelete"；僅 CloudIntegrationsView 用 |
| src/components/integrations/GoogleDriveIntegrationCard.vue:436 | `gdrive.rebuild_all_dialog_confirm_btn`（重建全部 Drive 資料夾） | POST /integrations/google-drive/tenant/rebuild-all | evidence.cloud-integration.update | isConnected；輸入 REBUILD 才可按 | 已有且正確 | dialog 內確認鈕本身沒判斷，判斷在入口 ⋯選單項 canUpdate（script:242）；僅 CloudIntegrationsView 用 |
| src/components/integrations/SyncJobHistoryDialog.vue:144 | `sync_jobs.retry`（重試同步工作） | POST /integrations/google-drive/sync-jobs/<uid>/retry | evidence.cloud-integration.update | data.status==='FAILED'（業務狀態） | 已有且正確 | v-if 含 canRetry（hasCap update）；僅 GoogleDriveIntegrationCard 用 |
| src/views/evidence-classification/AutoClassifyBatch.vue:529 | FileUpload 投遞區（上傳證據到批次） | POST /evidence-batches/<b>/files | evidence.classification.update | canWrite && batchEditable（canWrite＝isProjectManager && !storageBlockedMsg；batchEditable＝status 為 uploading/ready） | 無判斷 | 條件掛在外層 template :515；路由頁 auto-classify-batch |
| src/views/evidence-classification/AutoClassifyBatch.vue:647 | 列內 `action_remove_file`（移除批次檔） | DELETE /evidence-batches/<b>/files/<f> | evidence.classification.update | canWrite && batchEditable | 無判斷 | 走 confirm.require |
| src/views/evidence-classification/AutoClassifyBatch.vue:804 | `archive.purge_button`（第④步清理剩餘檔） | POST /evidence-batches/<b>/purge | evidence.classification.delete | canWrite && purgeableCount && isArchived（第④步 v-else 區塊） | 無判斷 | 走 confirm.require |
| src/views/evidence-classification/AutoClassifyBatch.vue:894 | `auto_classify.start_classify`（分類設定 dialog 內開始分類） | POST /evidence-batches/<b>/classify | evidence.classification.execute | 入口 :680 canWrite && batchEditable；入口 :702（重跑）canWrite && isFailed | 無判斷 | 附錄 A 新名欄註「只記錄」；入口兩顆開同檔 dialog |
| src/views/evidence-classification/AutoClassifyBatch.vue:928 | `archive.confirm_accept`（歸檔進任務，dialog 確認） | POST /evidence-batches/<b>/archive | evidence.classification.update | canWrite（以 :can-archive 傳入 EvidenceClassificationReview 控制入口） | 無判斷 | 入口在 EvidenceClassificationReview.vue:391（emit archive） |
| src/views/evidence-classification/AutoClassifyList.vue:293 | `auto_classify.create_batch`（新增批次，頁首） | POST /evidence-batches | evidence.classification.create | isProjectManager（目前使用者在 project.participants 的 role==='manager'） | 無判斷 | disabled 於 creating \|\| !roundUid \|\| storageBlockedMsg；路由頁 auto-classify-list |
| src/views/evidence-classification/AutoClassifyList.vue:355 | `auto_classify.create_batch`（新增批次，空狀態） | POST /evidence-batches | evidence.classification.create | isProjectManager | 無判斷 | 與 :293 同動作的第二入口 |
| src/views/evidence-classification/AutoClassifyList.vue:442 | 列內 `action_purge`（清理剩餘暫存檔） | POST /evidence-batches/<b>/purge | evidence.classification.delete | canPurge(data)＝isFinished(data)（已歸檔） && isProjectManager | 無判斷 | 走 confirm.require 後打 API |
| src/views/evidence-classification/AutoClassifyList.vue:506 | `delete_batch_accept`（刪除整批，dialog 確認） | DELETE /evidence-batches/<b> | evidence.classification.delete | isProjectManager（入口鈕 :454）；入口 disabled 於 isClassifying(data) \|\| isFinished(data) | 無判斷 | 入口列內垃圾桶 :454 開同檔 dialog |
| src/views/evidence-classification/EvidenceClassificationReview.vue:391 | `archive.archive_now` / `archive.rearchive`（歸檔入口） | emit `archive` → 由 AutoClassifyBatch.vue（onArchiveRequest→dialog :928→doArchive）處理：POST /evidence-batches/<b>/archive | evidence.classification.update | byRunUid && embedded && canArchive（canArchive 由父層傳 canWrite＝isProjectManager && !storageBlockedMsg）；disabled 於 hasUnsavedChanges \|\| archiving | 無判斷 | 入口鈕；送出在 AutoClassifyBatch.vue。本元件被 AutoClassifyBatch（嵌入）與路由 evidence-batch-review 用，獨立路由時不顯示此鈕，判斷放 props 由頁面傳入 |
| src/views/evidence-classification/components/ClassificationReviewHeader.vue:120 | `actions.save_changes`（儲存審閱結果） | emit `save` → 由 EvidenceClassificationReview.vue onSave → useEvidenceClassification.save 處理：PUT /classification-run/<r>/state | evidence.classification.update | — | 無判斷 | 僅 EvidenceClassificationReview 用；完全沒有角色條件（稽核員也看得到可按），只有 disabled 於 !hasUnsavedChanges \|\| saving；判斷需由 Review 頁傳 prop |

### survey（39 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/views/survey-v2/SurveyDesigner.vue:885 | 儲存選單項「儲存」（saveMenuItems :237） | PUT /survey/<uid> | survey.survey.update | — | 無判斷 | :879 儲存鈕只展開選單；只有 writeDisabled（授權唯讀） |
| src/views/survey-v2/SurveyDesigner.vue:885 | 儲存選單項「另存新版本」（saveMenuItems :242） | POST /survey（as_new_version） | survey.survey.create | — | 無判斷 | 同上 |
| src/views/survey-v2/SurveyDesigner.vue:887 | 發布／取消發布 | PUT /survey/<uid>/release、DELETE /survey/<uid>/release | survey.survey.publish | — | 無判斷 | 依 survey.isPublished 切換；只有 writeDisabled |
| src/views/survey-v2/SurveyDesigner.vue:927 | 題組樹「+」新增題組 | POST /survey-page | survey.survey.update | — | 無判斷 | 入口鈕；送出在 AddPageDialog.vue（emit submit → SurveyDesigner.onPageSubmit） |
| src/views/survey-v2/SurveyDesigner.vue:1017 | 空頁「新增題目」 | POST /survey-question | survey.survey.update | — | 無判斷 | 入口鈕；送出在 AddQuestionDialog.vue（emit submit → SurveyDesigner.onQuestionSubmit） |
| src/views/survey-v2/SurveyDesigner.vue:1028 | 題目間「插入題目」（第一題前） | POST /survey-question、POST /survey-questions/reorder | survey.survey.update | — | 無判斷 | 入口鈕；送出在 AddQuestionDialog.vue；原生 button |
| src/views/survey-v2/SurveyDesigner.vue:1114 | 子題上移 | POST /survey-questions/reorder | survey.survey.update | — | 無判斷 |  |
| src/views/survey-v2/SurveyDesigner.vue:1120 | 子題下移 | POST /survey-questions/reorder | survey.survey.update | — | 無判斷 |  |
| src/views/survey-v2/SurveyDesigner.vue:1128 | 子題編輯 | PUT /survey-question/<uid> | survey.survey.update | — | 無判斷 | 入口鈕；送出在 AddQuestionDialog.vue |
| src/views/survey-v2/SurveyDesigner.vue:1133 | 子題複製 | POST /survey-question | survey.survey.update | — | 無判斷 |  |
| src/views/survey-v2/SurveyDesigner.vue:1138 | 子題刪除 | DELETE /survey-question/<uid> | survey.survey.update | — | 無判斷 |  |
| src/views/survey-v2/SurveyDesigner.vue:1150 | 母題卡「+新增延伸題」 | POST /survey-question | survey.survey.update | — | 無判斷 | 入口鈕；送出在 AddQuestionDialog.vue |
| src/views/survey-v2/SurveyDesigner.vue:1161 | 題目上移 | POST /survey-questions/reorder | survey.survey.update | — | 無判斷 |  |
| src/views/survey-v2/SurveyDesigner.vue:1167 | 題目下移 | POST /survey-questions/reorder | survey.survey.update | — | 無判斷 |  |
| src/views/survey-v2/SurveyDesigner.vue:1175 | 題目編輯 | PUT /survey-question/<uid>（跳題精靈另打 PUT /survey-page/<uid>） | survey.survey.update | — | 無判斷 | 入口鈕；送出在 AddQuestionDialog.vue |
| src/views/survey-v2/SurveyDesigner.vue:1180 | 題目複製 | POST /survey-question | survey.survey.update | — | 無判斷 |  |
| src/views/survey-v2/SurveyDesigner.vue:1185 | 題目刪除 | DELETE /survey-question/<uid> | survey.survey.update | — | 無判斷 |  |
| src/views/survey-v2/SurveyDesigner.vue:1197 | 題目間「插入題目」（每題後） | POST /survey-question、POST /survey-questions/reorder | survey.survey.update | — | 無判斷 | 入口鈕；送出在 AddQuestionDialog.vue；原生 button |
| src/views/survey-v2/SurveyDesigner.vue:1205 | 底部虛線「新增題目」 | POST /survey-question | survey.survey.update | — | 無判斷 | 入口鈕；送出在 AddQuestionDialog.vue；原生 button |
| src/views/survey-v2/SurveyManage.vue:571 | 批次刪除問卷 | DELETE /survey/<uid>（逐筆） | survey.survey.delete | selectedSurveys.length > 0 | 無判斷 | 已填答（hasAnswer）的會被前端略過；僅 SurveyManage 用 |
| src/views/survey-v2/SurveyManage.vue:579 | SplitButton 項「從範本建立」（model 定義 :173） | POST /survey/<uid>/clone | survey.survey.create | — | 無判斷 | 入口鈕；送出在 SurveyPickerDialog.vue（emit confirm → SurveyManage.onPickerConfirm） |
| src/views/survey-v2/SurveyManage.vue:579 | SplitButton 項「匯入問卷」（model 定義 :182） | POST /survey/excel-import | survey.survey.import | — | 無判斷 | 入口鈕；送出在 BatchImportDialog.vue；SplitButton 主鈕只展開選單、「建立空白問卷」項開本檔 dialog（送出見 :899） |
| src/views/survey-v2/SurveyManage.vue:676 | 卡片 ⋮ 選單項「複製」（getContextMenuItems :417） | POST /survey/<uid>/clone | survey.survey.create | — | 無判斷 | 入口鈕；開 SurveyPickerDialog（copy 模式），送出由 SurveyManage.onPickerConfirm 打 clone |
| src/views/survey-v2/SurveyManage.vue:676 | 卡片 ⋮ 選單項「刪除」（getContextMenuItems :420） | DELETE /survey/<uid> | survey.survey.delete | — | 無判斷 | survey.hasAnswer 時前端擋下只跳 toast |
| src/views/survey-v2/SurveyManage.vue:775 | 列表模式列內刪除（pi-trash） | DELETE /survey/<uid> | survey.survey.delete | — | 無判斷 | 同 onDeleteSurvey |
| src/views/survey-v2/SurveyManage.vue:899 | 建立問卷 dialog「建立並設計」 | POST /survey | survey.survey.create | — | 無判斷 | 本檔 dialog 送出 |
| src/views/survey-v2/SurveyManage.vue:925 | 移動問卷 dialog「確定」 | POST /survey/move | survey.survey.update | — | 無判斷 | 入口為卡片選單「移動」（本檔 dialog，入口不列） |
| src/views/survey-v2/components/AddPageDialog.vue:248 | 題組 dialog「確定」 | emit `submit` → 由 SurveyDesigner.vue 處理：POST /survey-page（新增）或 PUT /survey-page/<uid>（編輯） | survey.survey.update | — | 無判斷 | 兩種模式同一權限；僅 SurveyDesigner.vue 用 |
| src/views/survey-v2/components/AddQuestionDialog.vue:667 | 題目 dialog「確定／儲存」（第 3 步） | emit `submit` → 由 SurveyDesigner.vue 處理：POST /survey-question 或 PUT /survey-question/<uid>（+ 精靈 PUT /survey-page/<uid>、PUT /survey-question/<uid>） | survey.survey.update | — | 無判斷 | 僅 SurveyDesigner.vue 用 |
| src/views/survey-v2/components/BatchImportDialog.vue:221 | 下載匯入範本 | GET /survey/import-template/token（再導 GET /survey/import-template） | survey.survey.import | — | 無判斷 | 範本下載本體在附錄 C 豁免，但換憑證那支守 import；僅 SurveyManage.vue 用 |
| src/views/survey-v2/components/BatchImportDialog.vue:245 | 開始匯入 | POST /survey/excel-import | survey.survey.import | — | 無判斷 | 只有 writeDisabled（授權唯讀）反灰；僅 SurveyManage.vue 用 |
| src/views/survey-v2/components/FolderTree.vue:177 | 批次刪除資料夾 | emit `folders-batch-deleted` → 由 SurveyManage.vue 處理：DELETE /survey-folder/<uid>（逐筆） | survey.survey.delete | selectedKeys.length > 0 | 無判斷 | 僅 SurveyManage.vue 用 |
| src/views/survey-v2/components/FolderTree.vue:222 | 資料夾 ⋯ 選單項「刪除資料夾」（ctxMenuItems :80） | emit `folder-deleted` → 由 SurveyManage.vue 處理：DELETE /survey-folder/<uid> | survey.survey.delete | — | 無判斷 | 「編輯資料夾」項開本檔 dialog，送出見 :247；僅 SurveyManage.vue 用 |
| src/views/survey-v2/components/FolderTree.vue:247 | 資料夾 dialog「確定」（新增模式） | emit `folder-created` → 由 SurveyManage.vue 處理：POST /survey-folder | survey.survey.create | !editMode | 無判斷 | 入口為 :162「+」（本檔 dialog）；同一顆鈕依 editMode 打不同 API；僅 SurveyManage.vue 用 |
| src/views/survey-v2/components/FolderTree.vue:247 | 資料夾 dialog「確定」（編輯模式） | emit `folder-updated` → 由 SurveyManage.vue 處理：PUT /survey-folder/<uid> | survey.survey.update | editMode | 無判斷 | 入口為 :222 選單「編輯資料夾」；僅 SurveyManage.vue 用 |
| src/views/survey-v2/components/PageTreeNode.vue:71 | 題組 ⋮ 彈出選單「編輯」 | PUT /survey-page/<uid> | survey.survey.update | — | 無判斷 | 入口鈕；emit `edit` → SurveyDesigner 開 AddPageDialog，送出由 SurveyDesigner.onPageSubmit 打；僅 SurveyDesigner.vue 用 |
| src/views/survey-v2/components/PageTreeNode.vue:75 | 題組 ⋮ 彈出選單「刪除」 | emit `delete` → 由 SurveyDesigner.vue 處理：DELETE /survey-page/<uid> | survey.survey.update | — | 無判斷 | 僅 SurveyDesigner.vue 用 |
| src/views/survey-v2/components/SurveyPickerDialog.vue:235 | 清單項雙擊（等同確認） | emit `confirm` → 由 SurveyManage.vue 處理：POST /survey/<uid>/clone | survey.survey.create | — | 無判斷 | 非按鈕但會直接觸發寫入，隱藏確認鈕時要一起擋；僅 SurveyManage.vue 用 |
| src/views/survey-v2/components/SurveyPickerDialog.vue:291 | 範本／複製確認鈕 | emit `confirm` → 由 SurveyManage.vue 處理：POST /survey/<uid>/clone | survey.survey.create | — | 無判斷 | 僅 SurveyManage.vue 用；開啟時另打 GET /surveys/menu（讀） |

### system（23 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/views/issue-integrate-config/IssueIntegrateConfigForm.vue:165 | 儲存（GitLab） | PUT /system/config/ISSUE_INTEGRATE_CONFIG/GITLAB | system.issue-integrate-config.update | — | 無判斷 | 群組 ISSUE_INTEGRATE_CONFIG；僅 router 用 |
| src/views/issue-integrate-config/IssueIntegrateConfigForm.vue:197 | 儲存（GitHub） | PUT /system/config/ISSUE_INTEGRATE_CONFIG/GITHUB | system.issue-integrate-config.update | — | 無判斷 | 群組 ISSUE_INTEGRATE_CONFIG；僅 router 用 |
| src/views/ldap-config/LdapConfigForm.vue:271 | 測試連線 | POST /ldap/connect-test | system.ldap-config.update | — | 無判斷 | 沒掛 writeDisabled；僅 router 用 |
| src/views/ldap-config/LdapConfigForm.vue:272 | 儲存 | PUT /system/config/THIRD_PARTY_LOGIN/LDAP | system.ldap-config.update | — | 無判斷 | 群組 THIRD_PARTY_LOGIN；另有非總部處理（useCompanyWideConfigAccess 接 GRC_403066，非能力點）；僅 router 用 |
| src/views/log-forwarding/LogForwardingForm.vue:433 | 發送測試訊息 | POST /log-forwarding/test | system.log-forwarding.update | — | 已有且正確 | hasCap('system.log-forwarding.update')（canUpdate）；僅 router 用 |
| src/views/log-forwarding/LogForwardingForm.vue:440 | 儲存 | PUT /log-forwarding | system.log-forwarding.update | — | 已有且正確 | hasCap('system.log-forwarding.update')（canUpdate）；僅 router 用 |
| src/views/notify-config/NotifyConfigForm.vue:254 | 測試（Telegram） | POST /notify-config/TELEGRAM/test | system.notify-config.update | — | 無判斷 | 沒掛 writeDisabled；僅 router 用 |
| src/views/notify-config/NotifyConfigForm.vue:262 | 儲存（Telegram） | PUT /system/config/NOTIFY_CONFIG/TELEGRAM | system.notify-config.update | — | 無判斷 | 群組 NOTIFY_CONFIG；僅 router 用 |
| src/views/notify-config/NotifyConfigForm.vue:310 | 測試（Discord） | POST /notify-config/DISCORD/test | system.notify-config.update | — | 無判斷 | 沒掛 writeDisabled；僅 router 用 |
| src/views/notify-config/NotifyConfigForm.vue:318 | 儲存（Discord） | PUT /system/config/NOTIFY_CONFIG/DISCORD | system.notify-config.update | — | 無判斷 | 群組 NOTIFY_CONFIG；僅 router 用 |
| src/views/security-policy/SecurityPolicyForm.vue:372 | 儲存 | PUT /system/security-policy | system.security-policy.update | — | 已有且正確 | hasCap('system.security-policy.update')（canUpdate）；另有非總部處理；僅 router 用 |
| src/views/setup/SetupWizard.vue:380 | 建立管理員帳號 | POST /setup/provision | 免判斷（附錄 C） | — | 免判斷 | 免登入、靠 X-Setup-Token；僅 router 用 |
| src/views/smtp-config/SmtpConfigForm.vue:341 | 儲存 | 無 uid：POST /system/config（group=SMTP）；有 uid：PUT /system/config/<uid> | system.smtp-config.create、system.smtp-config.update | — | 無判斷 | 依 query.uid 決定新增或修改（同一顆鈕兩種權限，依模式擇一，非 hasAny）；群組 SMTP；僅 router 用 |
| src/views/smtp-config/SmtpConfigForm.vue:365 | 寄送測試信（dialog 內「送出」） | POST /mail/test | system.smtp-config.update | — | 無判斷 | 入口鈕在同檔 :340（開 dialog，不列）；沒掛 writeDisabled；僅 router 用 |
| src/views/smtp-config/SmtpConfigManage.vue:126 | 新增設定 | POST /system/config（group=SMTP） | system.smtp-config.create | — | 無判斷 | 入口鈕；送出在 SmtpConfigForm.vue（導頁 name=smtp-config）；僅 router 用 |
| src/views/smtp-config/SmtpConfigManage.vue:133 | 編輯（列內 icon） | PUT /system/config/<uid> | system.smtp-config.update | — | 無判斷 | 入口鈕；送出在 SmtpConfigForm.vue（導頁帶 uid）；僅 router 用 |
| src/views/smtp-config/SmtpConfigManage.vue:134 | 刪除（列內 icon） | DELETE /system/config/<uid> | system.smtp-config.delete | data.key != '*' | 無判斷 | 業務條件：預設網域 '*' 不可刪；僅 router 用 |
| src/views/storage-config/StorageConfigForm.vue:553 | 恢復系統內建儲存 | POST /system/config/storage/restore-default | system.storage-config.update | showRestoreButton（出廠快照存在且目前非內建儲存） | 無判斷 | 僅 router 用 |
| src/views/storage-config/StorageConfigForm.vue:561 | 儲存 | PUT /system/config/STORAGE_CONFIG/CONFIG | system.storage-config.update | — | 無判斷 | 群組 STORAGE_CONFIG；僅 router 用 |
| src/views/system-menu-manage/SystemMenuManage.vue:332 | 批次刪除 | DELETE /system-menu/<id>（逐筆迴圈） | system.system-menu.delete | selectedMenus.length > 0 | 無判斷 | 僅 router 用 |
| src/views/system-menu-manage/SystemMenuManage.vue:348 | 編輯（列內 icon） | PUT /system-menu/<id> | system.system-menu.update | — | 無判斷 | 開同檔 dialog（:401 送出）；列出是因為實作棒要在入口隱藏，可與 :401 一併處理；僅 router 用 |
| src/views/system-menu-manage/SystemMenuManage.vue:349 | 刪除（列內 icon） | DELETE /system-menu/<id> | system.system-menu.delete | — | 無判斷 | 僅 router 用 |
| src/views/system-menu-manage/SystemMenuManage.vue:401 | 儲存（新增/編輯 dialog） | 無 id：POST /system-menu；有 id：PUT /system-menu/<id> | system.system-menu.create、system.system-menu.update | — | 無判斷 | 依 currentOption.id 決定新增或修改；新增入口在同檔 :338（開 dialog）；僅 router 用 |

### iam（40 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/components/user-manage/UserInitialRolePicker.vue:56 | 新增角色列（新增帳號時直接指派角色） | POST /user（payload 帶 user_roles；送出在 UserMTRBACForm.vue:451） | iam.user.assign | — | 無判斷 | 入口鈕；對話框在 UserInitialRoleDialog.vue（只回填前端列，不打 API），角色列隨帳號存檔送出，後端 service 層另要 iam.user.assign。僅 UserMTRBACForm.vue 用（mode==='ADD'） |
| src/components/user-manage/UserRoleSummary.vue:44 | 新增指派（帳號頁就地開指派對話框） | POST /role-assignment（送出在 AssignmentDialog.vue:210） | iam.user.assign | `userUid` 有值 | 已有且正確 | 入口鈕；用 `myCaps.has('iam.user.assign')`。僅 UserMTRBACForm.vue 用（編輯模式） |
| src/views/access/AssignmentDialog.vue:210 | 新增指派／儲存（編輯期間） | POST /role-assignment（新增，多人逐筆送）；PUT /role-assignment/<id>（編輯） | iam.user.assign | — | 無判斷 | 共用（被 AssignmentManage.vue、RoleDetail.vue、UserRoleSummary.vue 用），判斷放 props 由頁面傳入；三個入口今天都已用 iam.user.assign 擋 |
| src/views/access/AssignmentManage.vue:140 | 新增指派 | POST /role-assignment（送出在 AssignmentDialog.vue:210） | iam.user.assign | — | 已有且正確 | 入口鈕；`v-if="canAssign"`＝`myCaps.has('iam.user.assign')` |
| src/views/access/AssignmentManage.vue:199 | 列內編輯指派（鉛筆） | PUT /role-assignment/<id>（送出在 AssignmentDialog.vue:210） | iam.user.assign | — | 已有且正確 | 入口鈕；整欄 `v-if="canAssign"` |
| src/views/access/AssignmentManage.vue:201 | 列內刪除指派 | DELETE /role-assignment/<id> | iam.user.assign | — | 已有且正確 | 整欄 `v-if="canAssign"`；RoleAssignmentService.remove |
| src/views/access/RoleDetailHeader.vue:38 | 指派此角色 | emit `assign` → 由 RoleDetail.vue 處理（開 AssignmentDialog → POST /role-assignment） | iam.user.assign | `!isNew && role.enable === 1` | 已有且正確 | prop canAssign 由 RoleDetail.vue:39 算：`!isNew && role.enable===1 && can('iam.user.assign')`。僅 RoleDetail.vue 用 |
| src/views/access/RoleDetailHeader.vue:40 | 複製為新角色 | emit `copy` → 由 RoleDetail.vue 處理（導頁 access-role-new，存檔走 POST /role） | iam.role.create | — | 已有且正確 | 入口鈕（導頁）；prop canCopy＝`can('iam.role.create')`。僅 RoleDetail.vue 用 |
| src/views/access/RoleDetailHeader.vue:42 | 匯出角色 | emit `export` → 由 RoleDetail.vue 處理（POST /roles/export） | iam.role.export | — | 已有且正確 | prop canExport＝`can('iam.role.export')`。僅 RoleDetail.vue 用 |
| src/views/access/RoleImportDialog.vue:104 | 匯入（送出角色 JSON） | POST /roles/import | iam.role.import | — | 無判斷 | 對話框本身不判；入口 RoleList.vue:180 已用 `can('iam.role.import')` 擋。僅 RoleList.vue 用 |
| src/views/access/RoleList.vue:177 | 匯出全部 | POST /roles/export（先 POST /roles 取 uid） | iam.role.export | — | 已有且正確 | `v-if="can('iam.role.export')"` |
| src/views/access/RoleList.vue:180 | 匯入 | POST /roles/import（送出在 RoleImportDialog.vue:104） | iam.role.import | — | 已有且正確 | 入口鈕；`v-if="can('iam.role.import')"` |
| src/views/access/RoleList.vue:183 | 新增角色 | POST /role（導頁 access-role-new，送出在 RoleSaveBar.vue:39） | iam.role.create | — | 已有且正確 | 入口鈕（導頁）；`v-if="can('iam.role.create')"` |
| src/views/access/RoleList.vue:245 | 列內複製 | POST /role（導頁 access-role-new?template=，送出在 RoleSaveBar.vue:39） | iam.role.create | — | 已有且正確 | 入口鈕（導頁）；`v-if="can('iam.role.create')"` |
| src/views/access/RoleList.vue:247 | 列內匯出 | POST /roles/export | iam.role.export | — | 已有且正確 | `v-if="can('iam.role.export')"` |
| src/views/access/RoleList.vue:250 | 列內刪除 | DELETE /role/<uid> | iam.role.delete | `!data.is_system` | 已有且正確 | `v-if="!data.is_system && can('iam.role.delete')"` |
| src/views/access/RoleSaveBar.vue:39 | 建立角色／儲存變更 | emit `save` → 由 RoleDetail.vue 處理（新增 POST /role；編輯 PUT /role/<uid>） | iam.role.create、iam.role.update | `!isSystem`（內建角色唯讀） | 已有且正確 | 依模式擇一（isNew→create、否則 update）；RoleDetail.vue:152 `v-if="!readonly"`，readonly＝`isSystem \|\| (isNew ? !can('iam.role.create') : !can('iam.role.update'))`。僅 RoleDetail.vue 用 |
| src/views/auth/ChangePassword.vue:210 | 儲存新密碼 | POST /user/change-pwd-by-req | 免判斷（附錄 C） | — | 免判斷 | 免登入：忘記密碼／首登改密碼 |
| src/views/auth/ForgotPassword.vue:140 | 送出（寄重設信） | POST /forget-password | 免判斷（附錄 C） | — | 免判斷 | 免登入 |
| src/views/auth/Login.vue:386 | 登入 | POST /login | 免判斷（附錄 C） | — | 免判斷 | 免登入；成功後另打 GET /user/web-menu（附錄 C 本人） |
| src/views/auth/OtpVerification.vue:338 | 驗證 OTP | POST /otp-verify | 免判斷（附錄 C） | — | 免判斷 | 免登入：兩階段驗證 |
| src/views/auth/OtpVerification.vue:341 | 重新寄送驗證碼 | POST /otp-resend | 免判斷（附錄 C） | `mfa_type === 'email'` | 免判斷 | 免登入 |
| src/views/auth/OtpVerification.vue:344 | 改用 E-Mail 驗證 | POST /otp-resend（type=email） | 免判斷（附錄 C） | `mfa_type !== 'email'` | 免判斷 | 免登入 |
| src/views/org-unit/OrgUnitManage.vue:283 | 列內刪除部門 | DELETE /org-unit/<uid> | iam.org-unit.delete | — | 無判斷 | 只有授權唯讀反灰 writeDisabled |
| src/views/org-unit/OrgUnitManage.vue:381 | 部門對話框儲存 | POST /org-unit（新增）；PUT /org-unit/<uid>（編輯） | iam.org-unit.create、iam.org-unit.update | — | 無判斷 | 依模式擇一（currentData.uid 有值＝編輯）；入口在同檔 :275 新增、:282／:291 編輯（實作棒入口要分別用 create／update 擋） |
| src/views/tenant-manage/TenantManage.vue:263 | 列內刪除租戶 | DELETE /tenant/<uid> | iam.tenant.delete | — | 無判斷 | 連授權唯讀 writeDisabled 都沒有 |
| src/views/tenant-manage/TenantManage.vue:343 | 租戶對話框儲存 | POST /tenant（新增）；PUT /tenant/<uid>（編輯） | iam.tenant.create、iam.tenant.update | — | 無判斷 | 依模式擇一；入口在同檔 :255 新增、:262 編輯；全頁無 writeDisabled |
| src/views/user-manage/UserMTRBACForm.vue:451 | 帳號表單儲存 | POST /user（mode ADD）；PUT /user/<uid>（mode EDIT） | iam.user.create、iam.user.update | `mode === 'ADD'` → create；否則 update | 無判斷 | 依模式擇一；新增帶 user_roles 時後端 service 層另要 iam.user.assign（入口見 UserInitialRolePicker.vue:56）；只有 writeDisabled |
| src/views/user-manage/UserManage.vue:264 | 新增使用者 | POST /user（導頁 user-form，送出在 UserMTRBACForm.vue:451） | iam.user.create | — | 無判斷 | 入口鈕（導頁）；只有 writeDisabled。僅 UserManageTabView.vue 用 |
| src/views/user-manage/UserManage.vue:276 | 列內停用／啟用帳號 | POST /user/change-status/<uid>（status 1／-1） | iam.user.update | `data.login_name !== 'admin' && data.login_name !== userInfo.login_name` | 無判斷 | 只有 writeDisabled。僅 UserManageTabView.vue 用 |
| src/views/user-manage/UserManage.vue:284 | 列內刪除帳號 | POST /user/change-status/<uid>（status -2） | iam.user.update | `data.login_name !== 'admin' && data.login_name !== userInfo.login_name` | 無判斷 | 刪除走 change-status 不走 DELETE /user/<uid>，故後端守的是 iam.user.update 而非 iam.user.delete（見觀察） |
| src/views/user-manage/UserManageUpload.vue:260 | 下載 Excel 範本 | GET /user/download/sample | iam.user.import | — | 無判斷 | 僅 UserManageTabView.vue 用 |
| src/views/user-manage/UserManageUpload.vue:271 | 上傳區（點選／拖放選檔即上傳解析） | POST /users/upload | iam.user.import | — | 無判斷 | 原生 `<button>` dropzone；無 writeDisabled |
| src/views/user-manage/UserManageUpload.vue:312 | 建立勾選帳號 | POST /user/batch-upload | iam.user.import | — | 無判斷 | 只有 writeDisabled；payload 帶 Excel 的角色（後端是否另要 iam.user.assign 未見附錄 A 註記） |
| src/views/user-manage/UserManageUpload.vue:339 | 列編輯儲存（打勾 icon，重驗該列） | POST /user/validate | iam.user.import | — | 無判斷 | DataTable rowEditor 的 save icon（@row-edit-save） |
| src/views/user-manage/UserProfileForm.vue:549 | 個人資料儲存 | PUT /user-profile/<uid> | 免判斷（附錄 C） | — | 免判斷 | 只驗登入：本人 |
| src/views/user-manage/UserProfileForm.vue:582 | 解除 LDAP 綁定 | DELETE /auth-provider/<uid> | iam.user.update（本人豁免） | `isBindLdap` | 免判斷 | 此頁一律操作本人，落在附錄 C 末段「本人免能力點」 |
| src/views/user-manage/UserProfileForm.vue:605 | 啟用／關閉 TOTP（InputSwitch，即時生效） | PUT /user-mfa-type（切到 totp 時先打 POST /totp-secret） | 免判斷（附錄 C） | — | 免判斷 | 只驗登入：本人；非 Button 但會直接寫入 |
| src/views/user-manage/UserProfileForm.vue:651 | LDAP 綁定對話框儲存 | POST /auth-provider | iam.user.update（本人豁免） | — | 免判斷 | user_uid＝登入者本人；入口同檔 :578 |
| src/views/user-manage/UserProfileForm.vue:674 | 驗證並啟用 TOTP | POST /totp-verify＋PUT /user-mfa-type | 免判斷（附錄 C） | `totpSetupMode` | 免判斷 | 只驗登入：本人 |

### platform（12 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/views/action-log/UserLogs.vue:192 | 匯出 | GET /log/api-logs/export | platform.api-log.export | — | 無判斷 | 僅 user-logs 路由用 |
| src/views/ai-quota/AiQuotaCeilingTable.vue:126 | 儲存上限（點列開窗編輯後送出） | PUT /system/ai-quota/tenants/<id>/ceiling | platform.ai-quota-ceiling.update | — | 已有但粗 | 父層 AiQuotaPage.vue:161 用 `isPlatformAdmin && isRoot` 旗標決定整塊顯示，不是 hasCap；僅 AiQuotaPage.vue 用 |
| src/views/google-drive-app-config/GoogleDriveAppConfigForm.vue:304 | 清除密鑰（垃圾桶 icon） | 無即時 API，僅標記清除；按「儲存」時以 PUT /system/config/GOOGLE_DRIVE_APP_CONFIG/CONFIG 帶 clear_client_secret 送出 | platform.drive-app-credential.update | — | 無判斷 | 群組 GOOGLE_DRIVE_APP_CONFIG；頁面另有 canManage＝平台管理員旗標 DRIVE_APP_CONFIG_PLATFORM_ADMIN_ONLY（非能力點）；僅 router 用 |
| src/views/google-drive-app-config/GoogleDriveAppConfigForm.vue:448 | 驗證憑證 | POST /integrations/google-drive/verify-credentials | platform.drive-app-credential.read | — | 無判斷 | 不存檔、只驗；附錄 A 取 read；僅 router 用 |
| src/views/google-drive-app-config/GoogleDriveAppConfigForm.vue:452 | 儲存 | PUT /system/config/GOOGLE_DRIVE_APP_CONFIG/CONFIG | platform.drive-app-credential.update | — | 無判斷 | 群組 GOOGLE_DRIVE_APP_CONFIG；僅 router 用 |
| src/views/license/LicenseActivation.vue:283 | 線上開通 | POST /license/activation/online | 免判斷（附錄 C） | — | 免判斷 | 只在 host 版顯示 |
| src/views/license/LicenseActivation.vue:327 | 開通（上傳授權檔） | POST /license/activation/upload | 免判斷（附錄 C） | — | 免判斷 | 只在 host 版顯示 |
| src/views/license/LicenseManage.vue:440 | 解除停權（列內，confirm 後送） | POST /license/tenants/<uid>/unlock | platform.license.update | data.has_license && data.suspended_at | 無判斷 |  |
| src/views/license/LicenseManage.vue:524 | 停權（dialog 確認） | POST /license/tenants/<uid>/lock | platform.license.update | data.has_license && !data.suspended_at（入口 :432） | 無判斷 | 入口 :432 同檔 |
| src/views/license/LicenseManage.vue:571 | 指派／換發儲存（dialog） | POST /license/tenants/<uid>/assign | platform.license.assign | — | 無判斷 | 入口 :417 同檔 |
| src/views/license/LicenseManage.vue:663 | 向 LC 請照儲存（新簽／展延 dialog） | POST /license/tenants/<uid>/lc-issue 或 POST /license/tenants/<uid>/lc-extend | platform.license.assign、platform.license.update | — | 無判斷 | 依 lcMode：新簽＝assign、展延＝update；需 hasAny。入口 :411 同檔 |
| src/views/support/SupportPage.vue:226 | 匯出診斷包 | POST /support/diagnostic-bundle | platform.support.export | — | 無判斷 | 頁面另有 canManage＝平台管理員旗標 SUPPORT_PAGE_PLATFORM_ADMIN_ONLY（非能力點）；僅 router 用 |

### asset（11 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/views/device/DeviceExportMenu.vue:39 | 匯出（Menu 兩項 Excel／CSV，:33-34） | POST /devices/export | asset.device.export | — | 已有但粗 | 父層 DeviceManage.vue:361 用 v-if canRead＝hasCap('asset.device.read')；僅 DeviceManage.vue 用 |
| src/views/device/DeviceImportDialog.vue:120 | 下載範本 | GET /devices/import-template | asset.device.import | — | 無判斷 | dialog 內本身無判斷；入口 DeviceManage.vue:362 已用 asset.device.create 遮；僅 DeviceManage.vue 用 |
| src/views/device/DeviceImportDialog.vue:122 | 選擇檔案（選檔即上傳驗證） | POST /devices/import/verify | asset.device.import | — | 無判斷 | 同上 |
| src/views/device/DeviceImportDialog.vue:163 | 確認匯入 | POST /devices/import/confirm | asset.device.import | — | 無判斷 | 同上；「更新既有設備」勾選框（:154）另用 hasCap('asset.device.update') |
| src/views/device/DeviceManage.vue:354 | 批次刪除 | DELETE /device/<uid>（逐筆） | asset.device.delete | selectedDevices.length > 0 | 已有且正確 | v-if canDelete＝hasCap('asset.device.delete') |
| src/views/device/DeviceManage.vue:362 | 匯入 | 入口 → POST /devices/import/confirm | asset.device.import | — | 已有但名不對 | 用 hasCap('asset.device.create')；入口鈕，送出在 DeviceImportDialog.vue:163 |
| src/views/device/DeviceManage.vue:376 | 刪除（列內） | DELETE /device/<uid> | asset.device.delete | — | 已有且正確 | v-if canDelete＝hasCap('asset.device.delete') |
| src/views/device/DeviceManage.vue:604 | 儲存（新增／編輯 dialog） | POST /device 或 PUT /device/<uid> | asset.device.create、asset.device.update | — | 無判斷 | 依 currentData.uid 決定；需 hasAny。新增入口 :364、編輯入口 :375／名稱連結 :387 都在同檔，皆無判斷 |
| src/views/information-system/InformationSystemManage.vue:397 | 批次停用（刪除） | DELETE /information-system/<uid>（逐筆） | asset.information-system.delete | selectedSystems.length > 0 | 無判斷 | 後端為軟刪除（is_active=false） |
| src/views/information-system/InformationSystemManage.vue:436 | 停用（刪除，列內） | DELETE /information-system/<uid> | asset.information-system.delete | — | 無判斷 |  |
| src/views/information-system/InformationSystemManage.vue:782 | 儲存（新增／編輯 dialog） | POST /information-systems 或 PUT /information-system/<uid> | asset.information-system.create、asset.information-system.update | — | 無判斷 | 依 currentData.uid 決定；需 hasAny。新增入口 :411、編輯入口 :432／名稱連結 :447 同檔，皆無判斷 |

### ai（12 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/components/ChatBox.vue:184 | 送出訊息（↑ 鈕；Enter 鍵 onEnter 走同一條） | POST /ai-chatbot | ai.assistant.read | — | 無判斷 | 僅 AppLayout.vue 用 |
| src/layout/AppLayout.vue:129 | AI 小幫手浮鈕（開對話窗） | 入口 → POST /ai-chatbot | ai.assistant.read | — | 無判斷 | 入口鈕；送出在 ChatBox.vue:184。現在只看 writeDisabled()（授權唯讀反灰），沒有能力點判斷 |
| src/views/ai-call-log/AiCallLogList.vue:227 | 匯出 | POST /system/ai-call-logs/export | ai.call-log.export | — | 無判斷 | 僅 AiUsagePage.vue 用 |
| src/views/ai-call-log/AiUsageSummary.vue:182 | 匯出 | POST /system/ai-call-logs/usage/export | ai.call-log.export | — | 無判斷 | 僅 AiUsagePage.vue 用 |
| src/views/ai-quota/AiQuotaPage.vue:141 | 回復預設 | DELETE /system/ai-quota | ai.quota.update | — | 已有且正確 | 外層 v-if canEdit = hasCap('ai.quota.update') |
| src/views/ai-quota/AiQuotaPage.vue:143 | 儲存 | PUT /system/ai-quota | ai.quota.update | — | 已有且正確 | 外層 v-if canEdit = hasCap('ai.quota.update') |
| src/views/ai-service-config/AiServiceConfigForm.vue:231 | 清除金鑰（垃圾桶 icon） | 無即時 API，僅標記清除；按「儲存」時以 PUT /system/config/AI_PROVIDER_CONFIG/CONFIG 帶 clear:true 送出 | ai.ai-provider-config.update | — | 無判斷 | 群組 AI_PROVIDER_CONFIG；頁面另有 canManage＝平台管理員旗標 AI_SERVICE_CONFIG_PLATFORM_ADMIN_ONLY（非能力點）；僅 router 用 |
| src/views/ai-service-config/AiServiceConfigForm.vue:304 | 儲存 | PUT /system/config/AI_PROVIDER_CONFIG/CONFIG | ai.ai-provider-config.update | — | 無判斷 | 群組 AI_PROVIDER_CONFIG；同上平台管理員旗標；僅 router 用 |
| src/views/dynamic-dashboard/DynamicDashboard.vue:34 | 重新產生 | POST /ai-dashboard/auto-generate | ai.dashboard.read | — | 無判斷 |  |
| src/views/dynamic-dashboard/components/DashboardErrorMessage.vue:15 | 重試 | emit `retry` → 由 DynamicDashboard.vue 處理 → POST /ai-dashboard/auto-generate | ai.dashboard.read | error.canRetry | 無判斷 | 僅 DynamicDashboard.vue 用 |
| src/views/dynamic-dashboard/components/DashboardPromptBar.vue:23 | 產生 | emit `generate` → 由 DynamicDashboard.vue 處理（useDashboardGenerator.generateDashboard）→ POST /ai-dashboard/auto-generate | ai.dashboard.read | — | 無判斷 | 僅 DynamicDashboard.vue 用；輸入框 Enter 走同一條 |
| src/views/dynamic-dashboard/components/DashboardTemplates.vue:10 | 快速模板卡片（點了直接產生） | emit `select` → 由 DynamicDashboard.vue 處理（useTemplate → generateDashboard）→ POST /ai-dashboard/auto-generate | ai.dashboard.read | — | 無判斷 | 僅 DynamicDashboard.vue 用；是可點 Card 不是 Button |

### workspace（13 顆）

| 檔案:行號 | 按鈕 | 打的 API | 權限 | 專案層條件 | 現況 | 備註 |
|---|---|---|---|---|---|---|
| src/views/bulletins/BulletinForm.vue:310 | 儲存 | POST /bulletin 或 PUT /bulletin/<uid> | workspace.bulletin.create、workspace.bulletin.update | — | 無判斷 | 依 query.uid 決定新增或修改；需 hasAny（或依模式各判一個） |
| src/views/bulletins/BulletinManage.vue:208 | 新增公告 | 入口 → POST /bulletin | workspace.bulletin.create | !props.view | 無判斷 | 入口鈕；送出在 BulletinForm.vue:310。共用（bulletin-manage 路由直掛、BulletinList.vue 以 view=true 用），判斷放 props 由頁面傳入；view 模式已隱藏 |
| src/views/bulletins/BulletinManage.vue:216 | 編輯公告（列內） | 入口 → PUT /bulletin/<uid> | workspace.bulletin.update | canEdit(data)（＝ !props.view） | 無判斷 | 入口鈕；送出在 BulletinForm.vue:310。共用（同上） |
| src/views/bulletins/BulletinManage.vue:218 | 刪除公告（列內） | DELETE /bulletin/<uid> | workspace.bulletin.delete | canEdit(data)（＝ !props.view） | 無判斷 | 共用（同上） |
| src/views/feedback/system/SystemFeedbackForm.vue:432 | 下載附件 | GET /file/download/<uid> | 免判斷（附錄 C） | — | 免判斷 |  |
| src/views/feedback/system/SystemFeedbackForm.vue:442 | 刪除已上傳附件 | DELETE /feedback/file/<uid>/<file_uid> | workspace.feedback.update | — | 無判斷 |  |
| src/views/feedback/system/SystemFeedbackForm.vue:455 | 儲存 | POST /feedback 或 PUT /feedback/<uid> | workspace.feedback.create、workspace.feedback.update | — | 已有但名不對 | v-if `page_permissions.includes('CREATE') \|\| page_permissions.includes('UPDATE')`，來源是 getUserPagePermissions('/feedback/system-feedback-manage') 的選單動作，不是 hasCap；需 hasAny |
| src/views/feedback/system/SystemFeedbackManage.vue:265 | 匯出 | POST /feedback/export/excel | workspace.feedback.export | — | 無判斷 | 共用（被 FeedbackManageTabView.vue 用，mine／all 兩條路由），判斷放 props 由頁面傳入 |
| src/views/feedback/system/SystemFeedbackManage.vue:266 | 新增 | 入口 → POST /feedback | workspace.feedback.create | !view | 已有但粗 | view＝FeedbackManageTabView.vue:69 傳入 `permissions.length === 1`（選單頁能力點只有一個就當唯讀）；入口鈕，送出在 SystemFeedbackForm.vue:455；共用（同上） |
| src/views/feedback/system/SystemFeedbackManage.vue:279 | 編輯（列內） | 入口 → PUT /feedback/<uid> | workspace.feedback.update | !view && canEdit(data)（canEdit：is_admin 或本人建立） | 已有但粗 | 同上 view 來源；入口鈕，送出在 SystemFeedbackForm.vue:455 |
| src/views/feedback/system/SystemFeedbackManage.vue:282 | 刪除（列內） | DELETE /feedback/<uid> | workspace.feedback.delete | !view && canEdit(data)（canEdit：is_admin 或本人建立） | 已有但粗 | 同上 view 來源 |
| src/views/feedback/system/SystemFeedbackManage.vue:303 | 標題連結（可編輯者進編輯） | 入口 → PUT /feedback/<uid> | workspace.feedback.update | !view && canEdit(data) | 已有但粗 | 與 :279 同為編輯入口；送出在 SystemFeedbackForm.vue:455 |
| src/views/feedback/system/SystemFeedbackView.vue:127 | 下載附件 | GET /file/download/<uid> | 免判斷（附錄 C） | — | 免判斷 |  |

## 附錄：逐檔清單

每支讀過的檔與寫入類按鈕數；0 顆的寫明原因。

| 檔案 | 行數 | 寫入類按鈕數 | 說明 |
|---|---|---|---|
| src/components/BlockViewer.vue | 209 | 0 | 範例程式碼展示框，只有 Preview／Code 切換與複製到剪貼簿 |
| src/components/ChatBox.vue | 391 | 1 | 送出訊息；最小化鈕是 emit close 純導覽 |
| src/components/access/PermissionMenuTree.vue | 145 | 0 | 勾選與「只看頁面」都只改前端選取，不打 API |
| src/components/access/PermissionResourceMatrix.vue | 63 | 0 | 純前端勾選矩陣 |
| src/components/access/PermissionTree.vue | 124 | 0 | 展開收合、切視角、搜尋；元件不打 API |
| src/components/access/TriStateCheckbox.vue | 24 | 0 | 三態勾選元件，只 emit toggle |
| src/components/access/VisibleMenuPreview.vue | 104 | 0 | 「加入」emit add-gate → RoleDetail 只改前端勾選；其餘收合 |
| src/components/compliance-framework/article/FrameworkArticleContent.vue | 380 | 8 | 全是 emit 給父層；鉛筆鈕只開 inline 編輯不列；還原鈕（:159／:200／:279）只在匯入頁本地還原、編輯頁永不出現，不打 API 不列 |
| src/components/compliance-framework/article/FrameworkArticlePane.vue | 93 | 0 | 純轉手（Splitter＋PDF iframe＋把 FrameworkArticleContent 的 emit 原樣往上丟），按鈕已列在 Content；僅 FrameworkVersionEditView.vue 用 |
| src/components/compliance-framework/import/AssessmentListEditor.vue | 149 | 0 | 只改 draftKit 本地草稿不打 API；且全 repo 無人 import，疑似死元件 |
| src/components/compliance-framework/import/ControlListEditor.vue | 170 | 0 | 同上（本地草稿、無人 import） |
| src/components/compliance-framework/import/GroupTreeEditor.vue | 136 | 0 | 同上（本地草稿、無人 import） |
| src/components/grc/AuditControlRef.vue | 700 | 1 | 唯讀參考面板；僅證據下載（免判斷），其餘為預覽／展開收合／外部連結 |
| src/components/grc/AuditTreeNav.vue | 194 | 0 | 純導覽樹：搜尋、篩選待稽核、展開收合、emit select |
| src/components/grc/ComponentsLeveragedInventoryTab.vue | 1562 | 8 | 四區塊（元件／外部授權／設備資產／系統資產）各「刪除」＋dialog「儲存」；SSP 與 MF 兩情境各列一行，共 16 列 |
| src/components/grc/DepartmentsPanel.vue | 99 | 0 | 只 emit add/remove、資料型別來自 grc-mock-data；全 repo 無人引用（死碼），無 API |
| src/components/grc/DocumentLinkDialog.vue | 378 | 1 | 確認關聯（增刪 mapping） |
| src/components/grc/DocumentPoolPanel.vue | 416 | 3 | 上傳、刪除、下載（下載免判斷）；預覽為唯讀不列 |
| src/components/grc/DownloadSspBlankTemplateDialog.vue | 140 | 1 | 下載空白範本 |
| src/components/grc/EditIncludeControlsDialog.vue | 335 | 1 | 儲存 include_controls；展開／收合不列 |
| src/components/grc/EmptyState.vue | 20 | 0 | 純展示；action 按鈕由呼叫端傳入，現有兩個使用者（StageTransitionTimeline、EffectiveAccessView）都沒傳 action |
| src/components/grc/FlowPhaseBanner.vue | 627 | 3 | 階段推進／審核決策／返回上一階段 |
| src/components/grc/JobExecutionDrawer.vue | 3152 | 24 | 任務抽屜：檢測執行群組動作、證據檔案與連結、留言、完成／退回入口 |
| src/components/grc/JobImportDialog.vue | 425 | 3 | 任務批次匯入三步驟 |
| src/components/grc/ModuleFrameBasicInfoPanel.vue | 214 | 2 | 分享開關、儲存 |
| src/components/grc/ModuleFrameDocumentPoolPanel.vue | 442 | 3 | 上傳、刪除、下載（下載免判斷） |
| src/components/grc/ModuleFramePartiesPanel.vue | 529 | 2 | 刪除、dialog 儲存；SSP／MF 兩情境各列一行，共 4 列 |
| src/components/grc/ModuleFrameSspDevicesPanel.vue | 332 | 2 | 刪除、儲存；所在 dialog 無人引用 |
| src/components/grc/ModuleFrameSspInfoSystemsPanel.vue | 332 | 2 | 刪除、儲存；所在 dialog 無人引用 |
| src/components/grc/ModuleFrameTemplateImportDialog.vue | 580 | 3 | 上傳驗證／重新驗證／確認匯入；dropzone 內按鈕只開檔案選擇器、篩選 tab 為本地篩選不列 |
| src/components/grc/OpenInDriveButton.vue | 22 | 0 | 純 window.open 外連 Drive，屬導覽 |
| src/components/grc/ParticipantPicker.vue | 176 | 0 | 只改父層 v-model（新增／移除／換角色），不打 API；送出在 ProjectParticipantsPanel／ProjectCreateView |
| src/components/grc/PreviousFindingsPanel.vue | 153 | 0 | 唯讀展示，只有展開收合 |
| src/components/grc/ReferenceDocumentList.vue | 323 | 3 | 關聯程序書（入口）、取消關聯、下載；remove／upload 兩條 emit 無任何父層接（見觀察） |
| src/components/grc/RoundSwitcherBar.vue | 120 | 0 | 輪次切換 tab（導覽）；全 repo 無人引用 |
| src/components/grc/SspImportDialog.vue | 501 | 3 | 上傳、重新驗證、確認匯入；篩選鈕／選檔鈕不列 |
| src/components/grc/SspSystemForm.vue | 201 | 0 | 儲存鈕 :194 是 TODO stub，只跳「尚未開放」toast 不打 API；全 repo 無人引用 |
| src/components/grc/StatCard.vue | 36 | 0 | 統計卡，只 emit click 開浮層 |
| src/components/grc/UnfilledListOverlay.vue | 68 | 0 | 浮層清單，點項目跳轉（導覽） |
| src/components/grc/ap-docx-import/ApDocxImportDialog.vue | 795 | 2 | 選檔上傳解析＋確認匯入；取消／關閉為導覽 |
| src/components/grc/ar-import/ArImportDialog.vue | 718 | 3 | 選檔上傳解析＋捨棄＋確認匯入 |
| src/components/grc/job-drawer/JobActionCommentDialog.vue | 67 | 2 | 完成／退回送出鈕（emit 給抽屜） |
| src/components/grc/job-drawer/JobBlockedNotice.vue | 106 | 1 | 強制開始（emit 給父層） |
| src/components/grc/job-drawer/JobDrawerHeader.vue | 91 | 0 | 只有關閉鈕（導覽） |
| src/components/grc/planning/AoJobSection.vue | 149 | 0 | 任務列只 emit open（開抽屜），無寫入 |
| src/components/grc/planning/AoTaskForm.vue | 718 | 2 | 刪除／儲存（emit）；分派列增刪複製、排程皆本地狀態不打 API |
| src/components/grc/planning/AoTaskSection.vue | 270 | 2 | 新增任務（兩處入口，emit） |
| src/components/grc/planning/PlanningStatsBar.vue | 106 | 0 | 統計卡＋清單 overlay，只 emit 跳轉 |
| src/components/grc/project/ProjectBasicInfoPanel.vue | 144 | 1 | 儲存 |
| src/components/grc/project/ProjectCloudIntegrationsPanel.vue | 157 | 1 | 檢查並修復確認鈕；「開啟雲端資料夾」為外連導覽不列 |
| src/components/grc/project/ProjectParticipantsPanel.vue | 108 | 1 | 儲存 |
| src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue | 1344 | 2 | 上傳 dropzone＋確認匯入；取消／上一步會順手 DELETE 暫存解析，屬導覽不列 |
| src/components/grc/ssp-docx-import-v2/diff/ComponentDiffCard.vue | 271 | 0 | SelectButton emit `update:action` → 只寫本地 diff store，最終由 SspDocxImportPage 確認鈕送出 |
| src/components/grc/ssp-docx-import-v2/diff/ComponentsDiffSection.vue | 104 | 0 | 只呼叫 store.setComponentDecision／bulkApply／setFilter（本地狀態） |
| src/components/grc/ssp-docx-import-v2/diff/ControlDiffCard.vue | 301 | 0 | SelectButton emit 決策，不打 API |
| src/components/grc/ssp-docx-import-v2/diff/ControlDiffSection.vue | 150 | 0 | 只寫本地 diff store |
| src/components/grc/ssp-docx-import-v2/diff/DiffResolutionStep.vue | 273 | 0 | 上一步／下一步 emit prev／next（導覽） |
| src/components/grc/ssp-docx-import-v2/diff/DiffSectionShell.vue | 130 | 0 | 「全部採用新值／全部保留現值」只 emit bulk-apply 改本地決策，不打 API |
| src/components/grc/ssp-docx-import-v2/diff/InventoryItemDiffCard.vue | 273 | 0 | SelectButton emit 決策，不打 API |
| src/components/grc/ssp-docx-import-v2/diff/InventoryItemsDiffSection.vue | 101 | 0 | 只寫本地 diff store |
| src/components/grc/ssp-docx-import-v2/diff/LeveragedAuthorizationDiffCard.vue | 266 | 0 | SelectButton emit 決策，不打 API |
| src/components/grc/ssp-docx-import-v2/diff/LeveragedAuthorizationsDiffSection.vue | 101 | 0 | 只寫本地 diff store |
| src/components/grc/ssp-docx-import-v2/diff/ObjectiveDiffRow.vue | 226 | 0 | SelectButton emit 決策，不打 API |
| src/components/grc/ssp-docx-import-v2/diff/PartiesDiffSection.vue | 106 | 0 | 只寫本地 diff store |
| src/components/grc/ssp-docx-import-v2/diff/PartyDiffCard.vue | 294 | 0 | SelectButton emit 決策，不打 API |
| src/components/grc/ssp-docx-import-v2/diff/SystemCharacteristicDiffSection.vue | 320 | 0 | 只呼叫 store.setScDecision（本地） |
| src/components/grc/ssp-docx-import-v2/sections/ControlImplSection.vue | 471 | 0 | 「編輯／儲存」只 emit update:override 寫入頁面 draft（localStorage），不打 API |
| src/components/grc/ssp-docx-import-v2/sections/LeveragedSection.vue | 247 | 0 | 表格內欄位編輯 emit update-la／update-comp 寫 draft，無按鈕 |
| src/components/grc/ssp-docx-import-v2/sections/MetadataSection.vue | 49 | 0 | 已不被 SspDocxImportPage 引用（import 已註解）；只有輸入欄 |
| src/components/grc/ssp-docx-import-v2/sections/PartiesSection.vue | 387 | 0 | 連結／取消連結只改 draft，搜尋候選在 PartyLinkDialog |
| src/components/grc/ssp-docx-import-v2/sections/PartyLinkDialog.vue | 127 | 0 | 搜尋鈕打 POST /users、POST /org-units 屬查詢（iam.user.read／iam.org-unit.read），選中只 emit linked |
| src/components/grc/ssp-docx-import-v2/sections/SystemCharacteristicSection.vue | 115 | 0 | 只有輸入欄，emit update 寫 draft |
| src/components/grc/ssp-docx-import-v2/sections/UnmatchedSection.vue | 48 | 0 | 純顯示（且已無引用） |
| src/components/grc/ssp-excel-import/EntityPickerDialog.vue | 146 | 1 | 確認鈕 emit pick（死碼鏈） |
| src/components/grc/ssp-excel-import/InlineCreateDialog.vue | 174 | 1 | 確認鈕 emit save（死碼鏈） |
| src/components/grc/ssp-excel-import/MatchStatusCell.vue | 71 | 0 | 純顯示 Tag，無按鈕 |
| src/components/grc/ssp-excel-import/RowActionMenu.vue | 116 | 1 | Menu 兩項寫 store；pick/inline_new 兩項只開同檔子 dialog 不另列；⋯ 按鈕只開選單 |
| src/components/grc/ssp-excel-import/SheetPreviewBasic.vue | 161 | 0 | 只有表單欄位寫 store override，無按鈕 |
| src/components/grc/ssp-excel-import/SheetPreviewComponents.vue | 250 | 1 | 移除／復原 |
| src/components/grc/ssp-excel-import/SheetPreviewControls.vue | 567 | 0 | 只有 Checkbox（納入/群組全選）與欄位編輯；全部展開／收合為導覽 |
| src/components/grc/ssp-excel-import/SheetPreviewDevices.vue | 163 | 1 | 移除／復原（死碼） |
| src/components/grc/ssp-excel-import/SheetPreviewInfoSystems.vue | 143 | 1 | 移除／復原（死碼） |
| src/components/grc/ssp-excel-import/SheetPreviewInventoryItems.vue | 251 | 1 | 移除／復原 |
| src/components/grc/ssp-excel-import/SheetPreviewLeveraged.vue | 229 | 1 | 移除／復原（死碼） |
| src/components/grc/ssp-excel-import/SheetPreviewLeveragedAuth.vue | 230 | 1 | 移除／復原 |
| src/components/grc/ssp-excel-import/SheetPreviewParties.vue | 351 | 2 | 組織、人員各一組移除／復原 |
| src/components/grc/ssp-excel-import/SheetPreviewSystemAssets.vue | 196 | 1 | 移除／復原 |
| src/components/grc/ssp-excel-import/SheetPreviewSystemCharacteristic.vue | 262 | 0 | 只有表單欄位寫 store override，無按鈕 |
| src/components/grc/ssp/SspBasicSection.vue | 328 | 1 | 儲存 |
| src/components/grc/ssp/SspLeveragedSection.vue | 378 | 2 | 刪除、dialog 儲存（死碼）；新增／編輯鈕只開同檔 dialog |
| src/components/grc/ssp/SspTabPanel.vue | 510 | 2 | 下載 Excel、dialog 解析上傳；「從 Excel 匯入」入口鈕開同檔 dialog 不另列 |
| src/components/integrations/GoogleDriveIntegrationCard.vue | 446 | 5 | 連線／錯誤區重新授權／⋯選單重新授權／重建全部／中斷連線；「查看同步紀錄」選單項只開 dialog 不列 |
| src/components/integrations/SyncJobHistoryDialog.vue | 157 | 1 | 重試；重新整理鈕為 GET 不列 |
| src/components/license/LicenseExpiryBanner.vue | 115 | 0 | 「續約」是 router.push 到開通頁的導覽連結 |
| src/components/user-manage/UserInitialRoleDialog.vue | 69 | 0 | 「加入」只 emit add 回填前端列，不打 API（實際寫入隨 POST /user） |
| src/components/user-manage/UserInitialRolePicker.vue | 81 | 1 | 刪除列為純前端 |
| src/components/user-manage/UserMembershipEditor.vue | 82 | 0 | 只有 MultiSelect，無按鈕 |
| src/components/user-manage/UserRoleSummary.vue | 58 | 1 | 「管理指派」為導頁 |
| src/components/user-manage/UserTenantGroupList.vue | 91 | 0 | 唯讀顯示 |
| src/layout/AppLayout.vue（只看 :129-140） | — | 1 | 主 session 指定加看的 AI 小幫手浮鈕 |
| src/views/access/AssignmentDialog.vue | 220 | 1 | 取消為導覽 |
| src/views/access/AssignmentFormBody.vue | 99 | 0 | 表單欄位＋預覽，無送出鈕 |
| src/views/access/AssignmentManage.vue | 228 | 3 | 分組切換、篩選、顯示過期皆前端 |
| src/views/access/AssignmentMenuPreview.vue | 59 | 0 | 唯讀預覽 |
| src/views/access/CheckAccessBar.vue | 83 | 0 | 選人／清除只改網址 query |
| src/views/access/EffectiveAccessView.vue | 99 | 0 | 唯讀結果頁 |
| src/views/access/RoleDetail.vue | 174 | 0 | 按鈕都在子元件（Header／SaveBar）；本檔處理 emit，已計在子元件列 |
| src/views/access/RoleDetailHeader.vue | 67 | 3 | 範本下拉只套前端勾選 |
| src/views/access/RoleImportDialog.vue | 111 | 1 | 選檔為前端解析 |
| src/views/access/RoleInfoCard.vue | 52 | 0 | 「查看指派」為導頁 |
| src/views/access/RoleList.vue | 265 | 6 | 角色名連結、檢視／編輯鈕為導頁 |
| src/views/access/RolePatternCard.vue | 29 | 0 | 唯讀規則清單 |
| src/views/access/RoleSaveBar.vue | 49 | 1 | 「捨棄」為前端還原 |
| src/views/action-log/UserLogs.vue | 234 | 1 | 匯出；其餘為查詢／重置 |
| src/views/ai-call-log/AiCallLogList.vue | 293 | 1 | 匯出；列點擊開明細只打 GET |
| src/views/ai-call-log/AiUsagePage.vue | 42 | 0 | 只是兩個分頁的外殼 |
| src/views/ai-call-log/AiUsageSummary.vue | 241 | 1 | 匯出；「明細 →」是切分頁下鑽、展開鈕是展開收合 |
| src/views/ai-quota/AiQuotaCeilingTable.vue | 138 | 1 | 儲存上限；點列開窗同檔 |
| src/views/ai-quota/AiQuotaForm.vue | 101 | 0 | 純輸入欄位元件，送出在父層 |
| src/views/ai-quota/AiQuotaPage.vue | 168 | 2 | 回復預設、儲存 |
| src/views/ai-quota/AiQuotaUsageCards.vue | 87 | 0 | 純顯示統計卡 |
| src/views/ai-service-config/AiServiceConfigForm.vue | 311 | 2 | 清除金鑰＋儲存；「回復繼承」模型鈕是純前端重設不列 |
| src/views/auth/ChangePassword.vue | 243 | 1 | 返回登入為導覽 |
| src/views/auth/ForgotPassword.vue | 164 | 1 | 返回為導覽 |
| src/views/auth/Login.vue | 730 | 1 | 登入方式 tab、顯示密碼、忘記密碼連結、語言、主題切換皆前端 |
| src/views/auth/OtpVerification.vue | 363 | 3 | — |
| src/views/bulletins/BulletinForm.vue | 323 | 1 | 儲存；取消是返回 |
| src/views/bulletins/BulletinList.vue | 8 | 0 | 以 view 模式包 BulletinManage，本身無按鈕 |
| src/views/bulletins/BulletinManage.vue | 291 | 3 | 新增入口、編輯入口、刪除；瀏覽鈕是導覽 |
| src/views/bulletins/BulletinView.vue | 94 | 0 | 唯讀檢視頁，只有返回 |
| src/views/compliance-framework/ComplianceFrameworkManage.vue | 548 | 3 | 新增鈕／選單編輯只開同檔 dialog 不列；名稱連結與「檢視版本」為導覽 |
| src/views/compliance-framework/ComplianceFrameworkVersionManage.vue | 739 | 6 | 新增鈕／選單編輯開同檔 dialog 不列；子版本內容、版本連結為導覽；Excel 下載已註解停用 |
| src/views/compliance-framework/FrameworkImportPage.vue | 775 | 5 | 另內嵌 FrameworkArticleContent（:619，本地草稿編輯，已列在元件）；看草稿、開啟草稿、重新上傳、選檔區為導覽／本地 |
| src/views/compliance-framework/FrameworkVersionEditView.vue | 346 | 1 | 條文的改／刪由 FrameworkArticlePane emit 到本頁 handler 打 API，已列在 FrameworkArticleContent 列；選擇檔案、回列表為本地／導覽 |
| src/views/default/NotFound.vue | 131 | 0 | 404 頁，只有回首頁 |
| src/views/default/SystemLocked.vue | 209 | 0 | 系統鎖定頁，只有複製到剪貼簿 |
| src/views/detection-profile/DetectionProfileControlsView.vue | 721 | 0 | 本檔只有返回、全部展開、篩選；重新解析鈕在子元件 ControlExtractionSummary（已列在該檔，API 由本檔 handleRetry:329 打） |
| src/views/detection-profile/DetectionProfileManageView.vue | 968 | 8 | 新增鈕＋kebab 選單 7 項寫入（檢視／控制項為讀取不列） |
| src/views/detection-profile/components/ControlExtractionSummary.vue | 423 | 3 | 三顆重新解析／開始解析都 emit retry |
| src/views/detection-profile/components/ProfileCopyDialog.vue | 153 | 1 | fork／copy 共用送出鈕 |
| src/views/detection-profile/components/ProfileDetailDialog.vue | 170 | 0 | 唯讀詳細資料，只有關閉鈕 |
| src/views/detection-profile/components/ProfileFormDialog.vue | 458 | 1 | 新增／編輯共用送出鈕 |
| src/views/detection-profile/components/ProfileNewVersionDialog.vue | 179 | 1 |  |
| src/views/detection-profile/components/ProfileSourceDialog.vue | 211 | 1 |  |
| src/views/detection-profile/components/ProfileVersionTable.vue | 595 | 3 | 版本列 kebab 選單三項寫入（控制項為導覽不列） |
| src/views/device/DeviceExportMenu.vue | 42 | 1 | 匯出選單（兩種格式算一顆） |
| src/views/device/DeviceImportDialog.vue | 168 | 3 | 下載範本、選檔驗證、確認匯入 |
| src/views/device/DeviceManage.vue | 609 | 4 | 批次刪除、匯入入口、列內刪除、dialog 儲存（新增／編輯入口同檔不列） |
| src/views/dynamic-dashboard/DynamicDashboard.vue | 130 | 1 | 重新產生 |
| src/views/dynamic-dashboard/components/DashboardBattleRoom.vue | 80 | 0 | 只有展開／收合 |
| src/views/dynamic-dashboard/components/DashboardErrorMessage.vue | 36 | 1 | 重試（emit） |
| src/views/dynamic-dashboard/components/DashboardLoadingState.vue | 37 | 0 | 純顯示 |
| src/views/dynamic-dashboard/components/DashboardPromptBar.vue | 63 | 1 | 產生（emit） |
| src/views/dynamic-dashboard/components/DashboardTemplates.vue | 57 | 1 | 模板卡片（emit，點即產生） |
| src/views/evidence-classification/AutoClassifyBatch.vue | 1007 | 5 | 上傳／移除檔／開始分類／清理／歸檔確認；開 dialog 的入口鈕（:680、:702）與返回清單不列 |
| src/views/evidence-classification/AutoClassifyList.vue | 588 | 4 | 新增×2／清理／刪除確認；繼續、查看為導覽 |
| src/views/evidence-classification/EvidenceClassificationReview.vue | 523 | 1 | 歸檔入口（emit）；「重跑」鈕為 router.back() 導覽 |
| src/views/evidence-classification/components/AOPickerDialog.vue | 287 | 0 | 確認只 emit diff 給 FileDetail 改本地 state，不打 API（由頁頭儲存鈕統一 PUT） |
| src/views/evidence-classification/components/ClassificationFileDetail.vue | 472 | 0 | 確認並下一份／標非證據／取消非證據／加入檢查點／刪除檔（更多選單）都只 emit 改本地 state，不打 API |
| src/views/evidence-classification/components/ClassificationFileList.vue | 265 | 0 | 只有預覽鈕（emit preview）與排序，只讀 |
| src/views/evidence-classification/components/ClassificationFilterBar.vue | 128 | 0 | 純前端篩選與重置 |
| src/views/evidence-classification/components/ClassificationReviewHeader.vue | 251 | 1 | 儲存（emit save）；捨棄＝重新 GET、門檻套用＝本地重算，不列 |
| src/views/evidence-classification/components/EvidenceFilePreviewDialog.vue | 263 | 0 | 預覽為 GET；下載是把已取回的 blob 存檔，無獨立下載 API |
| src/views/evidence-classification/components/PlacementTable.vue | 237 | 0 | 移除／復原／加入只 emit 改本地 state |
| src/views/feedback/system/SystemFeedbackForm.vue | 469 | 3 | 下載附件、刪附件、儲存；FileUpload 選檔／清除只動前端暫存，隨儲存送出 |
| src/views/feedback/system/SystemFeedbackManage.vue | 343 | 5 | 匯出、新增入口、編輯入口×2、刪除；預覽鈕開新分頁是導覽 |
| src/views/feedback/system/SystemFeedbackView.vue | 157 | 1 | 下載附件；預覽、返回是導覽 |
| src/views/flow-template/FlowTemplateEditorView.vue | 1424 | 5 | 預覽、⋮ 選單的匯入／匯出 BPMN（純前端讀檔／產 blob）、屬性面板（改畫布）不打 API；topology validate 為自動呼叫非按鈕 |
| src/views/flow-template/FlowTemplateManageView.vue | 631 | 6 | 預覽、篩選、搜尋為只讀；新增 dialog 的「儲存」只跳頁不打 API |
| src/views/flow-template/components/FlowTemplatePreviewDialog.vue | 241 | 0 | 只讀預覽（GET stage-objects），唯一按鈕是關閉；共用（被 FlowTemplateManageView、FlowTemplateEditorView、ProjectCreateView 用） |
| src/views/google-drive-app-config/GoogleDriveAppConfigForm.vue | 471 | 3 | 清除密鑰、驗證、儲存；複製網址鈕純前端不列 |
| src/views/home/Home.vue | 60 | 0 | 舊流程設定頁，只寫 localStorage 再導頁，不打 API |
| src/views/information-system/InformationSystemManage.vue | 793 | 3 | 批次停用、列內停用、dialog 儲存（新增／編輯入口同檔不列） |
| src/views/issue-integrate-config/IssueIntegrateConfigForm.vue | 203 | 2 | GitLab／GitHub 各一顆儲存 |
| src/views/ldap-config/LdapConfigForm.vue | 278 | 2 | 測試連線、儲存 |
| src/views/license/LicenseActivation.vue | 338 | 2 | 線上開通、離線上傳開通（皆附錄 C 豁免）；複製、重新登入不打寫入 API |
| src/views/license/LicenseManage.vue | 673 | 4 | 解除停權、停權送出、指派送出、LC 請照送出；時間線唯讀 |
| src/views/license/LicenseStatus.vue | 249 | 0 | 「上傳新授權照」是 router.push 到開通頁的導覽 |
| src/views/log-forwarding/LogForwardingForm.vue | 451 | 2 | 測試、儲存，已用 hasCap |
| src/views/module_frame/ImportDocxPage.vue | 70 | 0 | 純包裝，按鈕全在 SspDocxImportPage.vue（batch 05） |
| src/views/module_frame/ImportExcelPage.vue | 430 | 1 | 另有「下載空白樣板」鈕(:330) 打 GET /ssp-import-template 或 GET /module-frame/<uid>/ssp-import-template，屬 read，未列 |
| src/views/module_frame/ImportExcelPreviewPage.vue | 628 | 1 | 取消(:366)／上一步(:371) 會打 DELETE /ssp-excel-import/<parse_uid> 丟棄解析，屬導覽未列 |
| src/views/module_frame/ModuleFrame.vue | 1587 | 7 | 新增選單「下載匯入範本」(:74) 開 DownloadSspBlankTemplateDialog 打 GET /ssp-import-template 屬 read，未列 |
| src/views/module_frame/ModuleFrameDetail.vue | 547 | 0 | 唯讀逐層瀏覽群組／控制項／檢查項目，按鈕全是下鑽、返回、跳流程編輯器 |
| src/views/module_frame/ModuleFrameItems.vue | 523 | 2 | 疑孤兒頁（無路由） |
| src/views/module_frame/ModuleFrameTemplateEditView.vue | 2279 | 9 | 內嵌 ModuleFrameBasicInfoPanel／SspBasicSection／ModuleFramePartiesPanel／ComponentsLeveragedInventoryTab／ModuleFrameDocumentPoolPanel 的按鈕由 batch 04／06 盤；匯出類（已填樣板、匯出現況資料、SSP 文件匯出 docx/pdf/odt）皆 `oscal.resource-library.read`，未列 |
| src/views/notify-config/NotifyConfigForm.vue | 331 | 4 | Telegram／Discord 各測試＋儲存 |
| src/views/org-unit/OrgUnitManage.vue | 385 | 2 | 新增／編輯開同檔 dialog 不另列；重置查詢為前端 |
| src/views/plugin/ToolPluginManage.vue | 927 | 6 | 卡片測試連線、選單三項、設定儲存、測試 dialog 送出（複製程式碼為純前端不列） |
| src/views/project-summary-report/ProjectSummaryReportForm.vue | 343 | 2 | 匯出／儲存；歷史、取消為導覽 |
| src/views/project-summary-report/ProjectSummaryReportHistory.vue | 158 | 1 | 還原；載入此版本為 GET |
| src/views/project-summary-report/ProjectSummaryReportManage.vue | 328 | 2 | 新增 dialog 儲存／刪除；編輯鈕為導覽、新增鈕開同檔 dialog |
| src/views/project-summary-report/ProjectSummaryReportTabView.vue | 52 | 0 | 只包一層 TabView 嵌 ProjectSummaryReportForm |
| src/views/project-summary-report/TipTapEditor.vue | 377 | 0 | 編輯器工具列全為前端操作；共用（被 ProjectSummaryReportForm、ProjectSummaryReportHistory、BulletinForm、SystemFeedbackForm 用） |
| src/views/project/DashboardView.vue | 505 | 0 | 只讀儀表板：篩選切換、「查看全部」導覽、卡片點擊導覽；各區塊已用 hasCap('grc.project.read'／'grc.audit.read') 控顯示 |
| src/views/project/MyAuditsView.vue | 229 | 0 | 只讀清單：狀態篩選、搜尋、「進入稽核／查看結果」皆為導覽 |
| src/views/project/MyTasksView.vue | 741 | 1 | 批次完成；任務抽屜 JobExecutionDrawer（列點擊開啟）的按鈕不在本批 |
| src/views/project/ProjectApListView.vue | 340 | 2 | 發起覆核、建輪次（建輪次入口目前常數寫死隱藏） |
| src/views/project/ProjectAuditorOverview.vue | 2161 | 5 | 啟動任務 1 顆＋審閱切換 4 顆；其餘為導覽（規劃頁、稽核計畫、進入稽核、POA&M、自動分類、Drive 資料夾）與展開收合；任務執行在 JobExecutionDrawer、階段推進在 FlowPhaseBanner（皆別批） |
| src/views/project/ProjectCreateView.vue | 512 | 1 | 建立專案；其餘為步驟導覽、流程範本預覽（只讀 GET） |
| src/views/project/ProjectListView.vue | 430 | 5 | 批次刪除、新增入口、Drive 同步、列刪除、編輯儲存 |
| src/views/project/ProjectPlanningView.vue | 2613 | 11 | 頁首啟動任務、批次維護選單 7 項、快速配置確定、SSP 儲存 2 顆；任務新增／儲存／刪除／強制啟動走 AoTaskSection emit（別批，handler 在本檔）；程序書關聯/取消走 ReferenceDocumentList emit＋DocumentLinkDialog（別批） |
| src/views/project/ProjectSettingsView.vue | 125 | 0 | 只是 TabView 殼，三個 panel（ProjectBasicInfoPanel／ProjectParticipantsPanel／ProjectCloudIntegrationsPanel）在別批；本檔只有麵包屑導覽 |
| src/views/project/RoundApAuthoringView.vue | 1029 | 3 | docx 匯入入口、儲存行程、新增稽核人員；新增／刪除行程只動本地陣列 |
| src/views/project/RoundAuditReviewView.vue | 1077 | 8 | AR 匯入入口、判定、觀察增改刪、風險增改刪 |
| src/views/project/RoundPoamView.vue | 872 | 6 | 改善計畫新增／編輯／刪除、里程碑新增／改／刪／狀態循環；無匯出鈕 |
| src/views/remote-agent/RemoteAgentManage.vue | 709 | 10 | token 兩顆、待核准兩顆、列內開關＋四顆、dialog 儲存（重新整理／健康檢查／測試連線／複製不列） |
| src/views/scan-zone/ScanZoneDialog.vue | 72 | 1 |  |
| src/views/scan-zone/ScanZoneManage.vue | 186 | 5 |  |
| src/views/security-policy/SecurityPolicyForm.vue | 383 | 1 | 儲存，已用 hasCap；「前往 SMTP 設定」為導覽不列 |
| src/views/setup/SetupWizard.vue | 580 | 1 | 建立帳號（附錄 C 免登入）；「下一步」只打 GET /setup/status 驗碼、重試、複製、去登入皆不列 |
| src/views/sitemap/SiteMap.vue | 51 | 0 | 只有導覽鈕 |
| src/views/smtp-config/SmtpConfigForm.vue | 368 | 2 | 儲存（新增/修改兩用）、測試信 dialog 送出 |
| src/views/smtp-config/SmtpConfigManage.vue | 170 | 3 | 新增入口、編輯入口、刪除 |
| src/views/storage-config/StorageConfigForm.vue | 568 | 2 | 恢復內建儲存、儲存 |
| src/views/support/SupportPage.vue | 275 | 1 | 匯出診斷包；重新整理與複製請求編號不列 |
| src/views/survey-v2/SurveyDesigner.vue | 1659 | 19 | 問卷設計頁；「問卷設定」dialog 只有 v-model 欄位＋關閉鈕，改動要靠儲存選單才落地 |
| src/views/survey-v2/SurveyManage.vue | 1133 | 8 | 問卷管理頁；SplitButton 主鈕／卡片選單「移動」「編輯」「預覽」為導覽或開本檔 dialog 不列 |
| src/views/survey-v2/SurveyPreview.vue | 2624 | 9 | 一支檔掛兩條 route：survey-v2-preview（設計者預覽，寫入鈕全被 mode 條件藏掉，0 顆）與 survey-v2-fill（任務填答／審核，歸 grc.job）；2164 行後全是 style |
| src/views/survey-v2/components/AddPageDialog.vue | 253 | 1 | 僅 SurveyDesigner 用；「清除條件」為本地操作 |
| src/views/survey-v2/components/AddQuestionDialog.vue | 1098 | 1 | 僅 SurveyDesigner 用；新增選項／刪選項／精靈增刪目標皆本地操作；600 行後全是 style |
| src/views/survey-v2/components/BatchImportDialog.vue | 310 | 2 | 僅 SurveyManage 用 |
| src/views/survey-v2/components/FolderTree.vue | 286 | 4 | 僅 SurveyManage 用；全靠 emit 交父層打 API |
| src/views/survey-v2/components/FolderTreeNode.vue | 94 | 0 | 只 emit select/toggle/ctx-menu，無寫入；且全 repo 無任何檔 import（死元件） |
| src/views/survey-v2/components/PageTreeNode.vue | 200 | 2 | 僅 SurveyDesigner 用（遞迴自用） |
| src/views/survey-v2/components/SurveyPickerDialog.vue | 396 | 2 | 僅 SurveyManage 用；「清除篩選」為本地操作 |
| src/views/system-menu-manage/SystemMenuManage.vue | 405 | 4 | 批次刪除、列內編輯/刪除、dialog 儲存；新增鈕（:338）開同檔 dialog 不列、重置查詢不列 |
| src/views/tenant-manage/TenantManage.vue | 347 | 2 | 新增／編輯開同檔 dialog 不另列；重置查詢為前端 |
| src/views/user-manage/UserMTRBACForm.vue | 455 | 1 | 取消為導覽；角色區在子元件 |
| src/views/user-manage/UserManage.vue | 401 | 3 | 編輯鈕與帳號連結導到可看可改的同一張表單頁，不另列；展開 chip、重置查詢為前端 |
| src/views/user-manage/UserManageTabView.vue | 76 | 0 | 只有兩個 tab |
| src/views/user-manage/UserManageUpload.vue | 481 | 4 | 重新上傳、刪除勾選列、錯誤訊息鈕皆前端 |
| src/views/user-manage/UserProfileForm.vue | 684 | 5 | 取消、看 QR（GET）為非寫入；LDAP 綁定入口開同檔 dialog |
| src/views/workflow/BpmnProcess/components/BpmnProcessAddEdit/index.vue | 97 | 0 | 舊頁殘骸：src/router 無掛；Element Plus 寫法，handleOk 只 console.log |
| src/views/workflow/BpmnProcess/components/BpmnProcessDrawer/index.vue | 217 | 0 | 舊頁殘骸：src/router 無掛；import 的 @/api/bpmnProcess 不存在，刪除只彈訊息 |
| src/views/workflow/BpmnProcess/components/BpmnProcessModelAddEdit/index.vue | 118 | 0 | 舊頁殘骸：src/router 無掛；import 的 @/designer、@/store 不存在，handleOk 只 console.log |
| src/views/workflow/BpmnProcess/index.vue | 183 | 0 | 舊頁殘骸：src/router 無掛、全 repo 無人 import；import 的 @/api/bpmnProcess 不存在，新增／編輯／刪除皆無真 API |
| src/views/workflow/WorkflowSetupEditor.vue | 588 | 2 | 匯出 XML 為本地 blob、任務屬性面板「儲存」只改畫布不打 API、返回為導覽；僅 WorkflowSetupForm.vue 用 |
| src/views/workflow/WorkflowSetupForm.vue | 36 | 0 | 純外框，包 WorkflowSetupEditor；路由 /workflow/workflow-setup 有掛（側邊選單已關，四支頁面以程式開啟） |

## 附錄：各批盤點的觀察原文

- 三支檔都沒有任何公司層權限判斷（無 hasCap／can／useMyCapabilities），寫入鈕只靠專案角色（isProjectManager／canEdit／canReviewControl）＋輪次狀態＋writeDisabled（授權唯讀）。
- ProjectPlanningView 批次維護選單的三種匯出（SSP Excel、任務 Excel、SSP 文件）完全無條件，只要 sspUid 存在即顯示，任何專案參與者含 viewer 都看得到。
- ProjectPlanningView 的子元件 emit 由本檔處理的寫入（給子元件批次核對）：AoTaskSection @create → POST /grc/project/<pid>/assessment-object/<ao>/jobs（grc.job.create）；@save → PUT /grc/project/<pid>/job/<job>（grc.job.update）；@delete → DELETE /grc/project/<pid>/job/<job>（grc.job.delete）；@force-start → useJobForceStart POST /grc/project/<pid>/job/<job>/force-start（grc.job.assign）；ReferenceDocumentList @unlink → DELETE /ssp/<ssp_uid>/.../document-mapping/<doc>（oscal.ssp.update）。AoTaskSection 的 can-edit 傳 canEdit、can-force-start 傳 isProjectManager。
- ProjectPlanningView 傳給設定類 panel 的 can-edit＝isProjectManager；ProjectSettingsView 傳的 canEdit 是自己算的同義 computed（participants 中 role==='manager'）。
- 死碼：ProjectAuditorOverview 的 handleCloseRound（打 POST /grc/project/<pid>/ap/<ap>/close-round，附錄 A 只有 /audit-round/<rid>/close-round）、handleSaveSspDesc（mock）、downloadEvidence 都沒有 template 引用；ProjectPlanningView 的 handleUploadRefDoc、handleRemoveRefDoc、showDocPoolDialog 入口也無引用。
- ProjectAuditorOverview 的審閱 POST/DELETE 打 /grc/project/<pid>/ap/<round>/control/... 舊 AP 路徑格式（round 模式下 ap 段塞 roundUid），附錄 A 有登記此路徑。
- ProjectListView 是本批唯一有公司層判斷的頁，但走 `getUserPagePermissions('/project/projects')` 比 action 字串（CREATE／UPDATE／DELETE），不是 hasCap；Drive 同步鈕完全沒判斷。
- ProjectCreateView 送出鈕無判斷，只靠 ProjectListView 入口擋；直接打網址進建立頁可看到送出鈕。
- RoundApAuthoringView 的 `saveControls`（PUT /ap/<ap>/reviewed-controls）、`saveSubjects`（PUT /ap/<ap>/assessment-subjects）與 RoundAuditReviewView 的 `linkFindings` 有定義但 template 沒有任何按鈕呼叫，是死碼。
- RoundAuditReviewView 判定／新增觀察／新增風險一顆鈕連打 create＋update 兩種權限，實際需兩者都有才不會半途失敗（比 hasAny 嚴），實作棒請留意。
- RoundPoamView 改善計畫「編輯」走 upsert POST，權限表歸 grc.audit.create；只給 update 的角色會編輯不了。
- 三支輪次頁（ApAuthoring／AuditReview／Poam）都嵌 FlowPhaseBanner（推進／退回階段鈕），該元件不在本批，需由涵蓋 components/grc 的批次處理。
- 12 支檔案全部沒有任何公司層權限判斷（無 hasCap／can／useMyCapabilities），寫入鈕一律只靠專案角色、props（canExecute／canReturn／canForceStart／canEdit）或 BE 回的旗標（user_can_advance、user_role、can_revert_job）。
- FlowPhaseBanner 的推進／退回顯示完全由 BE stage/info 的 user_can_advance／user_role 決定，FE 沒有自己的角色判斷；實作棒若加 hasCap 需與該旗標 AND。
- 抽屜「完成／退回」先打 flow-engine 再補打留言 API：若角色有 grc.job.update 但無 grc.job-comment.create，任務已完成／退回但留言 403，屬半套寫入（退回必帶留言，必中）。
- 檢測執行全部動作（執行、取消、重跑、立即開始、刪紀錄、整組刪）都對到 grc.job.update，與「完成任務」同一權限，無法單獨遮檢測操作。
- JobImportDialog 本身無任何條件，入口在 ProjectPlanningView.vue:242（不在本批），且入口用的是 canEditSsp 而非任務相關條件。
- AoTaskSection 新增任務鈕無 v-if，只用 disabled 呈現原因；若改「照權限隱藏」需留意與既有「反灰＋原因提示」設計的取捨。
- 源碼壓縮包上傳與報告／證據下載走 /file/* 豁免端點，前端不需判斷（真正守門在後續 execute）。
- 本批 24 支沒有任何一支用 hasCap／can／useMyCapabilities；`writeDisabled()` 全是 FR-062 授權唯讀反灰，不是權限判斷。
- 同一份 SSP 內容兩種專案層判準不一致：SspTabPanel 的 `canEdit` 要 planning 且 role==='manager'；ProjectPlanningView 給 DocumentPoolPanel／ReferenceDocumentList／SspImportDialog 的 `canEditSsp` 只看 apStatus==='planning'，不看專案角色——非 manager 參與者在規劃期也看得到上傳／刪除／關聯／匯入鈕。
- 共用 panel 一律以 apiBase 切 SSP（oscal.ssp.*）或資源庫（oscal.resource-library.*），實作棒需由頁面依情境傳入對應權限，元件內不可寫死單一名字。
- 死碼候選：DepartmentsPanel、RoundSwitcherBar、SspSystemForm、ModuleFramePartiesDialog、ModuleFrameSspResourcesDialog（連帶 Devices／InfoSystems 兩 panel 實際上不會出現在畫面）。
- ReferenceDocumentList 的「刪除」（:217）與上傳區（:236）只在 `!linkMode` 時顯示，但現有三個使用端不是 linkMode 就是 readonly，這兩條 emit（remove／upload）無父層處理，實際不會出現。
- ModuleFrameDocumentPoolPanel 的 ssp 分支（POST /ssp/<uid>/document-pool）目前無人以 api-base="ssp" 掛載。
- 三支匯入精靈本身都只有 `writeDisabled`（授權唯讀反灰），沒有任何公司層權限判斷；AP／AR 靠宿主頁的專案角色＋輪次狀態決定是否出現入口鈕，實作棒可直接在宿主入口鈕（RoundApAuthoringView.vue:664、RoundAuditReviewView.vue:721，屬 02 批）加 `grc.audit.import` 判斷。
- SspDocxImportPage 的權限依 `mode` 而變（同一元件服務專案 SSP 與資源庫兩條路），建議由兩個宿主頁依 mode 傳入「可匯入」旗標；兩支 views/*/ImportDocxPage.vue 目前也都沒有任何權限判斷（路由頁本身，屬 07 批與 project 批）。
- PartyLinkDialog 搜尋候選打 POST /users（iam.user.read）、POST /org-units（iam.org-unit.read）——只有 oscal.ssp.import 而無 iam 讀權限的人在 Step 3 連結人員時會搜不到（403），實作棒可能要注意這個相依。
- MetadataSection.vue、UnmatchedSection.vue 已無任何檔案引用，屬死碼。
- 本批 19 支沒有任何一支有 hasCap／can／useMyCapabilities；所見 `writeDisabled()` 全是 FR-062 授權唯讀（license），不是能力點。
- 死碼：SspLeveragedSection、SheetPreviewDevices／InfoSystems／Leveraged，以及只被它們用的 RowActionMenu → EntityPickerDialog／InlineCreateDialog，全專案無 import；實作棒可略過或順手刪。
- SSP Excel 預覽子元件全部只寫 sspExcelImportStore，唯一送出點是 ImportExcelPreviewPage.vue（不在本批）的確認鈕 POST /ssp-excel-import/<parse_uid>/confirm，權限依匯入來源（通則 C）三擇一；建議在該頁整頁／確認鈕判斷，子元件不逐顆判。
- SspTabPanel 下載已填 Excel（oscal.ssp.export）完全沒條件，非 manager 也看得到。
- SspBasicSection 在 MF 樣板頁（ModuleFrameTemplateEditView）不傳 readonly，儲存鈕對任何能進頁的人都顯示；需由頁面傳入 oscal.resource-library.update 判斷結果。
- 七支檔全部沒有 hasCap／can／useMyCapabilities，寫入鈕只靠 useLicenseReadonly 的 writeDisabled（授權唯讀反灰）；實作棒要另加能力點判斷。
- ModuleFrame.vue 的 onCloneClick（POST /module-frame/clone）與 cloneDialog 沒有任何 template 掛載，是死碼。
- ModuleFrameTemplateEditView 的 editMenuItems（:1207）沒有 Menu 渲染，也是死碼。
- ModuleFrameItems.vue 沒有路由指向（`module-frame-items` 路由指到 ModuleFrameDetail.vue），整支疑似孤兒頁；BE item DELETE 今天守 `oscal.resource-library.delete`、POST 守 `.create`，與附錄 A 的 update 不一致（附錄 A 已裁）。
- ModuleFrame 列動作「編輯預設值與內容」(:221) 只是導到 template-edit 頁（名稱連結 :1092 也會導過去），未列；該頁本身可讀，寫入鈕已在該頁逐顆列出。
- ImportExcelPreviewPage 確認匯入的權限要依 source_type 決定（同一顆鈕可能是資源庫新增／更新／專案 SSP 匯入），實作時判斷式要吃 parseResult.source_type。
- 合規框架三頁（管理／版本／匯入／編輯）今天全用 `isPlatformAdmin`（作用租戶 path 是否單層）遮寫入鈕，完全沒看能力點；實作棒要改成 hasCap 時需決定 isPlatformAdmin 是否保留為第二層條件（後端另有 require_platform_admin()）。
- 流程範本兩頁的發布／撤銷發布都用 `flow.template.update` 判斷，附錄 A 新名是 `flow.template.publish`。
- 編輯器「發布」在 dirty 時會先自動存檔（PUT update），只有 publish 沒有 update 的人會在存檔那步被擋。
- WorkflowSetupEditor 會從 ProjectPlanningView（專案情境）開啟，但存檔 PUT /module-frame/item/xml/<uid> 在附錄 A 只對到 oscal.resource-library.update；專案情境的存檔權限（通則 C）附錄 A 沒有另列，實作棒需確認。
- views/workflow/BpmnProcess/ 整包四支是外來樣板殘骸（Element Plus、依賴的 @/api、@/hooks、@/store、@/designer 目錄都不存在），無路由無引用，可列刪除候選。
- compliance-framework/import/ 三支 ListEditor 無人引用，疑似兩階段匯入改文章式後遺留。
- ComplianceFrameworkManage 存檔用 `currFramework.id` 判新增／編輯，但編輯 PUT 用的是 `uid`；若列表資料不帶 id，編輯會誤走 POST 新增（非本卡範圍，順帶記錄）。
- 掃描設定檔管理頁與版本子表的 hasCap 名稱全部對得上附錄 A（create／update／delete 三顆），寫入項是 disabled＋label 後綴「無權限」而非隱藏，實作棒若改成隱藏要先跟 CM-1078 的「不可做成不能做就不放」設計對齊。
- ScanZoneManage 所有寫入都只看 `detection.scan-zone.update`，但附錄 A 把新增／產生建議改守 create、刪除改守 delete（A.2 拆分），三顆要換名；常數檔 src/config/scanZone.js 目前只有 READ／UPDATE 兩個。
- ControlExtractionSummary 已預留 `canRetry` prop，父層 DetectionProfileControlsView 沒傳，直接傳 `hasCap('detection.profile.create')` 即可。
- ToolPluginManage、RemoteAgentManage 完全沒有任何公司層判斷（只有授權唯讀 writeDisabled）；RemoteAgentManage 列內的啟用開關 :608 與 ToolPluginManage 卡片測試連線 :595 連 writeDisabled 都沒掛。
- RemoteAgentManage 的「健康檢查」（:511）與 dialog「測試連線」（:662）打的是 POST /remote-agents/health-check，附錄 A 守 `detection.remote-agent.read`，屬查詢性質未列入表。
- 工具卡片測試連線附錄 A 守 `detection.plugin.update`（寫入級），只有 read 權限的人看得到這顆但按了會 403。
- 自動分類審閱頁的「儲存」（PUT /classification-run/<r>/state）前端完全沒有角色或能力點判斷，稽核員進第③步也能編輯並按儲存；歸檔鈕卻有 canArchive。實作棒隱藏儲存時，FileDetail／PlacementTable／AOPickerDialog 那批只改本地 state 的編輯鈕應一併收（否則能改不能存）。
- GoogleDriveIntegrationCard 錯誤橫幅內的「重新授權」(:341) 漏了 canCreate，同動作的⋯選單版本有。
- 專案調查結果三頁只用 writeDisabled()（授權唯讀反灰），沒有任何 grc.summary-report.* 判斷；匯出鈕先 PUT 再匯出，只有 export 權限的人按下會先跳一次 403 錯誤再成功匯出。
- 自動分類三頁的寫入鈕全靠 isProjectManager（專案層），公司層 evidence.classification.* 一律沒判。
- 十支檔沒有任何 hasCap／can／useMyCapabilities 判斷；部分寫入鈕只掛 FR-062 的 writeDisabled（授權唯讀反灰），與能力點無關。SurveyManage、FolderTree、SurveyPickerDialog、AddPageDialog、AddQuestionDialog、PageTreeNode 連 writeDisabled 都沒有。
- SurveyDesigner「問卷設定」dialog 的「發布狀態」InputSwitch 改的是本地 survey.isPublished，再經儲存選單 PUT /survey/<uid> 送出——等於用 survey.survey.update 繞過 survey.survey.publish，實作棒要決定該開關是否也要照 publish 判斷或移除。
- 建立問卷 dialog（SurveyManage:863）也有「發布狀態」開關，經 POST /survey（survey.survey.create）一併設定發布，同樣繞過 publish。
- SurveyPreview 填答模式的逐題作答是走 socket `/socket/fill-survey` 的 `update` 事件即時寫入（不在附錄 A 的 REST 表內），只靠 canEdit 決定能不能動欄位；檔案題的刪除鈕（:1698／:1931）只是本地清空答案，靠後續存檔落地，未列表。
- SurveyPreview 的 preview 模式從 SurveyManage／SurveyDesigner 進入（屬 survey），fill 模式從 JobExecutionDrawer 開新分頁（屬 grc.job）；同一支元件兩種歸屬，實作棒判斷要依 route.query.mode 分流。
- 部門管理（OrgUnitManage）與租戶管理（TenantManage）所有寫入鈕都沒有能力點判斷；TenantManage 連授權唯讀 writeDisabled 也沒接。
- 帳號列表「刪除」走 POST /user/change-status/<uid>（status -2），後端守 iam.user.update；iam.user.delete 守的 DELETE /user/<uid> 前端沒有任何呼叫者，兩者語意不一致待決策。
- 帳號管理整組（UserManage／UserManageUpload／UserMTRBACForm）只靠授權唯讀反灰，沒有 hasCap／can；UserMTRBACForm 的「變更密碼」開關用 `userInfo.is_admin` 判，不是能力點。
- 新增帳號時的「新增角色列」不看 iam.user.assign，沒權限的人照樣能加列，存檔才被後端擋；對照編輯模式的 UserRoleSummary 已用 myCaps 擋。
- AssignmentDialog／RoleImportDialog 本身不判，靠入口鈕擋；三個指派入口都已正確用 iam.user.assign。
- AssignmentManage 讀用 `hasCap('iam.user.read')`（選單權限）、寫用 `useMyCapabilities`；RoleList／RoleDetail 一律用 useRoleCaps（＝useMyCapabilities）。
- Excel 批次建立（POST /user/batch-upload）會帶每列角色，附錄 A 未註明是否另要 iam.user.assign，與單筆 POST /user 不對稱，建議首腦確認。
- AI 服務設定、Google Drive 應用程式設定、支援頁三頁今天都只靠「平台管理員」程式常數旗標（canManage／isPlatformAdmin）擋整頁，按鈕本身完全沒有能力點判斷。
- 只有 LogForwardingForm、SecurityPolicyForm 已用 hasCap 遮寫入鈕，且名字正確；其餘設定頁只掛 FR-062 授權唯讀的 writeDisabled。
- 多顆「測試」鈕（LDAP 測試連線、通知測試、SMTP 測試信、Drive 驗證）連 writeDisabled 都沒掛。
- SystemMenuManage 完全沒有任何判斷（無 hasCap、無 writeDisabled），批次刪除是前端迴圈打單筆 DELETE。
- SmtpConfigForm 的儲存鈕與 SystemMenuManage 的 dialog 儲存鈕都是新增/修改共用一顆，實作棒要依模式（uid／id 有無）選 create 或 update，不是 hasAny。
- SMTP 列表刪除與編輯走 `/system/config/<uid>`，群組由後端依該列推得（SMTP）；前端不帶群組代碼。
- 對話紀錄的「清除對話」打 DELETE /ai-chatbot（ai.assistant.read），在 src/stores/aiChatStore.js:57，不在本批清單內，觸發它的按鈕需由別批確認。
- 回饋頁沿用舊的「選單動作」機制（getUserPagePermissions 回 CREATE／UPDATE／READ），SystemFeedbackManage 的 view 模式是「選單能力點只有一個」推出來的，實作棒換 hasCap 時要連 FeedbackManageTabView.vue:69 一起改。
- 設備匯入入口用 asset.device.create 擋，但三支匯入 API 的新名是 asset.device.import；匯出入口用 asset.device.read 擋，新名是 asset.device.export。
- 設備新增鈕（DeviceManage.vue:364）與資訊系統新增／編輯／刪除全部沒有能力點判斷，只有 writeDisabled() 授權唯讀反灰。
- LicenseManage.vue 整頁沒有任何能力點判斷，完全靠選單過濾擋人。
- POST /bulletins 依能力點分讀者／管理模式，BulletinManage 前端用 props.view 區分；BulletinList 傳 view=true，寫入鈕已藏。
