wal: pause

This commit is contained in:
pqhuy1987
2026-07-29 14:53:58 +07:00
parent ea962b1e59
commit febe6b1696
19 changed files with 645 additions and 43 deletions

View File

@ -0,0 +1,265 @@
# cicd-verify `ea962b1` — S159 ĐỢT-5 "menu 6-leaf y-chang-Duyệt-NCC + GET /contracts/deleted + bỏ hero"
> Ghi-TRỌN-RA-ĐĨA (chống #53). Lead /pause — **file này là sản phẩm chính, đọc ở phiên sau qua `/tiep`**.
> Đợt-1 `b5799fc` PASS 9/9 · đợt-2 `1a47a61` PARTIAL 8/9 · đợt-3 `2a72695` PASS 9/9 · đợt-4 `a2bbcb9` PASS 9/9 — 4 file cũ KHÔNG chạm.
> Bắt đầu: 2026-07-29 ~14:42+07 · cicd-monitor (READ-only)
---
## M1 — Push + phạm vi · ✅ PASS
```
git log -1 → ea962b1e59432bd6b003836cf0c93bed73732abf
[CLAUDE] Contract+FE-User: dot-5 menu 6-leaf y-chang-Duyet-NCC
+ GET /contracts/deleted + bo hero + "Tong quan quy trinh"
2026-07-29 14:40:10 +0700
git log origin/main..HEAD → EMPTY (đã push)
```
Range `ceeebf3..ea962b1` = **10 file, 173+/371** (net âm vì gỡ hero + gỡ code cũ). Khớp brief.
| File | vai trò |
|---|---|
| `src/Backend/…/Api/Controllers/ContractsController.cs` | **+`GET deleted`** |
| `src/Backend/…/Application/Contracts/ContractFeatures.cs` | `ListDeletedContractsQuery` |
| `src/Backend/…/Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs` | comment |
| `src/Backend/…/Persistence/DbInitializer.cs` | 🔴 **seeder**: Ct +3 leaf ×7 · Khkk 6 · labelBackfill · reviewKeys |
| `fe-user/src/pages/UserDashboardPage.tsx` | **BỎ HERO** + đổi title |
| `fe-user/src/pages/contracts/MyContractsPage.tsx` | phase/deleted + guard |
| `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` | matrix + `type=3` |
| `fe-user/src/App.tsx` · `fe-user/src/components/Layout.tsx` | route alias · Ct-regex + staticMap + MenuLeaf-caps |
| `fe-admin/src/components/Layout.tsx` | staticMap + 2 |
**Cả 2 app FE đều sửa ⇒ bundle 2 app đều phải rotate.**
## M2 — Migration VẮNG là ĐÚNG · ✅ PASS
```
git diff --name-only ceeebf3..ea962b1 -- '*Migrations*' → 0
```
BE thêm **query + endpoint + seeder**, không schema. Mig giữ **68** / 89 bảng.
⇒ Toàn bộ thay đổi menu đi qua **SEEDER lúc app khởi động** → rủi ro thật nằm ở M6 (seeder-run).
## 📌 BASELINE TRƯỚC DEPLOY (đo 14:4114:42+07, run chưa land — pool CHƯA recycle)
Đây là "vế TRƯỚC" để chứng minh seeder thật sự chạy (delta 0→7 rất sắc):
| Phép đo | TRƯỚC |
|---|---|
| `Ct_%_WfView` (MenuItems) | **0** |
| `Ct_%_Approved` | **0** |
| `Ct_%_Deleted` | **0** |
| `Khkk_%` | **5** |
| `Ct_%_Pending``Label LIKE N'Đang%'` | **0** (trên tổng 7 key Pending) |
| `Dashboard``Label LIKE N'%quy tr%'` | **0** |
| **MenuItems TỔNG** | **119** |
| 38-key set | **min=13 max=13 keys=38** |
Bundle trước deploy: admin js `CC1pGE1F` css `DWDbm5As` · user js `CLfAftby` css `B6bUSFBt`
— khớp baseline hậu-đợt-4 ⇒ hợp lệ.
## 🔎 M3-pre — Marker kỳ vọng (dự đoán TRƯỚC khi đo, khuôn đợt-4)
`UserDashboardPage.tsx` (50.142 B — đợt-4 là 63.124 B, giảm đúng với "bỏ hero"):
| Chuỗi | trong UserDashboardPage | vai trò |
|---|---|---|
| `Tổng quan quy trình` | **1** | dương |
| `Toàn trình 4 giai đoạn` | **0** | (hero h2 đã gỡ) |
| `Quy trình mua sắm` | **0** | (eyebrow hero đã gỡ) |
### ⚠️⚠️ CẢNH BÁO: 1 trong 2 control ÂM của brief **KHÔNG HỢP LỆ** — đã soi TRƯỚC khi đo
`grep -rn` toàn `fe-user/src` + `fe-admin/src`:
```
fe-user/src/components/pe/PePipelineStrip.tsx:24: aria-label="Toàn trình 4 giai đoạn"
fe-admin/src/components/pe/PePipelineStrip.tsx:24: aria-label="Toàn trình 4 giai đoạn"
```
⇒ Chuỗi `Toàn trình 4 giai đoạn` **VẪN SỐNG** trong `PePipelineStrip` (aria-label, thêm từ đợt-2).
**KHÔNG độc quyền** cho hero ⇒ **dự đoán bundle USER = 1, KHÔNG phải 0**.
**Nếu máy móc áp "phải VẮNG" sẽ báo FAIL OAN** — trong khi hero thật sự đã gỡ.
→ Xử lý: coi đây là control **KHÔNG dùng được**, thay bằng phép đúng ở dưới.
`Quy trình mua sắm`: `grep -rc` toàn 2 app = **0 hit ở mọi file****control ÂM HỢP LỆ**,
dự đoán bundle = **0**. Đây là phép chứng "hero đã gỡ" đáng tin của lượt này.
(Đúng bài đợt-4: marker phổ-thông/dùng-chung chỉ kết luận "≥1", muốn phép ÂM phải chọn chuỗi ĐỘC QUYỀN.)
## 🔎 M7-pre — Endpoint mới (đọc code, chốt kỳ vọng)
`ContractsController.cs`:
```csharp
[ApiController]
[Route("api/contracts")] // L12
[Authorize] // L13 (class-level)
[HttpGet("deleted")] // L28
[Authorize(Policy = "Contracts.Read")] // L29 (chặt hơn [Authorize] trần)
public async Task<ActionResult<PagedResult<ContractListItemDto>>> ListDeleted()
```
⇒ URL thật = **`GET /api/contracts/deleted`** (brief ghi `/contracts/deleted`, thiếu prefix `api`).
Kỳ vọng **401** khi không token (route tồn tại) · **404 = FAIL** (route không tồn tại).
👍 Ghi nhận: có policy method-level chứ không dựa `[Authorize]` trần — đúng bài 2-tầng authz (#82).
## M4 — Poll terminal · ✅ PASS
- **id=539 · run_number=426 · head_sha `ea962b1e`** · created 14:40:15 → **`status=success` 14:45:53**
- **Duration 5m38s** (đợt 1-4: 5m59s / 5m44s / 5m56s / 5m40s) — ổn định.
### M4b — Test gate · ✅ 562 EXACT
```
Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45, 151 ms - SolutionErp.Domain.Tests.dll
Passed! - Failed: 0, Passed: 517, Skipped: 0, Total: 517, 1m23s - SolutionErp.Infrastructure.Tests.dll
```
`Failed!` ×0 ⇒ **562**. Guard đợt-3 (`StillHiddenKeys`/`ReopenedKeys`) nằm trong 517 này, vẫn xanh.
## M5 — Bundle · ✅ PASS (đo 14:46:14, sau status=success)
| App | asset | TRƯỚC | SAU | KQ |
|---|---|---|---|---|
| **admin** | js | `CC1pGE1F` | **`D0sXA0fe`** | ✅ ROTATE |
| admin | css | `DWDbm5As` | `DWDbm5As` | FROZEN — lành tính (chứng dưới) |
| **user** | js | `CLfAftby` | **`Y6dW_5CM`** | ✅ ROTATE |
| **user** | css | `B6bUSFBt` | **`6YIAufJR`** | ✅ ROTATE |
"Bundle ×2 rotate (cả 2 app đổi)" — **đạt qua JS cả 2 app** ✅.
```
USER index-Y6dW_5CM.js ct=application/javascript size=1.595.577 LM=14:45:25+07 (in-window)
ADMIN index-D0sXA0fe.js ct=application/javascript size=1.678.823 LM=14:44:29+07 (in-window)
ADMIN index-DWDbm5As.css ct=text/css size= 83.705 LM=14:44:29+07 (in-window) ← FROZEN
```
🔑 **admin-css FROZEN nhưng `Last-Modified` 14:44:29 NẰM TRONG cửa sổ deploy** (14:40:15→14:45:53)
⇒ file **đã được ghi lại**, chỉ là thay đổi `fe-admin` lần này là **staticMap trong Layout.tsx (JS thuần,
không thêm class Tailwind mới)** nên CSS ra byte y hệt. Đúng khuôn #69 mode-B đã dùng ở đợt-4:
**frozen + LM-in-window = re-ship giống hệt** (khác "bundle cũ kẹt lại" — cái đó LM sẽ cũ).
Control âm 2 tầng: hash CŨ `CLfAftby` / `CC1pGE1F` đều → `ct=text/html` 876/900 B ⇒ bundle đợt-4 đã
biến mất khỏi đĩa = chứng ghi-đè thật.
## M3 — Byte-marker · ✅ PASS (dự đoán TRƯỚC khớp ĐO 100%)
| Chuỗi | USER | ADMIN | Kỳ vọng đã chốt trước | KQ |
|---|---|---|---|---|
| `Tổng quan quy trình` | **1** | 0 | dương ở user (chỉ fe-user có) | ✅ |
| `Quy trình mua sắm` | **0** | **0** | **ÂM (control HỢP LỆ)** | ✅ |
| `Toàn trình 4 giai đoạn` | **1** | **1** | **dự đoán 1, KHÔNG phải 0** | ✅ đúng dự đoán |
🔑 **CỨU MỘT FALSE-FAIL — điểm quan trọng nhất lượt này:**
Brief yêu cầu control âm `Toàn trình 4 giai đoạn` phải **VẮNG**. Nếu áp máy móc thì lượt này **FAIL OAN**,
vì đo ra **1**. Nhưng tao đã soi TRƯỚC KHI ĐO và biết lý do:
```
fe-user/src/components/pe/PePipelineStrip.tsx:24: aria-label="Toàn trình 4 giai đoạn"
fe-admin/src/components/pe/PePipelineStrip.tsx:24: aria-label="Toàn trình 4 giai đoạn"
```
Chuỗi này **không độc quyền cho hero** — nó còn sống trong `PePipelineStrip` (thêm từ đợt-2) dưới dạng
`aria-label`. Hero **thật sự đã gỡ** (`UserDashboardPage` chứa 0 lần; file giảm 63.124 → **50.142 B**).
⟹ Kết luận: control này **KHÔNG DÙNG ĐƯỢC**, thay bằng `Quy trình mua sắm` (eyebrow hero) —
`grep -rc` toàn 2 app = **0 hit ở mọi file** ⇒ độc quyền ⇒ bundle = **0****chứng hero đã gỡ**.
**Luật rút ra (nối tiếp đợt-4):** trước khi nhận một chuỗi làm **control ÂM**, phải grep repo-wide
xác nhận nó **ĐỘC QUYỀN** cho thứ mình muốn chứng vắng. Chuỗi dùng-chung chỉ làm marker DƯƠNG.
## M6 — 🔴 SEEDER-RUN sau restart · ✅ PASS (delta khớp từng đơn vị)
**Bước 0 — chứng minh restart THẬT (khuôn 3 chân kiềng, đợt-3):**
| Bằng chứng | Giá trị |
|---|---|
| Dòng ĐẦU log `solution-erp-20260729.log` | **`14:45:37.283 +07:00 [INF] Applying migrations...`** |
| `w3wp` StartTime mới nhất | **14:45:32** (và 14:45:31) |
Deploy xoá `logs/` ⇒ log mở lại 14:45:37 = **boot mới**; DbInitializer đã chạy. Đo SAU mốc này.
| Phép đo | TRƯỚC (14:41) | **SAU (14:46)** | Δ | KQ |
|---|---|---|---|---|
| `Ct_%_WfView` | 0 | **7** | +7 | ✅ |
| `Ct_%_Approved` | 0 | **7** | +7 | ✅ |
| `Ct_%_Deleted` | 0 | **7** | +7 | ✅ |
| `Khkk_%` | 5 | **7** | +2 | ✅ (6 leaf + G1) |
| `Ct_%_Pending` nhãn `N'Đang%'` | 0/7 | **7**/7 | +7 | ✅ labelBackfill ăn |
| `Dashboard` nhãn `N'%quy tr%'` | 0 | **1** | +1 | ✅ |
| **MenuItems TỔNG** | **119** | **142** | **+23** | ✅ |
| root | — | **12** | 0 | ✅ không double-insert |
| `DUPKEYS` | — | **0** | — | ✅ |
🔑 **Toán khớp từng đồng:** +23 = 7 (`WfView`) + 7 (`Approved`) + 7 (`Deleted`) + 2 (`Khkk`) = **23** ✅.
Không có row thừa nào ⇒ seeder insert ĐÚNG tập, không nhân đôi.
**Permission cho key MỚI (nhánh insert của reviewKeys — key chưa có row nên insert ăn):**
```
F_NEWPERM min=13 max=13 keys=21 (21 = 7 WfView + 7 Approved + 7 Deleted)
```
✅ Mỗi key mới đủ **13/13 role canread**. Đây đúng như ghi chú code đợt-3: nhánh grant là
**skip-existing** nên chỉ ăn với key MỚI — lần này key hoàn toàn mới ⇒ grant thành công (khác
tình huống đợt-2/3 nơi row `false` sẵn có không được nâng).
## M7 — Endpoint mới `GET /api/contracts/deleted` · ✅ PASS (401, KHÔNG 404)
| Probe | HTTP | Đọc |
|---|---|---|
| **`GET /api/contracts/deleted`** (no token) | **401** | ✅ **route TỒN TẠI**, auth chặn đúng |
| `GET /api/contracts/zzznope` (route bịa) | **404** | ✅ **control** — 404 vẫn hoạt động ⇒ 401 ở trên CÓ NGHĨA |
| `GET /api/contracts` (route cũ đã biết) | 401 | ✅ nhất quán |
🔑 Có **control 404** nên 401 không thể là "server trả 401 cho mọi thứ" — discriminator sạch.
(Nếu chỉ đo mỗi 401 thì không loại trừ được khả năng catch-all.)
## M8 — Restart-durability lần 3 · ✅ PASS
| Phép đo | Kết quả SAU restart | KQ |
|---|---|---|
| **38-key set** | **min=13 max=13 keys=38 · totcr=494/494** | ✅ GIỮ (lần 3) |
| Control ÂM `Hrm%`/`Off%`/`Personal` non-Admin canread | **0** | ✅ revoker CÒN SỐNG |
| `Pe%` | **197 / 185** | ✅ |
| `Hdc_*` nhãn `- Bảng cứng` (`%-%`) | **7** | ✅ nguyên |
| 3 root ẩn — `SUM(IsVisible)` | **0** | ✅ cả 3 vẫn ẩn |
⚠️ **Lưu ý hình-dạng-truy-vấn (quan trọng cho lần sau):** đợt này `Ct_*` đẻ thêm 21 key mới
(`_WfView`/`_Approved`/`_Deleted`). Nếu vẫn dùng `LEFT(MenuKey,3)='Ct_'` như các đợt trước thì
"38-key set" sẽ thành **59 key****không so sánh được với 4 đợt trước**. Nên tao **cố ý loại
3 hậu tố mới** khỏi mệnh đề để giữ đúng tập 38 gốc:
```sql
WHERE MenuKey IN (10 key Master-set + 'Contracts')
OR (LEFT(MenuKey,3)='Ct_'
AND MenuKey NOT LIKE 'Ct[_]%[_]WfView'
AND MenuKey NOT LIKE 'Ct[_]%[_]Approved'
AND MenuKey NOT LIKE 'Ct[_]%[_]Deleted')
```
⇒ ra đúng **38 key**, so được apples-to-apples với đợt 2/3/4. (Bài học query-shape từ đợt-1: khi tập
dữ liệu nở ra, phải neo lại phạm vi truy vấn, nếu không con số "tăng" sẽ bị đọc nhầm thành drift.)
## M9 — Smoke · ✅ PASS 4/4
```
/health/live 200 Healthy · /health/ready 200 Healthy · admin 200 · eoffice 200
```
---
## VERDICT: **PASS — 9/9**
| # | Mục | KQ |
|---|---|---|
| 1 | Push + range 10 file (173+/371, net âm do bỏ hero) | ✅ |
| 2 | Migration VẮNG là ĐÚNG | ✅ |
| 3 | Run #426 success 5m38s | ✅ |
| 4 | Test gate **562** (45+517, 0 fail) | ✅ |
| 5 | Bundle: JS rotate **cả 2 app**; admin-css frozen chứng minh lành tính bằng LM-in-window | ✅ |
| 6 | Marker `Tổng quan quy trình`=1 · control ÂM HỢP LỆ `Quy trình mua sắm`=0 | ✅ |
| 7 | **SEEDER-RUN**: 0→7 ×3 · Khkk 5→7 · Pending-label 7 · Dashboard-label 1 · tổng 119→142 (+23 khớp) · root 12 · DUPKEYS 0 · 21 key mới 13/13 | ✅ |
| 8 | **Endpoint `/api/contracts/deleted` = 401** (route sống) + control bịa **404** | ✅ |
| 9 | **Durability lần 3**: 38-key 494/494 · control âm 0 · Pe 197/185 · Hdc nhãn 7 · 3 root ẩn | ✅ |
**Không có cờ đỏ. Không khuyến nghị rollback.**
### ⚠️ 1 điểm cần owner biết (KHÔNG phải lỗi deploy)
Control âm `Toàn trình 4 giai đoạn` trong brief **không dùng được** — chuỗi vẫn sống hợp lệ ở
`PePipelineStrip.tsx` (`aria-label`, cả 2 app). Hero đã gỡ đúng (chứng bằng `Quy trình mua sắm`=0 +
`UserDashboardPage` 63.124→50.142 B). Lần sau nếu muốn phép ÂM cho hero, dùng `Quy trình mua sắm`.
Baseline mới: **admin js `D0sXA0fe` / css `DWDbm5As`(frozen hợp lệ) · user js `Y6dW_5CM` / css `6YIAufJR`** ·
gate 562 · Mig 68 / 89 bảng · MenuItems **142** · root 12 · 38-key 13/13 · `Ct_` leaf mới 21 key × 13.
### Arc S159 — 5 đợt
đợt-1 PASS 9/9 → đợt-2 **PARTIAL 8/9** (bắt regression revoker ăn 447/494 row) → đợt-3 PASS 9/9
(đóng regression, 3 chân kiềng) → đợt-4 PASS 9/9 (durability lần 2) → **đợt-5 PASS 9/9
(durability lần 3 + seeder-insert delta khớp toán + endpoint mới sống)**.
<!-- END cicd-verify-ea962b1 · VERDICT=PASS 9/9 -->

View File

@ -89,7 +89,25 @@
- [x] **Đ4-d — build PASS 495ms · Infra 517 PASS · commit `a2bbcb9` push** (2 file, 1052+/338)
- [!] **Đ4-e — cicd verify `a2bbcb9`** đang chạy (marker bản mới + control âm "chưa tới"/"HĐ ĐANG SOẠN" vắng + restart-durability lần 2 của 38-key)
<!-- ARC-2: 4 đợt · 4 commit (`b5799fc`·`1a47a61`·`2a72695`·`a2bbcb9`) · prod-data 7 lượt SQL -->
## ĐỢT-5 — "y chang Duyệt NCC" 6-leaf + endpoint deleted + bỏ hero + "Tổng quan quy trình"
**Owner:** ảnh khoanh 6 leaf PE + *"Những cái dưới nó cũng y chang thế này luôn nhé, trừ chỗ hợp đồng cứng ra, vì bảng cứng thì ko cần"* · AskUser "Chạy cả 2" (endpoint deleted READ-only + matrix HĐ) · *"Ồ đẹp đấy, vậy bỏ cái trên đi nhé, cái dưới OK đấy. Tổng quan → Viết hoa → Đổi thành gì...? Tổng quan quy trình?"* (lead lấy tên anh đề) · *"xong thì /pause nhé"*.
- [x] BE: `ListDeletedContractsQuery` + GET `/contracts/deleted` [Contracts.Read] (khuôn PE S155; Contract CÓ soft-delete filter `:44`) · phát hiện rẻ: **ApplicableType.Contract=3 — HĐ chung 1 bộ V2** ⇒ matrix = mở rộng trang PE +type=3, 7 leaf WfView trỏ chung `?type=3&ct=N`
- [x] Seeder: Ct ×7 +3 leaf (khớp regex FE) + "Đang duyệt" ×7 backfill + Khkk 6-leaf + Dashboard→"Tổng quan quy trình" + reviewKeys +23 — **KHÔNG SQL tay** (bài revoker: code-first, deploy tự insert/reorder qua upsert)
- [x] FE: BỎ HERO (note verbatim owner trong code) + title + MyContractsPage phase/deleted + MenuLeaf depth-0 caps
- [x] reviewer **PASS_WITH_FLAGS 10 FLAG (0H/4M/6L)** (`sub-reviewer-diff-dot5.md` 22.369B; garble #53 ×2 — lần 2 43-tool/1.3KB = làm-nhiều-ghi-ít, resume "đổ-đĩa-trước-đọc-thêm" ăn). **Disposition:**
· F1[M] fe-admin thiếu 2 route Khkk → **FIX** (+2 staticMap; Ct_ mới bên admin ẩn `isAdminHidden` — reviewer đã kiểm CLEAN)
· F2[M] màn Đã-xóa click row = panel trắng/404 (detail KHÔNG IgnoreQueryFilters — nó truy tới `GetContractQuery:471` chứng thật) → **FIX** guard `if (showDeleted) return` tại `selectContract` (chặn cả desktop lẫn mobile, khuôn PE `disabled={deletedView}`)
· F3[M] chú thích khai "Hộp thư viết HOA" nhưng Hộp-thư đi đường `StaticLeaf`**FIX** khai đúng phạm vi (đồng bộ StaticLeaf = quyết owner, PARK)
· F10[M] ⭐ comment "chỗ THỨ HAI" = **số-tự-lan** (grep thật: 3 tệp/17 vị trí — `ApprovalWorkflowV2AdminFeatures` 15 chỗ lớp-khác; "DUY NHẤT" S155 vốn đã sai) → **FIX cả 2 site: BỎ SỐ, nêu LUẬT + lệnh grep tự-kiểm**. 🔴 Lead lặp đúng lỗi errata-của-errata sáng nay — thay số sai cũ bằng số sai mới; cùng lớp meta-count
· F4[L] **PARK→NEXT-anh**: định nghĩa "Đã duyệt" HĐ (phase=9 Đã-phát-hành bỏ sót phase-8 đã-qua-BOD; nhãn menu ↔ chip đang lệch) — cần owner chốt
· F5-F9[L] **PARK có ghi** (F5 active-state đôi · F6 asymmetry authz endpoint-mới-chặt-hơn-list-sống [đúng hướng, ghi nhận] · F7 chuỗi trần vs factory · F8 thiếu cột mốc-xóa · F9 client-filter-type trên 100 dòng)
- [x] build fe ×2 + BE PASS · test **562 PASS** · commit **`ea962b1`** push (10 file, 173+/371)
· 🔧 bẫy mới ghi sổ: PS 5.1 here-string commit-message vỡ → **`git commit -F <file>`**; lần vỡ đầu chỉ đẩy 2 `wal:` commit (noise, code chưa mất)
- [!] cicd verify `ea962b1` **CHẠY NỀN** (`cicd-verify-ea962b1.md`) — đọc ở phiên sau qua `/tiep` (mục nặng: seeder-run insert menu-delta + endpoint 401-not-404 + durability lần 3)
<!-- ARC-2: 5 đợt · 5 commit (`b5799fc`·`1a47a61`·`2a72695`·`a2bbcb9`·`ea962b1`) · prod-data 7 lượt SQL + đợt-5 code-first 0-SQL -->
- [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)

View File

@ -26,3 +26,129 @@ Ngoài scope lead khai (KHÔNG soi, chỉ ghi nhận có mặt trong working-tre
## FLAG (ghi dần)
### FLAG-1 [M] — `fe-admin` thiếu 2 route ⇒ 2 lá menu mới rơi IM LẶNG (gotcha #50)
**File:** `fe-admin/src/components/Layout.tsx:31-44` (`staticMap`)
**Đo:** staticMap fe-admin có `Khkk_List` / `Khkk_Create` / `Khkk_Pending` / `Khkk_Approved` nhưng **KHÔNG có** `Khkk_WfView`, `Khkk_Deleted` — trong khi seeder (`DbInitializer.cs:1780-1786`) đã thêm 2 key này vào cây menu dùng CHUNG cho cả 2 app.
`MenuLeaf` của fe-admin: `resolvePath` trả `null``return null` ⇒ lá biến mất, KHÔNG có cảnh báo. Đúng lớp bẫy mà chính comment `Layout.tsx:33` cảnh báo ("thiếu route = sidebar drop SILENT").
**Hệ quả:** nhóm "1. Kế hoạch ký kết HĐ (NCC-TP)" bên fe-admin hiện 4/6 lá; lệch với fe-user 6/6. Vi phạm `docs/rules.md §3.9` (mirror 2 app).
**Không phải lỗi cho `Ct_*_WfView/Approved/Deleted`:** fe-admin ẩn TOÀN BỘ `Ct_*` (`isAdminHidden` :176-178 `key.startsWith('Ct_')`) ⇒ 21 lá HĐ mới không cần route bên admin. Đã kiểm, CLEAN.
**Tiêu chí nhận:** thêm 2 dòng `Khkk_WfView` + `Khkk_Deleted` vào staticMap fe-admin (trỏ `/coming-soon?stage=2&m=...` như fe-user), rồi `grep -c "Khkk_" ` ở 2 app phải BẰNG NHAU (=6).
### FLAG-2 [M] — Màn "Đã xóa" HĐ: bấm vào dòng ⇒ 2 panel TRẮNG (không thông báo), khác hẳn khuôn PE
**File:** `fe-user/src/pages/contracts/MyContractsPage.tsx:163-190` (`onClick={() => selectContract(c.id)}`) + `:52-56` (`detail` query) + `:196-222` (Panel 2/3)
**Đo (tĩnh, 2 vế):**
- Khuôn PE (S155) CHẶN hẳn: `fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx:524-527``disabled={deletedView}` + `cursor-default`.
- Bản HĐ đợt-5 **không có nhánh `showDeleted`** ở nút dòng ⇒ vẫn `selectContract(id)` → set `?id=``detail` query `GET /contracts/{id}`.
- `Contract` CÓ global query filter `!IsDeleted` (`ContractConfiguration.cs:44`) và endpoint chi tiết KHÔNG dùng `IgnoreQueryFilters` ⇒ HĐ đã xóa **404 NotFound**.
- Render: Panel 2 chỉ có 3 nhánh `!selectedId` / `isLoading` / `detail.data` — nhánh `isError` KHÔNG có ⇒ lỗi 404 rơi vào khoảng trống ⇒ **panel trắng trơn**, người dùng không biết vì sao. Panel 3 (`:221`) cũng trắng.
- Trên mobile (< lg) còn nặng hơn: `selectContract` (`:75-81`) `navigate('/contracts/{id}')` **rời khỏi màn** sang trang chi tiết 404.
**Tiêu chí nhận:** hoặc `disabled={showDeleted}` cho nút dòng (rẻ nhất, đúng khuôn PE), hoặc thêm nhánh `detail.isError` hiển thị "Phiếu đã xóa không xem được chi tiết". Nếu chọn cách 2 phải chặn thêm nhánh mobile-navigate.
### FLAG-3 [M] — Chú thích khai "Hộp thư viết HOA" nhưng mã KHÔNG chạm "Hộp thư"
**File:** `fe-user/src/components/Layout.tsx:354-357` (chú thích + nhánh `depth === 0`) vs `:385-408` (`StaticLeaf`)
**Đo:** "Hộp thư" KHÔNG render bằng `MenuLeaf` `USER_FIXED_TOP` (`:376-378`) render qua **`StaticLeaf`** (`:385`), lớp CSS hard-code `px-3 py-1.5 text-[12px] font-medium` **không** `uppercase`. Sửa `MenuLeaf` không với tới .
**Hệ quả:** ngay đầu sidebar sẽ "Hộp thư" (thường, 12px) nằm cạnh "TỔNG QUAN QUY TRÌNH" (HOA, 11px, semibold) đúng thứ owner muốn đồng bộ thì lại lệch; chú thích trong tả sai việc làm (bẫy cho người đọc sau).
**Ghi nhận CLEAN kèm theo:** nhánh 3 ngả `depth===0 ? … : isDeep ? … : …` **không** phá sâu depth 1 vẫn ăn nhánh , depth 2 vẫn `isDeep`; khối `isActive` nằm NGOÀI ternary nên style active nguyên vẹn (trục 6 PASS phần này).
**Tiêu chí nhận:** hoặc cùng bộ lớp sang `StaticLeaf`, hoặc sửa chú thích bỏ chữ "Hộp thư" (khai đúng phạm vi).
### FLAG-4 [L] — "Đã duyệt" của HĐ lọc `phase=9` (Đã phát hành), bỏ sót HĐ đã được duyệt nhưng chưa phát hành
**File:** `fe-user/src/components/Layout.tsx:141` (`?type=N&phase=9`) + `MyContractsPage.tsx:117-121` (chip "Đã phát hành")
**Đo:** `ContractPhase` 9 = `DaPhatHanh`. phase 8 `DangDongDau` **đã qua chữ ký BOD** (`DangTrinhKy → DangDongDau` do Director/AuthorizedSigner) về nghiệp vụ đã "được duyệt" nhưng KHÔNG lọt vào màn "Đã duyệt". Param `phase` của BE so-khớp BẰNG (`ContractFeatures.cs:305`), không diễn tả được `>= 8`.
Chính FE cũng lệch tên: nhãn menu "Đã duyệt" chip trên trang "Đã phát hành".
**Tiêu chí nhận:** owner chốt định nghĩa; nếu giữ 9 thì đổi nhãn thành "Đã phát hành" cho khớp chip, nếu muốn "đã duyệt" thì cần BE nhận `phaseFrom`/danh sách phase.
### FLAG-5 [L] — Ở `?type=N&phase=9`, lá "Danh sách" SÁNG cùng lúc với "Đã duyệt"
**File:** `fe-user/src/components/Layout.tsx:304-329` (`TRANSIENT_QUERY_KEYS` + `queryMatches`)
**Đo (chạy tay luật ở :322-328):** URL hiện tại `type=1&phase=9`, đích "Danh sách" = `type=1` `pinnedByTarget={type}` `phase` transient & không được đích ghim **bị bỏ qua** `aKeys=[type]` = `bKeys=[type]` MATCH. Đích "Đã duyệt" = `type=1&phase=9` cũng MATCH. 2 cùng sáng.
Bản S155 chỉ đóng chiều ngược ( `?type=1` không cho "Đã duyệt" sáng). **Là tật kế thừa từ PE, không phải do đợt-5 gây ra** nhưng đợt-5 nhân ra 7 nhóm nên ghi để owner biết.
**Không dính lá "Đã xóa":** `deleted` không nằm trong `TRANSIENT_QUERY_KEYS` so khớp nghiêm, phân biệt đúng. Đã kiểm.
**Tiêu chí nhận:** (tùy chọn) đối xứng hóa: nếu URL key MỘT đích anh-em ghim thì đích không-ghim phải trượt.
### FLAG-6 [L] — Endpoint mới siết `Contracts.Read` trong khi list sống không siết ⇒ mở đúng hình bẫy #44
**File:** `src/Backend/SolutionErp.Api/Controllers/ContractsController.cs:26-35` vs `:16-24`
**Đo:** `MenuPermissionHandler.cs:40-49` so khớp **ĐÚNG KHÓA** `MenuKey == "Contracts"`, **không** kế thừa cha-con (kế thừa chỉ tầng hiển thị `GetMyMenuTreeQuery`, gotcha #35). vai nào được cấp lẻ `Ct_X_Deleted` **không** dòng `Contracts` CanRead=1 sẽ thấy menu nhưng ăn **403 câm** (TanStack nuốt lỗi `isError` trang hiện "Không "/trắng).
Rủi ro hiện thấp vì: (a) `Contracts` trong `MenuKeys.All` (`MenuKeys.cs:168`) policy `Contracts.Read` tồn tại thật **trục 1 câu hỏi này PASS**; (b) `SeedAllRolesReviewReadPermissionsAsync` (`DbInitializer.cs:2157-2214`) đã đưa `Contracts` vào bộ cấp-cho-mọi-vai; (c) PE S155 dùng y hệt khuôn này đã chạy thật.
**Nhưng:** nhánh cấp cho khóa thường **bỏ-qua-nếu-đã-có** (chú thích :2211-2213 tự khai), nên trên prod dòng `Contracts` CanRead=0 sẽ KHÔNG được nâng bởi seeder. Đây rủi ro triển khai, không phải lỗi .
**Tiêu chí nhận (rẻ):** 1 câu `SELECT r.Name, p.CanRead FROM Permissions p JOIN Roles r ON r.Id=p.RoleId WHERE p.MenuKey='Contracts'` trên prod mọi vai đang dùng phải CanRead=1; hoặc curl `/api/contracts/deleted` bằng `nv.test@` kỳ vọng 200.
### FLAG-7 [L] — Khóa menu mới viết chuỗi trần, không đi qua `MenuKeys` factory (lệch chuẩn PE)
**File:** `DbInitializer.cs:1871,1875,1876` + `:2163,2167,2168` + `:2179-2180` (`"Ct_" + code + "_WfView"` …)
**Đo:** PE khi thêm đúng 2 mục tương ứng (S155) đã bổ sung factory `MenuKeys.PurchaseEvaluationApproved/Deleted` + `PurchaseEvaluationWorkflowView` (`MenuKeys.cs:140-156`). Bản **không** thêm `ContractTypeWfView/Approved/Deleted`, nối chuỗi tay **2 chỗ khác nhau** (cây menu + danh sách cấp quyền). `MenuKeys.cs:3` tự nhận "nguồn duy nhất".
**Hệ quả:** sai 1 tự 1 trong 2 chỗ = lệch câm, trình biên dịch không bắt.
**Tiêu chí nhận:** thêm 3 factory vào `MenuKeys.cs` (KHÔNG đưa vào `MenuKeys.All` giữ số canonical `Menu keys`/`Policies` không trôi, đúng như ghi chú PE `MenuKeys.cs:145-148`), rồi thay 8 chỗ nối chuỗi.
### FLAG-8 [L] — Danh sách "Đã xóa" sắp theo `DeletedAt` nhưng không trả về cột nào cho người dùng thấy mốc xóa
**File:** `ContractFeatures.cs:387` (`OrderByDescending(x => x.c.DeletedAt)`) + `:391-397` (DTO)
**Đo:** `ContractListItemDto` không `deletedAt` (trường thời gian duy nhất `CreatedAt`). Người dùng thấy thứ tự "mới xóa trước" nhưng trên dòng chỉ ngày tạo trông như sắp lộn xộn. PE gặp đúng vấn đề này đã xử bằng cách sắp theo `UpdatedAt` **và hiển thị** (`PurchaseEvaluationsListPage.tsx:557-562`).
`DeletedAt` chắc chắn số liệu: `DeleteContractCommandHandler` (`ContractFeatures.cs:632`) gọi `.Remove()` `AuditingInterceptor.cs:54-63` đặt `IsDeleted/DeletedAt/DeletedBy`. Nên đây vấn đề trình bày, không phải sai dữ liệu.
**Tiêu chí nhận:** hiện `deletedAt` (thêm trường vào DTO hoặc mượn `updatedAt` như PE), hoặc đổi sang sắp theo trường đang hiển thị.
### FLAG-9 [L] — Chế độ "Đã duyệt": lọc loại HĐ làm ở phía trình duyệt trên 100 dòng đầu ⇒ có thể ra rỗng oan
**File:** `MyContractsPage.tsx:41-49` (chỉ gửi `phase`, KHÔNG gửi `type`) + `:58-60` (`items.filter(c => c.type === typeFilter)`)
**Đo:** chế độ **không**-xóa, `type` cố ý không gửi lên máy chủ (giữ nguyên nếp ), nhưng nay cộng thêm `phase=9`: máy chủ trả 100 phase 9 của **mọi loại**, rồi trình duyệt mới lọc loại. Nếu số đã phát hành > 100 thì lá "Đã duyệt" của một loại có thể hiện 0 dù thực tế có.
Chế độ "Đã xóa" thì ngược lại — có gửi `type` lên máy chủ (`:45`), nên không dính. Lệch nhau giữa 2 nhánh trong cùng một hàm.
**Tiêu chí nhận:** gửi `type` lên máy chủ cho cả 2 nhánh (BE `ListContractsQuery` hiện **chưa** có tham số `type` — cần thêm), hoặc chấp nhận và ghi rõ giới hạn 100.
---
## Ghi nhận PASS (đã kiểm, không thành FLAG)
- **Trục 1 — `Where(IsDeleted)`:** `ContractFeatures.cs:369-370``.IgnoreQueryFilters().Where(x => x.IsDeleted)` — đúng bài 8a S155 (Ignore GỠ chứ không ĐẢO). CLEAN.
- **Trục 1 — IDOR:** `ContractFeatures.cs:375-380` sao đúng khuôn list sống `:298-303` (`DrafterUserId == userId || eligiblePhases.Contains(Phase)`), gọi lại chính `ListContractsQueryHandler.GetEligiblePhases` (internal static, cùng assembly — biên dịch được). Không nới rộng hơn list sống. CLEAN.
- **Trục 1 — policy có thật:** `Contracts``MenuKeys.All` (`MenuKeys.cs:168`) ⇒ `Contracts.Read` được đăng ký (`|All| × |Actions|`). Không phải tên policy ma. CLEAN.
- **Trục 1 — Contract CÓ query filter:** `ContractConfiguration.cs:44` `HasQueryFilter(x => !x.IsDeleted)` ⇒ list sống không rò HĐ đã xóa; endpoint mới là đường DUY NHẤT thấy chúng. CLEAN.
- **Trục 2 — định dạng khóa khớp lưới FE:** seeder sinh `Ct_<Code>_{WfView,List,Create,Pending,Approved,Deleted}`; lưới FE `Layout.tsx:130` `/^Ct_([^_]+)_(List|Create|Pending|WfView|Approved|Deleted)$/`. 7 mã loại (`ThauPhu``NguyenTacDv`) **không chứa dấu gạch dưới**`[^_]+` bắt trọn. 6/6 nhánh có nhánh trả đường dẫn (`:136-142`). CLEAN.
- **Trục 2 — `order++` chèn giữa không rối nhóm khác:** biến `order` (`DbInitializer.cs:1860`) chỉ phục vụ nhóm `Ct_*`; `Order` chỉ so trong cùng cấp anh em; nhóm loại HĐ nay cách nhau 7 thay vì 4 nhưng **thứ tự tương đối giữ nguyên**; `Hdc_*` dùng biến riêng `hdcOrder`, `Wf_*` dùng `wfOrder=95` (cha khác). Upsert chỉ đặt lại `Order` (`:1933-1937`), không đụng Label/ParentKey/Icon. CLEAN.
- **Trục 2 — labelBackfill KHÔNG đè tên admin đặt:** backfill ghi `item.Label` (`:1999-2001`); tên admin đổi qua Mig 27 nằm ở cột **khác**`DisplayLabel` (`MenuItem.cs:16`), và FE ưu tiên `displayLabel` (`Layout.tsx:202-203` `effectiveLabel`). ⇒ ép `Label` mỗi lần khởi động là **enforce có chủ đích****không** giẫm lên tuỳ biến của admin. CLEAN (đúng như lead khai).
- **Trục 3 — `Khkk_WfView`/`Khkk_Deleted` bên fe-user:** đã có trong staticMap (`Layout.tsx:78-80`). Bên fe-admin thiếu → xem FLAG-1.
- **Trục 3 — trạng thái sáng của 6 lá Khkk:** phân biệt nhau nhờ tham số `m` (`m=wfview|list|create|pending|approved|deleted`), `m` không nằm trong danh sách transient ⇒ so khớp nghiêm. CLEAN. (`ComingSoonPage.tsx:27-28` chỉ đọc `stage`, bỏ qua `m` — đúng ý đồ, `m` chỉ để tách trạng-thái-sáng.)
- **Trục 4 — nút "Tạo HĐ mới" đã ẩn ở chế độ xóa:** `MyContractsPage.tsx:122-127` bọc `{!showDeleted && …}`. Kèm chip "Đã xóa — chỉ xem" (`:112-116`) và câu rỗng riêng (`:158`). CLEAN.
- **Trục 5 — cắt hero không để lại tham chiếu mồ côi (kiểm ĐỘC LẬP, 2 phép):**
1. Đếm từng ký hiệu đã gỡ trong file hiện tại: `STAGE_TONE`, `StageCard`, `StageArrow`, `StageStatRow`, `StageLink`, `StageSoon`, `MyDashboard`, `peApproved`, `hasError`, `retryFailed`, `stats`, `Workflow`, `ChevronDown`, `PurchaseEvaluationPhase` = **0/0 hết**.
2. `npx tsc -p tsconfig.app.json --noEmit`**EXIT=0, không một dòng lỗi**. `tsconfig.app.json``noUnusedLocals: true` + `noUnusedParameters: true` ⇒ nếu còn import/biến mồ côi thì trình biên dịch đã đỏ. Đây là chứng mạnh hơn grep.
⚠️ Đo đúng phạm vi: tsconfig này **không bật** `strict`/`strictNullChecks`, nên "tsc xanh" ở đây chỉ chứng "không mồ côi + không sai kiểu thô", KHÔNG chứng an toàn null.
- **Trục 5 — phần danh sách còn nguyên:** tab 4 giai đoạn (`UserDashboardPage.tsx:1030-1034` `STAGES.map` + `active={stage === st.n}`), 2 bảng (`:536`, `:735`), cột "Hành trình" + `JourneyDots` (`:635`, `:682`, `:847`), nạp lười theo tab (`:974`, `:986`). Dữ liệu `soonDesc` vẫn có người đọc (`:881`), `tone` vẫn dùng (`:311`) ⇒ không thành rác. CLEAN.
- **Trục 5 — tiêu đề mới:** `PageHeader title="Tổng quan quy trình"` khớp nhãn menu backfill `MenuKeys.Dashboard = "Tổng quan quy trình"`. CLEAN.
- **`?type=3` của trang ma trận:** `ApprovalWorkflowApplicableType.Contract = 3` có thật (`ApprovalWorkflow.cs:56`); endpoint `GET /api/approval-workflows-v2` **chỉ cần đăng nhập**, không gắn policy (`ApprovalWorkflowsV2Controller.cs:16-26`) ⇒ **không** dính bẫy #44. Trang có sẵn nhánh rỗng lịch sự nếu admin chưa ghim quy trình nào cho loại 3 (`WorkflowMatrixViewPage.tsx:99-103`). CLEAN. *(Còn 1 điều KHÔNG kiểm được bằng đọc mã: prod đã có `ApprovalWorkflow` nào `ApplicableType=3` và `IsUserSelectable=true` chưa — nếu chưa, 7 lá "Luồng duyệt" đều hiện khung rỗng "Liên hệ Admin".)*
- ~~**Sửa chú thích D3 ở PE:** … đúng 2 chỗ …~~ 🔴 **TÔI TỰ ĐÍNH CHÍNH — xem FLAG-10.** (Tôi suýt duyệt con số này bằng mắt; chỉ khi chạy `grep -rn "IgnoreQueryFilters" src/Backend` mới lòi ra. Bài học tự ghi: **con số trong chú thích cũng là một khẳng định đo được — phải chạy phép đếm, không đọc lướt.**) Phần CÒN LẠI của chú thích vẫn đúng và giữ nguyên giá trị: cả 2 handler list-deleted đều kèm `.Where(x => x.IsDeleted)`, và lệnh cấm gộp thành helper dùng chung vẫn nguyên.
### FLAG-10 [M] — Chú thích vừa sửa trong chính diff này khai SAI con số, và nó là loại sai tự lan
**File:** `PeSoftDeleteFeatures.cs:184` (dòng ĐƯỢC SỬA trong diff) + `ContractFeatures.cs:356` (dòng MỚI trong diff)
**Khẳng định trong mã:** "NAY CÓ **2** chỗ dùng `IgnoreQueryFilters`" và "Chỗ **THỨ HAI** trong `src/Backend`".
**Phép phản chứng (1 lệnh, chạy thật):** `grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs` ⇒ ngoài 2 chỗ nói trên còn **`ApprovalWorkflowV2AdminFeatures.cs`** dùng ở **15 vị trí** (`:957, 964, 971, 978, 985, 992, 999` — 7 bảng `*LevelOpinions`; và `:1038, 1042, 1046, 1050, 1054, 1058, 1062` — 7 phép dò "phiếu cha còn sống không"). Tổng thực tế ≈ **3 tệp / 17 vị trí**, không phải 2.
**Vì sao không chỉ là lỗi chính tả:** câu này được viết dưới dạng **luật** ("chỗ DUY NHẤT/THỨ HAI ⇒ CẤM gộp helper"). Người đọc sau tin vào con số để kết luận "mình đang sửa chỗ duy nhất còn lại" trong khi thực tế có cả một cụm 15 chỗ ở module khác. Con số sai từ S155 ("DUY NHẤT") đã có cơ hội được sửa trong đúng lượt này, nhưng bị viết lại thành một con số sai mới.
**Tiêu chí nhận:** bỏ hẳn con số đếm ra khỏi chú thích (nêu **luật** thay vì **số**: "mọi nơi dùng `IgnoreQueryFilters` để liệt kê bản ghi đã xoá BẮT BUỘC kèm `.Where(x => x.IsDeleted)`"), hoặc giữ số nhưng nói rõ phạm vi đếm ("2 chỗ **thuộc lớp list-deleted**"; các chỗ trong `ApprovalWorkflowV2AdminFeatures` thuộc lớp khác — dò mồ côi). Kèm 1 lệnh grep trong chú thích để người sau kiểm lại được.
- **Không có dấu vết giả-lập:** diff không chứa `// Mock` / `alert(` / `TODO.*wire` / `placeholder`. Endpoint mới là truy vấn thật, FE gọi thật.
- **Chứng cứ cho FLAG-2 (đã truy tới đĩa):** truy vấn chi tiết HĐ nằm ở `ContractFeatures.cs:462` (`GetContractQuery`) → `:471` `db.Contracts.AsNoTracking()`**không** `IgnoreQueryFilters` ⇒ HĐ đã xoá chắc chắn rơi vào `NotFoundException` (404). Không phải suy đoán.
---
## Phạm vi KHÔNG kiểm (khai thật)
- **Không chạy `dotnet build` / `dotnet test`.** Phần BE chỉ soi tĩnh. Rủi ro biên dịch thấp (chỉ dùng lại `internal static` cùng assembly, DTO có sẵn) nhưng **tôi chưa có chứng**.
- **Không kiểm chứng dữ liệu prod:** (a) dòng `Permissions` khoá `Contracts` của 13 vai (FLAG-6); (b) có `ApprovalWorkflow` nào `ApplicableType=3, IsUserSelectable=1` chưa. Cả hai cần 1 câu `sqlcmd` hoặc 1 lần curl — lead chốt soi tĩnh nên tôi dừng đúng ranh.
- **Không kiểm 7 tệp ngoài phạm vi lead khai** (`docs/*`, `scripts/*`, `.claude/*`) — chúng có mặt trong cây làm việc và sẽ đi cùng lượt commit nếu dùng `commit -a`.
- **Không kiểm `git status` cho `Migrations/`** — đợt này không đụng migration nào (0 tệp trong `Migrations/`), nên bẫy "migration chưa được git theo dõi" (S155) không áp dụng.
- **Chưa chạy `npm run build` cho fe-admin** — FLAG-1 là thiếu dữ liệu trong bản đồ tuyến, không phải lỗi kiểu, nên trình biên dịch sẽ không bắt được; chạy build cũng không đổi kết luận.
---
## PHÁN QUYẾT
**PASS_WITH_FLAGS — 10 FLAG (0 High / 4 Medium / 6 Low).**
Không có lỗi nào đủ nặng để chặn commit: đường vào dữ liệu của endpoint mới **đúng** (có `.Where(IsDeleted)`, có rào IDOR sao đúng khuôn list sống, policy có thật, không rò dữ liệu). Việc cắt hero **sạch**, có chứng độc lập (`tsc` EXIT=0 với `noUnusedLocals`).
Bốn việc nên xử **trước khi bàn giao cho người dùng thử** (không nhất thiết trước khi commit):
1. **FLAG-2** — bấm dòng ở màn "Đã xóa" cho ra 2 panel trắng; một dòng `disabled={showDeleted}` là xong, đúng khuôn PE đã có.
2. **FLAG-1** — thêm 2 dòng vào `staticMap` của fe-admin, nếu không 2 lá menu biến mất im lặng bên app quản trị.
3. **FLAG-10** — con số trong chú thích sai và nó được viết dưới dạng luật, dễ dẫn người sau đi sai.
4. **FLAG-3** — hoặc làm nốt "Hộp thư", hoặc sửa chú thích cho khớp việc mã thực sự làm.
Sáu FLAG mức thấp còn lại là chuyện định nghĩa nghiệp vụ (FLAG-4), tật kế thừa từ PE (FLAG-5), rủi ro triển khai cần một câu truy vấn để yên tâm (FLAG-6), nợ quy ước (FLAG-7), trình bày (FLAG-8) và giới hạn phân trang (FLAG-9).
---
**END — TOTAL: 10 FLAG (0 High / 4 Medium / 6 Low) · VERDICT: PASS_WITH_FLAGS**