Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
16 KiB
sub-reviewer-1 — T2 review đối-kháng cây 4-folder GĐ (S162, 2026-07-30)
Ghi-đĩa-trong-lúc-làm (chống #53). Append theo từng mục soi. Verdict cuối ở cuối file.
Phạm vi đo (từ đĩa, git status --porcelain + git diff --stat)
- 6 file tracked M:
fe-{user,admin}/src/pages/pe/PurchaseEvaluationsListPage.tsx·.../khkk/KhkkListPage.tsx·.../contracts/HardCopiesPage.tsx— tổng +168/−16. - 6 file MỚI untracked:
fe-{user,admin}/src/components/pipeline/{PipelineStageFolders,PipelineTreePanel}.tsx+fe-{user,admin}/src/hooks/usePipelineStages.ts. - Ngoài src: memory frontend-designer (MEMORY.md + archive/2026-06.md + archive/_INDEX.md),
.session-counter.json, run-folder artifacts (2 png + 2 harness .txt + sub-frontend-designer-0.md).
MỤC 1 — Claim "khối <li> phiếu giữ nguyên từng byte" — ĐẠT
git diff -- .../pe/PurchaseEvaluationsListPage.tsx (cả 2 app, diff IDENTICAL):
- Hunk A
@@ -29,6 +29,8— +2 import. 0 xoá. - Hunk B
@@ -47,6 +49,14— +8 dòng (6 comment +usePipelineStages()+filterActive). 0 xoá. - Hunk C
@@ -507,7 +517,12— xoá ĐÚNG 1 dòng<ul className="ml-3 divide-y divide-slate-100 border-l border-slate-200">. - Hunk D
@@ -607,6 +622,7— +1 dòng) })} />. 0 xoá. ⇒ Tổng xoá = 1 dòng. Khối<li>KHÔNG có dòng-nào ⇒ CLAIM ĐỨNG. Class cũ không mất:ml-3 border-l border-slate-200→ propclassNamewrapper;divide-y divide-slate-100giữ ở<ul>trong.
MỤC 6 — Biên giới — ĐẠT
git diff --name-only(tracked): 0 filesrc/Backend, 0tests/, 0 seeder/menu (#84), 0PePipelineStrip.tsx. Chỉ 6 file FE + memory/run-artifact ngoài src.grep -rn "export default"trong 6 file mới ⇒ NONE (named export only, đúng §3.9).- Nhãn VERBATIM
PipelineStageFolders.tsx:22-27:1:'Duyệt NCC'·2:'Kế hoạch Hợp đồng'·3:'Duyệt Hợp đồng'·4:'Hợp đồng cứng'— khớp lời owner từng chữ. - Mirror SHA256 6/6 PAIR (tự đo lại, không tin bảng designer):
PipelineStageFolders
05c93c3619a9cfec· PipelineTreePanel76e5dafb239e402a· usePipelineStagese762d5eb9c3a057a· PE page1d8d8f75c7d2cc14· Khkk page40a650bf6964ab4b· HardCopies paged30a911e7483f96a. types/contracts.tslệch 2 app CHỈ ở giá trịContractPhaseColor(cùng bộ key 1-10,99) ⇒ hook SHA-chung import an toàn,badgeClassundefined vẫn có fallbackPipelineStageFolders.tsx:189.
MỤC 3 — Permission-gate 2 tầng — 1 LỖ THẬT (latent) + 1 điểm sai luận-cứ
Key FE (usePipelineStages.ts:41-44) vs BE:
Khkk_List/Khkk_G1/KeHoachKyKet— cả 3 tồn tại thật (DbInitializer.cs:2184-2188KhkkKeys +MenuKeys.cs:41).Hdc_<7 code>+HopDongCung— tồn tại thật (DbInitializer.cs:1868/2191).Ct_<code>_List+Contracts— tồn tại thật.can(k)=node.canRead(usePermission.ts:20), findNode không thấy ⇒ false ⇒ OR-nhiều-key hợp lệ về mặt đọc cây.
🔴 F: OR-nhiều-key ĐÁ NHAU với policy API cho GĐ2. ContractSigningPlansController.cs:36 = [Authorize(Policy="KeHoachKyKet.Read")], và MenuPermissionHandler.cs:40 so khớp CHÍNH XÁC p.MenuKey == req.MenuKey (KHÔNG kế thừa, KHÔNG OR con). ⇒ role có Khkk_List=1 mà KeHoachKyKet=0 sẽ: FE cho canKhkk=true → gọi API → 403 → khkkFailed → GĐ2 hiện "—" dưới MỌI gói thầu, và claim "0 rác 403" vỡ. Đo Dev DB: 13/13 role đều có Contracts=1 ∧ KeHoachKyKet=1 ⇒ HIỆN CHƯA vỡ; prod chưa đo được (chỉ localhost+secret). Là lỗ THIẾT KẾ chờ 1 lần chỉnh ma trận quyền.
🟡 luận-cứ trong comment usePipelineStages.ts:10-13 ("gate phải OR nhiều key vì root có thể canRead=false") đúng cho TẦNG HIỂN THỊ nhưng sai đích cho tầng API: query nào cũng nên gate bằng ĐÚNG key policy của endpoint (KeHoachKyKet cho /contract-signing-plans).
🟢 /contracts KHÔNG có policy action-level (ContractsController.cs:16-17, chỉ [Authorize] trần class) ⇒ nhánh enabled: canContract || canHardCopy KHÔNG sinh 403. Đổi lại: đây là bare-[Authorize] sẵn có của hệ (#82), diff này không làm tệ thêm nhưng có nới hiển-thị: user chỉ được cấp Hdc_ThauPhu vẫn thấy leaf GĐ4 của MỌI loại HĐ (usePipelineStages.ts:211) vì gate là OR-toàn-nhóm chứ không theo c.type.
🟢 Không quyền ⇒ enabled:false (:77, :91, PipelineTreePanel.tsx:108) ⇒ KHÔNG gọi API; folder khoá render nhánh riêng KHÔNG <details> (PipelineStageFolders.tsx:99-113). Fetch lỗi ⇒ failed ⇒ badge "—" + body "Không tải được…" (:140-156) ⇒ cây không vỡ. ĐẠT.
MỤC 2 — Semantics filter / hideEmpty — ĐẠT phần cốt lõi, 3 gợn nhẹ
- Nguồn cờ: PE page
filterActive = search.trim()!=='' || phase!=='' || pendingMe || deletedView(PurchaseEvaluationsListPage.tsx:58); panelhideEmpty = term.length > 0(PipelineTreePanel.tsx:138). Không lọc ⇒buildStagesluôn trả ĐỦ 4 (usePipelineStages.ts:240) ⇒ "không filter = đủ 4 folder" ĐÚNG. - Ca "GĐ1 rỗng chiếm chỗ" KHÔNG tồn tại:
wg.itemslà nguồn dựng nhóm nên mọi gói thầu có ≥1 phiếu ⇒stage1.count = wg.items.length ≥ 1(usePipelineStages.ts:153). Thêm chốt chặnif (visible.length===0) return null(PipelineStageFolders.tsx:220) ⇒ không có wrapper rỗng, không vỡ layout. - 🟡 gợn 1: bộ lọc
hideEmptychấm bằngstageCount(s) > 0(:219) nên nó nuốt LUÔN folderlockedvà folderfailed(cả hai đều count=0). Đang lọc mà GĐ2 lỗi tải ⇒ dòng "—" BIẾN MẤT thay vì báo lỗi (spec dòng 12 muốn "—"). Cũng nuốt folder đangloading⇒ folder nhảy vào sau khi query xong (giật layout nhẹ). - 🟡 gợn 2: count GĐ2/3/4 KHÔNG chịu bộ lọc của trang (index nạp riêng, không truyền
search/phase) ⇒ "chỉ hiện folder có nội dung khớp" chỉ đúng theo nghĩa YẾU (folder khác rỗng), không phải "nội dung khớp từ khoá". - 🟡 gợn 3: ở view "Đã xóa" (
deletedView=1) cây vẫn treo GĐ2/3/4 SỐNG dưới phiếu đã xoá (index không lọc theo phiếu xoá) ⇒ bấm được sang KHKK/HĐ còn sống từ màn thùng rác.
MỤC 4 — Query hygiene + degrade pageSize:200
Không đụng cache trang cũ — ĐẠT. Key mới ['pipeline-khkk-index'] / ['pipeline-contract-index'] (usePipelineStages.ts:70,84) + ['pipeline-pe-index'] (PipelineTreePanel.tsx:101). Key đang sống: ['khkk-list',{…}] (KhkkListPage.tsx:70) · ['hard-copies',{…}] + ['contract',id] (HardCopiesPage.tsx:75,268) · ['pe-list',{…}] + ['pe-detail',id] (PurchaseEvaluationsListPage.tsx:75,114) · dashboard ['pipeline-pe-total']/['pipeline-contract-total'] (UserDashboardPage.tsx:906,913). Tất cả là mảng 1 phần tử CHUỖI KHÁC NHAU ⇒ 0 giao, 0 prefix-match. invalidateQueries() trống (nuốt tất) = 0 chỗ trong fe-user.
🟡 mặt trái: qc.invalidateQueries({queryKey:['hard-copies']}) sau khi lưu bản cứng (HardCopiesPage.tsx:290) KHÔNG chạm pipeline-contract-index ⇒ badge GĐ4 trên cây giữ số cũ tới lần mount/refocus kế (staleTime 60s, usePipelineStages.ts:34).
Degrade khi vượt 200 — graceful CÓ, im lặng CŨNG CÓ.
- Kẹp là THẬT:
PagedResult.cs:20PageSize = value switch { <1 => 20, >200 => 200, _ => value }. - Thứ tự lấy: 3 controller đều mặc định
sortDesc = true(ContractsController.cs:19·ContractSigningPlansController.cs:45·PurchaseEvaluationsController.cs:21) ⇒ 200 bản MỚI NHẤT, không phải 200 bản cũ nhất. Tốt. - 🔴 nhưng hook VỨT
total:.data.items(usePipelineStages.ts:76,90·PipelineTreePanel.tsx:107) ⇒ mã KHÔNG BIẾT mình bị cắt ⇒ không đâu có dấu "còn N nữa". GĐ2 mất leaf + tụt count; GĐ4 tụt count; header panel "{packageCount} gói" (PipelineTreePanel.tsx:188) cũng tụt. Đây là sai-số-im-lặng, không phải chỉ degrade. - 🔴 GĐ3 tệ hơn 1 nấc: HĐ ngoài index vẫn ra leaf nhưng DÁN NHÃN SAI
primary: c?.maHopDong ?? '(chưa cấp mã)'(usePipelineStages.ts:204) — HĐ có mã thật vẫn bị ghi "(chưa cấp mã)". Trộn 2 nguyên nhân khác hẳn nhau (chưa cấp mã ⟂ ngoài cửa sổ 200) vào một câu khẳng định sai. - 🔴 độc lập với 200:
pe.ContractId = contracts[0].Id(CreateContractFromEvaluationFeatures.cs:145, comment:143ghi rõ "N HĐ liên-danh truy qua changelog") ⇒ phiếu multi-winner đẻ N hợp đồng nhưng cây chỉ nối ĐÚNG 1 (usePipelineStages.ts:197) ⇒ GĐ3/GĐ4 hụt N−1 và badge in "1". Đúng lớp bàifeedback_cardinality_change_grep_consumers(S87/S88): consumer mới đọc field single trong miền multi. - Cỡ dữ liệu đo được: Dev
PE=4 · KHKK=0 · Contracts=7 · PE có ContractId=0/4⇒ chưa chạm trần 200, nhưng cũng có nghĩa GĐ3/GĐ4 trên Dev sẽ rỗng ("SẮP") toàn bộ; prod không đo được từ máy này.
MỤC 5 — Lưới 19rem 2 trang (KhkkListPage + HardCopiesPage)
- PageHeader còn full-width — ĐÚNG. KHKK: header đóng
/>ở:133, lưới mở ở:138(ngoài lưới). HardCopies: PageHeader + ghi chú 1-MỐC amber đứng trước, lưới mở sau (HardCopiesPage.tsxhunk@@ -110,6 +111,12). min-w-0có đủ: cột phải<div className="min-w-0 space-y-5">ở cả 2 trang; track lưới dùngminmax(0,1fr)⇒ bảng không thổi vỡ cột. ĐẠT.- Mobile:
<lglưới về 1 cột, panel nằm TRÊN; thân panelmobileOpen ? 'flex' : 'hidden'+lg:flex(PipelineTreePanel.tsx:200) vớimobileOpenmặc địnhfalse(:95) ⇒ mobile chỉ thêm 1 thanh tiêu đề + nút "Hiện cây", bảng không bị đẩy xuống sâu. ĐẠT. - 🟡
overflow-x-autobọc<table>gần như VÔ TÁC DỤNG như lời chú thích:<table className="w-full">+whitespace-nowrap= 0 lần ở cả 2 file ⇒ bảng co cột/xuống dòng chứ không sinh cuộn ngang. Vô hại, nhưng chú thíchKhkkListPage.tsx:175("bảng 7 cột cuộn ngang trong thẻ") mô tả một hành vi không xảy ra; thứ thật sự chặn tràn làminmax(0,1fr). - 🟡 đổi hành vi có thật (nhỏ): hàng KpiCard
lg:grid-cols-6→xl:grid-cols-6(KhkkListPage.tsx:137) ⇒ ở dải 1024–1279px KPI từ 6 cột rớt về 3 cột. Hợp lý vì cột trái ăn 19rem, nhưng đây là thay đổi hiển thị ngoài phạm vi "chỉ thêm cây". - 🟡
HardCopyPanel(HardCopiesPage.tsx:220, định nghĩa:259) nằm TRONG<tbody>nên nay nằm trong lớpoverflow-x-automới. Không mất chức năng (upload/tải/ý kiến vẫn nguyên, 0 dòng-trong diff), nhưng nó bị bó vào bề rộng cột phải hẹp hơn trước. - 🔴 Bằng chứng thị giác cho 2 trang này = KHÔNG CÓ. Harness chỉ import
PipelineStageFolders+PipelineTreePanel(harness-s162.tsx.txt:10-11), tự "sao markup" vỏ cây (:61), vàgrep 'grid-cols-\[19rem'trong harness = 0 hit ⇒ chưa bao giờ render KhkkListPage/HardCopiesPage/lưới 19rem. Vậy dòng rubric FD4 "responsive ✓ (1440/1366/375 … bảng không bị đẩy)" trongsub-frontend-designer-0.md:94là nhãn ĐO mà phép đo không tồn tại — phải hoặc đo lại, hoặc bỏ dấu ✓ cho trục đó. - (sạch) Harness đã xoá khỏi
src2 app —ls fe-*/src/harness*= NONE.
MỤC 7 — Self-write memory frontend-designer
ĐÚNG SỰ THẬT (tự đo lại, không tin lời khai):
PagedResult.cs:20kẹp ≤200 — ĐÚNG nguyên văn.GetMyMenuTreeQuery.cs:96=HasAccess(n) => n.CanRead || n.Children.Any(HasAccess)— ĐÚNG số dòng.- "Sau nén 14.6KB" —
wc -c MEMORY.md= 14.652 B. ĐÚNG. - Archive moved-not-cut (bài S102): 3 mục chuyển đi đều CÓ MẶT trong
archive/2026-06.md(WidgetCard3 hit ·OfficeDashboardPage3 hit ·Maximize22 hit) + 3 dòng trỏsubstringcòn ở L1. KHÔNG phải cut-giả-vờ-archive. - "MIRROR đủ 2 app / build PASS ×2 /
<li>giữ nguyên từng byte" — đã tự chứng ở MỤC 1 + MỤC 6. 2 chỗ SAI/NỬA-ĐÚNG sẽ đóng băng vào trí nhớ vai nếu commit nguyên: - 🔴 "
enabled:false⇒ 0 rác 403" viết như tính chất vô điều kiện — sai cho đường KHKK (xem F-1): gate ORKhkk_Listtrong khi policy API làKeHoachKyKet.Read. - 🟡 "
Ct_*kế thừaContracts" — chỉ đúng khi menu con KHÔNG có row quyền (GetMyMenuTreeQuery.cs:66!resolved.ContainsKey(m.Key)), mà seeder LẠI tạo row choCt_*(DbInitializer.cs:2168-2176) ⇒ trên DB đã seed, kế thừa gần như không bao giờ chạy. Câu này sẽ dẫn sai phiên sau. - 🟡 nhật ký
sub-frontend-designer-0.md:82"mount CHÍNH component thật (không vẽ lại markup)" — đúng với 2 component mới, nhưng vỏ cây thì có vẽ lại ("sao markup",harness-s162.tsx.txt:61).
VERDICT: PASS-WITH-FLAGS
Hợp đồng spec giữ đúng ở mọi ranh giới kiểm được (nhãn verbatim 4/4 · mirror SHA 6/6 · khối <li> phiếu 0 dòng xoá · 0 chạm BE/menu/seeder/strip/tests · named-export) và các đường hỏng đều có nhánh đỡ (khoá/lỗi/rỗng). Không có blocker. 8 flag: nặng nhất là im lặng về độ chính xác của con số (cắt 200 không dấu vết, contracts[0] nuốt HĐ liên-danh, nhãn "(chưa cấp mã)" sai nguyên nhân) và 1 nhãn ✓ responsive chưa từng được đo.
- F-1
usePipelineStages.ts:42— gate OR['Khkk_List','Khkk_G1','KeHoachKyKet']trong khi API là[Authorize(Policy="KeHoachKyKet.Read")](ContractSigningPlansController.cs:36) vàMenuPermissionHandler.cs:40so khớp CHÍNH XÁC 1 key ⇒ role cóKhkk_List=1/KeHoachKyKet=0sẽ 403 và GĐ2 hiện "—" khắp cây. Dev 13/13 role hiện đều =1 nên chưa lộ. - F-2
usePipelineStages.ts:197+CreateContractFromEvaluationFeatures.cs:145— phiếu liên-danh N HĐ chỉ nối 1 (pe.ContractId= HĐ đầu) ⇒ GĐ3/GĐ4 hụt N−1 mà badge vẫn in số như thật. - F-3
usePipelineStages.ts:204— HĐ ngoài cửa sổ 200 bị dán nhãn "(chưa cấp mã)"; trộn "chưa có mã" với "không nằm trong index". - F-4
usePipelineStages.ts:76,90+PipelineTreePanel.tsx:107— vứttotal⇒ không phát hiện được việc bị cắt 200 ⇒ mọi count/{n} góitụt im lặng, không dấu "còn N nữa". - F-5
sub-frontend-designer-0.md:94— rubric "responsive ✓ … bảng không bị đẩy" không có phép đo: harness chỉ mount 2 component (harness-s162.tsx.txt:10-11),grid-cols-[19rem= 0 hit ⇒ 2 trang lưới chưa từng render. - F-6
PipelineStageFolders.tsx:219—hideEmptylọc theo count>0 nên nuốt cả folderlocked/failed/loading; đang lọc thì dấu "—" báo lỗi biến mất. - F-7
HardCopiesPage.tsx:290— invalidate['hard-copies']không chạm['pipeline-contract-index']⇒ badge GĐ4 lệch tới lần mount/refocus kế. - F-8 memory vai: "0 rác 403" (vô điều kiện — sai theo F-1) + "
Ct_*kế thừaContracts" (chỉ đúng khi con không có row; seeder có tạo rowCt_*) — nên sửa trước khi commit vì đây là trí nhớ dài hạn. - (nhẹ, không đánh số)
overflow-x-autovô tác dụng vìtable w-full+ 0whitespace-nowrap; KPIlg:→xl:đổi hiển thị dải 1024–1279; view "Đã xóa" vẫn treo GĐ2/3/4 sống; 2 query index chạy sớm ngay cả khi người dùng không mở folder nào.