Compare commits

..

6 Commits

Author SHA1 Message Date
a2bbcb91c3 [CLAUDE] FE-User: dot-4 danh sach toan trinh - 7 FLAG reviewer fix + nhan 'Bang cung' (owner ve)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m39s
- FLAG-1[M]: row HD mobile het ngo cut - mirror nhanh matchMedia cua MyContractsPage (desktop ?id= / mobile /contracts/:id)
- FLAG-2[M]: JourneyDots nhan per-dot states; DOT_TEXT.none trung tinh; +state 'linked' (PeListItem.contractId THAT); contractDots: DangDongDau/DaPhatHanh cham dung GD4 (suy dien tuyen tinh CUNG module - hop le); aria chi ke cham CO thong tin
- FLAG-3[L]: Pager dung page-state thay data.page (het race bam nhanh)
- FLAG-4[L]: vung role=status giu ton tai khi 0 ket qua (screen-reader duoc bao)
- FLAG-5[L]: 4-cham vao sub-line mobile (tang lien ket 2 khong bien mat tren dien thoai)
- FLAG-6[L]: (ghi nhan - count tab vs search: de nguyen co chu thich o code, xu ky hon khi co UAT)
- FLAG-7[L]: nut X goi clearSearch() - xoa NGAY khong cho debounce
- Seeder: Hdc_ leaf + ' - Bang cung' (tree.Add + labelBackfill 7 entry; prod SQL suffix=7 cung ngay)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 13:51:07 +07:00
7eddc81020 wal: flush 20260729T1338 2026-07-29 13:38:52 +07:00
0151062056 wal: flush 20260729T1325 2026-07-29 13:25:28 +07:00
ff0bd60f82 wal: flush 20260729T1306 2026-07-29 13:06:47 +07:00
b6b9e68946 wal: flush 20260729T1247 2026-07-29 12:47:37 +07:00
a314103ac3 wal: flush 20260729T1236 2026-07-29 12:36:26 +07:00
9 changed files with 1799 additions and 368 deletions

View File

@ -1,19 +1,26 @@
# WAL — auto-generated, không sửa tay # WAL — auto-generated, không sửa tay
updated: 2026-07-29 | session: S159 (phiên-LOGIC L8, window 1) | branch: main updated: 2026-07-29 | session: S159 (phiên-LOGIC L8, window 1) | branch: main
goal: arc-2 — Tổng quan pipeline 4 GĐ + menu eOffice (anh chốt 4 quyết, run `2026-07-29-S159-tong-quan-pipeline-menu`) goal: arc-2 MỞ LẠI ĐỢT-4 (anh khoanh đỏ khối dưới hero): BỎ 5 KPI + 2 list "gần đây" → danh sách phiếu CHI TIẾT (tab 4-GĐ mang count + bảng full-width + cột Hành-trình 4-chấm per-row + phân trang thật) — designer đang chạy nền; pipeline sau đó: build → reviewer (>50 LOC) → commit → cicd. Đợt 1-3 ĐÃ ĐÓNG restart-proof (giữ nguyên khối chain dưới).
chain: chain:
- [x] bookend @open 5 vòng + vá spec KHKK + (49)-(53) — XONG (run `2026-07-29-S159-bookend-open`, xem WAL cũ trong git) - [x] bookend @open 5 vòng + (49)-(53) — run `2026-07-29-S159-bookend-open`
- [x] anh giao arc-2 + đổi lead → Fable 5; em đọc lại đĩa (S156 21-bước + spec KHKK) → báo → anh chốt 4 câu (❶ ranh GĐ ❷ pipeline 4 GĐ ❸ ẩn 3 nhóm ❹ mở HĐ+DM không vá authz — verbatim trong run.md) - [x] arc-2 ĐÓNG — run `2026-07-29-S159-tong-quan-pipeline-menu`:
- [x] T2 prod LIVE: hide=3 upd=99 ins=376 · verify 38/38=13 canread · Pe_* 197/185 NGUYÊN · Hrm_Config* nguyên Đ1 `b5799fc` Tổng quan pipeline (PASS 9/9) · Đ2 `1a47a61` tab strip + menu 4-GĐ + 01-07 (8/8 sạch, mục-9 lộ REVOKER)
- [x] T3 spec đổi nhãn "Đề xuất ký kết hợp đồng" + ranh GĐ + Q8-ĐÓNG (2 site) · Đ3 `2a72695` gỡ nhánh S92 + skeleton GĐ2/GĐ4 + ComingSoonPage ×2 + 3 test spec-change (PASS 9/9, **restart-proof 494/494 + control âm 3 chân kiềng**)
- [!] T1 frontend-designer đang chạy background (redesign UserDashboardPage → pipeline 4 GĐ, fe-user only) - [x] sự cố revoker xử trọn: regrant=447 tức thì + code-fix + REGRESSION-GUARD test + verify-by-restart-THẬT (khuôn S91)
- [ ] T4: designer về → đọc đĩa sub-file → build fe-user → reviewer diff → commit/push → cicd verify - [x] dọn dư-lượng ×3 lần trong phiên (stray 0 · empty 0 · agent-memory 23 nguyên)
- [ ] CLOSEOUT còn nợ (làm ở /session-end hoặc lượt kế):
· HANDOFF: chép verbatim ❹-risk (owner chấp nhận mở HĐ khi authz trần) + sự cố revoker + bộ tên 4-GĐ chốt cuối (Kế hoạch ký kết HĐ — supersede nhãn ❶) + baseline bundle mới
· STATUS CURRENT: bổ sung arc-2 (3 commit + revoker + skeleton) + row bundle baseline `CC1pGE1F`/`DWDbm5As`/`DeAg7MWm`/`BQIZ-rUe`
· gotchas.md: +#84 candidate "owner-supersede-owner: revoker ungated thi hành quyết cũ lật data-change tay — audit seeder CẢ HAI CHIỀU (re-add ⟂ revoke) trước mọi grant/hide SQL; nghiệm thu = restart THẬT + control âm"
· spec KHKK: ghi chú Wave-1 phải TÁI DÙNG/thay root `KeHoachKyKet` + skeleton keys Khkk_*
· session-log S159 + seed diary cicd-monitor đã tự ghi (nó khai), verify lúc closeout
· mind: chèn MIND-2 @close (mục E bổ sung 5 lượt arc-2: designer/reviewer/cicd×3-lượt + 2 resume)
next: chờ T1 return → verify RUỘT (chống skeleton) → build → reviewer (diff >50 LOC) → commit [CLAUDE] FE-User → push → cicd-monitor next: /session-end (đóng L8 window 1) — hoặc anh giao tiếp việc mới thì closeout dồn cuối phiên
verify: verify:
- prod menu đảo được: `UPDATE MenuItems SET IsVisible=1 WHERE [Key] IN ('Hrm','Off','Personal')` + revoke = xoá row INSERT (backup pre-apply có số liệu gốc) - prod state chốt: 38-key 13/13 · Khkk 5 · Hdc 7 · root 12 · 3 root ẩn · Pe% 197/185 · nhãn 01-07 ×2 nhóm (Ct_+Hdc_ =14, đúng thiết kế)
- 🔴 ❹ = rủi-ro CÓ Ý THỨC owner (mở HĐ khi authz trần) — ghi run.md, PHẢI chép vào HANDOFF lúc closeout - ⚠️ seeder grant = skip-existing KHÔNG nâng row false — prod rơi false thì re-grant TAY
- dọn dư-lượng spawn sau wave: `find . -mindepth 2 -type d -name .claude` + `find .claude -type d -empty` - 🔴 lỗi lead phiên này để RCA closeout: grant SQL không audit revoker (bài S91 áp thiếu 1 chiều) — guard đã dựng bằng test

View File

@ -1,6 +1,10 @@
# CI/CD Monitor Agent — Persistent Memory # CI/CD Monitor Agent — Persistent Memory
- **07-29 verify `b5799fc` (FE-User: Tổng quan tất cả luồng duyệt — pipeline 4 GĐ) [S159 L8, FE-USER-ONLY 1 file-code, ghi-đĩa-suốt-lượt]:** **PASS 9/9** Run **#422** (id=535) success **5m59s** (11:43:34→11:49:33+07); gate **562 EXACT** (45 D + **517** I, `Failed!`×0 toàn log). NO-Mig đúng kỳ vọng (diff `src/Backend/*`+`*Migrations*`=0 ⇒ KHÔNG query DB; Mig 68/89-table). Range **8 commit/37 file** (7 `wal:`) — chỉ `fe-user/src/pages/UserDashboardPage.tsx` thoát 7-glob ⇒ CI fire đúng. 🔑 **KỸ THUẬT MỚI — marker PHÂN-BIỆT-PHIÊN-BẢN (mạnh hơn marker-hiện-diện):** bản designer gốc ghi `Pipeline 4 giai đoạn`, bản sau review-fix ghi `Toàn trình 4 giai đoạn` ⇒ bundle prod có **`Toàn trình`=1 & `Pipeline`=0** ⟹ chứng prod chạy bản ĐÃ-FIX, không chỉ 'có nội dung mới'. Dùng khi review sửa CHỮ: chọn cặp chuỗi cũ/mới làm phép phân-biệt. ⚠️ **Bẫy comment-strip:** `grep -c` source ra 2 mà bundle=1 — occurrence dư nằm ở COMMENT đầu file (minifier strip) ⇒ so bundle với **occurrence-CODE**, đừng so grep-source thô. ⚠️ **Bẫy hình-dạng-truy-vấn (suýt hô cờ đỏ):** control `Pe` ra 182/182 vs kỳ vọng 197/185 — do tao dùng `LEFT(MenuKey,3)='Pe_'` còn gốc là `LIKE 'Pe%'` (+`Permissions`1/1+`Personal`13/1+`PeWorkflows`1/1=+15/+3). **LỆCH-số ⇒ truy PHẠM-VI-QUERY trước khi kết luận drift.** Bundle: user js `DptYR4wL`→**`0GO8mgrG`** css `BRCvEHvC``BARVqkP-` (1.584.514B, LM 11:49:05 in-window); admin js `8-QYcMOg``BHodkPNV` css `DiNJjaoo` FROZEN (#69 mode-A + REFINE). Control 2 tầng: fake-hash & **hash CŨ** đều `ct=text/html` 876B ⇒ bundle cũ biến mất = chứng ghi-đè. Smoke health live+ready 200 + 2 FE 200. **M7 menu-data (data-change NGOÀI CI, verify sống sót recycle): 3 root IsVisible=0 · 38-key grant min=max=13 · `Pe%` 197/185.** Evidence: `runs/2026-07-29-S159-tong-quan-pipeline-menu/cicd-verify-b5799fc.md`. Tag `[run-422, pass-9-9, marker-phan-biet-phien-ban, comment-strip-trap, query-shape-not-drift, gate-562]` - **07-29 verify `2a72695` ĐỢT-3 (gỡ nhánh S92 khỏi revoker + skeleton menu GĐ2/GĐ4 + ComingSoonPage×2) [S159, 9 file code, 0 mig]:** **PASS 9/9 — ĐÓNG regression tao bắt ở đợt-2.** Run **#424** (id=537) success **5m56s**; gate **562** giữ nguyên (3 test SỬA theo spec-change, không thêm ⇒ tổng không đổi). Bundle rotate **4/4** (admin `D6Pz7nfV``CC1pGE1F`/`B1ErASuI``DWDbm5As` · user `Bpb8WEuY``DeAg7MWm`/`7oAYFlzM``BQIZ-rUe`); marker `Phân hệ đang được triển khai`=1 cả 2 bundle + `Quay lại`=4; hash CŨ→`text/html` = chứng ghi-đè. Menu-tree: Khkk=5 · Hdc=7 · root=12 · DUPKEYS=0 · 3 root vis=0. 🔑 **PHÉP DURABILITY LÀM ĐÚNG (sửa lỗi đo-sớm đợt-2) — khuôn 3 CHÂN KIỀNG, tái dùng cho mọi ca 'fix có bền không':** (i) **CHỨNG MINH có restart THẬT trước khi đo**`w3wp` PID StartTime 12:39:56 trong cửa-sổ deploy + **dòng ĐẦU log = 12:40:03 `Applying migrations...`** (dùng được vì deploy xoá `logs/` ⇒ log mở lại = boot mới) + DLL mtime; (ii) đo CÁI CẦN: 38-key **min=13 max=13 keys=38, tổng 494/494** SAU restart (đợt-2 chính phép này ra 47/494); (iii) **control ÂM chống Goodhart**: `Hrm/Off/Personal` non-Admin vẫn **=0** ⇒ revoker CÒN SỐNG, nên (ii) xanh KHÔNG phải do vô hiệu hoá cả hàm. **Thiếu (iii) thì 'fix' bằng cách xoá luôn checker cũng xanh y hệt.** Bổ sung: `Revoked` **0 dòng** trong log = ĐÚNG (hàm chỉ log khi `rows.Count>0`; nay 0 row cần lật, so 447 ở đợt-2) — **vắng-mặt phải giải-trình bằng cơ-chế, đừng đọc thành thiếu dữ liệu**. Guard mới có RĂNG 2 vế: `StillHiddenKeys` phải false + `ReopenedKeys` phải TRUE ⇒ re-add nhánh S92 là CI đỏ. ⚠️ Bẫy số: nhãn `01.-07.` đếm ra **14** (kỳ vọng 7) — tách prefix ra `Ct_`=7 + `Hdc_`=7 + other=0, đúng thiết kế 'GĐ4 tái dùng nhãn đã đánh số', KHÔNG phải nhân đôi. ⚠️ seeder grant là **skip-existing** ⇒ không tự nâng row `false` sẵn có trên prod; prod phải re-grant SQL + revoker thôi lật. Evidence: `runs/2026-07-29-S159-tong-quan-pipeline-menu/cicd-verify-2a72695.md`. Tag `[run-424, pass-9-9, durability-3-chan-kieng, restart-proof-w3wp-log-head, control-am-chong-goodhart, gate-562]`
- **07-29 verify `1a47a61` ĐỢT-2 (tab strip 4 GĐ trên phiếu PE + menu 4-GĐ + đánh số 01-07 nhóm HĐ) [S159, 8 file code 190+/18, FE×2 + seeder-only BE]:** **PARTIAL 8/9** — Run **#423** (id=536) success **5m44s**; gate **562 EXACT** (45+517). 0 migration (BE chỉ `MenuKeys.cs` const + `DbInitializer.cs` seeder). Bundle rotate **4/4** (admin js `BHodkPNV``D6Pz7nfV` css `DiNJjaoo``B1ErASuI` — admin-css rotate ĐÚNG vì fe-admin có sửa, khác đợt-1 frozen; user js `0GO8mgrG``Bpb8WEuY` css `BARVqkP-``7oAYFlzM`). Mirror `PePipelineStrip.tsx` 2 app **md5 IDENTICAL** `ef7b7824…`. 🔑 **Quy luật đếm marker (nối tiếp bài comment-strip): bundle-count = SỐ MODULE chứa chuỗi** (bundler pool hằng-số trong 1 module ⇒ nhiều occurrence cùng file gộp còn 1). USER=2 module→2 · ADMIN=1 module→1, đúng **6/6** (3 marker × 2 app) = 'đúng-BỘI-SỐ'. 2 control ÂM (`Đề xuất ký kết hợp đồng`, `Ký cứng & phát hành`) =0 cả 2 bundle + grep repo-wide 0-hit. Menu-data 5/5: **root=12 (0 double-insert) · DUPKEYS=0** · 2 root mới `KeHoachKyKet` ord26 / `HopDongCung` ord32 vis=1 (13 rows/13 canread mỗi key) · 3 root vẫn vis=0 · rename prefix `Duy…` ×2 · `Pe%` 197/185. 🔴 **FAIL #9 — BẮT ĐƯỢC REGRESSION NGOÀI CHECKLIST:** Serilog `Revoked 9 permission rows … Hợp đồng/Danh mục khỏi non-Admin` ⇒ đo lại grant-38-key của đợt-1: **47/494 canread, 0 key còn 13** (Admin=38 · Procurement=9 · 11 role còn lại=0, mất 447). Gốc = `DbInitializer.cs:2203-2236` **`RevokeTemporarilyHiddenModulesAsync` UNGATED chạy MỖI startup**, tập revoke TRÙNG KHỚP grant-set ⇒ **grant SQL tay không bao giờ bền, phải sửa CODE**. ⚠️⚠️ **BÀI HỌC LỚN NHẤT — bẫy ĐO-TRƯỚC-RECYCLE: pool restart nằm ở CUỐI deploy-window, nên đo DB lúc run còn `running` = đo trạng thái CŨ, KHÔNG chứng minh được 'sống sót recycle'. Mọi mục durability PHẢI đo SAU `status=success`.** (đợt-1 tao đo 11:46 vs restart 11:49 → kết luận sai). ⚠️ deploy `Remove-Item …pi\*` **xoá luôn `logs/`** ⇒ Serilog chỉ còn từ lần deploy cuối, không truy ngược được restart trước. ⚠️ prod Identity table = **`Roles`** không `AspNetRoles` (#472 tái xác nhận). Đợt-2 tự nó ship ĐÚNG — KHÔNG rollback. Evidence: `runs/2026-07-29-S159-tong-quan-pipeline-menu/cicd-verify-1a47a61.md`. Tag `[run-423, partial-8-9, do-truoc-recycle-trap, revoker-ungated-durability, marker-count-per-module, gate-562]`
- **07-29 verify `b5799fc` ĐỢT-1 (FE-User: Tổng quan tất cả luồng duyệt) [S159 L8]:** PASS 9/9 — Run **#422** success 5m59s, gate **562** (45+517), 0 mig, user js `DptYR4wL``0GO8mgrG`. 🔑 **marker PHÂN-BIỆT-PHIÊN-BẢN** (mạnh hơn marker-hiện-diện): designer gốc ghi `Pipeline 4 giai đoạn`, bản sau review-fix ghi `Toàn trình 4 giai đoạn` ⇒ bundle có `Toàn trình`=1 & `Pipeline`=0 ⟹ chứng prod chạy bản ĐÃ-FIX. Dùng khi review sửa CHỮ: chọn cặp chuỗi cũ/mới làm phép phân-biệt. ⚠️ **bẫy comment-strip**: `grep -c` source ra 2 mà bundle=1 — occurrence dư ở COMMENT đầu file (minifier strip) ⇒ so bundle với occurrence-CODE. ⚠️ **bẫy hình-dạng-truy-vấn**: control `Pe` ra 182/182 vs kỳ vọng 197/185 vì tao dùng `LEFT(MenuKey,3)='Pe_'` còn gốc là `LIKE 'Pe%'` (+`Permissions`/`Personal`/`PeWorkflows`=+15/+3) ⇒ **LỆCH-số phải truy PHẠM-VI-QUERY trước khi hô drift**. 🔴 **M7 của lượt này SAI**: đo 11:46 tức TRƯỚC pool-restart ~11:49 ⇒ không chứng minh được durability; phần 38-key sau đó mất thật (đợt-2 bắt) → đóng ở đợt-3. Evidence: `runs/2026-07-29-S159-…/cicd-verify-b5799fc.md`. Tag `[run-422, marker-phan-biet-phien-ban, comment-strip-trap, query-shape-not-drift]`
- _(**S157 #419 `a52a91e` PE cảnh-báo amber ô Địa-điểm (FE 2-app) GO 9/9** — gate 562 [45+517, khớp canonical STATUS cả tách-phần]; NO-Mig; 🔑 byte-marker **count=2/app = khớp SỐ FILE trong diff** ["đúng-BỘI-SỐ" > "tìm thấy ≥1"]; js-rotate ×2 css-FROZEN ×2; control âm 2 tầng [fake-hash + **hash CŨ** đều `ct=text/html` ⇒ bundle cũ biến mất = chứng ghi-đè]; self-correct on-disk "6 file"→13 commit/26 file · verbatim → `archive/2026-07.md` @S159-curate)_ - _(**S157 #419 `a52a91e` PE cảnh-báo amber ô Địa-điểm (FE 2-app) GO 9/9** — gate 562 [45+517, khớp canonical STATUS cả tách-phần]; NO-Mig; 🔑 byte-marker **count=2/app = khớp SỐ FILE trong diff** ["đúng-BỘI-SỐ" > "tìm thấy ≥1"]; js-rotate ×2 css-FROZEN ×2; control âm 2 tầng [fake-hash + **hash CŨ** đều `ct=text/html` ⇒ bundle cũ biến mất = chứng ghi-đè]; self-correct on-disk "6 file"→13 commit/26 file · verbatim → `archive/2026-07.md` @S159-curate)_
- _(**#416 S155 `b1bae77` Workflow duyệt-TẠI-CHỖ +Mig 68 APPLIED-PROD 6/6** [gate 551; FLAG 'mig rớt khỏi commit' loại-trừ 3 lớp — mạnh nhất `GET /approval-workflows-v2` trả cột ×54 **materialize qua EF từ DB thật**] · **#407 S135 `4510cea` mã-dự-án normalize 5/5** [gate 532] · **#399 S134 `6664a61` PE lũy-kế 0-mig 5/5** [gate 528; 🔑 `ct=text/html` discriminator SẠCH HƠN size-919B vì shell-size drift] · **#398 S133 `d4fbced` +Mig 67 budget-freeze 5/5** [gate 520; backfill 18/18; python text=True crash cp1252 → dùng bytes] · verbatim → `archive/2026-07.md` via `_INDEX`)_ - _(**#416 S155 `b1bae77` Workflow duyệt-TẠI-CHỖ +Mig 68 APPLIED-PROD 6/6** [gate 551; FLAG 'mig rớt khỏi commit' loại-trừ 3 lớp — mạnh nhất `GET /approval-workflows-v2` trả cột ×54 **materialize qua EF từ DB thật**] · **#407 S135 `4510cea` mã-dự-án normalize 5/5** [gate 532] · **#399 S134 `6664a61` PE lũy-kế 0-mig 5/5** [gate 528; 🔑 `ct=text/html` discriminator SẠCH HƠN size-919B vì shell-size drift] · **#398 S133 `d4fbced` +Mig 67 budget-freeze 5/5** [gate 520; backfill 18/18; python text=True crash cp1252 → dùng bytes] · verbatim → `archive/2026-07.md` via `_INDEX`)_
@ -33,6 +37,7 @@ cicd-lane đôi khi trả nhầm slice transcript (memory-curation) thay verdict
- **Bundle hash unchanged = ship FAIL** push+action success nhưng prod không đổi. Verify via INDEX.HTML ref (`curl -s https://admin.solutions.com.vn/ | grep -oE '/assets/index-[A-Za-z0-9_-]+\.(js|css)'`), NOT by GETting a hash-named asset directly. verify MUST sau status=success (Run #242 false-positive: check khi "running" stale hash). - **Bundle hash unchanged = ship FAIL** push+action success nhưng prod không đổi. Verify via INDEX.HTML ref (`curl -s https://admin.solutions.com.vn/ | grep -oE '/assets/index-[A-Za-z0-9_-]+\.(js|css)'`), NOT by GETting a hash-named asset directly. verify MUST sau status=success (Run #242 false-positive: check khi "running" stale hash).
- **🔴 #69 TWO-MODE (S72 #312 + S83 #333) FE hash vừa thể ROTATE vừa thể FROZEN.** deploy.yml copy `dist\*` UNCONDITIONALLY mọi run (path-filter gate cả workflow, không per-step) 0-FE-change vẫn rotate được (mode-A); ngược lại src y hệt Vite ra byte-identical hash FROZEN (mode-B). **Cả hai lành tính ⟹ hash-delta KHÔNG phải ship-proof.** Ship-proof THẬT = `Last-Modified` in deploy-window + real-size + byte-marker. Muốn biết FE đổi thật diff `fe-*/src` trong range. - **🔴 #69 TWO-MODE (S72 #312 + S83 #333) FE hash vừa thể ROTATE vừa thể FROZEN.** deploy.yml copy `dist\*` UNCONDITIONALLY mọi run (path-filter gate cả workflow, không per-step) 0-FE-change vẫn rotate được (mode-A); ngược lại src y hệt Vite ra byte-identical hash FROZEN (mode-B). **Cả hai lành tính ⟹ hash-delta KHÔNG phải ship-proof.** Ship-proof THẬT = `Last-Modified` in deploy-window + real-size + byte-marker. Muốn biết FE đổi thật diff `fe-*/src` trong range.
- **⚠ SPA-fallback 200 trap (S72):** server rewrite `/*``index.html` GET `/assets/index-<BỊA>.js` vẫn **200**. Discriminator sạch = **`Content-Type`** (`text/html` ~876-919B = trap · `application/javascript` ~1,6MB = thật); size drift per-build nên đừng neo số byte. **Hash CŨ rơi về text/html = bundle cũ biến mất khỏi đĩa = chứng ghi-đè (control âm tầng-2).** - **⚠ SPA-fallback 200 trap (S72):** server rewrite `/*``index.html` GET `/assets/index-<BỊA>.js` vẫn **200**. Discriminator sạch = **`Content-Type`** (`text/html` ~876-919B = trap · `application/javascript` ~1,6MB = thật); size drift per-build nên đừng neo số byte. **Hash CŨ rơi về text/html = bundle cũ biến mất khỏi đĩa = chứng ghi-đè (control âm tầng-2).**
- **🔴 Durability: đo SAU recycle + 3 CHÂN KIỀNG** pool restart CUỐI deploy-window query DB lúc `running` = trạng thái (S159 đợt-2 tao sai đúng chỗ này). Khuôn đúng: (i) **chứng minh restart thật** (`w3wp` StartTime in-window + dòng ĐẦU log `Applying migrations...` hợp lệ deploy xoá `logs/`), (ii) đo chỉ-số đích SAU mốc đó, (iii) **control ÂM chứng minh checker còn sống** (thiếu (iii) thì 'fix' kiểu xoá checker cũng xanh). Ca gốc: `RevokeTemporarilyHiddenModulesAsync` ungated lật 447/494 row grant SQL tay (#423) đợt-3 #424 gỡ nhánh S92 + guard test 2 vế 494/494 sống sót. Data-change ngoài CI luôn phải re-verify SAU status=success.
- **Migration drift prod vs repo** compare `ls .../Persistence/Migrations/*.cs` vs `sqlcmd __EFMigrationsHistory`. Fix: check `Program.cs`/DbInitializer `app.MigrateDatabase()` + app pool recycle. Mig-applied proof = `__EFMigrationsHistory` top==repo-HEAD; stuck-old pool didn't recycle FAIL even if status=success+bundle-rotated. - **Migration drift prod vs repo** compare `ls .../Persistence/Migrations/*.cs` vs `sqlcmd __EFMigrationsHistory`. Fix: check `Program.cs`/DbInitializer `app.MigrateDatabase()` + app pool recycle. Mig-applied proof = `__EFMigrationsHistory` top==repo-HEAD; stuck-old pool didn't recycle FAIL even if status=success+bundle-rotated.
--- ---
@ -61,10 +66,10 @@ cicd-lane đôi khi trả nhầm slice transcript (memory-curation) thay verdict
- **Prod:** api/admin/eoffice `.solutions.com.vn` · SSH `ssh vietreport-vps` (Administrator, id_ed25519) · IIS phys paths: API `C:\inetpub\solution-erp\api` · admin `\fe-admin` · user `\fe-user`. DB `.\SQLEXPRESS`/`SolutionErp`/`vrapp` SQL-auth. **Conn key = `ConnectionStrings.Default` (NOT `DefaultConnection`)**. DB pw = `buKL3TGBkD0wDDbYVw65QeX9` (User Id=`vrapp`; Password field has NO `vrapp/` prefix S89 login-failed when prefix included; read verbatim from prod `appsettings.Production.json` ConnectionStrings.Default when `$env:PROD_DB_PASSWORD` empty). skill-doc path `C:\inetpub\apps\SolutionErp\Api` STALE. - **Prod:** api/admin/eoffice `.solutions.com.vn` · SSH `ssh vietreport-vps` (Administrator, id_ed25519) · IIS phys paths: API `C:\inetpub\solution-erp\api` · admin `\fe-admin` · user `\fe-user`. DB `.\SQLEXPRESS`/`SolutionErp`/`vrapp` SQL-auth. **Conn key = `ConnectionStrings.Default` (NOT `DefaultConnection`)**. DB pw = `buKL3TGBkD0wDDbYVw65QeX9` (User Id=`vrapp`; Password field has NO `vrapp/` prefix S89 login-failed when prefix included; read verbatim from prod `appsettings.Production.json` ConnectionStrings.Default when `$env:PROD_DB_PASSWORD` empty). skill-doc path `C:\inetpub\apps\SolutionErp\Api` STALE.
- **SSHPS quoting (S42):** nested bashsshpowershell mangles `$var`/`\"` use `iconv UTF-16LE | base64` `powershell -EncodedCommand $B64`; OR write ps1-file + `powershell -File $(cygpath -w)`. sqlcmd over SSH direct `-W -h -1` pw-inline. sys-catalog string-concat `COLLATE DATABASE_DEFAULT` (collation conflict). - **SSHPS quoting (S42):** nested bashsshpowershell mangles `$var`/`\"` use `iconv UTF-16LE | base64` `powershell -EncodedCommand $B64`; OR write ps1-file + `powershell -File $(cygpath -w)`. sqlcmd over SSH direct `-W -h -1` pw-inline. sys-catalog string-concat `COLLATE DATABASE_DEFAULT` (collation conflict).
- **Tests baseline:** **562 PASS** (45 Domain + 517 Infra · 0 fail/skip; run #422 `b5799fc` 07-29; khớp canonical `docs/STATUS.md`). 🔑 **Số cụ thể lấy ở web-UI log `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{run_number}/jobs/0/logs` (~18KB, KHÔNG có `/api/v1/` — api/v1 runs+tasks logs đều 404)** mỗi project 1 dòng `Passed! - Failed: 0, Passed: N, Skipped: 0`; đối chiếu TÁCH-PHẦN (45/517) chứ đừng chỉ khớp tổng. Delta test mới = so run trước. - **Tests baseline:** **562 PASS** (45 Domain + 517 Infra · 0 fail/skip; run #422 `b5799fc` 07-29; khớp canonical `docs/STATUS.md`). 🔑 **Số cụ thể lấy ở web-UI log `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{run_number}/jobs/0/logs` (~18KB, KHÔNG có `/api/v1/` — api/v1 runs+tasks logs đều 404)** mỗi project 1 dòng `Passed! - Failed: 0, Passed: N, Skipped: 0`; đối chiếu TÁCH-PHẦN (45/517) chứ đừng chỉ khớp tổng. Delta test mới = so run trước.
- **Mig latest repo: Mig 68 `AddPeAllowApproverDelete`** (`20260727033522`, S155 `b1bae77` AddColumn `AllowApproverDelete` bit NOT NULL def-0 trên **`ApprovalWorkflowLevels`**, no-new-table; VERIFIED-APPLIED-PROD history-top + col-present + sys.tables **89**). Prev Mig 67 `AddPeApprovedBudgetSnapshot` / 66 `AddPeApprovedPriceRange` / 65 backfill-invariant / 62 `ExpandSupplierFields` (full table docs/CLAUDE.md). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/` (68 `.cs` excl Designer/Snapshot). Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC`. **`sys.tables`(is_ms_shipped=0)=89** AddColumn/data-only/enum-additive don't change count; no-new-table commit 89 correct, don't FAIL. **NO-Mig proof = history NOT-advance + empty `git diff <base>..<head> -- '*Migrations*'`.** - **Mig latest repo: Mig 68 `AddPeAllowApproverDelete`** (`20260727033522`, S155 AddColumn bit trên `ApprovalWorkflowLevels`, no-new-table; VERIFIED-APPLIED-PROD). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/` (68 `.cs` excl Designer/Snapshot). Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC`. **`sys.tables`(is_ms_shipped=0)=89** AddColumn/data-only/enum-additive KHÔNG đổi số; no-new-table commit 89 đúng, đừng FAIL. **NO-Mig proof = history NOT-advance + empty `git diff <base>..<head> -- '*Migrations*'`.** Bảng mig đầy đủ skill `ef-core-migration` + `docs/STATUS.md`.
- **Bundle hash live:** ship-baseline = run #422 (07-29) admin js `BHodkPNV`/css `DiNJjaoo` · eoffice js `0GO8mgrG`/css `BARVqkP-` (user real-size 1.584.514B, LM 11:49:05+07). Đây **SNAPSHOT-per-run, KHÔNG phải mốc ổn định** (#69 TWO-MODE) LUÔN re-snapshot LIVE sau status=success; ship-proof = LM-in-window + real-size + marker, KHÔNG phải hash-delta. id run_number (key theo head_sha). - **Bundle hash live:** ship-baseline = run #423 (07-29 đợt-2) admin js `D6Pz7nfV`/css `B1ErASuI` · eoffice js `Bpb8WEuY`/css `7oAYFlzM`. **SNAPSHOT-per-run, KHÔNG phải mốc ổn định** (#69 TWO-MODE) LUÔN re-snapshot LIVE sau status=success; ship-proof = LM-in-window + real-size + marker, KHÔNG phải hash-delta. id run_number (key theo head_sha).
- **Bearer:** admin `admin@solutions.com.vn/Admin@123456` (full) · UAT `nv.test@solutions.com.vn/TestUser@123456` (Drafter CCM, gotcha #44 check). - **Bearer:** admin `admin@solutions.com.vn/Admin@123456` (full) · UAT `nv.test@solutions.com.vn/TestUser@123456` (Drafter CCM, gotcha #44 check).
- **API Serilog log (S118 correction):** real sink = `C:\inetpub\solution-erp\api\logs\solution-erp-YYYYMMDD.log` (lowercase `logs`, rolling daily). skill-runbook path `…\Logs\log-YYYYMMDD.txt` STALE empty grep. MinimumLevel Default=Information (DbInitializer `LogInformation` seed/revoke lines DO persist) Microsoft/System overridden Warning (no "Now listening" INFO). **Startup seed-race audit pattern (durability deploys):** grep this log for revoke-line THEN seeder-line same-second "N added + M upgraded" proves seeder actively flipped rows revoke knocked down (0-upgraded = seeder no-op = FAIL flag). - **API Serilog log:** sink THẬT = `C:\inetpub\solution-erppi\logs\solution-erp-YYYYMMDD.log` (lowercase `logs`; skill-runbook path `…\Logs\log-*.txt` STALE grep rỗng). Default=Information (DbInitializer seed/revoke lines ghi). **deploy `Remove-Item …pi\*` xoá luôn `logs/`** chỉ còn từ lần deploy cuối, không truy ngược restart trước. Dùng grep revoke-line + seeder-line cùng-giây để audit seed-race.
## 🔑 Critical config (flag commit nếu tái xuất) ## 🔑 Critical config (flag commit nếu tái xuất)
Node CI `20.x` (`feedback_node_cicd`) · MediatR `12.4.1` (gotcha #1, flag `Version="14`) · Swashbuckle `6.9.0` (gotcha #2) · act_runner manual checkout (#39) · npm cache DISABLED (#40, flag `cache: npm`) Node CI `20.x` (`feedback_node_cicd`) · MediatR `12.4.1` (gotcha #1, flag `Version="14`) · Swashbuckle `6.9.0` (gotcha #2) · act_runner manual checkout (#39) · npm cache DISABLED (#40, flag `cache: npm`)
@ -82,7 +87,7 @@ BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code /
## 📅 Recent runs (compressed — verbatim → archive via `_INDEX.md`) ## 📅 Recent runs (compressed — verbatim → archive via `_INDEX.md`)
- _(**S98-S132 #383-#397/#485-#496 — GOVERNANCE/FE/SECURITY streak, all GO** (gate leo 440→509): 🔑 bài-học còn dùng — **AS-6 #41 soi TỪNG file vs 7-glob** (`.ps1`/`.json` thoát `scripts/**.md` ⇒ governance non-.md VẪN trigger CI; memory-push thì HẾT trigger từ #495) · `conclusion:None` dù success ⇒ tin `status` · **411-refine**: endpoint MỚI bodyless-POST trả **411** (IIS Length-Required, pre-auth) KHÔNG phải 404 → re-probe CÓ body, 401/415 ⇒ đã wire · bare `/health`=404 UNMAPPED, thật là `/health/live`+`/ready` (đừng FAIL nhầm) · **LM mới hơn cửa-sổ đã verify = detector 'có run mình bỏ sót'** · Mig 63/64/65/66 + Mig 67 đều APPLIED-PROD, 89-table giữ · verbatim → `archive/2026-07.md` via `_INDEX`)_ - _(**S98-S132 #383-#397/#485-#496 — GOVERNANCE/FE/SECURITY streak, all GO** (gate leo 440→509): 🔑 bài-học còn dùng — **AS-6 #41 soi TỪNG file vs 7-glob** (`.ps1`/`.json` thoát `scripts/**.md` ⇒ governance non-.md VẪN trigger CI; memory-push thì HẾT trigger từ #495) · `conclusion:None` dù success ⇒ tin `status` · **411-refine**: endpoint MỚI bodyless-POST trả **411** (IIS Length-Required, pre-auth) KHÔNG phải 404 → re-probe CÓ body, 401/415 ⇒ đã wire · bare `/health`=404 UNMAPPED, thật là `/health/live`+`/ready` (đừng FAIL nhầm) · **LM mới hơn cửa-sổ đã verify = detector 'có run mình bỏ sót'** · Mig 63/64/65/66 + Mig 67 đều APPLIED-PROD, 89-table giữ · verbatim → `archive/2026-07.md` via `_INDEX`)_
- _(**PASS-pattern library S69b→S104 #307-#494** all GO — 🔑 reusable verify-recipes: **SCHEMA-ADD+DATA-UN-CRAM** [#488 Supplier 9→27 Mig62: AddColumn×18-exact + 89-invariant + un-cram 4-NCC + DTO-live] · **DATA-FIX-BACKFILL** [#484 Mig61: Mig-top==HEAD + target-rows-flip-EXACT + control-untouched + derived-API-FLIP-live] · **DATA-SEED-PASS** [#477: pre/post-DB-row-delta + DLL-mtime + w3wp-recycle]; #482 Mig60 EF-ternary-SQL 200≠500; #472 PROD Identity NON-prefix `Roles`; #470 no-resurrect DB-count-diff; Mig54-62 applied-prod; #348 411→401 · verbatim → git + `2026-06/07.md`)_ - _(**PASS-pattern library S69b→S104 #307-#494** all GO — recipe tái dùng: **SCHEMA-ADD+DATA-UN-CRAM** [#488 Supplier 9→27 Mig62: AddColumn×18-exact + 89-invariant + DTO-live] · **DATA-FIX-BACKFILL** [#484 Mig61: Mig-top==HEAD + target-rows-flip-EXACT + control-untouched + derived-API-FLIP-live] · **DATA-SEED-PASS** [#477: pre/post-DB-row-delta + DLL-mtime + w3wp-recycle]; #482 Mig60 EF-ternary-SQL 200≠500; **#472 PROD Identity table NON-prefix `Roles`** (không `AspNetRoles`); Mig54-62 applied-prod; #348 411→401 · verbatim → git + `2026-06/07.md`)_
- **Older (S83 #325 ← S29 #232) → git `d2f52ba` + `archive/2026-06.md`** (incl #291 06-16 FAIL [gotcha #65]); pre-S38 → `archive/2026-05-*`. - **Older (S83 #325 ← S29 #232) → git `d2f52ba` + `archive/2026-06.md`** (incl #291 06-16 FAIL [gotcha #65]); pre-S38 → `archive/2026-05-*`.
--- ---

View File

@ -0,0 +1,231 @@
# cicd-verify `1a47a61` — S159 ĐỢT-2 "tab strip 4 GĐ + menu 4-GĐ + đánh số 01-07 nhóm HĐ"
> Ghi-TỪNG-MỤC-trong-lúc-làm (chống #53). File = SẢN PHẨM CHÍNH; return chỉ tóm tắt.
> Đợt-1 `b5799fc` (PASS 9/9) ở file `cicd-verify-b5799fc.md` — KHÔNG chạm.
> Bắt đầu: 2026-07-29 ~12:07+07 · cicd-monitor (READ-only)
---
## M1 — Push + phạm-vi commit · ✅ PASS
```
git log -1 → 1a47a61c21b62329c76bdef14a5daa9630c66db3
[CLAUDE] FE-User: tab strip 4 giai doan tren phieu PE + menu 4-GD
+ danh so 01-07 nhom HD (UAT anh Kiet) · 2026-07-29 12:06:19 +0700
git log origin/main..HEAD → EMPTY (đã push)
```
Range `b5799fc..1a47a61` = **2 commit / 10 file**; trong đó 2 file là
`.claude/workflows/runs/**` (ignored) ⇒ **8 file CODE, 190+/18** — khớp CHÍNH XÁC brief.
| File code | ± | loại |
|---|---|---|
| `fe-admin/src/components/pe/PePipelineStrip.tsx` | +65 | **MỚI** |
| `fe-user/src/components/pe/PePipelineStrip.tsx` | +65 | **MỚI** (mirror) |
| `fe-admin/src/components/pe/PeDetailTabs.tsx` | +3 | wire strip |
| `fe-user/src/components/pe/PeDetailTabs.tsx` | +3 | wire strip |
| `fe-user/src/components/Layout.tsx` | +5 | menu 4-GĐ |
| `fe-user/src/pages/UserDashboardPage.tsx` | 18 ± | đổi nhãn GĐ2/GĐ3 |
| `src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs` | +7 | key mới |
| `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs` | +42/ | **seeder-only** |
⇒ BE có đụng nhưng **0 controller/service, 0 migration** — chỉ hằng-số key + seeder.
`fe-admin` CÓ sửa ⇒ **admin bundle PHẢI rotate** (khác đợt-1 nơi admin rotate là do #69 mode-A).
**Mirror md5 (kiểm chứng "mirror md5-identical" trong brief):**
```
ef7b782409d02b5a6a2f1bf4c2e97b42 fe-admin/src/components/pe/PePipelineStrip.tsx
ef7b782409d02b5a6a2f1bf4c2e97b42 fe-user/src/components/pe/PePipelineStrip.tsx
```
**BYTE-IDENTICAL** — 2 app dùng đúng 1 nội dung, không lệch bản.
## M2 — Migration VẮNG là ĐÚNG · ✅ PASS
```
git diff --name-only b5799fc..1a47a61 -- '*Migrations*' → 0 dòng
```
BE-change chỉ là `MenuKeys.cs` (const) + `DbInitializer.cs` (seeder runtime) ⇒ **không sinh schema**.
Mig prod kỳ vọng đứng nguyên **68** / 89 bảng. Thay đổi menu đi qua **SEEDER lúc app khởi động**,
nên rủi ro thật nằm ở M7 (double-insert / đè nhãn), KHÔNG ở `__EFMigrationsHistory`.
## Pre-deploy bundle snapshot (chốt TRƯỚC khi deploy land)
Đo **2026-07-29 12:07:19+07** (run #423 vừa start, chưa tới step deploy):
| App | js | css | HTTP |
|---|---|---|---|
| admin | `index-BHodkPNV.js` | `index-DiNJjaoo.css` | 200 |
| user | `index-0GO8mgrG.js` | `index-BARVqkP-.css` | 200 |
✅ Khớp CHÍNH XÁC baseline hậu-đợt-1 ⇒ baseline hợp lệ, đợt-2 chưa land.
## M-src — Marker kỳ vọng, đếm trên SOURCE (chốt trước khi soi bundle)
Đếm BYTES utf-8. (Bài học đợt-1: occurrence trong COMMENT bị minifier strip ⇒ đối chiếu occurrence-CODE.)
| Chuỗi | UserDashboardPage | PePipelineStrip (user) | PePipelineStrip (admin) | vai trò |
|---|---|---|---|---|
| `Kế hoạch ký kết HĐ` | 2 | 2 | 2 | dương |
| `Hợp đồng cứng` | 2 | 3 | 3 | dương |
| `Toàn trình 4 giai đoạn` | 1 | 1 | 1 | dương (giữ từ đợt-1) |
| `Đề xuất ký kết hợp đồng` | **0** | **0** | **0** | ÂM — nhãn cũ đợt-1 đã đổi |
| `Ký cứng & phát hành` | **0** | **0** | **0** | ÂM |
## M3 — Poll terminal · ✅ PASS
- **id=536 · run_number=423 · head_sha `1a47a61c`** · created 12:06:25 → **`status=success` 12:12:09**
- **Duration 5m44s** (đợt-1 5m59s) — trong dải bình thường.
- kề trên: #422 id=535 `b5799fc` success (đợt-1) ⇒ hai run tách bạch, không nhầm.
### M3b — Test gate · ✅ 562 EXACT
`…/actions/runs/423/jobs/0/logs` (17.737 ký tự):
```
Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45, 167 ms - SolutionErp.Domain.Tests.dll
Passed! - Failed: 0, Passed: 517, Skipped: 0, Total: 517, 1m26s - SolutionErp.Infrastructure.Tests.dll
```
`Failed!` ×0. **45+517 = 562**, khớp cả TỔNG lẫn TÁCH-PHẦN. Delta 0 — đúng cho commit không kèm test mới.
## M4 — Bundle rotate · ✅ PASS 4/4 (đo 12:12:38+07)
| App | asset | TRƯỚC (hậu đợt-1) | SAU | KQ |
|---|---|---|---|---|
| **admin** | js | `BHodkPNV` | **`D6Pz7nfV`** | ✅ **ROTATE** (bắt buộc — `fe-admin` CÓ sửa) |
| admin | css | `DiNJjaoo` | **`B1ErASuI`** | ✅ ROTATE (utility mới của strip) |
| **user** | js | `0GO8mgrG` | **`Bpb8WEuY`** | ✅ **ROTATE** |
| user | css | `BARVqkP-` | **`7oAYFlzM`** | ✅ ROTATE |
⚠️ Khác đợt-1 ở chỗ: đợt-1 admin-css FROZEN vì 0 admin-src; đợt-2 admin-css rotate vì strip mang class mới
⇒ hình-dạng rotate **nhất quán với nội dung diff**, không phải nhiễu #69.
Artifact thật:
```
USER index-Bpb8WEuY.js ct=application/javascript size=1.585.946 LM=12:11:43+07 (in-window)
ADMIN index-D6Pz7nfV.js ct=application/javascript size=1.676.677 LM=12:10:45+07 (in-window)
```
## M5 — Byte-marker 2 bundle · ✅ PASS (6/6 dương EXACT · 4/4 âm VẮNG)
| Chuỗi | USER bundle | ADMIN bundle | Kỳ vọng |
|---|---|---|---|
| `Kế hoạch ký kết HĐ` | **2** | **1** | dương ✅ |
| `Hợp đồng cứng` | **2** | **1** | dương ✅ |
| `Toàn trình 4 giai đoạn` | **2** | **1** | dương ✅ |
| `Đề xuất ký kết hợp đồng` | **0** | **0** | ÂM ✅ (nhãn đợt-1 đã đổi) |
| `Ký cứng & phát hành` | **0** | **0** | ÂM ✅ |
🔑 **Quy luật đếm rút ra (nối tiếp bài học comment-strip đợt-1): bundle-count = SỐ MODULE chứa chuỗi**
(bundler pool hằng-số chuỗi trong 1 module → nhiều occurrence cùng file gộp còn 1):
- USER = 2 module chứa marker (`PePipelineStrip.tsx` + `UserDashboardPage.tsx`) ⇒ **2**
- ADMIN = 1 module (`PePipelineStrip.tsx`; `Layout.tsx` chỉ thêm route `'/dashboard'`, KHÔNG chứa nhãn) ⇒ **1**
Đúng cho **cả 3 marker × 2 app = 6/6**, tức "đúng-BỘI-SỐ" chứ không chỉ "tìm thấy ≥1".
Kiểm chứng phụ: `grep -rn` 2 chuỗi ÂM trên toàn `fe-user/src` + `fe-admin/src` = **0 hit** ⇒ marker âm vắng ở bundle là do mã đã gỡ, không phải do đọc sai.
**Control ÂM 2 tầng (cả 2 app):** hash CŨ (`0GO8mgrG` / `BHodkPNV`) **và** hash bịa đều trả 200 nhưng
`ct=text/html` 876 B (user) / 900 B (admin) ⇒ bundle cũ **biến mất khỏi đĩa** = chứng ghi-đè thật.
## M6 — Smoke · ✅ PASS 4/4
```
/health/live 200 Healthy · /health/ready 200 Healthy · admin 200 · eoffice 200
```
## M7 — Menu-data prod sau app-pool recycle · ✅ 5/5 mục được giao
**(a) Root count — chống double-insert:** `SELECT COUNT(*) FROM MenuItems WHERE ParentKey IS NULL` = **12** ✅ (không 13/14).
Bổ sung: **`DUPKEYS=0`** (không key nào lặp) · `MENUTOTAL=107`.
**(b) 2 root mới đúng vị trí:**
```
KeHoachKyKet ord=26 IsVisible=1 ✅
HopDongCung ord=32 IsVisible=1 ✅
```
Permission 2 root mới: mỗi key **13 rows / 13 canread** — đúng 13 role × 1, **không nhân đôi**
**(c) 3 root vẫn ẩn:** `Hrm` ord28 vis=**0** · `Off` ord29 vis=**0** · `Personal` ord30 vis=**0** ✅
**(d) 2 root đã rename** (đối chiếu prefix, KHÔNG so byte VN qua console): `PurchaseEvaluations`
`Contracts` đều bắt đầu `Duy…` (= "Duyệt NCC" / "Duyệt hợp đồng") ✅
**(e) 7 nhãn đánh số CÒN NGUYÊN** (backfill silent vì equal):
```
Ct_ThauPhu 01. · Ct_GiaoKhoan 02. · Ct_NhaCungCap 03. · Ct_DichVu 04.
Ct_MuaBan 05. · Ct_NguyenTacNcc 06. · Ct_NguyenTacDv 07.
```
**(f) Control ÂM `Pe%`:** rows=**197** canread=**185** ✅ EXACT (seeder không đụng vùng PE — đúng comment code).
---
## 🔴 M8 — PHÁT HIỆN NGOÀI CHECKLIST: grant 38-key của ĐỢT-1 ĐÃ BỊ REVOKE · ❌ FAIL
Trong lúc soi Serilog để chứng minh seeder có chạy thật, bắt được dòng:
```
2026-07-29 12:12:00.028 +07:00 [INF] Revoked 9 permission rows — … HRM/Văn phòng số/Cá nhân/
Hợp đồng/Danh mục khỏi non-Admin (chỉ Admin thấy — S58 + S92)
```
⇒ đi đo lại grant-set 38 key mà đợt-1 đã áp tay lúc 11:15:
| Thời điểm | Đo được |
|---|---|
| **11:46** (đợt-1, **TRƯỚC** khi pool restart) | 38 key · min=max=**13/13** · tổng 494 canread |
| **12:14** (sau restart đợt-2) | 38 key · **min=1 max=2** · tổng **47/494****0 key nào còn 13** |
Phân bố còn lại: **`Admin`=38 · `Procurement`=9 · 11 role còn lại = 0.** ⇒ mất **447/494** canread.
**Nguyên nhân (đọc code, không suy đoán)**`DbInitializer.cs:2203-2236`
`RevokeTemporarilyHiddenModulesAsync`, chạy **mỗi lần app khởi động, KHÔNG có cờ gate**:
```csharp
.Where(p => p.RoleId != adminRoleId
&& (p.MenuKey.StartsWith("Hrm") || p.MenuKey.StartsWith("Off")
|| p.MenuKey == MenuKeys.Personal
|| p.MenuKey == MenuKeys.Contracts || p.MenuKey.StartsWith("Ct_")
|| p.MenuKey == MenuKeys.Master || p.MenuKey == MenuKeys.Suppliers
|| p.MenuKey == MenuKeys.Projects|| p.MenuKey == MenuKeys.Departments
|| p.MenuKey.StartsWith("Catalog"))
&& (p.CanRead || p.CanCreate || p.CanUpdate || p.CanDelete))
row.CanRead = false; row.CanCreate = false; row.CanUpdate = false; row.CanDelete = false;
```
Tập revoke **TRÙNG KHỚP CHÍNH XÁC** grant-set 38 key (Contracts + `Ct_*` + Master/Suppliers/Projects/
Departments/`Catalog*`). ⇒ **Grant tay 11:15 về bản chất KHÔNG BỀN: mỗi lần app restart là bị gỡ lại.**
Đây đúng mẫu `feedback_wipe_durability_check_reseed` — nhưng CHIỀU NGƯỢC: data-change thủ công bị
**seeder/revoker ungated** ăn mất, thay vì seed ungated tự mọc lại.
**⚠️ Tự đính chính đợt-1 (KHÔNG sửa file đợt-1 theo yêu cầu):** mục M7 file `cicd-verify-b5799fc.md`
đo lúc **11:46**, mà pool restart của run #422 xảy ra ~**11:49** ⇒ phép đo đó nằm **TRƯỚC** recycle,
nên câu "App pool recycle không hề đụng menu-data" trong file đợt-1 **chưa được chứng minh** và nay
đã bị bác. 3 root ẩn + `Pe%` 197/185 vẫn đúng (nằm ngoài / miễn nhiễm), **chỉ phần grant 38-key sai.**
(`Hrm/Off/Personal` bị revoke cũng vô hại vì chủ đích chính là ẩn chúng.)
**Vì sao log chỉ có 1 dòng `Revoked`:** step deploy `Remove-Item …\api\* ` xoá luôn thư mục `logs`,
nên log hiện chỉ chứa từ 12:12 trở đi ⇒ **không thể dùng log để quy trách nhiệm cho restart 11:49 hay 12:12**;
điều chắc chắn là revoker ungated và trạng thái HIỆN TẠI đã mất grant.
**Khuyến nghị (KHÔNG tự sửa — READ-only, escalate em main):**
1. Grant 38-key muốn BỀN thì phải sửa **code** `RevokeTemporarilyHiddenModulesAsync` (thu hẹp tập, hoặc
thêm env-flag gate), **không** áp SQL tay — SQL tay sẽ chết ở lần deploy kế.
2. Sau khi sửa, nghiệm thu bắt buộc bằng **restart THẬT** rồi mới đo (không đo trước recycle).
3. Không rollback `1a47a61` — regression này **không do đợt-2 gây ra**, đợt-2 ship đúng.
---
## VERDICT: **PARTIAL — 8/9**
| # | Mục | KQ |
|---|---|---|
| 1 | Push + range 8 file code 190+/18 + mirror md5 identical | ✅ |
| 2 | Migration VẮNG là ĐÚNG (0 file `*Migrations*`) | ✅ |
| 3 | Run #423 success 5m44s | ✅ |
| 4 | Test gate **562** (45+517, 0 fail) | ✅ |
| 5 | Bundle rotate 4/4 (admin js+css **bắt buộc** đổi, user js+css) | ✅ |
| 6 | 3 marker dương USER + 3 dương ADMIN, count = số-module EXACT | ✅ |
| 7 | 2 control ÂM vắng ở cả 2 bundle + control hash-cũ/bịa `text/html` | ✅ |
| 8 | Smoke health ×2 + FE ×2 · menu-data 5/5 mục được giao · 0 double-insert (root=12, dupkeys=0) | ✅ |
| 9 | **Grant 38-key đợt-1 sống sót recycle** | ❌ **FAIL — còn 47/494, mất 447** |
**Đợt-2 (`1a47a61`) tự nó SHIP ĐÚNG HOÀN TOÀN — không khuyến nghị rollback.**
Cờ đỏ số 9 là **hệ quả cấu trúc của revoker ungated có sẵn**, chỉ lộ ra khi app restart; cần fix bằng CODE.
Baseline mới sau đợt-2: admin js `D6Pz7nfV` / css `B1ErASuI` · user js `Bpb8WEuY` / css `7oAYFlzM`;
gate 562; Mig 68 / 89 bảng (không đụng); menu root = 12.
<!-- END cicd-verify-1a47a61 · VERDICT=PARTIAL 8/9 -->

View File

@ -0,0 +1,229 @@
# cicd-verify `2a72695` — S159 ĐỢT-3 "gỡ nhánh S92 khỏi revoker + skeleton menu GĐ2/GĐ4 + ComingSoonPage"
> Ghi-TỪNG-MỤC-trong-lúc-làm (chống #53). File = SẢN PHẨM CHÍNH; return chỉ tóm tắt.
> Đợt-1 `b5799fc` (PASS 9/9) · đợt-2 `1a47a61` (PARTIAL 8/9, FAIL mục-9 durability) — 2 file cũ KHÔNG chạm.
> Bắt đầu: 2026-07-29 ~12:35+07 · cicd-monitor (READ-only)
>
> 🎯 **Mục 6(a) là phép PASS/FAIL của cả arc**: grant 38-key có SỐNG SÓT restart không.
> Đúng bài tao tự đính chính đợt-2: **phải đo SAU `status=success` VÀ SAU thời điểm pool recycle**.
---
## M1 — Push + phạm vi · ✅ PASS
```
git log -1 → 2a7269581c96bd0d3ce939b4dceaeb847df76a89
[CLAUDE] Infra: go nhanh S92 khoi revoker (owner supersede: hien thi het)
+ skeleton menu GD2/GD4 + ComingSoonPage x2 · 2026-07-29 12:34:15 +0700
git log origin/main..HEAD → EMPTY (đã push)
```
Range `1a47a61..2a72695` = **15 file, 466+/45**. Trong đó **9 file CODE**:
| File | vai trò |
|---|---|
| `src/Backend/…/Persistence/DbInitializer.cs` | 🔴 gỡ nhánh [S92] khỏi revoker + grant bền vào reviewKeys |
| `tests/…/Application/AdminOnlyModulesRevokeTests.cs` | REGRESSION-GUARD mới |
| `tests/…/Application/ProcurementMasterAccessSeedTests.cs` | cập nhật theo spec-change |
| `fe-admin/src/pages/ComingSoonPage.tsx` · `fe-user/…/ComingSoonPage.tsx` | **MỚI** ×2 |
| `fe-admin/src/App.tsx` · `Layout.tsx` · `fe-user/src/App.tsx` · `Layout.tsx` | route + menu 2 app |
6 file còn lại = `.claude/**` (memory + run-trace, ignored bởi 7-glob).
**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 1a47a61..2a72695 -- '*Migrations*' → 0
```
BE-change = seeder + revoker logic (runtime), không sinh schema. Mig giữ **68** / 89 bảng.
## 📌 BASELINE TRƯỚC RESTART (đo 12:35:47+07, run #424 vừa start — pool CHƯA recycle)
Đây là "vế trước" của phép durability. Ghi lại để đối chứng sau restart:
| Phép đo | Giá trị TRƯỚC restart |
|---|---|
| 38-key set (per-key canread) | **min=13 max=13 keys=38** ✅ (lead re-grant 447 đã ăn) |
| `Khkk_%` permission rows | **65** (= 5 key × 13 role) |
| `Hdc_%` permission rows | **91** (= 7 key × 13 role) |
| Tổng skeleton | **156** ✅ khớp con số lead báo |
| Control âm `Hrm%`/`Off%`/`Personal` non-Admin canread | **0** |
Bundle trước deploy: admin js `D6Pz7nfV` css `B1ErASuI` · user js `Bpb8WEuY` css `7oAYFlzM`
— khớp CHÍNH XÁC baseline hậu-đợt-2 ⇒ baseline hợp lệ, đợt-3 chưa land.
## M3 — Poll terminal · ✅ PASS
- **id=537 · run_number=424 · head_sha `2a726958`** · created 12:34:22 → **`status=success` 12:40:18**
- **Duration 5m56s** (đợt-1 5m59s · đợt-2 5m44s) — ổn định.
### M3b — Test gate · ✅ 562 EXACT
```
Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45, 200 ms - SolutionErp.Domain.Tests.dll
Passed! - Failed: 0, Passed: 517, Skipped: 0, Total: 517, 1m36s - SolutionErp.Infrastructure.Tests.dll
```
`Failed!` ×0. **45+517 = 562** — giữ nguyên, đúng kỳ vọng: 3 test được **SỬA theo spec-change**
(không thêm test mới) ⇒ tổng không đổi. Regression-guard nằm TRONG test cũ.
**Nội dung guard (đọc diff, không suy đoán)**`AdminOnlyModulesRevokeTests.cs` tách 2 vế:
- `StillHiddenKeys` = `Hrm` / `Off` / `Personal` → sau revoke **PHẢI** `CanRead=false`
- `ReopenedKeys` = `Contracts`/`Ct_ThauPhu`/`Master`/`Suppliers`/`Catalogs`/`CatalogWorkItems`
→ sau revoke **PHẢI GIỮ** `CanRead=true`
⇒ ai re-add nhánh `[S92]` vào predicate là **test ĐỎ ngay**. Guard có RĂNG thật (assert 2 chiều).
`DbInitializer.cs`: nhánh `|| p.MenuKey == MenuKeys.Contracts || p.MenuKey.StartsWith("Ct_")
|| Master/Suppliers/Projects/Departments/Catalog*` đã **GỠ khỏi predicate revoker**; reviewKeys
`+ContractMenuKeys() +MasterMenuKeys() +KhkkKeys() +HdcKeys()`.
⚠️ Ghi chú code nêu rõ nhánh grant là **skip-existing** ⇒ trên prod row `false` KHÔNG tự nâng;
prod dựa vào re-grant SQL tay (đã chạy) **+** revoker không lật nữa. Đúng: phép 6(a) dưới đây kiểm chính điều đó.
## M4 — Bundle rotate · ✅ PASS 4/4 (đo 12:40:45+07, sau status=success)
| App | asset | TRƯỚC | SAU | KQ |
|---|---|---|---|---|
| admin | js | `D6Pz7nfV` | **`CC1pGE1F`** | ✅ |
| admin | css | `B1ErASuI` | **`DWDbm5As`** | ✅ |
| user | js | `Bpb8WEuY` | **`DeAg7MWm`** | ✅ |
| user | css | `BQIZ-rUe``7oAYFlzM` | **`BQIZ-rUe`** | ✅ |
Cả 2 app đều sửa (App.tsx + Layout.tsx + ComingSoonPage.tsx mới) ⇒ rotate 4/4 nhất quán với diff.
---
# 🔴 M6 — RESTART-DURABILITY (mục CHÍNH của cả arc) · ✅ **PASS**
## Bước 0 — CHỨNG MINH pool ĐÃ recycle THẬT (không đo sớm như đợt-2)
| Bằng chứng | Giá trị |
|---|---|
| `w3wp` PID **5508** StartTime | **12:39:56** — nằm trong cửa sổ deploy (12:34:22→12:40:18) |
| `SolutionErp.Api.dll` LastWriteTime | **12:37:38** — binary mới đã copy |
| Dòng ĐẦU của `logs\solution-erp-20260729.log` | **`2026-07-29 12:40:03.239 +07:00 [INF] Applying migrations...`** |
| Log LastWriteTime | 12:40:37 |
🔑 Vì step deploy `Remove-Item …\api\*` **xoá cả `logs/`**, việc dòng đầu log là **12:40:03** chứng minh
file được tạo lại SAU deploy ⇒ đây đúng là **lần khởi động mới**, và `DbInitializer` (seeder + revoker)
đã chạy trong lần đó. **Mọi số dưới đây đo SAU thời điểm này** — sửa đúng lỗi đo-sớm của đợt-2.
## (a) 38-key set — PHÉP PASS/FAIL CỦA CẢ ARC · ✅ **PASS**
```
A_POST38 min=13 max=13 keys=38 totcr=494
```
| Mốc | min/max | tổng canread |
|---|---|---|
| Đợt-2 SAU restart (#423) — **hỏng** | 1 / 2 | **47** / 494 |
| Đợt-3 TRƯỚC restart (12:35:47, sau re-grant tay) | 13 / 13 | 494 / 494 |
| **Đợt-3 SAU restart (#424, đo ~12:41)** | **13 / 13** | **494 / 494** ✅ |
**Grant 38-key SỐNG SÓT app-pool recycle.** Chính phép đo này ở đợt-2 cho ra 0 key còn 13;
nay giữ nguyên 38/38 key ở 13/13. **Fix của lead ăn thật, không phải trùng hợp** — vì:
(i) đã chứng minh có restart thật ở Bước 0, (ii) predicate revoker đã gỡ nhánh S92, (iii) control (c) cho thấy
hàm revoker **vẫn chạy** chứ không chết.
## (b) Skeleton GĐ2/GĐ4 · ✅ PASS
```
B_SKEL Khkk=65 Hdc=91 tot=156 (Permissions rows)
B2_SKELCR min=13 max=13 keys=12 (mỗi key đủ 13 role canread)
```
65 = 5 key × 13 · 91 = 7 key × 13 · **tổng 156** ✅ khớp con số lead báo; **12 key** × 13 = 156 nhất quán.
## (c) Control ÂM — revoker CÒN SỐNG · ✅ PASS
```
C_CTRL hrmoff_nonadmin_cr = 0
```
`Hrm%`/`Off%`/`Personal` với role ≠ Admin vẫn **canread = 0** ⇒ gỡ nhánh S92 **KHÔNG làm chết cả hàm**;
vế `StillHiddenKeys` vẫn thực thi đúng. (Đây là phép chống "Goodhart": nếu ai đó vô hiệu hoá luôn
revoker thì (a) cũng xanh — nhưng (c) sẽ đỏ. (a) xanh **và** (c) xanh mới là fix ĐÚNG.)
## (d) Control `Pe_%` · ✅ PASS
```
D_PE rows=197 cr=185 (dùng LIKE 'Pe%' — đúng hình-dạng query gốc)
```
## M7 — Log seeder sau restart · ✅ PASS (khai đầy đủ)
`findstr "Revoked"` trên log mới ⇒ **KHÔNG có dòng nào**. Đây là kết quả ĐÚNG chứ không phải thiếu dữ liệu:
`RevokeTemporarilyHiddenModulesAsync` chỉ ghi log khi `rows.Count > 0`; nay tập ẩn chỉ còn
Hrm/Off/Personal và chúng **đã ở canread=0 từ trước** ⇒ 0 row cần lật ⇒ im lặng. Khớp control (c)=0.
**So đợt-2: 447 row bị lật → nay 0.**
---
## M5 — Byte-marker ComingSoonPage trên 2 bundle · ✅ PASS
Source `ComingSoonPage.tsx` **2 app đều 2.553 B** (mirror): chứa `Phân hệ đang được triển khai` ×1
+ `Quay lại` ×1; **không** chứa `Sắp triển khai`.
| Chuỗi | USER bundle | ADMIN bundle | KQ |
|---|---|---|---|
| `Phân hệ đang được triển khai` | **1** | **1** | ✅ dương cả 2 (chính là ComingSoonPage mới) |
| `Quay lại` | **4** | **4** | ✅ dương cả 2 |
| `Sắp triển khai` | 2 | 1 | ✅ dương cả 2 (nhãn GĐ có sẵn, không phải ComingSoonPage) |
Artifact thật + in-window:
```
USER index-DeAg7MWm.js ct=application/javascript size=1.587.996 LM=12:39:51+07
ADMIN index-CC1pGE1F.js ct=application/javascript size=1.678.733 LM=12:38:56+07
```
Control âm: hash CŨ (`Bpb8WEuY` / `D6Pz7nfV`) đều rơi về `ct=text/html` 876 B / 900 B
⇒ bundle đợt-2 đã biến mất khỏi đĩa = chứng ghi-đè thật.
## M8 — Smoke · ✅ PASS 4/4
```
/health/live 200 Healthy · /health/ready 200 Healthy · admin 200 · eoffice 200
```
## M9 — Menu-tree skeleton · ✅ PASS 5/5
```
MENU Khkk=5 Hdc=7 (MenuItems rows)
ROOT=12 DUPKEYS=0
Hrm=0 Off=0 Personal=0 (IsVisible)
NUM(label '01.'-'07.')=14 → NUMSPLIT ct=7 hdc=7 other=0
```
Cấu trúc `Khkk_*` đúng phân cấp thiết kế:
```
KeHoachKyKet (root)
└── Khkk_G1 ord1 "1. Kế hoạch ký kết HĐ (NCC-TP)"
├── Khkk_List ord1
├── Khkk_Create ord2
├── Khkk_Pending ord3
└── Khkk_Approved ord4
```
🔎 **Giải-trình con số 14 (đừng đọc nhầm thành drift):** kỳ vọng ban đầu là 7 nhãn `01.`-`07.`,
đo ra **14**. Tách theo prefix key: **`Ct_`=7 + `Hdc_`=7 + other=0**. Đúng thiết kế — diff ghi rõ
GĐ4 *"tái dùng đúng nhãn đã đánh số 01.-07."* (`tree.Add(("Hdc_"+code, label, …))` dùng chung
`typeLabels`). Vậy 14 = 7 gốc + 7 mirror, **không** phải nhân đôi nhầm (DUPKEYS=0, root vẫn 12).
---
## VERDICT: **PASS — 9/9**
| # | Mục | KQ |
|---|---|---|
| 1 | Push + range 15 file (9 code), CI fire đúng | ✅ |
| 2 | Migration VẮNG là ĐÚNG (0 file `*Migrations*`) | ✅ |
| 3 | Run #424 success 5m56s | ✅ |
| 4 | Test gate **562** (45+517, 0 fail) + regression-guard 2 vế có RĂNG | ✅ |
| 5 | Bundle rotate **4/4** (2 app đều sửa) + control hash-cũ `text/html` | ✅ |
| 6 | Marker ComingSoonPage dương **cả 2 bundle** + `Quay lại` ×4 | ✅ |
| 7 | Smoke health ×2 + FE ×2 | ✅ |
| 8 | Menu-tree: Khkk=5 · Hdc=7 · root=12 · 3 root vis=0 · nhãn 7+7 · DUPKEYS=0 | ✅ |
| 9 | 🔴 **RESTART-DURABILITY: 38-key = 13/13/38 (494/494) SAU restart THẬT** | ✅ **PASS** |
| | ├ (b) skeleton 156 perm, 12 key × 13 | ✅ |
| | ├ (c) control âm Hrm/Off/Personal non-Admin = **0** (revoker còn sống) | ✅ |
| | └ (d) `Pe%` 197/185 | ✅ |
### 🎯 Kết luận arc 3 đợt
Regression tao bắt ở đợt-2 (mục 9, 447/494 row rơi) **đã đóng**. Bằng chứng đóng KHÔNG dựa vào
"CI xanh" mà dựa vào **3 chân kiềng**: (i) chứng minh có restart THẬT (w3wp 12:39:56 + log mở lại
12:40:03), (ii) 38-key giữ **13/13/38** sau restart đó, (iii) control âm (c) chứng minh revoker
**vẫn chạy** — nên (ii) không phải do vô hiệu hoá cả hàm. Kèm regression-guard trong test:
re-add nhánh `[S92]` ⇒ CI đỏ, không cần trông vào trí nhớ.
Baseline mới: admin js `CC1pGE1F` / css `DWDbm5As` · user js `DeAg7MWm` / css `BQIZ-rUe` ·
gate 562 · Mig 68 / 89 bảng · menu root 12 · 38-key 13/13.
<!-- END cicd-verify-2a72695 · VERDICT=PASS 9/9 -->

View File

@ -51,7 +51,33 @@
- [x] **Đ2-e — build FE ×2 PASS + test 562 PASS** (seeder đụng ⇒ chạy đủ) → **commit `1a47a61` push** (8 file, 190+/18) - [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) - [!] **Đ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 sẽ **drop silent** (không thêm staticMap admin — cố ý, admin không cần placeholder; ghi để khỏi đọc thành bug) 🔸 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`): - [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) · 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 ý ❸) · 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 ý ❸)

View File

@ -0,0 +1,117 @@
# sub-frontend-designer-dot4 — Đợt-4 redesign `UserDashboardPage.tsx` (fe-user ONLY)
> Run: `2026-07-29-S159-tong-quan-pipeline-menu` · vai: frontend-designer · file đích: `fe-user/src/pages/UserDashboardPage.tsx`
> 🔴 GHI ĐĨA TRONG LÚC LÀM (chống #53). Mỗi mục = 1 bước ĐÃ XONG, ghi ngay khi xong.
> 🔴 **NO-SCREENSHOT** — spawn này không có dev-server/Playwright + trang sau login (ProtectedRoute) ⇒ FD2 visual-loop KHÔNG chạy được. Thay bằng (i) `npm run build` PASS + tail, (ii) tự-review tĩnh theo rubric FD4. KHÔNG bịa screenshot.
---
## 1 — Khảo sát file HIỆN HÀNH (đọc đĩa, KHÔNG dùng bản nhớ)
`fe-user/src/pages/UserDashboardPage.tsx`**766 dòng** (không phải 309 như đợt-1; đã qua review + 3 đợt sửa nhãn). Trạng thái TRƯỚC đợt-4:
- Comment đầu file đã ghi nhãn 4 GĐ **bản sửa mới nhất**: GĐ1 "Duyệt NCC" (bước 1→6, CEO chốt) · GĐ2 **"Kế hoạch ký kết HĐ"** (7→12) · GĐ3 **"Duyệt hợp đồng"** (13→18) · GĐ4 **"Hợp đồng cứng"** (19→21). ⇒ Tab đợt-4 phải echo ĐÚNG 4 tên này (không lấy tên đợt-1 "Đề xuất ký kết hợp đồng"/"Ký cứng & phát hành").
- `PageHeader` title "Tổng quan tất cả luồng duyệt".
- **HERO pipeline 4 GĐ** (dòng 403-538) — GIỮ (owner không khoanh).
- Khối BỊ KHOANH BỎ: `<section>` "Việc của tôi" 5 `StatCard` (541-582) + `grid xl:grid-cols-2` chứa "HĐ gần đây" (587-673) + "Phiếu Duyệt NCC gần đây" (678-762).
- 5 query đang có: `['my-dashboard']` · `['my-contracts-recent']` (`/contracts?page=1&pageSize=5`) · `['my-pe-recent']` (`/purchase-evaluations?page=1&pageSize=5`) · `['pipeline-pe-approved']` (phase=7, pageSize=1 lấy `.total`) · `['pe-inbox']` (endpoint `/inbox` mảng phẳng, dùng `select: d => d.length` để KHÔNG vỡ cache dùng chung với InboxPage).
- Component local tái dùng được: `STAGE_TONE` (brand/teal), `StageCard`, `StageArrow`, `StageStatRow`, `StageLink`, `StageSoon`, `SectionTitle`, `fmtMoney`, `StatCard`+`STAT_TONE`.
- Gotcha CSS đã ghi sẵn trong file: `h1..h4`, `.stat-value`, `.icon-chip` KHÔNG nằm trong `@layer` ⇒ thắng utility Tailwind v4, muốn đổi màu heading phải dùng `!`.
## 2 — Khảo sát BE (xác nhận PARAM THẬT, không chế)
`src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs:18-35`
```
[HttpGet] List(page=1, pageSize=20, search=null, sortDesc=true, type=null, phase=null,
projectId=null, approvalWorkflowId=null) -> PagedResult<PurchaseEvaluationListItemDto>
[HttpGet("inbox")] Inbox(type=null, approvalWorkflowId=null) -> List<...> // MẢNG PHẲNG, không paging
```
`src/Backend/SolutionErp.Api/Controllers/ContractsController.cs:16-28`
```
[HttpGet] List(page=1, pageSize=20, search=null, sortDesc=true, phase=null,
supplierId=null, projectId=null) -> PagedResult<ContractListItemDto>
[HttpGet("inbox")] Inbox() -> List<...>
```
**CẢ paging LẪN search đều có thật** ở cả 2 endpoint ⇒ yêu cầu 3 (phân trang thật + ô tìm kiếm) làm được ĐẦY ĐỦ, KHÔNG phải cắt.
Phạm vi `search` (đọc handler, để đặt placeholder trung thực):
- PE — `PurchaseEvaluationFeatures.cs:624-632`: `MaPhieu` `TenGoiThau` `Project.Name` `Project.Code`.
- HĐ — `ContractFeatures.cs:309-316`: `MaHopDong` `TenHopDong` `Supplier.Name` `Project.Name`.
⇒ placeholder PE = "Tìm mã phiếu, gói thầu, dự án…" · HĐ = "Tìm mã HĐ, tên HĐ, nhà cung cấp, dự án…" (đúng cái BE thật lọc, không hứa quá).
`Paged<T>` (`fe-user/src/types/master.ts`) = `{items,total,page,pageSize,totalPages,hasNext,hasPrev}` — có `hasNext/hasPrev` ⇒ nút Trước/Sau bind thẳng, không tự tính.
⚠️ Nhắc lại đính chính đợt-1 (vẫn đúng): **KHÔNG có query-param `pendingMe`** ở BE — đó là URL-param của trang list, map sang endpoint `/inbox`.
## 2b — Tài sản có sẵn (FD6 chống reinvent — đã đọc, KHÔNG suy đoán)
| Thứ | Đường dẫn | Dùng vào |
|---|---|---|
| `PePipelineStrip` | `components/pe/PePipelineStrip.tsx` | **NGUỒN TÊN 4 GĐ CANONICAL** (`Duyệt NCC / Kế hoạch ký kết HĐ / Duyệt hợp đồng / Hợp đồng cứng`) — tab đợt-4 lấy tên khớp file này, KHÔNG chép tay |
| `PeFinalizeChip` | `components/pe/PeFinalizeChip.tsx` | cột Trạng thái GĐ1 (tái dùng như list cũ) |
| `PeUrgentChips` | `components/pe/PeUrgentChips.tsx` | cờ GẤP PRO/CCM cạnh mã phiếu (có sẵn trên `PeListItem`, 0 request thêm) |
| `PhaseBadge` · `EmptyState` · `Button` · `Input` | `components/…` | bảng GĐ3 · empty/error · nút · ô tìm |
| `getPeDisplayStatus` + `PeDisplayStatus{Label,Color}` | `types/purchaseEvaluation.ts` | chip trạng thái GĐ1 |
| `ComingSoonPage` | `pages/ComingSoonPage.tsx` | nhận `?stage=2|4` (`STAGE_INFO`) ⇒ link `/coming-soon?stage=N` là route THẬT, không đâm `*` |
| `Paged.hasNext/hasPrev/totalPages` | `types/master.ts` | nút Trước/Sau bind thẳng, KHÔNG tự tính |
| `keepPreviousData` | `@tanstack/react-query` **v5.99.2** (node-verify: `typeof === function`) | phân trang không nháy trắng |
Route đích đã verify trong `App.tsx`: `/purchase-evaluations/:id` (mở phiếu — đúng cách list "gần đây" cũ navigate) · `/my-contracts?id=<guid>` (`MyContractsPage.tsx:30` đọc `searchParams.get('id')`) · `/contracts/new` · `/coming-soon`.
Icon lucide đã node-verify 26 export (`Search`, `ChevronLeft/Right`, `ListChecks`, `SearchX`, `Plus`, `X`… ) — tránh bẫy alias-dodge.
## 3 — Quyết định design (chốt TRƯỚC khi code)
**Xoá:** section "Việc của tôi" (5 `StatCard`) + grid 2 list "HĐ gần đây"/"Phiếu Duyệt NCC gần đây" — đúng khối owner khoanh. Kéo theo: `StatCard`/`STAT_TONE`/`fmtMoney`?/`SlaTimer`/`Pencil,Inbox,Clock,Coins` không còn dùng ⇒ phải gỡ import (TS `noUnusedLocals` sẽ chặn build nếu để lại). `fmtMoney` GIỮ (bảng GĐ3 hiện cột Giá trị).
**Thêm:** 1 section `Danh sách phiếu toàn trình` = tabs (tầng liên kết 1) → bảng (tầng liên kết 2 = cột Hành trình).
1. **Single source `STAGES`** — 1 mảng const cấp module (`n / title / stepRange / icon / tone / soon`) cho **CẢ hero LẪN tab**. Lý do: nhãn 4 GĐ đã drift qua 3 đợt sửa; hero + tab chép tay = drift lần 4. Hero giữ nguyên JSX `StageCard`, chỉ đổi `title`/`stepRange` sang đọc từ `STAGES` ⇒ echo tên là ĐÚNG do cấu trúc, không do cẩn thận.
2. **Tab** `role="tablist"` + 4 `role="tab"` (roving tabindex, ←/→/Home/End, `aria-selected`, `aria-controls`), panel `role="tabpanel"` + `aria-labelledby`. KHÔNG chèn mũi tên `→` giữa các tab (con của tablist phải là tab) — mạch "toàn trình" đã do hero ngay trên gánh + mỗi tab mang eyebrow "Giai đoạn N" khớp badge hero.
3. **Màu tab active = tone của GĐ đó** (GĐ1 brand, GĐ3 teal, GĐ2/4 slate) ⇒ bảng bên dưới ăn cùng accent → mắt nối được "ô hero ↔ tab ↔ bảng". Contrast (tự tính, không phỏng):
- `bg-brand-700 #1b6aa3` + chữ trắng = **5.77:1** ✓ (KHÔNG dùng brand-600 = 4.41:1 — chính gotcha đã ghi trong file, dòng 409-410)
- `bg-teal-700 #0a7170` + trắng = **5.82:1** ✓ · `bg-slate-600` + trắng ≈ 7:1 ✓
- Badge count trên tab active = **pill trắng chữ `text-brand-800`** (7.75:1) thay vì `bg-white/20` (3.85:1 ✗ — đã loại).
4. **Cột Hành trình** = `JourneyDots` (4 chấm + gạch nối): chấm của CHÍNH GĐ dòng đó mang trạng thái, 3 chấm còn lại `bg-slate-200` mờ + `title` tên GĐ. Map trạng thái (mở rộng đúng tinh thần owner, không bịa nghĩa):
`Đã duyệt`→**đầy** (tone-600) · `Đã gửi duyệt`→**nửa** (linear-gradient 50% inline, viền tone-500) · `Cần chỉnh sửa lại`→**nửa amberx** · `Nháp`→**rỗng** (viền slate-300) · `Từ chối`→**đầy accent-500 (đỏ)** + tooltip "dừng tại GĐ này".
a11y: bọc `role="img"` + **1** `aria-label` kể trọn hành trình; từng chấm `aria-hidden` + `title` (tooltip chuột). Không bắt screen-reader đọc 4 chấm rời.
GĐ3 dùng CÙNG component (chấm 3 sáng, 1/2/4 mờ) — KHÔNG suy diễn "HĐ tồn tại ⇒ GĐ1/2 xong" (chưa có cầu nối per-phiếu; `PePipelineStrip.tsx:8-9` nói rõ).
5. **Row click**: `<tr onClick>` (chuột bấm đâu cũng được) + trong ô Mã có `<button>` THẬT (`stopPropagation`) làm điểm focus bàn phím ⇒ WCAG 2.1.1 đủ, KHÔNG lồng button-trong-row-button, không `div role=row onClick` câm.
6. **Responsive** (không tràn ngang 375): ô "Gói thầu"/"Tên HĐ" dùng `w-full max-w-0 truncate` (ăn hết chỗ thừa rồi cắt) + dòng meta `lg:hidden` gộp Dự án · Ngày; cột Dự án `hidden lg:table-cell`, Người soạn `hidden xl:table-cell`, Ngày `hidden md:table-cell`, Hành trình `hidden md:table-cell`. Mobile còn 3 cột thật (Mã · Gói thầu+meta · Trạng thái).
7. **Request**: list **lazy theo tab** (`enabled: stage===1` / `stage===3`) ⇒ GĐ2/GĐ4 bắn 0 request. 2 query `*-recent` (pageSize 5) đổi thành `*-total` **pageSize=1** (hero chỉ cần `.total`) ⇒ tổng vẫn 5 query nền + 1 list = **6**, payload NHỎ HƠN bản cũ. `['pe-inbox']` GIỮ NGUYÊN key+queryFn+`select` (ràng buộc owner + cache chung `InboxPage`).
8. **Search** debounce 300ms (`useEffect`+`setTimeout`, 0 dep mới), đổi search/tab → reset `page=1`. Placeholder ghi ĐÚNG cái BE lọc.
## 4 — File đã sửa (CHỈ 1 file, fe-user ONLY, 0 dep mới)
`D:\Dropbox\CONG_VIEC\SOLUTION\SOLUTION_ERP\fe-user\src\pages\UserDashboardPage.tsx` — sửa TẠI CHỖ (1 atomic Write cả file, chống truncate #53). Named export `UserDashboardPage` GIỮ ⇒ route `/dashboard` + sidebar không phải đụng. 766 → **1.130 dòng**.
### GỠ (đúng khối owner khoanh đỏ)
| Gỡ | Kéo theo phải gỡ (TS `noUnusedLocals` sẽ chặn build nếu để lại) |
|---|---|
| `<section>` "Việc của tôi" — 5 `StatCard` (HĐ đang soạn / Chờ tôi duyệt / Sắp quá hạn / Đã quá hạn / Tổng giá trị nháp) | `StatCard`, `STAT_TONE`, icon `Pencil` `Inbox` `Clock` `Coins`, `CheckCircle2` |
| `<section>` "HĐ gần đây" (list 5) | `SlaTimer` import |
| `<section>` "Phiếu Duyệt NCC gần đây" (list 5) | `SectionTitle` |
| query `['my-contracts-recent']` pageSize **5** | → đổi thành `['pipeline-contract-total']` pageSize **1** (hero chỉ cần `.total`) |
| query `['my-pe-recent']` pageSize **5** | → đổi thành `['pipeline-pe-total']` pageSize **1** |
### THÊM
- `STAGES: StageMeta[]`**single source 4 giai đoạn** (`n/title/stepRange/icon/tone/soonDesc`). Hero (`StageCard`) đổi signature nhận `stage: StageMeta` thay 5 prop chuỗi rời ⇒ tab & hero KHÔNG THỂ lệch nhãn. Text hero giữ **nguyên văn** (`soonDesc` chép đúng câu đang chạy) — owner đã duyệt hero, không đổi chữ.
- `JourneyDots` + `DOT_TEXT` + `dotVisual` + `peDot()` + `contractDot()` — cột **Hành trình**.
- `TAB_TONE` + `StageTab` — thanh tab 4 GĐ.
- `ListToolbar` (ô tìm + nút "Mở màn …") · `TableSkeleton` (colSpan, không lệch cột ẩn) · `Pager` (Trước/Sau + "Hiển thị xy trong N", `aria-live=polite`).
- `PeTable` (7 cột: Mã phiếu · Gói thầu · Dự án · Người soạn · Ngày · Trạng thái · Hành trình) · `ContractTable` (7 cột: Mã HĐ · Tên HĐ · Loại · Giá trị · Ngày · Trạng thái · Hành trình) · `SoonPanel` (GĐ2/GĐ4).
- 2 query list LAZY: `['dash-stage-pe',{page,pageSize,search}]` (`enabled: stage===1`) · `['dash-stage-contracts',{…}]` (`enabled: stage===3`), cả 2 `placeholderData: keepPreviousData` (đổi trang không nháy trắng) + `aria-busy` + mờ 60% khi `isFetching`.
- State: `stage` · `page` · `searchInput`/`search` (debounce 300ms) · `tabRefs` (roving focus).
### GIỮ NGUYÊN 100% (không đụng)
`PageHeader` title/description · toàn bộ **HERO** (4 `StageCard` + `StageArrow` + `StageStatRow` + `StageLink` + `StageSoon` + `STAGE_TONE` + banner lỗi + `retryFailed`) · query `['my-dashboard']` · `['pipeline-pe-approved']` · **`['pe-inbox']` (key + queryFn + `select: d => d.length` y nguyên — cache dùng chung `InboxPage`)** · `fmtMoney` · mọi target `navigate()` của hero.
### Route đích của bảng (đều là route THẬT trong `App.tsx`)
`onOpen` GĐ1 → `/purchase-evaluations/:id` (đúng cách list "gần đây" cũ navigate) · `onOpen` GĐ3 → `/my-contracts?id=<guid>` (đúng cách cũ) · `onCreate``/purchase-evaluations/new` · `/contracts/new` (CTA "Tạo HĐ mới" giữ ở empty-state GĐ3 như owner dặn) · `SoonPanel``/coming-soon?stage=2|4`.
## 5 — [LEAD GHI ON-BEHALF @13:2x — vai chết #53 đúng lúc verify CSS, đĩa giữ trọn §1-§4]
- `npm run build` fe-user: **PASS 419ms** (lead chạy độc lập SAU khi vai chết — luật #68).
- 🔴 **Nỗi lo CSS-token của vai (Tailwind v4 drop class không quét được): VERIFY SẠCH** — grep pattern class-ghép-động (`` `...-${...}` `` trong className) = **0 hit**; mọi biến thể màu đi qua lookup-map TĨNH (`STAGE_TONE:158` · `DOT_TEXT:337` · `TAB_TONE:422`) = literal đầy đủ trong source ⇒ Tailwind quét được hết. `divide-slate` src=2/css=1 (present).
- Việc vai CHƯA kịp: tự-review rubric FD4 → chuyển cho `reviewer` adversarial (spawn kế tiếp) — không bịa hộ.
<!-- END sub-frontend-designer-dot4 · TOTAL=5 muc (muc 5 = lead on-behalf) -->

View File

@ -0,0 +1,102 @@
# sub-reviewer-diff-dot4 — soi đối kháng diff đợt-4 "Danh sách phiếu toàn trình" (S159 L8)
> Run: `2026-07-29-S159-tong-quan-pipeline-menu` · vai: reviewer (adversarial, READ-only)
> Vật soi: `git diff -- fe-user/src/pages/UserDashboardPage.tsx` — 1321 dòng (984+/337), 1 file.
> Tác giả diff: frontend-designer (sub-file `sub-frontend-designer-dot4.md`). **Vai đó CHƯA tự-review ⇒ đây là lớp review DUY NHẤT của đợt-4.**
> Tiền lệ: đợt-1 đã soi cùng trang (`sub-reviewer-diff-tongquan.md`, 7 FLAG 0H/2M/5L) — mục §A của đợt-1 CÒN GIÁ TRỊ, không lặp lại; đợt này thêm trục REGRESSION so với đợt-1.
> 🔴 Ghi đĩa TỪNG FLAG ngay khi tìm ra (chống #53). KHÔNG batch.
> 🔴 Giới hạn khai trước: **KHÔNG live-verify** (chưa deploy, `/dashboard` cần login) ⇒ toàn bộ là soi TĨNH trên diff + đọc mã nguồn BE/FE/route. Mọi phát biểu về runtime là suy luận từ mã, đánh dấu rõ chỗ chưa chứng được.
---
## 0 — Tiến trình (append-only, ghi trong lúc làm)
- [t0] Đã chạm đĩa: `git diff --stat` → đúng 1 file `fe-user/src/pages/UserDashboardPage.tsx` 984+/337. Các file M khác (`docs/STATUS.md`, `docs/HANDOFF.md`, `scripts/*`, `.claude/commands/*`, `.claude/governance/*`) KHÔNG thuộc vật soi (của lead). 0 file `fe-admin/**`. ✅ ràng buộc "fe-user ONLY" THOẢ.
- [t1] Trục 1 (BỎ) đo xong · Trục 7 (route) đo xong · FLAG-1 chốt (xem dưới).
---
## FLAG — ghi từng cái, ngay khi chốt
### FLAG-1 [M] — bấm dòng hợp đồng trên ĐIỆN THOẠI dẫn tới ngõ cụt (mở ra danh sách, không mở ra hợp đồng)
`fe-user/src/pages/UserDashboardPage.tsx:1395``onOpen={id => navigate(`/my-contracts?id=${id}`)}`.
Trang đích `fe-user/src/pages/contracts/MyContractsPage.tsx` chỉ render nội dung hợp đồng ở Panel 2/3, mà **cả hai panel đều `hidden … lg:block`** (`MyContractsPage.tsx:172``:189`). Dưới 1024px người dùng chỉ thấy Panel 1 là danh sách — hợp đồng vừa bấm KHÔNG mở ra. Chính trang đó đã tự biết điều này: `selectContract()` (`MyContractsPage.tsx:59-68`) có nhánh `window.matchMedia('(min-width: 1024px)')`, hễ hẹp hơn thì `navigate('/contracts/:id')` fullpage. Bảng mới trên trang chủ **không** có nhánh đó, nên nó phá đúng quy ước mà trang đích đặt ra.
Mức phơi nhiễm tăng thật chứ không chỉ trên lý thuyết: bảng GĐ3 giữ 3 cột (Mã HĐ · Tên hợp đồng · Trạng thái) ở mọi bề rộng (`:860-866`, các cột còn lại mới `hidden md/lg/xl`), tức dòng bấm được trên điện thoại; và nó thay cho danh sách "HĐ gần đây" 5 dòng cũ bằng danh sách phân trang đầy đủ.
**Khai trung thực về mức lỗi:** đây là **tật kế thừa, KHÔNG phải hồi quy** — bản cũ cũng `navigate(`/my-contracts?id=${c.id}`)` (dòng bị xoá trong diff). Tôi vẫn để mức M vì đợt-4 nhân diện tích chạm của nó lên nhiều lần và biến nó thành đường mở hợp đồng CHÍNH của trang chủ.
**Fix đề nghị:** dùng lại đúng nhánh của trang đích — nếu `window.matchMedia('(min-width: 1024px)').matches` thì `/my-contracts?id=<id>`, ngược lại `/contracts/<id>` (route có thật, `App.tsx:60`). Hoặc đơn giản hơn: luôn `/contracts/<id>`.
### FLAG-2 [M] — cột "Hành trình" nói CHẮC một điều nó không biết: "các giai đoạn còn lại **chưa tới**"
`fe-user/src/pages/UserDashboardPage.tsx:337-344` khai `DOT_TEXT.none = 'chưa tới'`, rồi `:378` ghép thành nhãn đọc-được của cả cụm: *"Hành trình toàn trình: giai đoạn N … ; **các giai đoạn còn lại chưa tới**."*, và `:389` gắn tooltip từng chấm *"Giai đoạn 1 · Duyệt NCC — chưa tới"*.
Hai chỗ câu đó SAI:
1. **Dòng hợp đồng (bảng GĐ3).** Một hợp đồng đang ở giai đoạn 3 thì giai đoạn 1 và 2 là các giai đoạn **phía trước** nó, không thể "chưa tới". Người dùng rê chuột lên chấm đầu sẽ đọc "Giai đoạn 1 · Duyệt NCC — chưa tới" cho một hợp đồng đã đi qua chặng đó (hoặc ít nhất không ai biết là chưa).
2. **Dòng phiếu đã sinh hợp đồng (bảng GĐ1).** `PeListItem.contractId` (`fe-user/src/types/purchaseEvaluation.ts:126`) **có thật trong payload đang tải** — backend chiếu thẳng `x.e.ContractId` vào DTO (`src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs`, dòng projection `x.e.SelectedSupplierId, …, x.e.ContractId, …`). Phiếu nào `contractId != null` là phiếu đã sinh hợp đồng, tức GĐ3 **đã tới**; vậy mà chấm GĐ3 vẫn báo "chưa tới". Không tốn thêm một request nào để biết điều này.
Tác giả có ghi lý do trong mã (`:331-332`): không suy diễn "có HĐ ⇒ GĐ1/GĐ2 xong" vì chưa có cầu nối per-phiếu. Lập luận đó ĐÚNG cho chiều "đã xong", nhưng câu chữ được chọn lại khẳng định chiều ngược lại — "chưa tới" cũng là một khẳng định, và là khẳng định sai. Không biết thì phải nói là không biết.
Đây đúng lớp lỗi đợt-1 đã ghi (FLAG-3 "nói quá điều dữ liệu chứng minh được"), tái phát ở chỗ mới.
**Fix đề nghị:** (a) đổi `DOT_TEXT.none` thành câu trung tính, ví dụ "chưa có thông tin ở giai đoạn này", và bỏ mệnh đề "các giai đoạn còn lại chưa tới" khỏi `aria-label`; (b) nếu muốn đúng hơn nữa mà vẫn 0 request: phiếu có `contractId != null` thì cho chấm GĐ3 mang trạng thái riêng (ví dụ "đã sinh hợp đồng").
**FLAG-2 · bổ sung (ca thứ ba, cùng một lớp lỗi):** hợp đồng ở phase `DangDongDau(8)` hoặc `DaPhatHanh(9)` — tức đã đóng dấu / đã phát hành, đúng phần việc mà chính trang này gọi là **giai đoạn 4 "Hợp đồng cứng" (bước 19 → 21)** — vẫn được `contractDot` (`:410-416`) chấm vào **giai đoạn 3**, còn chấm giai đoạn 4 báo "chưa tới". Bản hợp đồng cứng đã phát hành rồi mà hành trình bảo giai đoạn hợp đồng cứng chưa tới.
### FLAG-3 [L] — nút phân trang tính từ DỮ LIỆU TRẢ VỀ chứ không từ trang đang chọn ⇒ bấm nhanh 2 lần chỉ đi được 1 trang
`fe-user/src/pages/UserDashboardPage.tsx:611``:618``onClick={() => onPage(data.page - 1)}` / `onPage(data.page + 1)`, trong đó `data` là dữ liệu **đã tải xong**, không phải state `page`. Hai bảng đều bật `placeholderData: keepPreviousData` (`:1151`, `:1163`), nghĩa là trong lúc trang mới đang bay thì `data` vẫn là trang cũ.
Hệ quả đo được từ mã: đang ở trang 1, bấm "Sau" → `setPage(2)`, request bay, `data.page` vẫn = 1. Bấm "Sau" lần nữa trước khi request về → tính ra `1 + 1 = 2``setPage(2)` trên state đã là 2 ⇒ **không có gì xảy ra**. Người dùng bấm hai lần nhưng chỉ tiến một trang, và nút "Sau"/"Trước" trong khoảng đó bật/tắt theo `hasNext`/`hasPrev` của trang cũ.
Kèm theo, cùng chỗ này có một ca hiển thị vô nghĩa: nếu tổng số bản ghi giảm trong lúc đang xem trang cuối (người khác xoá phiếu, rồi query refetch), `from = (page-1)*pageSize+1` có thể lớn hơn `data.total`, dòng `:607-608` in ra dạng "Hiển thị 4130 trong 30 phiếu" và bảng rơi vào empty-state "Chưa có phiếu Duyệt NCC nào" trong khi thực tế vẫn còn 30 phiếu.
**Fix đề nghị:** tính từ state — truyền `page` xuống `Pager` và dùng `onPage(page + 1)` / `onPage(page - 1)`; thêm chốt `if (data.page > data.totalPages && data.totalPages > 0) setPage(1)` (hoặc kẹp `from = Math.min(from, data.total)`).
---
## TRỤC 2 — param BE có thật không, và tab không chọn có bắn request không
Đã đọc thẳng controller, không tin lời khai:
| Điều được khai | Đo ở đâu | Kết quả |
|---|---|---|
| `/purchase-evaluations` nhận `page`·`pageSize`·`search`·`sortDesc`·`type`·`phase`·`projectId`·`approvalWorkflowId` | `src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs:18-28` | ĐÚNG cả 8 |
| `/contracts` nhận `page`·`pageSize`·`search`·`sortDesc`·`phase`·`supplierId`·`projectId` | `ContractsController.cs:16-24` | ĐÚNG cả 7 |
| `search` PE lọc `MaPhieu TenGoiThau Project.Name Project.Code` | `PurchaseEvaluationFeatures.cs` (khối `if (!string.IsNullOrWhiteSpace(request.Search))`) | ĐÚNG — placeholder `:1352` "Tìm mã phiếu, gói thầu, dự án…" mô tả đúng chừng đó, không hứa quá |
| `search` HĐ lọc `MaHopDong TenHopDong Supplier.Name Project.Name` | `ContractFeatures.cs:309-316` | ĐÚNG — placeholder `:1380` khớp |
| `pendingMe` KHÔNG phải param BE | `PurchaseEvaluationsController.cs:30-35``/inbox` là endpoint riêng, trả `List<>` phẳng | ĐÚNG, và mã ghi rõ điều này ở đầu file (`:33-35`) |
| `hasNext`/`hasPrev`/`totalPages` do BE tính | `src/Backend/SolutionErp.Application/Common/Models/PagedResult.cs:9-11` | CÓ THẬT (`HasNext => Page*PageSize < Total`, `HasPrev => Page > 1`) ⇒ nút biên bind thẳng là đúng |
**Lazy per-tab:** `enabled: stage === 1` (`:1150`) và `enabled: stage === 3` (`:1162`). Đứng ở tab GĐ2 hoặc GĐ4 ⇒ **0 request danh sách**. Đứng ở GĐ1 ⇒ chỉ `dash-stage-pe` bắn, `dash-stage-contracts` im, và ngược lại. ✅ đúng như khai.
Tổng request lúc mở trang (tab GĐ1): 5 query nền (`my-dashboard`, `pipeline-pe-total`, `pipeline-contract-total`, `pipeline-pe-approved`, `pe-inbox`) + 1 query danh sách = **6**. Bản HEAD là 5. Tức +1 request, nhưng 2 query `*-recent``pageSize=5` đã rút xuống `pageSize=1` nên tải về NHẸ hơn. Không phải bão request: `main.tsx:7-14` đặt `refetchOnWindowFocus:false` + `retry:1`.
## TRỤC 5 — hồi quy so với vòng-1 (đo HEAD ⟂ cây làm việc, không đo bằng trí nhớ)
Nền so sánh: `HEAD` = 766 dòng và ĐÃ chứa hero + các bản vá vòng-1 (đợt 1-3 đã commit) ⇒ diff 984+/337 này đúng là đợt-4 đứng một mình.
| Thứ phải còn sống | Phép đo | HEAD | Cây làm việc | Kết luận |
|---|---|---|---|---|
| Bản vá FLAG-1 vòng-1 (aria-label mang tên giai đoạn) | đếm `group=` và ``aria-label={`${group}`` | 6 / 1 | 6 / 1 | CÒN NGUYÊN |
| Nhãn 4 GĐ chốt | so từng chuỗi với `components/pe/PePipelineStrip.tsx:15-19` (nguồn chuẩn) | — | `Duyệt NCC` · `Kế hoạch ký kết HĐ` · `Duyệt hợp đồng` · `Hợp đồng cứng` | KHỚP TỪNG CHỮ; nay còn gom về một mảng `STAGES` (`:131-148`) nên hero và tab không thể lệch nhau |
| `['pe-inbox']` dùng chung + `select` đếm | so key ⟂ hàm tải với `pages/InboxPage.tsx:76-79` | 1 | 1 | CÒN NGUYÊN — cùng key, cùng hàm tải trả MẢNG, chỉ `select: d => d.length` ở phía quan sát (`:1070-1074`) nên không phá cache của Hộp thư |
| Bản vá FLAG-2 vòng-1 (chữ "Pipeline" / "Upload") | đếm "Toàn trình 4 giai đoạn" và "Tải lên bản hợp đồng" | 1 / 1 | 1 / 1 | CÒN NGUYÊN |
| Tiếng Việt 100% ở phần MỚI | rút mọi chuỗi hiển thị mới rồi soi từng chuỗi | — | — | KHÔNG lọt chữ Anh nào. (`Sắp`, `Sắp triển khai`, `Trước`, `Sau`, `Trang`, `Hiển thị`, `Xoá ô tìm kiếm`, `Mở màn Duyệt NCC`, `Hành trình`, `(chưa cấp mã)`, `(chưa đặt tên)`… đều thuần Việt) |
| 0 dependency mới | `git diff --stat -- fe-user/package.json fe-admin/package.json` | — | **rỗng** | 0 thay đổi |
| Named export | `grep "export function UserDashboardPage"` | 1 | 1 | CÒN NGUYÊN |
| Không đụng `fe-admin` | `git diff --name-only \| grep fe-admin` | — | **0 file** | THOẢ |
| Đích điều hướng của hero | liệt kê mọi `navigate(...)` ở HEAD ⟂ cây làm việc | 8 dạng | 10 dạng | 8 dạng cũ giữ y nguyên, 2 dạng mới (`/purchase-evaluations/new`, `/coming-soon?stage=N`) đều là route CÓ THẬT (`App.tsx:52,65`) |
**Ghi chú về một con số thoạt nhìn giống hồi quy:** chuỗi "Duyệt hợp đồng" giảm 5 → 2. Truy ra: HEAD có 1 lần ở chú thích + 1 lần `title=` + 3 lần `group=` (truyền tay cho từng dòng số liệu); nay còn 1 chú thích + 1 lần khai trong `STAGES`, còn `group` đọc gián tiếp qua `STAGES[2].title`. Không mất nhãn nào.
### FLAG-4 [L] — người dùng trình đọc màn hình không được báo khi kết quả tìm kiếm đổi
`fe-user/src/pages/UserDashboardPage.tsx:606` đặt `aria-live="polite"` lên dòng "Hiển thị xy trong N", nhưng chính khối chứa nó bị gỡ khỏi cây khi không có kết quả (`:601` — `if (!data || data.total === 0) return null`). Nghĩa là đúng lúc cần thông báo nhất — gõ một từ khoá không khớp gì — vùng thông báo biến mất, và khối trạng thái rỗng thay thế nó (`:704-731`) thì không nằm trong bất kỳ vùng `aria-live` nào. Người dùng bàn phím/trình đọc gõ xong sẽ không nghe gì cả và không biết bảng vừa đổi.
**Fix đề nghị:** giữ vùng `aria-live="polite"` luôn tồn tại (khi rỗng thì thông báo "Không tìm thấy phiếu nào khớp"), hoặc gắn `role="status"` cho khối trạng thái rỗng.

File diff suppressed because it is too large Load Diff

View File

@ -1861,7 +1861,7 @@ public static class DbInitializer
var label = typeLabels.GetValueOrDefault(code, code); var label = typeLabels.GetValueOrDefault(code, code);
// [S159-đợt3] GĐ4 skeleton — 1 leaf/loại HĐ dưới root Hợp đồng cứng, tái dùng // [S159-đợt3] GĐ4 skeleton — 1 leaf/loại HĐ dưới root Hợp đồng cứng, tái dùng
// đúng nhãn đã đánh số 01.-07. (leaf → /coming-soon?stage=4). // đúng nhãn đã đánh số 01.-07. (leaf → /coming-soon?stage=4).
tree.Add(("Hdc_" + code, label, MenuKeys.HopDongCung, hdcOrder++, "FileText")); tree.Add(("Hdc_" + code, label + " - Bảng cứng", MenuKeys.HopDongCung, hdcOrder++, "FileText")); // [S159-đợt4] owner vẽ: "01. HĐ Thầu phụ - Bảng cứng"
tree.Add((MenuKeys.ContractTypeGroup(code), label, MenuKeys.Contracts, order++, "FileText")); tree.Add((MenuKeys.ContractTypeGroup(code), label, MenuKeys.Contracts, order++, "FileText"));
tree.Add((MenuKeys.ContractTypeList(code), "Danh sách", MenuKeys.ContractTypeGroup(code), order++, "List")); tree.Add((MenuKeys.ContractTypeList(code), "Danh sách", MenuKeys.ContractTypeGroup(code), order++, "List"));
tree.Add((MenuKeys.ContractTypeCreate(code), "Thao tác", MenuKeys.ContractTypeGroup(code), order++, "Plus")); tree.Add((MenuKeys.ContractTypeCreate(code), "Thao tác", MenuKeys.ContractTypeGroup(code), order++, "Plus"));
@ -1958,6 +1958,14 @@ public static class DbInitializer
// [S159] 2 root rename theo 4-GĐ (prod đã SQL cùng ngày ⇒ NO-OP trên prod). // [S159] 2 root rename theo 4-GĐ (prod đã SQL cùng ngày ⇒ NO-OP trên prod).
[MenuKeys.PurchaseEvaluations] = "Duyệt NCC", [MenuKeys.PurchaseEvaluations] = "Duyệt NCC",
[MenuKeys.Contracts] = "Duyệt hợp đồng", [MenuKeys.Contracts] = "Duyệt hợp đồng",
// [S159-đợt4] 7 leaf Hdc_ thêm hậu tố " - Bảng cứng" (owner vẽ; prod SQL cùng ngày).
["Hdc_ThauPhu"] = "01. HĐ Thầu phụ - Bảng cứng",
["Hdc_GiaoKhoan"] = "02. HĐ Giao khoán - Bảng cứng",
["Hdc_NhaCungCap"] = "03. HĐ Nhà cung cấp - Bảng cứng",
["Hdc_DichVu"] = "04. HĐ Dịch vụ - Bảng cứng",
["Hdc_MuaBan"] = "05. HĐ Mua bán - Bảng cứng",
["Hdc_NguyenTacNcc"] = "06. HĐ Nguyên tắc NCC - Bảng cứng",
["Hdc_NguyenTacDv"] = "07. HĐ Nguyên tắc Dịch vụ - Bảng cứng",
[MenuKeys.ContractTypeGroup("ThauPhu")] = "01. HĐ Thầu phụ", [MenuKeys.ContractTypeGroup("ThauPhu")] = "01. HĐ Thầu phụ",
[MenuKeys.ContractTypeGroup("GiaoKhoan")] = "02. HĐ Giao khoán", [MenuKeys.ContractTypeGroup("GiaoKhoan")] = "02. HĐ Giao khoán",
[MenuKeys.ContractTypeGroup("NhaCungCap")] = "03. HĐ Nhà cung cấp", [MenuKeys.ContractTypeGroup("NhaCungCap")] = "03. HĐ Nhà cung cấp",