19 KiB
GATE-GD3D: PASS-WITH-FLAGS-10 — Đúng phương án D, khuôn nguyên vẹn (SHA-pair identical), import sạch; 0 blocker, nhưng max-h của khuôn vượt chiều cao Panel-3 trên InboxPage (~26px) đẻ 2 thanh cuộn lồng nhau + 2 class khuôn chết âm thầm.
Sub-reviewer gate GĐ3-D (S168, 2026-08-01)
Scope: 3 file FE uncommitted, phương án D "cây thay chỗ Panel-3 khi chưa chọn HĐ".
Spec nguồn: .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-gd3.md §1 bảng 4 phương án (D).
Vai: reviewer đối-kháng, propose-only — không sửa code, không commit.
Đo trước (baseline)
git status --porcelaintoàn repo: 5M—.claude/WAL.md,.claude/agent-memory/memory-budget.json, + 3 file FE.git status --porcelain --untracked-files=all -- fe-admin/src fe-user/src: ĐÚNG 3 file, 0 untracked.git diff --stat: mỗi file FE 13 dòng đổi (+6/−7), tổng 3 file.
R1 — Đúng-D (điều kiện render) → PASS
Cả 3 file giữ NGUYÊN cặp điều kiện:
{!selectedId && <PipelineTreePanel currentStage={3} />}
{selectedId && detail.data && <WorkflowHistoryPanel contract={detail.data} />}
- Cây CHỈ render khi
!selectedId— đúng D. - Chọn HĐ →
WorkflowHistoryPanelnhư cũ; vế điều kiện không đổi một ký tự. - Trạng thái "đã chọn HĐ nhưng
detailđang tải" render RỖNG ở CẢ bản cũ lẫn bản mới (bản cũ:!selectedIdsai nên ô dashed tắt,detail.datachưa có nên panel trống) ⇒ hành vi loading giữa chừng KHÔNG đổi. currentStage={3}đúng GĐ Hợp đồng cho cả 3 trang (xem F-10 cho lưu ý riêng về InboxPage).
R2 — 0-đụng-khuôn → PASS
sha256sum (16 ký tự đầu), fe-admin ⟂ fe-user:
| File | admin | user | Kết quả |
|---|---|---|---|
components/pipeline/PipelineTreePanel.tsx |
d70ac315bae485e6 |
d70ac315bae485e6 |
IDENTICAL |
components/pipeline/PipelineStageFolders.tsx |
bed52f39d595c86a |
bed52f39d595c86a |
IDENTICAL |
hooks/usePipelineStages.ts |
76e1823345d4e78b |
76e1823345d4e78b |
IDENTICAL |
git status --porcelain trên 4 đường dẫn khuôn = RỖNG ⇒ khuôn chưa bị sửa. SHA-pair identical vừa chứng mirror 2 app còn khớp, vừa chứng không ai chạm vào khuôn trong đợt này.
R3 — Unused-import X → PASS
- Cây làm việc: 0 hit mã ở cả 3 file. Hit duy nhất là chuỗi
type=Xtrong commentContractsListPage.tsx:6(không phải icon). - Control-âm: cùng regex chạy trên
git show HEAD:<file>cho 2 hit/file (dòngimport … X …+<X className="mx-auto mb-2 h-5 w-5" />) ⇒ phép đo CÓ răng, không phải 0-hit vì regex hỏng. - Không gỡ nhầm icon khác:
FileText/Search/Plus/Inbox/AlertTriangle/Clock/ClipboardListđều còn nguyên trong dòng import mới.
FINDINGS
F-1 (MAJOR) — R4: max-h của khuôn hiệu-chỉnh cho host page-flow, vượt chiều cao Panel-3 trên InboxPage ⇒ 2 thanh cuộn lồng nhau
Chiều cao TỐI ĐA của khuôn khi đặt ở lg:
| Thành phần | File:dòng | Lớp CSS | px |
|---|---|---|---|
| Đầu panel | PipelineTreePanel.tsx:181 |
px-3 py-2.5 (20) + icon-chip h-7! (28) + border-b (1) |
49 |
| Ô lọc | PipelineTreePanel.tsx:204 |
p-2 (16) + input h-8 (32) + border-b (1) |
49 |
| Thân cây | PipelineTreePanel.tsx:215 |
lg:max-h-[calc(100vh-16rem)] |
100vh − 256 |
| Tổng | 100vh − 158 |
Chiều cao KHẢ DỤNG của Panel-3 = 100vh − 4rem (vỏ trang) − đầu trang − dải phụ − p-4 (32):
| Trang | Vỏ / đầu trang | Đầu trang | Dải phụ | Khả dụng | So với 100vh−158 |
|---|---|---|---|---|---|
fe-user/src/pages/contracts/MyContractsPage.tsx |
:105 / :107 py-3 + Button size=md h-8 + border |
57px | 0 | 100vh − 153 | thừa 5px (khít) |
fe-admin/src/pages/contracts/ContractsListPage.tsx |
:106 / :107 py-3 + h1 24px + border |
49px | 0 | 100vh − 145 | thừa 13px |
fe-user/src/pages/InboxPage.tsx |
:166 / :168 py-3 + StatPill 34px (:50 px-3 py-1.5 + h-5) + border |
59px | 29px — dải "Vai trò bạn đang xử lý" :183-187 (py-1.5 + text-[11px] + border) |
100vh − 184 | THIẾU ~26px |
Hệ quả trên /inbox: <aside className="hidden overflow-y-auto … p-4 lg:block"> mọc thanh cuộn dọc thứ hai bao ngoài thanh cuộn của thân cây, tức trong cột nội dung rộng 328px (360 − 2×16) có hai thanh cuộn lồng nhau. Nội dung vẫn với tới được (không vỡ layout, không mất dữ liệu), nhưng người dùng phải cuộn hai tầng, và đầu panel cùng ô lọc trôi khỏi tầm nhìn khi cuộn tầng ngoài.
Dải "Vai trò" chỉ render khi user?.roles.length — đo prod: /api/roles trả 13 vai, mọi tài khoản thật đều thuộc ít nhất một vai ⇒ nhánh này gần như LUÔN bật, không phải ca hiếm.
Hai trang còn lại chỉ thừa 5px và 13px ⇒ mong manh: bất kỳ thay đổi nào ở đầu trang (đổi Button sang size=lg = h-10, hoặc thêm một chip trạng thái làm flex-wrap xuống dòng ở bề rộng 1024px) đều đẩy chúng sang đúng trạng thái của InboxPage.
Tiêu chí nghiệm thu đề nghị: ở 1280×800 và 1366×768, mở /inbox khi chưa chọn HĐ — số thanh cuộn dọc trong vùng Panel-3 phải bằng 1, và cuộn tới đáy cây không cần cuộn <aside> ngoài. Lặp lại phép đo cho /my-contracts và fe-admin /contracts.
F-2 (MINOR) — R4: hai class của khuôn chết ÂM THẦM trong host mới
PipelineTreePanel.tsx:177 — cn('card-accent flex flex-col self-start overflow-hidden lg:sticky lg:top-4', className):
self-startlàalign-self:flex-start, chỉ có tác dụng khi cha là flex hoặc grid. Host mới có cha là<aside … lg:block>⇒display:block⇒ vô hiệu.lg:sticky lg:top-4: tổ tiên cuộn gần nhất chính là<aside overflow-y-auto>, mà cây lại là con DUY NHẤT của nó ⇒ khối chứa gần bằng chính nó ⇒ quãng "dính" xấp xỉ 0 ⇒ vô hiệu.
Ở ba host cũ (KhkkListPage.tsx:197 và HardCopiesPage.tsx:116, đều là lg:grid-cols-[19rem_minmax(0,1fr)]) cha là grid và trang cuộn ở <main className="flex-1 overflow-auto">, nên cả hai class đều sống. Đây không phải lỗi chức năng, nhưng là dấu hiệu khuôn đang chạy ngoài giả định thiết kế của nó — cùng gốc với F-1. Nên ghi một dòng chú thích ở chỗ cắm để người sau không tưởng rằng cây có bám dính ở đây.
F-3 (MINOR) — R4 / a11y: <aside> lồng trong <aside>
Host Panel-3 là <aside>, và gốc khuôn cũng là <aside> (PipelineTreePanel.tsx:176) có kèm aria-label="Cây toàn trình theo gói thầu" (:179). Theo HTML-AAM, một aside nằm trong sectioning content chỉ tụt xuống role=generic khi KHÔNG có tên khả truy cập; ở đây nó có tên nên giữ nguyên role=complementary ⇒ sinh ra hai landmark complementary lồng nhau. Điều này hợp lệ về HTML nhưng gây nhiễu khi người dùng điều hướng theo landmark bằng trình đọc màn hình. Ba host cũ không dính vì cha của chúng là <div className="grid …">.
F-4 (MINOR) — R5: !canPe đổi thông điệp Panel-3 từ chỉ dẫn sang than phiền quyền; đo prod cho thấy hiện chưa vai nào dính
Đường quyền, đọc thẳng từ mã chứ không suy:
usePipelineStages.ts:83—canPe = PE_MENU_KEYS.some(k => can(k)), vớiPE_MENU_KEYSở:55là['PurchaseEvaluations','Pe_DuyetNcc_List','Pe_DuyetNccPhuongAn_List'].- Khi
canPesai,PipelineTreePanel.tsx:111đặtenabled: canPenên truy vấnpipeline-pe-indexkhông chạy ⇒ không sinh rác 403, đúng ý đồ đã khai ởusePipelineStages.ts:5-8. - Nhưng Panel-3 khi đó hiển thị khung cây rỗng, huy hiệu "0 gói", và câu ở
:216-220: "Bạn chưa có quyền xem phiếu Duyệt NCC nên cây toàn trình chưa hiển thị được." Câu này nằm trên trang Hợp đồng, thay cho câu cũ "Quy trình duyệt sẽ hiện khi chọn HĐ." ⇒ người dùng mất chỉ dẫn hữu ích và nhận về một lời than phiền lạc đề.
Đo prod /api/permissions/by-role/{id} trên 13 vai (tất cả trả http=200): PurchaseEvaluations.canRead bằng True ở 13/13 vai ⇒ nhánh !canPe hiện không xảy ra với vai nào.
Control-âm của cùng phép đo: Permissions.canRead và Users.canRead bằng False ở 12/13 vai (chỉ Admin True; số dòng quyền là 169 so với 191) ⇒ phép đo có phân biệt thật, không phải "True tuốt vì đọc sai trường".
Kết luận: F-4 là rủi ro tương lai (nếu ai đó gỡ quyền PE của một vai), không phải lỗi đang chảy máu. Không chặn commit.
F-5 (MINOR) — R5: cây bơm thêm tối đa ba truy vấn index vào lượt tải đầu của ba trang
PipelineTreePanel gọi usePipelineStages, hàm này gọi usePipelineLinkage, sinh ra ba truy vấn — pipeline-pe-index (PipelineTreePanel.tsx:100), pipeline-khkk-index (usePipelineStages.ts:90) và pipeline-contract-index (:108) — mỗi truy vấn lấy pageSize=200.
Trên InboxPage, MyContractsPage và ContractsListPage, truy vấn pipeline-contract-index gọi GET /contracts?pageSize=200 song song với truy vấn danh sách của chính trang (/contracts/inbox ở InboxPage.tsx:71, /contracts ở MyContractsPage) — cùng một tài nguyên nhưng khác queryKey, nên mỗi lần vào trang mà chưa chọn HĐ là hai lần nạp danh sách hợp đồng. Đo prod cho thấy 13/13 vai đều có đủ canPe, canKhkk và canContract ⇒ cả ba truy vấn đều bắn cho mọi vai, không vai nào được miễn. Mức này chấp nhận được vì staleTime là 60 giây và đây là hệ quả nội tại của phương án D, nhưng nên biết trước để không bất ngờ khi UAT phản ánh trang tải chậm hơn.
F-6 (MINOR) — R1 phụ: trade-off của D khai thiếu một vế — ô lọc cây mất chữ khi chọn HĐ rồi bỏ chọn
Vì điều kiện là {!selectedId && <PipelineTreePanel …>}, việc chọn một HĐ sẽ gỡ bỏ (unmount) component. Hệ quả:
expandedSetsống sót nhờlocalStorage(PipelineTreePanel.tsx:119-126đọc,:170ghi).search(:94) vàmobileOpen(:95) là state thuần nên mất trắng: người dùng gõ "FLOCK" để lọc cây, bấm xem một HĐ, quay lại thì ô lọc rỗng và cây bung về nhánh mặc định.
Spec D ở sub-implfe-gd3.md §1 chỉ khai trade-off "cây nhảy chỗ khi chọn HĐ", không khai việc mất chữ đang lọc. Đề nghị bổ sung vế này khi báo owner để owner chốt trên thông tin đầy đủ.
F-7 (MINOR) — R8 mobile: không hồi quy, nhưng lệch với ba host cũ
- Host là
<aside className="hidden overflow-y-auto … lg:block">nên dưới 1024px Panel-3 không render. Bản cũ (ô dashed) nằm trong đúng cùng<aside>đó, vì vậy không có hồi quy trên mobile. - Nhưng khuôn có sẵn cả một nhánh mobile riêng: nút "Hiện cây / Ẩn cây" gắn
lg:hidden(:193-200), statemobileOpenkhống chế thân cây (:203), vàmax-h-[28rem]dành cho mobile (:215). Trong host này toàn bộ nhánh mobile của khuôn chết. - Hệ quả nghiệp vụ: người dùng mobile CÓ cây ở GĐ2 (
KhkkListPage) và GĐ4 (HardCopiesPage, hai trang page-flow nên cây xếp ở trên), nhưng không có cây ở GĐ3.
Điều này đúng chủ đích của HOST (bố cục 3-panel vốn chỉ dành cho desktop) nhưng lệch chủ đích của KHUÔN. Owner cần biết để chốt, đây không phải lỗi code.
F-8 (INFO — phán quyết: lead ĐÚNG) — R6: WorkflowMatrixViewPage không áp D là quyết định đúng
fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:104 cho thấy gốc trang là <div className="space-y-4 px-2 py-5">: đây là trang page-flow, không có <aside>, không có lưới 3-panel, không có Panel-3. Vì vậy "cây thay chỗ Panel-3" không có chỗ cắm, và bỏ qua trang này là đúng theo đúng định nghĩa của phương án D. Đây không phải chỗ lead bỏ sót.
Tuy nhiên có một câu cần owner chốt: trang này được mount ở ba route (fe-user/src/App.tsx:71 là /purchase-evaluations/workflow-matrix, :73 là /contracts/workflow-matrix, :83 là /khkk/workflow-matrix), tức nó là trang giai đoạn của cả ba GĐ. Nếu ý owner khi nói "(a) cứ mở rộng" là "mọi trang giai đoạn đều phải có cây", thì đây là trang duy nhất còn thiếu, và cách cắm đúng phải là lưới page-flow theo khuôn KhkkListPage.tsx:197 chứ không phải D. Việc này nằm ngoài phạm vi wave hiện tại, nêu ra để owner quyết.
F-9 (INFO — phán quyết: KHÔNG mâu thuẫn K6) — R7: sửa trang đang bị ẩn khỏi sidebar
fe-admin/src/components/Layout.tsx:227-240— tậpADMIN_HIDDEN_KEYScó chứa'Contracts'; thêm nữa,:248trongisAdminHiddencòn chặn mọi key bắt đầu bằngCt_. Vì vậy admin không nhìn thấy mục Hợp đồng ở sidebar.- Route thì vẫn sống:
fe-admin/src/App.tsx:82map/contractssangContractsListPage, kèm:83cho/contracts/newvà:84cho/contracts/:id. - Có tiền lệ khớp: key
'HopDongCung'cũng nằm trongADMIN_HIDDEN_KEYS(:231), vậy màHardCopiesPagebên fe-admin vẫn được S162 cắm cây (fe-admin/src/pages/contracts/HardCopiesPage.tsx:117) và route của nó vẫn sống (App.tsx:87).
Kết luận: K6 ẩn phần hiển thị, chứ không xoá trang. Sửa một trang bị ẩn chính là giữ mirror theo docs/rules.md §3.9, nên không có mâu thuẫn.
Một lưu ý thực dụng: thay đổi bên fe-admin gần như vô hình với admin vì phải gõ thẳng URL mới tới được. Giá trị của nó là parity giữa hai app, nên đừng tính nó vào "ba trang người dùng nhìn thấy" khi báo owner, và UAT phải làm ở fe-user chứ không phải fe-admin.
F-10 (INFO) — R1 phụ: currentStage={3} trên InboxPage, trong khi hộp thư trộn GĐ1 và GĐ3
fe-user/src/pages/InboxPage.tsx:71 gọi /contracts/inbox và :79 gọi /purchase-evaluations/inbox, nên trang liệt kê cả hợp đồng lẫn phiếu Duyệt NCC (bấm vào phiếu PE thì :283 điều hướng sang /purchase-evaluations/:id). Giá trị currentStage={3} làm cây mở sẵn folder GĐ3.
Cách đặt này vẫn hợp lý, vì Panel-3 là chỗ dành cho HĐ đang chọn (selectedId được dùng cho /contracts/${selectedId} ở :84), tức ngữ cảnh của panel đúng là GĐ3. Nêu ra để tránh hiểu nhầm nếu sau này owner coi Inbox là trang GĐ1. Không tính là lỗi.
Tổng kết theo trục
| Trục | Kết quả | Ghi chú |
|---|---|---|
| R1 Đúng-D + loading không đổi | PASS | điều kiện render không đổi một ký tự; kèm F-6 và F-10 dạng thông tin |
| R2 0-đụng-khuôn + SHA-pair | PASS | 3/3 cặp IDENTICAL, git status trên khuôn rỗng |
R3 Unused-import X |
PASS | 0 hit mã, control-âm cho 2 hit/file ở HEAD |
| R4 Ngữ cảnh render / CSS | FLAG | F-1 (MAJOR), F-2, F-3 |
| R5 Permission-path / 403 rác | PASS có cờ | không sinh rác 403; F-4 là rủi ro tương lai (prod 13/13 chưa dính), F-5 là chi phí truy vấn |
| R6 Trang không áp | PASS | F-8 — quyết định của lead là đúng |
| R7 fe-admin ẩn sidebar | PASS | F-9 — không mâu thuẫn K6, có tiền lệ HardCopiesPage |
| R8 Mobile | PASS | F-7 — không hồi quy, nhưng nhánh mobile của khuôn chết trong host này |
Blocker: 0. Không finding nào chặn commit. F-1 là thứ đáng xử lý nhất trước khi UAT, hoặc phải khai rõ với owner như một trade-off đã biết.
Ranh vai: không sửa file mã nào, không commit, không push. Mọi finding đều ở dạng mô tả vấn đề kèm tiêu chí nghiệm thu.
END sub-reviewer-gate-gd3d — VERDICT=PASS-WITH-FLAGS-10 — TOTAL=10 finding
LEAD DISPOSITION @S168 (từng dòng — không số tổng)
| F | Xử | Chi tiết |
|---|---|---|
| F-1 MAJOR | ✅ VÁ | Nới khuôn đúng ngoại-lệ "thiếu prop cho GĐ3" artifact lane khoanh: PipelineTreePanel +prop bodyMaxHClass? (default GIỮ NGUYÊN — 3 host giấy-cuộn cũ 0 đổi hành vi), 3 host Panel-3 pass max-h-[28rem] lg:max-h-none ⇒ panel cuộn = đúng 1 thanh, bền mọi trang (không phụ thuộc 5px/13px mong manh). Mirror ×2 app SHA-pair mới 7baf6247af3d. Lệch tiêu-chí-CHỮ của gate ("cuộn tới đáy cây không cần cuộn aside ngoài") — đạt tiêu-chí-Ý (1 thanh cuộn, không lồng): chọn panel-cuộn thay cây-tự-cuộn vì bền cấu-trúc, đầu cây + ô lọc trôi theo là trade-off khai |
| F-2 MINOR | ✅ VÁ (comment) | Chú thích tại 3 chỗ cắm: sticky/self-start vô hiệu ở host display:block — by-design |
| F-3 MINOR | 🔸 CHẤP NHẬN-khai | aside-lồng-aside 2 landmark complementary: hợp lệ HTML, nhiễu screen-reader — không đổi khuôn lượt này (đổi root khuôn = đụng 4 host); ghi nợ a11y cùng lô a11y-<a>-label K5 |
| F-4 MINOR | 🔸 CHẤP NHẬN | !canPe than-phiền-quyền thay chỉ-dẫn: prod 13/13 vai có PE.Read (gate đo + control-âm) — rủi ro tương lai, không chảy máu |
| F-5 MINOR | 🔸 CHẤP NHẬN | +tối đa 3 query index lượt tải đầu (staleTime 60s, enabled canPe) — giá của D |
| F-6 MINOR | 🔸 CHẤP NHẬN-khai | chọn HĐ → cây unmount → bỏ chọn → ô lọc mất chữ (state trong component) — trade-off D "cây nhảy chỗ" đã khai với owner |
| F-7 MINOR | 🔸 CHẤP NHẬN | mobile: panel-3 hidden dưới lg = khuôn 3-panel by-design (mobile đi fullpage route) |
| F-8 INFO | ✓ (gate phán lead ĐÚNG) | WorkflowMatrixView không áp D |
| F-9 INFO | ✓ (gate phán KHÔNG mâu-thuẫn K6) | fe-admin /contracts ẩn sidebar — route sống, tiền lệ HardCopiesPage |
| F-10 INFO | 🔸 GIỮ currentStage={3} |
/inbox trộn GĐ1+GĐ3 nhưng trang chủ-đích là HĐ Pending; PE item navigate sang trang PE riêng |
Vá F-1/F-2 nằm trong commit GĐ3-D (build+test re-verify sau vá). 6 finding chấp-nhận đi vào UAT-NÓI-TRƯỚC.