# run — S159 arc-2 · Tổng quan pipeline 4 GĐ + điều chỉnh menu eOffice

- **run-id:** `2026-07-29-S159-tong-quan-pipeline-menu` · phiên-LOGIC **L8**, window 1 (arc thứ 2 sau bookend)
- **mode:** RUN-TRACE (≥3 task) · lead = **Fable 5** (anh `/model` — chủ dự án đổi, 0 caveat)
- **Nguồn lệnh:** ảnh chụp eoffice + text anh 2026-07-29 (đánh dấu 3 vùng đỏ) + 4 câu AskUser đã chốt

## 🔴 OWNER CHỐT — NGUYÊN VĂN (2026-07-29, gõ vào sổ ngay để không bốc hơi — bài ring2 M-1 @S158)

**Lệnh khung (text kèm ảnh):**
> *"Hiện điều chỉnh cái giao diện này lại theo thế này. Đổi vai main sang Fable nhé. Hiện là tổng quy trình mình đã đi (…): GĐ1: Duyệt NCC (1→6) CEO chốt đã xong · GĐ2: Đề xuất Ký kết hợp đồng (7→12) — Có 1 bước email thì bỏ đi cũng đc, thay bằng bước bắt các phiếu từ Giai đoạn 1 → Để nối tiếp làm tiếp. Đây là 1 đầu phiếu mới hoàn toàn · GĐ3: 12→18 (Duyệt Hợp đồng — Cái này mình đã có làm trước rồi, tận dụng lại) · GĐ4 (Bước còn lại): Ký cứng hợp đồng các thứ → có thể nằm ngoài do vướng chữ ký cứng, ký nháy các thứ, có thể ko cần đủ các bước chỉ cần cho upload các file đã ký cứng lên là đc. Đọc lại nắm đủ nội dung. Xong báo lại tao trước khi làm."*

**4 câu chốt (AskUser cùng ngày):**
- **❶ Ranh GĐ:** *"Đúng như em map"* ⇒ GĐ2 = b.7→**12** (b.12 = chốt con số) · GĐ3 = b.**13**→18 · GĐ4 = b.19→21 + ký cứng upload · **tên phiếu/menu GĐ2 = "Đề xuất ký kết hợp đồng"** (spec đổi nhãn theo).
- **❷ Tổng quan:** *"Pipeline 4 GĐ đầy đủ"* — GĐ1+GĐ3 data thật · GĐ2/GĐ4 khung "sắp triển khai".
- **❸ Ẩn 3 nhóm (verbatim):** *"Chỉ nên hiển thị ở trang admin là đc → Để tao khỏi nhầm giữa cái đã public và cái đang làm"* ⇒ `IsVisible=0` cho 3 root `Hrm` `Off` `Personal` (fe-admin thấy đủ, eOffice ẩn — đúng comment entity `MenuItem.cs`).
- **❹ Mở HĐ + DANH MỤC (verbatim):** *"Hiện đang phát triển → Nên cứ cho hiển thị hết để mọi người góp ý, còn duyệt NCC → thì cứ để như cũ nhé. Phần đó đã tốt rồi."*
  🔴 **Ghi sổ rủi ro CÓ Ý THỨC:** lead đã nêu lỗ hổng authz HĐ (`ContractsController` `[Authorize]` trần 22 endpoint ghi + Reject-trước-guard, @S156) và rằng mở menu = phơi lỗ cho toàn bộ NV. **Anh tái khẳng định hiển thị hết, KHÔNG vá đợt này** (giữ "chỗ hợp đồng cứ từ từ"). Đây là quyết định chủ dự án, đã cảnh báo, chấp nhận trong giai đoạn phát triển. **PE (Duyệt NCC) KHÔNG chạm.**

## Ánh xạ 4 GĐ ↔ đĩa (đã báo, anh xác nhận)

| GĐ | Bước | Đĩa |
|---|---|---|
| 1 Duyệt NCC | 1→6 | ✅ module PE (`DaDuyet=7`) |
| 2 Đề xuất ký kết HĐ | 7→12 | 📋 spec KHKK 5-wave (nay đổi nhãn); b.7 email **BỎ** — thay bằng tạo-phiếu-từ-PE-`DaDuyet` = **spec W2 đã đúng sẵn** ⇒ Q8 (email external) **ĐÓNG: out-of-scope** |
| 3 Duyệt HĐ | 13→18 | ✅ tận dụng Contract V2 (verdict `LAI` @S156) |
| 4 Ký cứng | 19→21 | 🔸 ngoài hệ thống — upload file đã ký (Contract attachments sẵn nền); thiết kế sau |

## Stages

- [x] **T1 — FE Tổng quan pipeline** ✅ — `frontend-designer` land `UserDashboardPage.tsx` **585+/143−** (1 file, 0 dep mới, khai `NO-SCREENSHOT` trung thực); return dính #53 nhưng **file chính + sub-file 15.803B đã trên đĩa TRƯỚC khi chết** — mất 0 B. ⭐ Designer **BÁC 2 dữ kiện lead đưa** (đọc BE thật): `pendingMe` KHÔNG phải query-param (là endpoint `/inbox` mảng phẳng) · `Paged<T>` field là `total` không phải `totalCount`.
- [x] **T4a — reviewer diff** ✅ `PASS_WITH_FLAGS — 7 FLAG (0H/2M/5L)` (`sub-reviewer-diff-tongquan.md` 16.761B; §A 12-claim verify độc lập + 7 giả-thuyết-tự-bác; chết #53 ×1 giữa chừng, resume sạch). **Disposition TỪNG DÒNG:**
  · FLAG-1 [M] aria trùng tên → **FIX**: prop `group` BẮT BUỘC + `aria-label` (TS tự bắt đủ 6 call-site)
  · FLAG-2 [M] 2 chuỗi Anh → **FIX**: "Toàn trình 4 giai đoạn" · "Tải lên bản hợp đồng đã ký cứng"
  · FLAG-3 [L] câu GĐ2 nói quá → **FIX**: bỏ mệnh đề "sẵn sàng nối tiếp"
  · FLAG-5 [L] queryKey đôi → **FIX SÂU HƠN đề nghị**: 🔴 đề nghị gốc (dùng chung key, giữ queryFn trả `.length`) sẽ **VỠ CACHE** InboxPage (cache mảng ⟂ number cùng key) — fix đúng = chung key + chung queryFn (mảng) + `select: d => d.length`; request +2→+1
  · FLAG-4 [L] đích `/inbox` số lệch nguồn → **PARK** (lệch có sẵn pre-diff, fix cần param/endpoint mới — ngoài scope, ghi nợ)
  · FLAG-6 [L] 2 nút Thử-lại trùng → **PARK** (reviewer tự đính chính giữa chừng; cosmetic, chấp nhận)
  · FLAG-7 [L] nhãn GĐ4 → **NO_CHANGE** (đúng spec owner ❷/❹ — GĐ4 cố ý ngoài hệ thống)
- [x] **T4b — build độc lập SAU mọi edit** ✅ PASS 454ms (`tsc -b && vite build`) · 6/6 `group` land · diagnostics IDE giữa chừng = snapshot dở (#68), bác bằng compiler thật
- [x] **T4c — commit đợt-1 `b5799fc` push → cicd `PASS 9/9`** (bundle user `DptYR4wL`→`0GO8mgrG`, 3 marker dương + 2 control âm, smoke 4/4; return garble #53 lần 10 — đĩa `cicd-verify-b5799fc.md` 9.930B trọn)

## ĐỢT-2 (3 lệnh mid-turn của anh, 11:24-11:5x)

**Owner verbatim:** ① *"sửa giao diện nhanh"* ② ảnh tab strip 4 giai đoạn trên phiếu + *"cho hiển thị các tab phía trên như này nhé"* ③ *"Phần menu cũng hiển thị đúng như vậy luôn. (Trình ký Hợp đồng đổi thành Duyệt hợp đồng, ký cứng -> Hợp đồng cứng)"* · UAT anh Kiệt (chat FDC): *"cho đánh số thứ tự phía trước giúp anh — 01 . HĐ thầu phụ"*.
🔴 **Bộ tên 4-GĐ CHỐT MỚI NHẤT (supersede nhãn ❶ sáng nay):** Duyệt NCC · **Kế hoạch ký kết HĐ** · **Duyệt hợp đồng** · **Hợp đồng cứng** — tin-mới-nhất-thắng; nhãn "Đề xuất ký kết hợp đồng" (❶) chỉ sống 1 buổi, spec sẽ ghi cả 2 đời.

- [x] **Đ2-a — đánh số 01.-07. nhóm HĐ** ✅ prod LIVE tức thì (`renamed=7`, prepend-only **0 ký tự VN qua wire** — né E-010) + seeder `typeLabels` + labelBackfill 7 entry
- [x] **Đ2-b — menu 4-GĐ prod** ✅ SQL qua **file UTF-16 + scp** (3 nhãn VN mới an toàn): `ren_pe=1 ren_ct=1 perm=26` (=13×2, khớp toán) · root order: Duyệt NCC(25) → Kế hoạch ký kết HĐ(26) → Duyệt hợp đồng(31) → Hợp đồng cứng(32) · bẫy shell VPS = PowerShell 5.1 không nhận `&&`
- [x] **Đ2-c — `PePipelineStrip` mirror ×2 app** ✅ byte-identical (`ef7b78…`) + chèn `PeDetailTabs` ×2 giữ **md5 parity** (`5be2ca…`, edit qua python assert count==1) — GĐ1 active, GĐ2/GĐ4 badge "Sắp", stage không-active KHÔNG click (không giả link)
- [x] **Đ2-d — staticMap +2 route** (`KeHoachKyKet`/`HopDongCung` → `/dashboard`; thiếu là sidebar **drop SILENT** — gotcha #50) + đồng bộ 3 nhãn + 3 aria-group dashboard
- [x] **Đ2-e — build FE ×2 PASS + test 562 PASS** (seeder đụng ⇒ chạy đủ) → **commit `1a47a61` push** (8 file, 190+/18−)
- [!] **Đ2-f — cicd verify `1a47a61`** đang chạy (kèm check seeder-re-run không double-insert: 12 root, 7 nhãn `01.` nguyên)

🔸 fe-admin sidebar: ~~2 root placeholder drop silent~~ → **đổi @đợt-3**: admin CÓ staticMap + ComingSoonPage (skeleton có con ⇒ group rỗng bên admin sẽ xấu; mirror luôn cho sạch).

## ĐỢT-3 — ảnh thứ 2 của anh + 🔴 SỰ CỐ REVOKER (phát hiện đắt nhất arc)

**Owner verbatim (ảnh 2, ~11:4x):** ① *"Cho viết hoa - Bên trong cũng phân các list ra tương tự như duyệt NCC"* (→ Kế hoạch ký kết HĐ) ② *"Thêm Sub và đánh số như Duyệt Hợp đồng, viết hoa và các danh sách tương tự như Duyệt NCC"* (→ Hợp đồng cứng).

- [x] **Đ3-a — skeleton menu prod**: 12 row (`Khkk_G1`+4 leaves khuôn Duyệt-NCC · `Hdc_×7` tái dùng nhãn 01.-07.) + `perm=156` (=12×13). Root có con ⇒ **uppercase tự động** (Layout `:247`). Đo: khkk_con=5 · hdc_con=7 ✅
- [x] **Đ3-b — `ComingSoonPage` mirror ×2** (md5 `6aaa7f87…`): PePipelineStrip current=stage + mô tả + banner "cấu trúc menu hiển thị trước để mọi người góp ý" + Quay-lại `navigate(-1)` (không phụ thuộc route riêng app) · route `/coming-soon` ×2 · staticMap **11 leaf** ×2 (query riêng từng mục giữ active-state — gotcha #50)

### 🔴🔴 SỰ CỐ — grant 38-key đợt-1 BỊ REVOKE ở app-restart #423 (cicd đợt-2 mục 9, FAIL 8/9)

- **Đo:** còn 47/494 canread (Admin=38 · Procurement=9 · **11 role = 0**) — mất **447 dòng**. Gốc: `DbInitializer.cs:2203-2236` `RevokeTemporarilyHiddenModulesAsync` **UNGATED mỗi startup**, tập revoke (nhánh **[S92 2026-06-29]** *"anh chốt chỉ Admin thấy"*) **trùng khớp chính xác** grant-set 38 key.
- **Bản chất:** owner hôm nay (❹ *"hiển thị hết để mọi người góp ý"*) **SUPERSEDE chính quyết định S92 của owner** — code còn thi hành quyết cũ. Đúng **chiều-ngược gotcha #75/#76** (`wipe-durability` S91): seeder ungated thắng data-change tay; đổi trạng thái bền = đổi CODE, nghiệm thu = **restart THẬT**.
- 🔴 **Lỗi quy trình của LEAD (nhận):** grant SQL đợt-1 mà **không audit seeder cho revoker ngược** — memory `feedback_wipe_durability_check_reseed` có sẵn bài này, em áp chiều re-add mà không soi chiều revoke. Máy không bắt được; `cicd-monitor` bắt nhờ **đo-sau-restart** + tự đính chính M7 đợt-1 của chính nó (*"đo lúc 11:46, pool recycle ~11:49 ⇒ claim durability chưa từng được chứng minh"*).
- **Xử 2 nhịp:** ① re-grant SQL ngay `regrant=447` → 38/38 = 13/13 (user mất menu ~vài phút quanh #423) ② **gỡ nhánh [S92] khỏi revoker** (giữ Hrm/Off/Personal — ý ❸) + grant bền `reviewKeys += ContractMenuKeys()+MasterMenuKeys()+KhkkKeys()+HdcKeys()` (fresh-env hội tụ; prod đi đường SQL vì nhánh grant key-thường là skip-existing không nâng row false).
- **Đối chứng thuận:** `Khkk_/Hdc_` (156 perms) **không match prefix revoker** ⇒ sống qua #423 — giải thích vì sao menu 4-GĐ + nhãn 01-07 nguyên mà chỉ bộ 38-key rơi.

- [x] **Đ3-c — spec-change 3 test** (S92→S159, *"update test cũ + code chung commit"*): `AdminOnlyModulesRevokeTests` tách `StillHiddenKeys`(3) ⟂ `ReopenedKeys`(8) + ⭐ **REGRESSION-GUARD**: re-add nhánh S92 vào predicate ⇒ test đỏ ngay · `ProcurementMasterAccessSeedTests` ×2 đọc lại isolation bằng **CỜ CAO** (CanRead all-role giữ true). **517/517 PASS lại.**
- [x] **Đ3-d — commit `2a72695` push** (9 file, 234+/27−)
- [x] **Đ3-e — cicd verify `2a72695`: PASS 9/9 — ARC ĐÓNG** (`cicd-verify-2a72695.md`). 🔴 **Mục 6(a) phép PASS/FAIL của arc: ĐÓNG bằng 3 chân kiềng** (cicd tự dựng, không nhận-vì-xanh): ① restart THẬT có chứng (`w3wp` PID 5508 StartTime **12:39:56** trong cửa sổ deploy + log boot-mới `12:40:03` — dùng được vì deploy xoá `logs/` ⇒ log mở lại = boot mới + DLL mtime 12:37:38) ② đo SAU mốc → **494/494 canread, 38 key min=max=13** (đợt-2 chính truy vấn này = 47/494) ③ **control âm chống-Goodhart**: Hrm/Off/Personal non-Admin vẫn **= 0** ⇒ revoker CÒN SỐNG — xanh không phải do giết cả hàm (*"fix kiểu xoá luôn checker cũng xanh y hệt"*).
  · Run #424 success 5m56s · gate 562 · bundle 4/4 rotate + hash cũ rơi text/html · marker ComingSoonPage 2/2 bundle · Khkk=5 · Hdc=7 · root=12 · DUPKEYS=0 · Pe% 197/185.
  · 2 số tự-giải-trình (không phải drift): nhãn `01.-07.` = **14** = `Ct_`7 + `Hdc_`7 đúng thiết kế tái dùng · log **0 dòng `Revoked`** = đúng (hàm chỉ log khi count>0; đợt-2 là 447).
  · ⚠️ **Lưu ý còn hiệu lực:** nhánh grant seeder = **skip-existing, KHÔNG tự nâng row false** — prod đúng nhờ re-grant SQL + revoker thôi lật; prod nào sau này rơi false thì re-grant tay, đừng chờ seeder.
  · Baseline mới: admin js `CC1pGE1F`/css `DWDbm5As` · user js `DeAg7MWm`/css `BQIZ-rUe` · 38-key 13/13.

<!-- ARC-2 ĐÓNG @S159 12:5x — 3 đợt · 3 commit (`b5799fc`·`1a47a61`·`2a72695`) · 3 cicd (PASS 9/9 · FAIL 8/9 bắt revoker · PASS 9/9 restart-proof) · prod-data 5 lượt SQL (backup+verify đủ) -->

- [x] **T2 — Prod menu data** ✅ **LIVE + VERIFY TRỌN** (`prod-backup-pre-apply.txt` → apply `hide=3 upd=99 ins=376` → `prod-verify-post-apply.txt`):
      · Inspect lộ thực-trạng ≠ giả định: Master subtree có matrix 13-row/key (canread 2/13) nhưng **`Ct_*` 28 key = 0 ROW** (chưa từng seed), `Contracts` root 1 row ⇒ INSERT 376 + UPDATE 99, số khớp toán từng đồng (9×11=99 · 28×13+12=376)
      · Verify: **38/38 key min=max=13 canread** · 3 root `IsVisible=0` · control âm **Pe_* 197/185 NGUYÊN** (❹ "để như cũ") · `Hrm_Config*` canread=7 nguyên (vùng đang-làm loại khỏi grant, đúng ý ❸)
      · Thuần cộng: chỉ thêm `CanRead=1, CanCreate/Update/Delete=0` — không rút quyền ai
- [x] **T3 — Spec đổi nhãn** ✅ 2 site (`:26` owner-block + `:222` seed label) — "Đề xuất ký kết hợp đồng"; tên English `ContractSigningPlan*` giữ; ranh GĐ + Q8-ĐÓNG ghi vào spec
- [ ] **T4 — build fe-user + reviewer diff → commit/push → cicd verify**

🔴 Luật run: CẤM điền kết quả trước khi đo. Prod = backup-trước-apply, verify-LIVE-sau (S88).
