# FR-074.5 / CM-1541 — 04-04 錄影 LOG（append-only，每棒追加一 block）

> 現況看 `fr074-04-04-STATE.md`（living）。本檔只記「發生過什麼、為什麼這樣決定、推翻了什麼」。

---

## 第 1 棒 — 2026-09-03（製片 runner，Opus 5）

**交付**：A1／A2 錄完並 commit；B1～C 未開始（等 OpenVAS）。

### commits（test repo，branch `feature/FR-069.5`，未 push）

| hash | 內容 |
|------|------|
| `a1d2ace` | 阻塞診斷寫進 outline（未寫 feature、未錄影）|
| `05f0941` | A1／A2 依 user 拍板選項 1 改寫劇本 |
| `0e6dab8` | feature 產出 ＋ A1 錄影 ＋ merge 腳本補條目 |
| `1346fa8` | A2 錄製完成 ＋ 修 S0 上傳 bug ＋ 03-status 補八個坑 |

### 🔴 推翻了什麼：原大綱的 A 段劇本前提不成立

開錄前唯讀盤點發現：04-03 建的七個任務狀態全是 `TODO`，**執行者抽屜沒有「開始執行」鈕**
（FE `JobExecutionDrawer.vue:218` `canEdit = props.canExecute && currentStatus === 'PROCESSING'`；
`MyTasksView.vue:553` 只在 PROCESSING／COMPLETED 給 canExecute）。
原大綱「Alex 逐一按開始執行」在真實流程裡做不到。

轉 PROCESSING 的唯一入口是 PM 規劃頁的「開始執行任務」，而該 service
（`jedi_task_platform` `TaskExecutionService.start_task_execution`）內含
`_auto_dispatch_detection_scans`（FR-056.5 #5）：轉狀態的同時**自動派掉尚無執行紀錄的
detection 任務的第一次掃描**，只排除 `scan_mode=upload` 的列
（`infra/flow_control/repository/task_execution_query.py:149-212`）。
190 只有 SonarQube 是 upload → 一按下去六支自動開掃、執行者一個鈕都沒按。

**給了 user 三個選項**（改劇本／改資料／只錄得了的），**user 拍板選項 1：改劇本貼合產品**。
→ A1 改 PM 視角（`@auth-pm`）、A2 改執行者視角，全章只有 SonarQube 示範手動發動。

**沒有選 2（清資料）是對的**：清執行紀錄屬 190 寫入型異動，且 CINC 後來真的失敗又重跑成功，
反而成了 C 段「掃描失敗怎麼重試」的真實素材。

### A2 錄了四次，前三次各撞一個坑（逐項修，第四次 100/100 全綠）

| 次 | 死在 | 原因 | 修法 |
|---|------|------|------|
| 1 | 步驟 36（CINC）| 硬等「執行中」，但該支 19:46 已 failed | 拿掉全部「等待執行狀態出現執行中」，改導覽執行紀錄區＋字卡講時間差 |
| 2 | 步驟 36（GCB）| GCB 是 auto 完成模式，掃完 `COMPLETED`、從預設「進行中」清單消失 | A2 開頭補 `我篩選「全部」的任務`，字卡順勢解釋此行為 |
| 3 | 步驟 92（SonarQube）| **S0 `uploadSourceArchive()` 的 bug**（見下）| 改走 filechooser 真實路徑 |
| 4 | — | **全綠，9:37** | — |

### 🔴 修了一支 S0 檔（越過卡片停手線，已在 commit 與卡片寫明）

`training/pages/detection-tools/DetectionTaskExecPage.js` 的 `uploadSourceArchive()`：
原本直接對隱藏 input `setInputFiles`（S0 判斷 Playwright 對隱藏 input 可行——這點本身沒錯，
**錯在跳過了 FE 的歸屬邏輯**）。

FE 的壓縮包是**按分派列歸屬**的：`pickSourceFile(row)` 先把 `sourceFilePickTarget` 設成該列 key，
`uploadSourceFile()` 再用它決定檔案掛哪一列（`JobExecutionDrawer.vue:428-437`）。
直接餵 input → target 停在 null → 檔案落到 `_default` key → 該列仍算缺檔 →
`executeBlockedByMissingSource` 成立 → 「開始執行」維持 `p-disabled`。

**症狀極具欺騙性**：上傳 toast 有出、看起來成功，但按鈕點不下去，畫面還留著
「此任務需上傳原始碼壓縮包才能執行」。

**為什麼選擇修而不是繞過**：這是 S0 的真 bug（S0 當時 190 上 0 檢測任務、測不出來），
繞過只會讓別章再踩一次。只動這一個方法，未動其他 S0 檔與共用層，並在方法上寫明成因。

### 順帶拍到的真實素材：CINC 失敗與重跑

CINC 第一次派工真的失敗，錯誤是
`Sudo cannot prompt for password because there is no terminal`
（sudo 需要 TTY／NOPASSWD，**不完全等於帳號權限不足**——回報 user 時已指出這點）。
user 補權限後，A2 錄影中由劇本按下「重新執行」**重跑成功**（執行紀錄 id 8、9）。
失敗紀錄不是 active 執行 → 按下去不跳確認框，故不接 `我確認重新執行`。

這段順勢改寫成「掃描失敗怎麼重試」的字卡（權限不足會失敗／失敗紀錄留著方便排查／
補好後在抽屜按一次即可），比原本平鋪直敘的「看一眼設定」更有教學價值。

**留給 B2 的坑**：CINC 抽屜現在失敗與成功紀錄並存，`第一筆` 取的是 nth(0)，
錄前務必先唯讀確認畫面排序。

### 其他實查校正（已寫進 outline 與 03-status）

- 抽屜標題是「任務指南」不是「任務指引」
- OpenSCAP 是兩組分派版型（`exec-info-grid` 3 個、完整參數 toggle 2 個、「第 N 組」Tag）
- OpenVAS 無「完整參數」摺疊
- Seg-H 設的 22:00 排程組**提早跑完了** → 原本寫「排在當晚十點」的字卡與畫面不符，
  已改成講兩組各跑各的、排程組會先顯示排程中

### 方法論筆記

**開錄前的唯讀盤點是這棒最值錢的動作**。若照卡片直接寫 feature 再跑，會在第一個動作就失敗、
且看起來像 selector 問題（實際是產品前置流程），很容易被誤診成「S0 的 step 寫錯」而去改 S0 檔。
先用 Playwright 唯讀 probe ＋ DB `SELECT` 把七個抽屜逐一看過，才定位到真因。

同理，A2 每次失敗後都先唯讀 probe 確認「按鈕到底是什麼狀態」再改劇本，
避免了「改一次錄一次、一次十分鐘」的盲修迴圈（實際仍花四輪，但每輪都修在真因上）。

---

## 第 2 棒 — 2026-09-03（製片 runner，接續第 1 棒）

**交付**：B1／B2／B3／B4／C 五段撰寫並全數正式錄完；OpenVAS 從 B3 拆出成 B5 獨立段，
重跑成功後一併正式錄；八段合併出 `04-detection-04-execute-collect.mp4`（26:23、91.5MB）。
本章拍攝到此收工。

### commits（test repo，branch `feature/FR-069.5`，未 push）

| hash | 內容 |
|------|------|
| `4cb71f9` | B1~C 五段完成、B5 新增、全八段合併完片 |

### 🔴 開錄前實查：OpenVAS 第一輪逾時失敗

接手時第 1 棒留的狀態是「只差 OpenVAS 還在跑」，但唯讀盤點（`SELECT` 執行紀錄）發現
它其實已經**逾時失敗**（`OpenVAS task … 逾時（3600s）未完成`，平台對單次掃描有一小時
上限）。這推翻了「等一下就會全部成功」的假設。

user 拍板：**不為它停工**——把 B3 拆開，OpenVAS 連同「弱點分級怎麼處理」字卡整段搬到
新的 B5（獨立後補段），B3 只留 ZAP。合併腳本原本用固定陣列＋`merge_one` 遇缺檔會整組
中止的機制，若照抄會讓 B5 缺席時連其餘七段都合不出來——改成動態陣列，B5 有就併入、
沒有就跳過，並在插點放在 B3（網站）與 B4（原始碼）之間，維持「主機弱點→網站→原始碼」
的敘事層次。

第二輪重跑 21:08 起算，**21:50 成功**（42 分鐘，`summary` 含完整嚴重度分級：
critical 0／high 0／medium 6／low 2／log 78／findings 86）。時間點剛好落在本棒批次
正式錄影開始之前，B5 因此**沒有真的走到「後補」那條路**——直接和其他七段一起正式錄完。

### 批次驗證抓到兩個第 1 棒交接時沒預見的真問題

第一輪 `SKIP_OVERLAYS=true` 批次驗證（B1/B2/B3/B4/C，194 步）跑出 4/5 綠，只有 C 段死：

1. **C 段前提本身是錯的**：PM 在「我的任務」頁一筆任務都看不到。查根因是
   `public.vw_user_job_queue` 硬篩 `user_id = 當前使用者`
   （`infra/readmodel/tasks/my_grc_jobs_query.py:114`），七個檢測任務 `user_id` 全是
   Alex（id 9），Bob（id 8）本來就不在這張表上——**這是產品設計，不是權限漏配**。
   改走專案規劃頁的任務面板，內容照舊（完成模式回顧、重跑取消、無實質檢查、總結）。

2. **B1 人工完成段的按鈕會消失**：批次驗證跑完後 OpenSCAP 任務被真的按完成結案
   （`status = COMPLETED`），FE `canEdit = canExecute && status === 'PROCESSING'`
   條件不再成立，「完成任務」鈕不再顯示。正式錄若照原劇本硬按會卡住。
   兩條路：退回 PROCESSING（190 寫入型異動，依環境鐵律要 user 明示）或改字卡。
   user 拍板 B 案：改字卡說明「已經按過完成」，**不動 190 資料**，而且畫面上這個
   任務現在正好就是完成狀態，字卡跟畫面一致，不是硬凹。

### 附帶發現：GCB／CINC 沒有「發現 N 項」（陷阱④，第 1 棒交接檔沒抓到）

DB 實查兩支的 `summary` 形狀，只有 `pass/fail/error/notapplicable`，**沒有 `findings`
欄位**。FE `shouldShowFindings()`（`JobExecutionDrawer.vue:944`）在 `findings ===
undefined` 時直接不顯示「發現 N 項」文字，而 step `應看到發現項目統計` 比對的正是
`/發現 \d+ 項/`——對這兩支下這個斷言必然逾時 30 秒失敗。B1（GCB）、B2（CINC）已拿掉
斷言、字卡改口徑講「只給通過／失敗／不適用三個數，失敗那幾條就是要處理的」。

### 自己犯的錯：字卡文案用真實換行而不是 `\n`

改寫 B1、C 兩段的說明卡片時，我把新增內容用真實多行字串貼進去，但 Gherkin step 的
參數必須是單行、內部用 `\n` 字面值換行（前面所有段落都是這樣寫）。第一輪批次驗證前
`cucumber-js` 就 parse error 擋下，訊息很清楚（`expected: #EOF ... got '這段說明會跟
證據一起...'`），修正兩次（第一次漏改了 C 段）才過。**這不是產品或劇本問題，是我
自己寫 feature 時違反既有格式慣例**，記在這裡提醒之後改字卡一律檢查有沒有誤用真實
換行。

### CINC 排序陷阱：第 1 棒標「未能驗證」，第 2 棒已排除

第 1 棒交接檔標記「CINC 第一筆可能取到失敗那筆，錄前務必先唯讀確認」。第 2 棒查了
BE 排序邏輯：`jedi-detection` 的 `list_by_job_execution_ordered` 是
`started_at DESC, id DESC`，加上 DB 實查 CINC 三筆紀錄的實際順序（id 9 → 8 → 1），
確認**最新的成功紀錄排在最上面**，`我開啟第一筆執行的詳情` 可以直接用，不必改成
指定第 N 筆。這個陷阱從「風險」降級為「已驗證安全」。

### merge 前的 TRIM_HEAD 決策

第 1 棒交接檔提醒本章 A2／B 段起點（我的任務頁）比 04-03 規劃頁短，要求合併前先截圖
決定 TRIM_HEAD。第 2 棒實測：B 系列（我的任務頁）4 秒就到乾淨畫面，A1／C（規劃頁）
在 6～9 秒才到位。沿用既有預設 `TRIM_HEAD=8`——對最慢的規劃頁路徑安全，對 B 系列
只是多切 4 秒空景不傷內容，最短的 B3（99 秒）扣 8 秒後仍有 91 秒，不會被砍成空檔。
沒有另開特例值。

### 方法論筆記：交接檔的「現況」是快照，不是承諾

第 1 棒交接檔寫「只差 OpenVAS 還在跑」，這句話在交接當下是真的，但第 2 棒接手時
（隔了一段時間）它已經逾時失敗又重跑。**任何交接檔裡「正在進行」「即將完成」這類
動態狀態，接手時都要重新唯讀確認，不能當作接手當下依然成立的事實**——這跟
`feedback_card_env_assertions_verify_or_mark_assumption` 是同一個教訓在錄影場景的
版本。若當時直接照抄「等 OpenVAS 跑完」去等，會多空等超過一小時才發現它已死透。

---

## 第 3 棒 — 2026-09-03（製片 runner，接續第 2 棒，同日）

**交付**：修正 B1~B5 全五段的統計導覽 selector 誤圈、修正 B5 的 OpenVAS PDF 預覽卡死，
五段重錄、重新合併。

### commits（test repo，branch `feature/FR-069.5`，未 push）

| hash | 內容 |
|------|------|
| `15f7b7f` | 修正統計導覽 selector 誤圈與 OpenVAS PDF 預覽卡死 |

### 觸發：user 開合併片實看，一眼抓到 OpenVAS 統計導覽圈錯地方

user 打開第 2 棒交付的合併片，截圖指出 B5 段「弱點掃描統計」的導覽黃框圈住的是
「開始時間 2026-09-03 21:08:03／結束時間 21:50:35／觸發者 Alex Wang／逾時秒數
72000」這個區塊，但字卡講的是「嚴重、高、中、低幾條；發現數是這幾級的加總」——
真正的統計數字（`critical/發現項目/high/log/low/medium`）在框外面下方。同時
user 也回報 OpenVAS 報告預覽一直在轉圈，不對。

### 排查方式：不猜，寫唯讀 probe 直接對 190 驗證

兩個問題都沒有直接改 code 猜答案，而是先寫 Playwright 唯讀腳本（帶
`support/auth-states/executor.json` 直連 190，開頁面→開任務→開執行詳情/證據區）
逐一驗證：

- **selector 問題**：讀 FE 源碼確認 `.exec-info-grid` 在 `JobExecutionDrawer.vue`
  裡出現 5 次，其中執行詳情對話框那個（`:2987`）圈的是 metadata，真正含統計數字的
  區塊在 `:3087`（`v-if="execInfo.summary"`），標題文字是「掃描結果統計」（i18n key
  `execution_info_summary`）。改用 `text="掃描結果統計" >> xpath=..` 這個 selector
  在 probe 裡實測：`count=1`，`innerText` 完整輸出六個統計值。**B1（兩處）、B2（兩處）、
  B3、B4、B5 全部七處都圈錯**，不只 user 截圖抓到的那一處——GCB／CINC 因為字卡文字
  模糊，肉眼比較看不出矛盾，但底層是同一個 bug。

- **PDF 預覽問題**：查 DB 確認 OpenVAS 報告的 `upload_files.file_ext = 'pdf'`
  （其他六支都是 `html`）。probe 用 `page.on('requestfailed')` 攔到 iframe 的
  請求被標記 `net::ERR_ABORTED`；同一個 URL 用 `page.request.get()`（純 HTTP
  fetch，不經瀏覽器渲染邏輯）打卻回應完全正常（200、`application/pdf`、
  139846 bytes 有效 PDF）。再用 `page.on('download')` 確認瀏覽器把這個請求
  當成下載觸發，不是 inline 顯示。最後測了拿掉 context 的
  `acceptDownloads: true` 選項，行為完全沒變化——**排除是測試環境設定的問題，
  是 headless Chromium 沒有內建 PDF viewer 這個既定行為**，跟其他六支的 HTML
  報告是完全不同的處理路徑。

### 為什麼兩個問題在批次驗證階段都沒被抓到

`SKIP_OVERLAYS=true` 的驗證模式**完全跳過導覽框渲染**（`overlay.steps.js` 每個
導覽 step 開頭都是 `if (SKIP_OVERLAYS === 'true') return`），所以 selector 選錯
不會讓驗證失敗——`.exec-info-grid` 這個 selector 本身是存在的、count > 0，只是
指向錯的元素，批次驗證的「綠燈」只證明流程走得完，完全沒有驗證畫面內容是否正確。
PDF 預覽問題同理：`我點擊第一份掃描報告開啟預覽` 這個 step 只等 `iframe` 元素
`attached`（DOM 節點存在），不驗證它是否真的載入了內容，所以卡死轉圈的畫面在
自動化驗證裡也是「通過」。**兩個 bug 都只有真人看過實際畫面或影片才會被發現**，
這正是本棒方法論教訓要強調的：批次驗證證明「跑得完」，不證明「對不對」，最終
交付前一定要有人真的看過產出。

### 處理範圍：selector 修正影響全部五段，B5 修 fallback

因為 `.exec-info-grid` 的錯誤是七處共用，只重錄 B5 沒有意義——B1~B4 的導覽框
在畫面上也是圈錯的，只是沒被 user 明確指出。第 3 棒把 selector 修正套用到全部
七處，`SKIP_OVERLAYS=true` 批次驗證 B1~B5（selector 改動影響全部五段）跑
197 步全綠後，**五段全部重錄**（不只 B5），再重新合併八段。B5 額外加了
「改導覽下載報告」的 fallback，其餘四段只有導覽框指向修正，字卡與流程不變。

### 交付確認

- `SKIP_OVERLAYS=true` 批次驗證：5 scenarios、197 steps 全綠
- 正式重錄 B1/B2/B3/B5/B4：5 scenarios、197 steps 全綠
- 截圖抽查 B5 執行詳情對話框：黃框正確圈住「掃描結果統計：critical：0　發現項目：
  86　high：0　log：78　low：2　medium：6」整行，與字卡「嚴重、高、中、低各幾條；
  發現數是這幾級的加總」一致
- 截圖抽查 B5 PDF fallback：字幕「這份報告是 PDF，直接下載看」正常顯示，無轉圈殘留
- merge 重新跑：8 段全合，`04-detection-04-execute-collect.mp4`（26:23、90.5MB）

### 方法論筆記：綠燈是流程完整性的證明，不是畫面正確性的證明

這是本棒最重要的教訓，值得獨立記一次：`SKIP_OVERLAYS=true` 這種跳過視覺渲染的
快速驗證模式，天生就無法抓到「selector 選對元素但圈錯範圍」或「元素存在但內容
沒載入」這類問題——它只回答「這個 selector 找不找得到東西」，回答不了「找到的
東西是不是使用者真正該看到的東西」。這跟第 1 棒「開錄前唯讀盤點最值錢」、
第 2 棒「交接檔的現況是快照不是承諾」是同一條主線的延伸：**任何形式的自動化
綠燈都只覆蓋它實際檢查的那個維度，不能被當成「這段內容沒問題」的證明**。
統計導覽這種「選對元素、但選錯是不是視覺上正確的那個元素」的錯誤，只有真人
看畫面或看最終影片才抓得到，這正是為什麼 user 最後看片這一步不能省。

---

## 第 4 棒 — 2026-09-04（製片 runner，接續第 3 棒）

**交付**：重錄 A1（文案調整）、重新合併八段、對合併片做頭尾裁切（去頭 6 秒、去尾 5 秒）。

### commit

無（本棒未動任何 outline／feature 原始碼，純錄影/合併/裁切操作；mp4 不入版控）。

### A1 重錄原因與範圍

user 反映 A1「開始執行任務」確認對話框的文案有調整，要求重錄該段。查證目前 FE 文案：

> 開始執行任務
> 將發出批次通知給所有已指派人員，並讓已指派的證據蒐集任務進入執行階段（轉為進行中）。
> 上傳型掃描任務需由執行者上傳檔案後手動執行。確定要繼續嗎？

跟劇本既有字卡（「按下去，會先跳確認」「按下去的瞬間發生三件事」）內容不衝突，
但現在的 step 點下去只等 800ms 就自動按「確定開始」，畫面上這個對話框幾乎一閃而過。
問 user 是否要順勢加停留＋導覽讓觀眾看清楚，**user 選擇維持現狀，只重錄不調整時序**。

`SKIP_OVERLAYS=true` 驗證 23 步全綠後正式重錄，2:14（129.3s，跟原版 2:09 接近）。

### 重新合併八段＋裁切頭尾

新 A1 mp4 換上後重新跑 `merge-phase-videos.sh`，八段全合成功。

user 要求「總影片前 6 秒、後 8 秒剪掉，都是沒用的片段」。**逐幀截圖查證片尾發現
按字面 8 秒會切到還在畫面上的「這章學了什麼」總結卡**（倒數 5~8 秒左右都是總結卡
或它的淡出動畫，倒數 5 秒之後才是真正的空景／過場畫面）。問 user 確認後，改成只切
倒數 5 秒的真空景，保留完整總結卡。

最終用 `ffmpeg -ss 6 -i <合併版> -t <keep> -c:v libx264 ...` 重新編碼裁切
（去頭 6 秒、去尾 5 秒，`keep = 1583.12 - 6 - 5 = 1572.12`），輸出
`04-detection-04-execute-collect.mp4`（26:12、88.9MB）。截圖抽查頭尾確認：
開頭直接是規劃頁乾淨畫面，片尾在總結卡完整結束後才收在規劃頁畫面，沒有殘留字卡
或淡出動畫。

### 方法論筆記：使用者給的秒數是感覺值，不是精確測量值，剪之前先截圖核對

user 說「後 8 秒」是觀影當下的直覺印象，不是逐秒算出來的精確值。若照字面直接執行，
會把總結卡整段切掉，變成一個新的內容缺陷（片尾用戶說剪掉「沒用的片段」，但若切掉
的是總結卡，觀眾反而看不到完整結論）。**任何裁切指令在真的下刀之前，先逐秒截圖
核對邊界在畫面上到底是什麼內容**，跟第 3 棒「批次驗證綠燈不等於畫面正確」是同一條
主線——這次是「使用者口頭指令的秒數不等於畫面真實邊界」，一樣不能盲信數字直接執行。
