Compare commits

...

2 Commits

Author SHA1 Message Date
2a7269581c [CLAUDE] Infra: go nhanh S92 khoi revoker (owner supersede: hien thi het) + skeleton menu GD2/GD4 + ComingSoonPage x2
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m56s
- RevokeTemporarilyHiddenModulesAsync: GO Contracts+Ct_*/Master-set khoi predicate (bai tra gia: grant SQL tay bi revoker lat o restart #423 - 447/494 row roi; regrant=447 da chay truoc commit nay). Hrm/Off/Personal GIU (y owner: vung dang-lam chi admin)
- reviewKeys += ContractMenuKeys + MasterMenuKeys + KhkkKeys + HdcKeys (CanRead-only all-role, fresh-env hoi tu)
- Seeder tree.Add: Khkk_G1 + 4 leaves (khuon Duyet NCC) + Hdc_x7 (tai dung nhan 01.-07.) - prod da INSERT truc tiep (perm=156)
- ComingSoonPage mirror x2 (md5-identical) + route /coming-soon x2 + staticMap 11 leaf x2 (gotcha #50)
- Test: AdminOnlyModulesRevokeTests tach StillHidden/Reopened + REGRESSION-GUARD S159 (re-add nhanh S92 => do); ProcurementMasterAccessSeedTests doc lai isolation bang CO CAO (spec-change S92->S159, update test + code chung commit)
- 562 test PASS - nghiem thu cuoi = restart THAT khi deploy run nay (khuon S91)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 12:34:15 +07:00
b5a7dd8a92 wal: flush 20260729T1207 2026-07-29 12:07:47 +07:00
15 changed files with 466 additions and 45 deletions

View File

@ -1,11 +1,9 @@
# CI/CD Monitor Agent — Persistent Memory # CI/CD Monitor Agent — Persistent Memory
- **07-28 verify `a52a91e` (PE: cảnh báo amber ô Địa điểm khi dự án thiếu Location) [S157, FE-ONLY 2-app, CLEAN-return]:** VERDICT **PASS 9/9** — Run **#419** (id=532) success **5m40s** (09:40:18→09:45:58+07); gate **562 EXACT từ log** (45 Domain + **517** Infra, 0 fail/skip) — khớp canonical `docs/STATUS.md:452` **cả TỔNG lẫn TÁCH 45/517** (khớp thành-phần > khớp tổng). NO-Mig đúng kỳ vọng: `git diff f4494cf..a52a91e -- 'src/Backend/*' '*Migrations*' | wc -l` = **0** ⇒ Mig vẫn 68, 89-table, KHÔNG query DB (không có gì để đối chiếu). 🔑 **Phép quyết định = byte-marker #77, KHÔNG tin status CI:** tải thẳng JS prod, `bytes.count(marker.encode())` 4 chuỗi VN độc lập → **2/2 app, count=2 mỗi app = KHỚP SỐ FILE/app trong diff** (`PeHeaderForm`+`PeWorkspaceCreateView`) — "đúng-BỘI-SỐ" mạnh hơn "tìm thấy ≥1", bắt được cả ca ship nửa-vời. Bundle js rotate CẢ 2 (admin `BK8w3QxU`→**`8-QYcMOg`** · eoffice `DYnxSI0T`→**`DptYR4wL`**; css `DiNJjaoo`/`BRCvEHvC` FROZEN ×2 = #69-REFINE lành tính — utility `text-amber-700`/`text-[11px]` đã có sẵn) real-size 1.675.282/1.575.513B, LM 09:44:33/09:45:30+07 in-window. 🆕 **Control âm 2 TẦNG (tái dụng):** (i) fake-hash→200 nhưng `ct=text/html` 900/876B marker=0; (ii) **hash CŨ cũng rơi về text/html ⇒ bundle cũ BIẾN MẤT khỏi đĩa** = chứng GHI-ĐÈ thật (mạnh hơn hash-delta đơn thuần). Smoke: health live+ready 200 · login 200 tok-468 · `/api/projects` 200 (đúng nguồn `location` feature đọc) · PE/menus/suppliers 200. ⚠️ **Self-correct on-disk:** viết "range=6 file" SAI (đó là số của 2 commit) — range thật **13 commit/26 file** (11 `wal:` xen giữa); đính chính ngay, BE=0 không đổi. Tag `[run-419, pass-9-9, fe-only, marker-2of2-count-exact, old-hash-gone-control, js-rotate-css-frozen, gate-562]` - **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]`
- _(**S155 #416 `b1bae77` Workflow duyệt-TẠI-CHỖ + cờ F6 `AllowApproverDelete` GO 6/6** — gate 551 [45+506, khớp local 2-nguồn]; **Mig 68 `AddPeAllowApproverDelete` APPLIED-PROD** [history-top + INFORMATION_SCHEMA cùng có, 89-table]; 🔑 reviewer-FLAG F8 "mig rớt khỏi commit" loại-trừ 3 lớp — mạnh nhất = `GET /approval-workflows-v2` trả `allowApproverDelete` ×54 **materialize qua EF từ DB thật** (thiếu cột thì query đã ném) · verbatim → `archive/2026-07.md` @S157-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)_
- _(**S135 #407 `4510cea` mã-dự-án normalize + PE attach-gate GO 5/5** gate 532 [45+487, +4 delta-proof]; NO-Mig `git diff -- *Migrations*` EMPTY + history-top unchanged; js-rotate/css-frozen ×2 · verbatim → `archive/2026-07.md` @S155-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`)_
- _(**S134 #399 `6664a61` PE lũy-kế tạm-tính (0-mig) GO 5/5** — gate 528 [45+483, +8 PePendingAccumulation]; js+css rotate ×2 admin `BVCukFP5`/`CBzUlgOA` eoffice `DoxyEOxh`/`BHsBUA8e`; byte-verify #77 urllib-BYTES; 🔑 **ct=text/html discriminator SẠCH HƠN size-919B** [shell size drift per-build]; `body.count(marker.encode())` bulletproof cp1252 · verbatim → `archive/2026-07.md` via `_INDEX`)_
- _(**S133 #398 `d4fbced` PE budget-freeze + Mig 67 `AddPeApprovedBudgetSnapshot` APPLIED-PROD GO 5/5** — gate 520 [45+475]; 11-col ApprovedBudget\* + backfill 18/18 DaDuyet + 89-table; js+CSS rotate ×2 [badge utility-class → CSS content-hash đổi, benign #69]; python text=True crash cp1252 → text=False bytes+json.loads · verbatim → `archive/2026-07.md` via `_INDEX`)_
> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT). > **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT).
> Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53). > Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53).
@ -21,7 +19,7 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve
## ⚠️ cicd-lane return-garble tracker (#53) ## ⚠️ cicd-lane return-garble tracker (#53)
cicd-spawn đôi khi trả memory-curation slice ("Compressing S112/S111") thay verdict (×5/6 qua S114; cumulative ×18 all-agents [[feedback_agent_return_garble_recover]], root-cause = curate-at-return). Recover: em-main-solo curl-verify. **S115 (#385)+S116 (#386) cicd-lane CLEAN-return no-garble ×2; S117 (#387) garble→SendMessage-resume (em-main curl cross-verify khớp, verdict disk-true) — return-mechanics garbled NHƯNG verdict-substance OK.** **S118 (#388+#389) cicd-lane garble ×2 → em-main-solo curl+sqlcmd recover (#388 durability tra.bui=20 seed-race "9 upgraded" · #389 Reports.Read→Admin-only+401), both verdict-substance disk-verified OK; curate-at-return root-cause persists (harvest-curator S118-end bắt nhãn "CLEAN-return" tự-dán sai → habitual pattern).** cicd-lane đôi khi trả nhầm slice transcript (memory-curation) thay verdict **tao TÁI PHẠM dai nhất trong roster** (×5/6 qua S114; S117/S118 ×3 nữa; cumulative all-agents ×66-cận-dưới [[feedback_agent_return_garble_recover]]). Từng **tự dán nhãn 'CLEAN-return' SAI** (harvest-curator bắt S118) **KHÔNG tự phong return sạch** mình không quan sát được return của chính mình. 🔒 **Đối-sách đã chứng minh (S158, xác nhận lần 5): ghi-đĩa TRONG LÚC LÀM.** Prompt đúng KHÔNG chặn được garble; artifact mới chặn vai có-ép-ghi-đĩa mất **0 B**, vai không-ép mất TOÀN BỘ. Mỗi lượt: file evidence trong run-folder SẢN PHẨM CHÍNH, ghi từng mục ngay khi bằng chứng, kết bằng dòng `END … VERDICT=`; return chỉ tóm tắt. Recover khi garble: em-main re-curl + sqlcmd 2-nguồn-khớp, hoặc đọc thẳng file evidence.
--- ---
@ -33,9 +31,8 @@ cicd-spawn đôi khi trả memory-curation slice ("Compressing S112/S111") thay
- **#25 IIS WebSocket** `notification-hub/negotiate` 401/404 prod. Fix: WebSocket module enable `web.config` site api (skill `iis-deploy-runbook`). - **#25 IIS WebSocket** `notification-hub/negotiate` 401/404 prod. Fix: WebSocket module enable `web.config` site api (skill `iis-deploy-runbook`).
- **#48 SQLite tie-break** `OrderByDescending(CreatedAt).First()` pick wrong khi 2+ `.Add()` cùng frozen-clock. Fix: discriminator filter BEFORE OrderBy. - **#48 SQLite tie-break** `OrderByDescending(CreatedAt).First()` pick wrong khi 2+ `.Add()` cùng frozen-clock. Fix: discriminator filter BEFORE OrderBy.
- **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 (S72 #312) FE bundle hash NON-DETERMINISTIC per rebuild.** `deploy.yml` `Remove-Item fe-*\* -Exclude web.config`+`Copy-Item dist\*` runs UNCONDITIONALLY every run (path-filter gates whole-workflow, NOT per-step). "BE/governance frozen" can be FALSE; rotation possible every deploy. **To detect REAL FE change, diff `fe-*/src` in commit/range, NOT hash-delta.** - **🔴 #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 rewrites `/*``/index.html` so GET `/assets/index-<ANYTHING>.js` returns **200** even for fake hash (control `ZZdoesnotexist0.js`200, size~919b text/html). Old-hash-still-200 MEANINGLESS. RELIABLE signal = parse index.html `<script src>`, GET that exact path + check `size_download` LARGE (real ~1.6MB vs fake ~919b) + `Last-Modified` in deploy window + 2nd-fetch byte-stable (no mid-deploy transient). - **⚠ 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).**
- **🟢 #69-REFINE (S83 #333) hash can stay IDENTICAL across deploy when fe-*/src 0-change.** #69 "rotates EVERY deploy" NOT absolute: identical Vite source byte-identical output same content-hash. **MOST RELIABLE ship-proof = `Last-Modified` on real bundle IN deploy-window + real-size, NOT hash-delta** (hash-delta false-NEG when src unchanged, false-POS risk per #69). 0-FE-change hash EITHER same (#333/#485-487/#495/#385-S115) OR rotated (#312/#489) both benign; ship-proof = fresh LM-in-window + real-size + marker.
- **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.
--- ---
@ -45,7 +42,7 @@ cicd-spawn đôi khi trả memory-curation slice ("Compressing S112/S111") thay
- **Stage 0 RAG infra:** `Get-Service Qdrant` Running + `http://localhost:6333/healthz`. Collection `proj_solution_erp`. - **Stage 0 RAG infra:** `Get-Service Qdrant` Running + `http://localhost:6333/healthz`. Collection `proj_solution_erp`.
- **Stage 1 Push+filter:** `git log -1 --format='%H %s'` + `git log origin/main..HEAD` empty + diff vs paths-ignore (docs-only SKIPPED-DOCS return). - **Stage 1 Push+filter:** `git log -1 --format='%H %s'` + `git log origin/main..HEAD` empty + diff vs paths-ignore (docs-only SKIPPED-DOCS return).
- **Stage 2 Gitea poll** (max 10 iter × 60s): API `.../actions/tasks?limit=5` (NOT `/runs` 404). Match `head_sha` (NOT run_number cancelled runs shift numbering). `updated_at` stale ~2min (gotcha #46) cross-check VPS mtime. Foreground-sleep BLOCKED Windows Monitor-style busy-wait-curl-spin ~30s/iter. - **Stage 2 Gitea poll** (max 10 iter × 60s): API `.../actions/tasks?limit=5` (NOT `/runs` 404). Match `head_sha` (NOT run_number cancelled runs shift numbering). `updated_at` stale ~2min (gotcha #46) cross-check VPS mtime. Foreground-sleep BLOCKED Windows Monitor-style busy-wait-curl-spin ~30s/iter.
- **Stage 3 Test gate:** baseline **509 PASS** (45 Domain + 464 Infra; S118 #389 `c808b9a` 505+4 `AuthorizePolicyRegressionTests` ReportsController Reports.Read gate; Infra 460464, Domain frozen 45; prior 505 [#388 `c2135ba` ProcurementMasterAccessSeed], 501, 495, 486, 458, 440, 421). CI runs both proj BEFORE build/deploy status=success test gate passed (`tasks` terminal=`status:success`, `conclusion` NOT populated trust success NOT log-numeric). Local grep undercounts (Theory/InlineData). Phase 9 UAT skip OK. - **Stage 3 Test gate:** baseline **562 PASS** (45 Domain + 517 Infra; run #422 `b5799fc` 07-29; climb 562/551/532/528/520/509/505/501/495/486/458/440/421). CI chạy cả 2 proj TRƯỚC build/deploy `status:success` = gate passed (`tasks` KHÔNG populate `conclusion` tin `status`). Local grep undercount (Theory/InlineData). Phase 9 UAT skip OK.
- **Stage 4 Post-deploy** (if SUCCESS): auth login bearer (admin + nv.test gotcha #44; token=`accessToken` route `/api/auth/login`) 3-5 endpoint smoke 2XX (incl new NEW-endpoint bodyless-POST returns **411 IIS Length-Required pre-auth NOT 404**; re-probe WITH body 401 confirms wired) FE bundle hash 2 app (per #69 = real-size-vs-fake not hash-delta) SignalR negotiate (gotcha #25) EF mig prod==repo. - **Stage 4 Post-deploy** (if SUCCESS): auth login bearer (admin + nv.test gotcha #44; token=`accessToken` route `/api/auth/login`) 3-5 endpoint smoke 2XX (incl new NEW-endpoint bodyless-POST returns **411 IIS Length-Required pre-auth NOT 404**; re-probe WITH body 401 confirms wired) FE bundle hash 2 app (per #69 = real-size-vs-fake not hash-delta) SignalR negotiate (gotcha #25) EF mig prod==repo.
- **Stage 4.6 (S29 CRITICAL):** sqlcmd seed sample verify post-deploy (NOT chỉ schema). Seed=runtime-row-insert (no history/tables advance) verify via DB-query (e.g. Permissions count), NOT bundle-frozen-alone. - **Stage 4.6 (S29 CRITICAL):** sqlcmd seed sample verify post-deploy (NOT chỉ schema). Seed=runtime-row-insert (no history/tables advance) verify via DB-query (e.g. Permissions count), NOT bundle-frozen-alone.
- Discovery: ASP.NET 10 record enum needs numeric input unless `JsonStringEnumConverter` (SOL has NO converter FE sends numeric). sqlcmd ssh Windows-auth `\\\\SQLEXPRESS` 4-backslash. INFRASTRUCTURE seed MUST run (NOT inside `if(!demoSeedDisabled)`); DEMO seed gated gotcha #51. - Discovery: ASP.NET 10 record enum needs numeric input unless `JsonStringEnumConverter` (SOL has NO converter FE sends numeric). sqlcmd ssh Windows-auth `\\\\SQLEXPRESS` 4-backslash. INFRASTRUCTURE seed MUST run (NOT inside `if(!demoSeedDisabled)`); DEMO seed gated gotcha #51.
@ -63,9 +60,9 @@ cicd-spawn đôi khi trả memory-curation slice ("Compressing S112/S111") thay
- **Gitea:** `git.baocaogiaoduc.vn/vietreport-admin/solution-erp` · workflow `.gitea/workflows/deploy.yml` · paths-ignore 7-entry (D10 S111 #495): `['docs/**','**/*.md','.claude/skills/**','.claude/agent-memory/**','.claude/workflows/runs/**','.gitignore','scripts/**.md']`. Anon API works (public repo) when GITEA_TOKEN absent. - **Gitea:** `git.baocaogiaoduc.vn/vietreport-admin/solution-erp` · workflow `.gitea/workflows/deploy.yml` · paths-ignore 7-entry (D10 S111 #495): `['docs/**','**/*.md','.claude/skills/**','.claude/agent-memory/**','.claude/workflows/runs/**','.gitignore','scripts/**.md']`. Anon API works (public repo) when GITEA_TOKEN absent.
- **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 #419 `a52a91e` 07-28, khớp canonical `docs/STATUS.md:452` cả tách-phần; prior climb 551/532/528/520/509/505/501/495/486/458/440/421). Phase 9 UAT skip OK. 🔑 **Concrete count via web-UI log `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{run_number}/jobs/0/logs` (~18KB, NO `/api/v1/` prefix — api/v1 runs+tasks logs BOTH 404) → per-project `Passed! - Failed: 0, Passed: N, Skipped: 0`. Delta-proof new-tests = compare prev-run count (dotnet summary hides names). status=success alone ⟹ gate-passed (tests pre-deploy).** - **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 `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*'`.**
- **Bundle hash live:** ship-baseline = run #416 (07-27) admin js `CiUBEEJr`/css `DX5ew0wg` · eoffice js `DFh7GK0t`/css `BHsBUA8e` (real-size 1,670,035/1,570,257b; LM 12:41:28/12:42:22+07). PER #69 TWO-MODE = SNAPSHOT-per-run NOT stable-baseline (rotate on real-src, freeze byte-id on 0-src) ALWAYS re-snapshot LIVE post-success; **ship-proof = LM-in-window + real-size (vs 900b `ct=text/html` SPA-trap) + marker-grep, NOT hash-delta**. js-rotate/css-frozen = FE-logic-no-style fingerprint (S134-refine: `ct=text/html` cleaner discriminator than 919b, shell size drifts per-build). idrun_number (head_sha-keyed). - **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).
- **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 (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).
@ -75,7 +72,7 @@ Node CI `20.x` (`feedback_node_cicd`) · MediatR `12.4.1` (gotcha #1, flag `Vers
--- ---
## 🎯 Per-NV admin opt-in wire — 10-point checklist (cumulative S22→S23) ## 🎯 Per-NV admin opt-in wire — 10-point checklist (cumulative S22→S23)
Cross-ref `feedback_per_nv_permission_scope`. Per-NV/per-Level refactor MUST verify: 1 Domain field · 2 EF `HasDefaultValue(false)` · 3 Mig 3-file · 4 Service read · 5 Domain+App DTO mirror · 6 Designer FE checkbox · 7 AwLevelDto+ToDto · 8 CreateAwLevelInput+Update mutation · 9 **Lookup discrimination** (`FirstOrDefault` ADD `ApproverUserId==actorId` + admin fallback) · 10 **Controller body record count == Command record count**. Bug latency 2-3 days prod silent khi miss 9-10. Scan `grep -n "FirstOrDefault.*Order.*==" *.cs` after OR-of-N refactor. Cross-ref `feedback_per_nv_permission_scope`. Refactor per-NV/per-Level phải verify 10 điểm: Domain field · EF `HasDefaultValue(false)` · Mig 3-file · Service read · Domain+App DTO mirror · Designer FE checkbox · AwLevelDto+ToDto · Create/Update input · **(9) Lookup discrimination** (`FirstOrDefault` thêm `ApproverUserId==actorId` + admin fallback) · **(10) Controller body record count == Command record count**. Miss 9-10 bug prod im-lặng 2-3 ngày. Sau refactor OR-of-N: `grep -n "FirstOrDefault.*Order.*==" *.cs`.
## 📊 Run stats baseline ## 📊 Run stats baseline
BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code / 0s docs-only**. >5min → escalate. Recent runs all ~4m40-5m. BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code / 0s docs-only**. >5min → escalate. Recent runs all ~4m40-5m.
@ -84,10 +81,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`)
- _(**S128 #395 `289ba96` + S129 #396 `295c70c` + S132 #397 `72f68d1` GOVERNANCE/SCRIPTS/TOOLING GO** gate **509** [45+464, 0 fail/skip]; smoke 4×200; bundle benign #69 [S128 FULL-ROTATE from S117 → `yCkX-2Bp`/`DKSD4p9k`; S129+S132 FROZEN byte-id 0-fe-src] ship-proof = LM-in-window + real-size ≠919b NOT hash-delta; NO-BE/NO-Mig [Mig 66/89-table]; 🔑 **AS-6 #41: soi TỪNG file vs 7-glob** `.ps1` thoát `scripts/**.md` glob + `.claude/rag.json` / `.session-counter.json` thoát ⟹ non-.md governance VẪN trigger CI; `conclusion:None` dù success trust `status` [tái-xác-nhận S126/S128/S129/S132] · verbatim → `archive/2026-07.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`)_
- _(**S118 #389 `c808b9a` SECURITY-AUTHZ** ReportsController→Reports.Read GO 5m34s [gate 509=45+464 (+4 authz-reflection vs 505); DB `Reports.Read`→Admin-ONLY sqlcmd (Procurement CanRead=0 → non-admin 403 by-construction); no-token 401 class-`[Authorize]` intact; NO-FE/NO-Mig; cicd-lane GARBLED #53 → em-main curl+sqlcmd recovered, verdict disk-TRUE] · **+S117 #387 `316a82f`** PE duyệt-khoảng MinMax + Mig 66 `AddPeApprovedPriceRange` APPLIED-PROD [gate 501; js-rotate×2 css-frozen; byte-verify #77 4-str PRESENT; Mig66 `COL_LENGTH=9`+sys.tables 89; 🔑 pre-deploy snapshot BEFORE poll-success] · verbatim → `archive/2026-07.md` via `_INDEX`)_
- _(**S111-S116 #383-#386/#495/#496** PE multi-NCC per-hạng-mục (Mig 65 backfill) + nhập-giá-ÂM + submit-guard + Supplier-import Phase-B/v2 (Mig63 `AddSupplierImportSourceFields`/Mig64 `AddSupplierPublishState` APPLIED-PROD, IsPublic 37/37) + H22-WAL-adopt (deploy.yml self-change +2 paths-ignore) + GOVERNANCE GO [gate 458→495; js-rotate/css-frozen; 🔑 AS-6 baseline-shift memory-push HẾT trigger; 411-refine bodyless-POST→411 → re-probe-with-body 401/415≠404; #69-REFINE 0-src FROZEN-proof; byte-verify #77 str-PRESENT] · verbatim → `archive/2026-07.md` via `_INDEX`)_
- _(**S98-S110 + S126 #485-#494/#394** GOVERNANCE/MEMORY/TOOLING-push FROZEN-streak sau ROTATE #489 (#69 TWO-MODE; ship-proof = LM-in-window + real-size, hash-delta phụ); gate 440→509; Mig 62 + 89-table frozen · 🔑 #494 LM-newer-than-verified-window = missed-run detector; bare `/health`=404 UNMAPPED (đừng FAIL — thật `/health/live`+`/ready`) · 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 — 🔑 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`)_
- **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

@ -50,3 +50,8 @@
## Run #416 `b1bae77` (2026-07-27) — moved from L1 @S157 curate, byte-exact ## Run #416 `b1bae77` (2026-07-27) — moved from L1 @S157 curate, byte-exact
- **07-27 verify b1bae77 (Workflow: quy-trình duyệt TẠI CHỖ + cờ F6 `AllowApproverDelete` đợt-1) [S155, MIG-BEARING deploy, CLEAN-return]:** VERDICT **PASS 6/6** — Run **#416** (id=529) success **5m31s** (12:37:18→12:42:49+07); gate **551 EXACT từ log** (45 Domain + **506** Infra, 0 fail/skip) — **+19 vs run #407 (532)**, **KHỚP CHÍNH XÁC số lead đo local (551)** = cross-check 2-nguồn. **Mig 68 `20260727033522_AddPeAllowApproverDelete` VERIFIED-APPLIED-PROD** (AddColumn `AllowApproverDelete` bit NOT NULL default `(CONVERT([bit],(0)))` trên `ApprovalWorkflowLevels`; history-TOP + `INFORMATION_SCHEMA.COLUMNS` **cả 2 cùng có ⟹ không lệch**; sys.tables **89** giữ nguyên đúng AddColumn-no-new-table). 🔑 **Reviewer FLAG F8 (mig-file rớt khỏi commit ⟹ `Invalid column name` hạ 7 module V2) LOẠI TRỪ bằng 3 lớp:** (i) pre-deploy baseline 12:38 history-top còn `AddPeApprovedBudgetSnapshot` ⟹ mig áp TRONG cửa-sổ này không phải sẵn có; (ii) smoke 8/8 HTTP 200 `Invalid column name`=0 (PE·HĐ·Đề-xuất·Nghỉ-phép·Tăng-ca·Công-tác·Đặt-xe + AwV2); (iii) **phép mạnh nhất = `GET /api/approval-workflows-v2` trả `allowApproverDelete` ×54 materialize qua EF từ DB thật** — DB thiếu cột thì chính query này đã ném. Endpoint mới `PUT /api/approval-workflows-v2/{id}` no-token → **401** + **control route bịa → 404** (discriminator sạch, 401 có nghĩa). Bundle **JS rotate CẢ 2** (admin `hHISK1Ig`→**`CiUBEEJr`** css `CBzUlgOA`→**`DX5ew0wg`** rotate; eoffice `CJeXZso2`→**`DFh7GK0t`** css `BHsBUA8e` **FROZEN** = #69-REFINE lành tính, 2 file fe-user là logic/type no-new-utility) real-size 1.670.035/1.570.257B, LM 12:41:28/12:42:22+07 **in-window**; byte-verify #77 marker `allowApproverDelete` admin ×7 / eoffice ×1 ⟹ đúng mã F6 đã ship. SPA-trap control `ct=text/html` 900/876B. health live+ready 200. Tag `[run-416, pass-6-6, mig-bearing, F8-averted-3-layer, col-materialize-x54, js-rotate-both, gate-551-khớp-local]` - **07-27 verify b1bae77 (Workflow: quy-trình duyệt TẠI CHỖ + cờ F6 `AllowApproverDelete` đợt-1) [S155, MIG-BEARING deploy, CLEAN-return]:** VERDICT **PASS 6/6** — Run **#416** (id=529) success **5m31s** (12:37:18→12:42:49+07); gate **551 EXACT từ log** (45 Domain + **506** Infra, 0 fail/skip) — **+19 vs run #407 (532)**, **KHỚP CHÍNH XÁC số lead đo local (551)** = cross-check 2-nguồn. **Mig 68 `20260727033522_AddPeAllowApproverDelete` VERIFIED-APPLIED-PROD** (AddColumn `AllowApproverDelete` bit NOT NULL default `(CONVERT([bit],(0)))` trên `ApprovalWorkflowLevels`; history-TOP + `INFORMATION_SCHEMA.COLUMNS` **cả 2 cùng có ⟹ không lệch**; sys.tables **89** giữ nguyên đúng AddColumn-no-new-table). 🔑 **Reviewer FLAG F8 (mig-file rớt khỏi commit ⟹ `Invalid column name` hạ 7 module V2) LOẠI TRỪ bằng 3 lớp:** (i) pre-deploy baseline 12:38 history-top còn `AddPeApprovedBudgetSnapshot` ⟹ mig áp TRONG cửa-sổ này không phải sẵn có; (ii) smoke 8/8 HTTP 200 `Invalid column name`=0 (PE·HĐ·Đề-xuất·Nghỉ-phép·Tăng-ca·Công-tác·Đặt-xe + AwV2); (iii) **phép mạnh nhất = `GET /api/approval-workflows-v2` trả `allowApproverDelete` ×54 materialize qua EF từ DB thật** — DB thiếu cột thì chính query này đã ném. Endpoint mới `PUT /api/approval-workflows-v2/{id}` no-token → **401** + **control route bịa → 404** (discriminator sạch, 401 có nghĩa). Bundle **JS rotate CẢ 2** (admin `hHISK1Ig`→**`CiUBEEJr`** css `CBzUlgOA`→**`DX5ew0wg`** rotate; eoffice `CJeXZso2`→**`DFh7GK0t`** css `BHsBUA8e` **FROZEN** = #69-REFINE lành tính, 2 file fe-user là logic/type no-new-utility) real-size 1.670.035/1.570.257B, LM 12:41:28/12:42:22+07 **in-window**; byte-verify #77 marker `allowApproverDelete` admin ×7 / eoffice ×1 ⟹ đúng mã F6 đã ship. SPA-trap control `ct=text/html` 900/876B. health live+ready 200. Tag `[run-416, pass-6-6, mig-bearing, F8-averted-3-layer, col-materialize-x54, js-rotate-both, gate-551-khớp-local]`
## Run #419 `a52a91e` (2026-07-28) — moved from L1 @S159 curate, byte-exact
- **07-28 verify `a52a91e` (PE: cảnh báo amber ô Địa điểm khi dự án thiếu Location) [S157, FE-ONLY 2-app, CLEAN-return]:** VERDICT **PASS 9/9** — Run **#419** (id=532) success **5m40s** (09:40:18→09:45:58+07); gate **562 EXACT từ log** (45 Domain + **517** Infra, 0 fail/skip) — khớp canonical `docs/STATUS.md:452` **cả TỔNG lẫn TÁCH 45/517** (khớp thành-phần > khớp tổng). NO-Mig đúng kỳ vọng: `git diff f4494cf..a52a91e -- 'src/Backend/*' '*Migrations*' | wc -l` = **0** ⇒ Mig vẫn 68, 89-table, KHÔNG query DB (không có gì để đối chiếu). 🔑 **Phép quyết định = byte-marker #77, KHÔNG tin status CI:** tải thẳng JS prod, `bytes.count(marker.encode())` 4 chuỗi VN độc lập → **2/2 app, count=2 mỗi app = KHỚP SỐ FILE/app trong diff** (`PeHeaderForm`+`PeWorkspaceCreateView`) — "đúng-BỘI-SỐ" mạnh hơn "tìm thấy ≥1", bắt được cả ca ship nửa-vời. Bundle js rotate CẢ 2 (admin `BK8w3QxU`→**`8-QYcMOg`** · eoffice `DYnxSI0T`→**`DptYR4wL`**; css `DiNJjaoo`/`BRCvEHvC` FROZEN ×2 = #69-REFINE lành tính — utility `text-amber-700`/`text-[11px]` đã có sẵn) real-size 1.675.282/1.575.513B, LM 09:44:33/09:45:30+07 in-window. 🆕 **Control âm 2 TẦNG (tái dụng):** (i) fake-hash→200 nhưng `ct=text/html` 900/876B marker=0; (ii) **hash CŨ cũng rơi về text/html ⇒ bundle cũ BIẾN MẤT khỏi đĩa** = chứng GHI-ĐÈ thật (mạnh hơn hash-delta đơn thuần). Smoke: health live+ready 200 · login 200 tok-468 · `/api/projects` 200 (đúng nguồn `location` feature đọc) · PE/menus/suppliers 200. ⚠️ **Self-correct on-disk:** viết "range=6 file" SAI (đó là số của 2 commit) — range thật **13 commit/26 file** (11 `wal:` xen giữa); đính chính ngay, BE=0 không đổi. Tag `[run-419, pass-9-9, fe-only, marker-2of2-count-exact, old-hash-gone-control, js-rotate-css-frozen, gate-562]`

View File

@ -139,4 +139,5 @@
- 2026-07-17 · S133 #398 `d4fbced` PE budget-freeze + Mig67 AddPeApprovedBudgetS… · PASS · `2026-07.md` · substring:"S133 (07-17) verify d4fbced" - 2026-07-17 · S133 #398 `d4fbced` PE budget-freeze + Mig67 AddPeApprovedBudgetS… · PASS · `2026-07.md` · substring:"S133 (07-17) verify d4fbced"
- 2026-07-17 · S134 #399 `6664a61` PE lũy-kế tạm-tính (0-mig) GO — js+css rotate… · PASS · `2026-07.md` · substring:"S134 (07-17) verify 6664a61" - 2026-07-17 · S134 #399 `6664a61` PE lũy-kế tạm-tính (0-mig) GO — js+css rotate… · PASS · `2026-07.md` · substring:"S134 (07-17) verify 6664a61"
- 2026-07-23 · S155 #407 `4510cea` App chuan-hoa ma du-an normalize + PE attach-gate FE both-apps… · PASS 5/5 · `2026-07.md` · substring:"Run #407 `4510cea` (2026-07-23)" - 2026-07-23 · S155 #407 `4510cea` App chuan-hoa ma du-an normalize + PE attach-gate FE both-apps… · PASS 5/5 · `2026-07.md` · substring:"Run #407 `4510cea` (2026-07-23)"
- 2026-07-27 · S155 #416 `b1bae77` Workflow duyet-TAI-CHO + co F6 AllowApproverDelete… · PASS · `2026-07.md` · substring:"Run #416 `b1bae77` (2026-07-27)" - 2026-07-27 · S155 #416 `b1bae77` Workflow duyet-TAI-CHO + co F6 AllowApproverDelete… · PASS · `2026-07.md` · substring:"Run #416 `b1bae77` (2026-07-27)"
- 2026-07-28 · S157 #419 `a52a91e` PE canh-bao amber o Dia-diem khi du-an thieu Location… · PASS 9/9 · `2026-07.md` · substring:"Run #419 `a52a91e` (2026-07-28)"

View File

@ -0,0 +1,189 @@
# cicd-verify `b5799fc` — S159 L8 "Tổng quan tất cả luồng duyệt"
> 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.
> Bắt đầu: 2026-07-29 ~11:44+07 · agent cicd-monitor (READ-only)
---
## M1 — Push + path-filter · ✅ PASS
```
git log -1 → b5799fc5d373feaf94604cc4b4d9207cfb277826
[CLAUDE] FE-User: Tong quan tat ca luong duyet - pipeline 4 giai doan
2026-07-29 11:43:25 +0700
git log origin/main..HEAD → EMPTY (đã push thật)
```
**Range `bbc39be..b5799fc` = 8 commit** (b5799fc + 7 `wal:` flush xen giữa), **37 file**.
⚠️ Tự-đính-chính vs spec: spec nói "1 file" — đúng ở nghĩa **1 file CODE**, nhưng range thật 37 file
(36 file còn lại đều `.claude/**` governance/memory/run-trace + `*.md`).
**Soi TỪNG file vs 7-glob `paths-ignore` (AS-6, gotcha #41):**
- `.claude/agent-memory/**` (12 file) → ignored
- `.claude/workflows/runs/**` (21 file, gồm 2 `.txt`) → ignored
- `.claude/WAL.md` + `.claude/sessions/session-8/*.md` (3 file) → khớp `**/*.md` → ignored
- **`fe-user/src/pages/UserDashboardPage.tsx` → KHÔNG khớp glob nào ⇒ CI PHẢI fire.**
`git diff --stat` file code: **`fe-user/src/pages/UserDashboardPage.tsx` | 743 +++/---**
## M3 — Migration VẮNG là ĐÚNG · ✅ PASS (kỳ vọng, không phải cờ đỏ)
```
git diff --name-only bbc39be..b5799fc -- 'src/Backend/*' '*Migrations*' → 0 dòng
```
⇒ 0 BE, 0 migration. **KHÔNG query `__EFMigrationsHistory`** (không có gì để đối chiếu).
Mig prod vẫn kỳ vọng 68 `AddPeAllowApproverDelete` / 89 `sys.tables` — không đụng.
## Pre-deploy bundle snapshot (chốt TRƯỚC khi deploy land — bài học S117)
Đo lúc **2026-07-29 11:44:56+07** (run #422 đang `running`, chưa tới step deploy):
| App | js | css | HTTP |
|---|---|---|---|
| admin | `index-8-QYcMOg.js` | `index-DiNJjaoo.css` | 200 |
| user | `index-DptYR4wL.js` | `index-BRCvEHvC.css` | 200 |
**Khớp CHÍNH XÁC baseline S158** (admin `8-QYcMOg` · user `DptYR4wL`) ⇒ baseline hợp-lệ, deploy chưa land.
## M5-pre — Marker có trong SOURCE local (chốt kỳ vọng trước khi soi bundle prod)
`fe-user/src/pages/UserDashboardPage.tsx` (31.768 B), đếm BYTES utf-8:
| Chuỗi | count source | vai trò |
|---|---|---|
| `Tổng quan tất cả luồng duyệt` | **2** | header trang |
| `Toàn trình 4 giai đoạn` | **1** | h2 — CHỈ có ở bản SAU review-fix |
| `Đề xuất ký kết hợp đồng` | **2** | nhãn GĐ2 |
| `Pipeline 4 giai đoạn` | **0** | ✅ bản designer gốc đã bị thay |
| `sẵn sàng nối tiếp` | **0** | ✅ control ÂM — FLAG-3 đã gỡ |
⇒ Local ĐÚNG là bản-đã-fix. Bundle prod phải phản chiếu y hệt (dương 3 / âm 0).
## M7 — Menu-data prod áp TRƯỚC push (data-change, KHÔNG qua CI) · ✅ PASS 3/3
Đo **2026-07-29 ~11:46+07** (sau khi run #422 đã start, tức đã qua ít nhất 1 nhịp) — `sqlcmd -S .\SQLEXPRESS -E -d SolutionErp`:
**(a) 3 root ẩn — kỳ vọng 0×3:**
```
Hrm:0 · Off:0 · Personal:0 ✅ NGUYÊN (backup pre-apply ghi 1/1/1 → apply hạ về 0)
```
**(b) grant-set 38 key — kỳ vọng min=max=13:**
```
GRANT min=13 max=13 keys=38 minrows=13 ✅ khớp verify-post-apply
```
(grant-set = 9 key Master-subtree + `Contracts` + 28 `Ct_*`; khớp toán 9×11=99 UPDATE · 28×13+12=376 INSERT)
**(c) control ÂM `Pe_*` — kỳ vọng 197/185:**
```
WHERE MenuKey LIKE 'Pe%' → rows=197 canread=185 ✅ EXACT
```
🔎 **Tự-bắt lỗi hình-dạng-truy-vấn (đừng đọc nhầm thành cờ đỏ):** lần đo đầu tao dùng
`LEFT(MenuKey,3)='Pe_'` → ra **182/182**, LỆCH so với 197/185. Không vội FAIL — enumerate ra
nguyên nhân là **phạm-vi truy vấn**, không phải dữ liệu đổi:
- 14 key `Pe_*` × 13 = **182 rows / 182 canread**
- `LIKE 'Pe%'` gom thêm `Permissions` (1/1) + `Personal` (13/1) + `PeWorkflows` (1/1) = **+15 rows / +3 canread**
- 182+15 = **197** · 182+3 = **185** ⇒ khớp từng đồng, control âm NGUYÊN VẸN.
⇒ App pool recycle (nếu có) **không hề đụng** menu-data. M7 PASS.
## M2 — Poll terminal · ⏳ đang chờ
Run tìm được qua `actions/tasks?limit=5` khớp `head_sha`:
- **id=535 · run_number=422 · head_sha `b5799fc5` · created 2026-07-29T11:43:34+07 · status=`running`**
- (kề trên: #421 id=534 `4169e14` success — S158; #420 `33e7f68` cancelled ⇒ run_number lệch, phải key theo head_sha)
- poll 11:46:26 · 11:47:37 · 11:48:09 · 11:48:42 · 11:49:13 → `running`
- **11:49:45 → `status=success`** (upd 11:49:33). `conclusion=null` là BÌNH THƯỜNG với endpoint `tasks`
(tái xác nhận từ S126/S128/S132) ⇒ tin `status`, không tin `conclusion`.
- **Duration: 11:43:34 → 11:49:33 = 5m59s** (nhỉnh hơn dải gần đây 4m405m40, vẫn dưới ngưỡng escalate 10m)
### M2b — Test gate trong log · ✅ 562 EXACT
`…/actions/runs/422/jobs/0/logs` (17.910 B):
```
Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45, Duration: 149 ms - SolutionErp.Domain.Tests.dll
Passed! - Failed: 0, Passed: 517, Skipped: 0, Total: 517, Duration: 1m37s - SolutionErp.Infrastructure.Tests.dll
```
`Failed!` xuất hiện **0 lần** trong toàn log.
**45 + 517 = 562**, khớp baseline S158 **cả TỔNG lẫn TÁCH-PHẦN** (khớp thành-phần > khớp tổng).
Delta = 0 — đúng kỳ vọng cho commit FE-only không kèm test mới.
## M4 — Bundle rotate · ✅ PASS (đo, không giả định)
Đo **11:49:58+07**, ~25s sau khi run terminal:
| App | asset | TRƯỚC | SAU | KQ |
|---|---|---|---|---|
| **user** | js | `DptYR4wL` | **`0GO8mgrG`** | ✅ **ROTATE** (điều kiện PASS bắt buộc) |
| user | css | `BRCvEHvC` | **`BARVqkP-`** | ROTATE (utility-class mới: lưới pipeline/amber) |
| admin | js | `8-QYcMOg` | **`BHodkPNV`** | ROTATE — #69 mode-A, lành tính (CI build cả 2 app dù 0 admin-src) |
| admin | css | `DiNJjaoo` | `DiNJjaoo` | FROZEN byte-identical — #69-REFINE, 0 admin-src ⇒ CSS y hệt |
Cặp admin (js rotate + css frozen) = dấu-vân-tay "không có thay đổi nguồn admin", nhất quán với diff.
## M5 — Byte-marker trong bundle USER prod · ✅ PASS 3 dương + 2 âm
Tải THẲNG `https://eoffice.solutions.com.vn/assets/index-0GO8mgrG.js`, đếm trên **BYTES** (`b.count(s.encode('utf-8'))`, tránh bẫy decode cp1252):
```
200 · ct=application/javascript · size=1.584.514 B · LM=Wed, 29 Jul 2026 04:49:05 GMT (= 11:49:05+07)
```
LM **nằm trong cửa-sổ deploy** (run kết thúc 11:49:33) + real-size 1,58 MB ⇒ artifact thật, mới ghi.
| Chuỗi | bundle | code-occurrence trong source | KQ |
|---|---|---|---|
| `Tổng quan tất cả luồng duyệt` | **1** | 1 (L398 `title=`) | ✅ EXACT |
| `Toàn trình 4 giai đoạn` | **1** | 1 (L414 `<h2>`) | ✅ EXACT |
| `Đề xuất ký kết hợp đồng` | **1** | 1 (L476 `StageCard index={2}`) | ✅ EXACT |
| `Pipeline 4 giai đoạn` | **0** | 0 | ✅ bản designer CŨ VẮNG |
| `sẵn sàng nối tiếp` | **0** | 0 | ✅ **control ÂM VẮNG** (FLAG-3 đã gỡ) |
🔎 **Giải-trình lệch 2-vs-1 (đo được, không phẩy tay):** `grep -c` trên source ra **2** cho marker 1 và 3,
nhưng bundle chỉ **1**. Truy ra: occurrence dư nằm ở **COMMENT đầu file** (L1 `// "Tổng quan…"`,
L5 `// GĐ2 Đề xuất ký kết hợp đồng …`) — minifier strip comment. Literal JSX THẬT đúng **1 lần/marker**.
⇒ Bundle **khớp CHÍNH XÁC số occurrence-code**, mạnh hơn "tìm thấy ≥1".
🔒 **Kết luận then-chốt:** thấy `Toàn trình 4 giai đoạn` + KHÔNG thấy `Pipeline 4 giai đoạn`
⇒ prod đang chạy **bản SAU review-fix**, KHÔNG phải bản designer gốc. Đây là phép quyết định của lượt này.
### Control ÂM 2 TẦNG (SPA-fallback trap + chứng ghi-đè)
| Probe | HTTP | Content-Type | size | Đọc thế nào |
|---|---|---|---|---|
| `index-0GO8mgrG.js` (thật) | 200 | `application/javascript` | 1.584.514 | artifact thật |
| `index-ZZdoesnotexist0.js` (bịa) | **200** | **`text/html`** | **876** | SPA rewrite → 200 VÔ NGHĨA; `ct` là bộ phân-biệt sạch |
| `index-DptYR4wL.js` (**hash CŨ**) | **200** | **`text/html`** | **876** | 🔑 **bundle cũ ĐÃ BIẾN MẤT khỏi đĩa** ⇒ chứng GHI-ĐÈ thật, mạnh hơn hash-delta đơn thuần |
## M6 — Smoke · ✅ PASS 4/4
```
GET https://api.solutions.com.vn/health/live → 200 "Healthy"
GET https://api.solutions.com.vn/health/ready → 200 "Healthy"
GET https://admin.solutions.com.vn/ → 200 <!doctype html> lang="vi"
GET https://eoffice.solutions.com.vn/ → 200 <!doctype html> lang="vi"
```
(BE 0-change nên health = kiểm tra app pool sống sau recycle, không phải kiểm feature.)
---
## VERDICT: **PASS 9/9**
| # | Mục | KQ |
|---|---|---|
| 1 | Push + path-filter (CI fire đúng, không skip #41) | ✅ |
| 2 | Run #422 terminal `success` 5m59s | ✅ |
| 3 | Test gate **562** = 45 D + 517 I, 0 fail/skip | ✅ |
| 4 | Migration VẮNG là ĐÚNG (0 file BE/Mig) | ✅ |
| 5 | Bundle **user** rotate `DptYR4wL``0GO8mgrG` | ✅ |
| 6 | 3 marker DƯƠNG, count-exact vs code | ✅ |
| 7 | 2 marker ÂM vắng (`sẵn sàng nối tiếp` + `Pipeline 4 giai đoạn`) | ✅ |
| 8 | Smoke API health ×2 + FE origin ×2 | ✅ |
| 9 | Menu-data prod nguyên (0×3 · 38 key 13/13 · Pe 197/185) | ✅ |
**Không có cờ đỏ. Không khuyến nghị rollback.**
Ghi-chú cho lượt sau: baseline ship mới = **user js `0GO8mgrG` / css `BARVqkP-` · admin js `BHodkPNV` / css `DiNJjaoo`**;
gate giữ **562**; Mig vẫn **68** / 89 bảng (không đụng).
<!-- END cicd-verify-b5799fc · VERDICT=PASS 9/9 -->

View File

@ -28,3 +28,13 @@ Key IsVisible
Hrm 1 Hrm 1
Off 1 Off 1
Personal 1 Personal 1
## label-backup TRUOC danh so (2026-07-29T11:50:52+07:00)
Key Order Label
--- ----- -----
Ct_ThauPhu 31 HD Th?u ph?
Ct_GiaoKhoan 35 HD Giao kho<68>n
Ct_NhaCungCap 39 HD Nh<4E> cung c?p
Ct_DichVu 43 HD D?ch v?
Ct_MuaBan 47 HD Mua b<>n
Ct_NguyenTacNcc 51 HD Nguy<75>n t?c NCC
Ct_NguyenTacDv 55 HD Nguy<75>n t?c D?ch v?

View File

@ -37,6 +37,21 @@
· FLAG-6 [L] 2 nút Thử-lại trùng → **PARK** (reviewer tự đính chính giữa chừng; cosmetic, chấp nhận) · FLAG-6 [L] 2 nút Thử-lại trùng → **PARK** (reviewer tự đính chính giữa chừng; cosmetic, chấp nhận)
· FLAG-7 [L] nhãn GĐ4 → **NO_CHANGE** (đúng spec owner ❷/❹ — GĐ4 cố ý ngoài hệ thống) · FLAG-7 [L] nhãn GĐ4 → **NO_CHANGE** (đúng spec owner ❷/❹ — GĐ4 cố ý ngoài hệ thống)
- [x] **T4b — build độc lập SAU mọi edit** ✅ PASS 454ms (`tsc -b && vite build`) · 6/6 `group` land · diagnostics IDE giữa chừng = snapshot dở (#68), bác bằng compiler thật - [x] **T4b — build độc lập SAU mọi edit** ✅ PASS 454ms (`tsc -b && vite build`) · 6/6 `group` land · diagnostics IDE giữa chừng = snapshot dở (#68), bác bằng compiler thật
- [x] **T4c — commit đợt-1 `b5799fc` push → cicd `PASS 9/9`** (bundle user `DptYR4wL``0GO8mgrG`, 3 marker dương + 2 control âm, smoke 4/4; return garble #53 lần 10 — đĩa `cicd-verify-b5799fc.md` 9.930B trọn)
## ĐỢT-2 (3 lệnh mid-turn của anh, 11:24-11:5x)
**Owner verbatim:***"sửa giao diện nhanh"* ② ảnh tab strip 4 giai đoạn trên phiếu + *"cho hiển thị các tab phía trên như này nhé"**"Phần menu cũng hiển thị đúng như vậy luôn. (Trình ký Hợp đồng đổi thành Duyệt hợp đồng, ký cứng -> Hợp đồng cứng)"* · UAT anh Kiệt (chat FDC): *"cho đánh số thứ tự phía trước giúp anh — 01 . HĐ thầu phụ"*.
🔴 **Bộ tên 4-GĐ CHỐT MỚI NHẤT (supersede nhãn ❶ sáng nay):** Duyệt NCC · **Kế hoạch ký kết HĐ** · **Duyệt hợp đồng** · **Hợp đồng cứng** — tin-mới-nhất-thắng; nhãn "Đề xuất ký kết hợp đồng" (❶) chỉ sống 1 buổi, spec sẽ ghi cả 2 đời.
- [x] **Đ2-a — đánh số 01.-07. nhóm HĐ** ✅ prod LIVE tức thì (`renamed=7`, prepend-only **0 ký tự VN qua wire** — né E-010) + seeder `typeLabels` + labelBackfill 7 entry
- [x] **Đ2-b — menu 4-GĐ prod** ✅ SQL qua **file UTF-16 + scp** (3 nhãn VN mới an toàn): `ren_pe=1 ren_ct=1 perm=26` (=13×2, khớp toán) · root order: Duyệt NCC(25) → Kế hoạch ký kết HĐ(26) → Duyệt hợp đồng(31) → Hợp đồng cứng(32) · bẫy shell VPS = PowerShell 5.1 không nhận `&&`
- [x] **Đ2-c — `PePipelineStrip` mirror ×2 app** ✅ byte-identical (`ef7b78…`) + chèn `PeDetailTabs` ×2 giữ **md5 parity** (`5be2ca…`, edit qua python assert count==1) — GĐ1 active, GĐ2/GĐ4 badge "Sắp", stage không-active KHÔNG click (không giả link)
- [x] **Đ2-d — staticMap +2 route** (`KeHoachKyKet`/`HopDongCung``/dashboard`; thiếu là sidebar **drop SILENT** — gotcha #50) + đồng bộ 3 nhãn + 3 aria-group dashboard
- [x] **Đ2-e — build FE ×2 PASS + test 562 PASS** (seeder đụng ⇒ chạy đủ) → **commit `1a47a61` push** (8 file, 190+/18)
- [!] **Đ2-f — cicd verify `1a47a61`** đang chạy (kèm check seeder-re-run không double-insert: 12 root, 7 nhãn `01.` nguyên)
🔸 fe-admin sidebar: 2 root placeholder sẽ **drop silent** (không thêm staticMap admin — cố ý, admin không cần placeholder; ghi để khỏi đọc thành bug)
- [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

@ -5,6 +5,7 @@ import { ProtectedRoute } from '@/components/ProtectedRoute'
import { Layout } from '@/components/Layout' import { Layout } from '@/components/Layout'
import { LoginPage } from '@/pages/LoginPage' import { LoginPage } from '@/pages/LoginPage'
import { DashboardPage } from '@/pages/DashboardPage' import { DashboardPage } from '@/pages/DashboardPage'
import { ComingSoonPage } from '@/pages/ComingSoonPage'
import { SuppliersPage } from '@/pages/master/SuppliersPage' import { SuppliersPage } from '@/pages/master/SuppliersPage'
import { ProjectsPage } from '@/pages/master/ProjectsPage' import { ProjectsPage } from '@/pages/master/ProjectsPage'
import { DepartmentsPage } from '@/pages/master/DepartmentsPage' import { DepartmentsPage } from '@/pages/master/DepartmentsPage'
@ -55,6 +56,8 @@ function App() {
} }
> >
<Route path="/dashboard" element={<DashboardPage />} /> <Route path="/dashboard" element={<DashboardPage />} />
{/* [S159] trang dich cho menu skeleton GD2/GD4 */}
<Route path="/coming-soon" element={<ComingSoonPage />} />
<Route path="/master/suppliers" element={<SuppliersPage />} /> <Route path="/master/suppliers" element={<SuppliersPage />} />
<Route path="/master/projects" element={<ProjectsPage />} /> <Route path="/master/projects" element={<ProjectsPage />} />
<Route path="/master/departments" element={<DepartmentsPage />} /> <Route path="/master/departments" element={<DepartmentsPage />} />

View File

@ -29,6 +29,19 @@ const TYPE_CODE_TO_INT: Record<string, number> = {
// Ct_<Type>_<Action> sub-menu entries. // Ct_<Type>_<Action> sub-menu entries.
function resolvePath(key: string): string | null { function resolvePath(key: string): string | null {
const staticMap: Record<string, string> = { const staticMap: Record<string, string> = {
// [S159-đợt3] skeleton GĐ2/GĐ4 — leaf trỏ trang "Sắp triển khai" (query giữ
// active-state riêng từng mục; gotcha #50: thiếu route = sidebar drop SILENT).
Khkk_List: '/coming-soon?stage=2&m=list',
Khkk_Create: '/coming-soon?stage=2&m=create',
Khkk_Pending: '/coming-soon?stage=2&m=pending',
Khkk_Approved: '/coming-soon?stage=2&m=approved',
Hdc_ThauPhu: '/coming-soon?stage=4&t=01',
Hdc_GiaoKhoan: '/coming-soon?stage=4&t=02',
Hdc_NhaCungCap: '/coming-soon?stage=4&t=03',
Hdc_DichVu: '/coming-soon?stage=4&t=04',
Hdc_MuaBan: '/coming-soon?stage=4&t=05',
Hdc_NguyenTacNcc: '/coming-soon?stage=4&t=06',
Hdc_NguyenTacDv: '/coming-soon?stage=4&t=07',
Dashboard: '/dashboard', Dashboard: '/dashboard',
Suppliers: '/master/suppliers', Suppliers: '/master/suppliers',
Projects: '/master/projects', Projects: '/master/projects',

View File

@ -0,0 +1,54 @@
// ComingSoonPage — trang đích cho các mục menu skeleton GĐ2/GĐ4 (module chưa build).
// [S159 2026-07-29] Owner: menu hiển thị đủ 4 giai đoạn + phân list như Duyệt NCC
// NGAY TỪ BÂY GIỜ để mọi người góp ý — leaf chưa có module trỏ về đây thay vì
// link chết. MIRROR ×2 app BYTE-IDENTICAL (nút quay lại dùng navigate(-1) để
// không phụ thuộc route riêng từng app).
import { useNavigate, useSearchParams } from 'react-router-dom'
import { ArrowLeft, Hourglass } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { PePipelineStrip } from '@/components/pe/PePipelineStrip'
const STAGE_INFO: Record<string, { name: string; steps: string; desc: string }> = {
'2': {
name: 'Kế hoạch ký kết HĐ',
steps: 'Bước 7 → 12',
desc: 'Phiếu nối tiếp từ phiếu Duyệt NCC đã duyệt — duyệt mẫu/shopdrawing, so sánh giá và chốt giá trị ký kết hợp đồng.',
},
'4': {
name: 'Hợp đồng cứng',
steps: 'Bước 19 → 21',
desc: 'Lưu bản hợp đồng đã ký cứng (tải lên bản scan sau khi ký, đóng dấu và phát hành).',
},
}
export function ComingSoonPage() {
const navigate = useNavigate()
const [params] = useSearchParams()
const stage = params.get('stage') ?? '2'
const info = STAGE_INFO[stage] ?? STAGE_INFO['2']
return (
<div className="p-6">
<div className="mx-auto max-w-3xl rounded-lg border border-slate-200 bg-white shadow-sm">
<PePipelineStrip current={Number(stage)} />
<div className="flex flex-col items-center px-6 py-10 text-center">
<span className="icon-chip mb-3">
<Hourglass className="h-4 w-4" />
</span>
<h2 className="text-lg font-semibold text-slate-900">{info.name}</h2>
<div className="mt-1 text-xs font-medium text-slate-500">
Giai đoạn {stage} · {info.steps}
</div>
<p className="mt-3 max-w-md text-sm leading-relaxed text-slate-600">{info.desc}</p>
<p className="mt-2 rounded-md bg-amber-50 px-3 py-1.5 text-xs font-medium text-amber-700">
Phân hệ đang đưc triển khai cấu trúc menu hiển thị trước đ mọi người góp ý.
</p>
<Button variant="outline" size="sm" className="mt-5" onClick={() => navigate(-1)}>
<ArrowLeft className="h-3.5 w-3.5" />
Quay lại
</Button>
</div>
</div>
</div>
)
}

View File

@ -5,6 +5,7 @@ import { ProtectedRoute } from '@/components/ProtectedRoute'
import { Layout } from '@/components/Layout' import { Layout } from '@/components/Layout'
import { LoginPage } from '@/pages/LoginPage' import { LoginPage } from '@/pages/LoginPage'
import { UserDashboardPage } from '@/pages/UserDashboardPage' import { UserDashboardPage } from '@/pages/UserDashboardPage'
import { ComingSoonPage } from '@/pages/ComingSoonPage'
import { InboxPage } from '@/pages/InboxPage' import { InboxPage } from '@/pages/InboxPage'
import { SuppliersPage } from '@/pages/master/SuppliersPage' import { SuppliersPage } from '@/pages/master/SuppliersPage'
import { ProjectsPage } from '@/pages/master/ProjectsPage' import { ProjectsPage } from '@/pages/master/ProjectsPage'
@ -47,6 +48,8 @@ function App() {
} }
> >
<Route path="/dashboard" element={<UserDashboardPage />} /> <Route path="/dashboard" element={<UserDashboardPage />} />
{/* [S159] trang dich cho menu skeleton GD2/GD4 */}
<Route path="/coming-soon" element={<ComingSoonPage />} />
<Route path="/inbox" element={<InboxPage />} /> <Route path="/inbox" element={<InboxPage />} />
<Route path="/master/suppliers" element={<SuppliersPage />} /> <Route path="/master/suppliers" element={<SuppliersPage />} />
<Route path="/master/projects" element={<ProjectsPage />} /> <Route path="/master/projects" element={<ProjectsPage />} />

View File

@ -54,6 +54,19 @@ const INT_TO_PE_CODE: Record<number, string> = Object.fromEntries(
// /my-contracts (user's own drafts), Duyệt to /inbox (pending THEIR approval). // /my-contracts (user's own drafts), Duyệt to /inbox (pending THEIR approval).
function resolvePath(key: string): string | null { function resolvePath(key: string): string | null {
const staticMap: Record<string, string> = { const staticMap: Record<string, string> = {
// [S159-đợt3] skeleton GĐ2/GĐ4 — leaf trỏ trang "Sắp triển khai" (query giữ
// active-state riêng từng mục; gotcha #50: thiếu route = sidebar drop SILENT).
Khkk_List: '/coming-soon?stage=2&m=list',
Khkk_Create: '/coming-soon?stage=2&m=create',
Khkk_Pending: '/coming-soon?stage=2&m=pending',
Khkk_Approved: '/coming-soon?stage=2&m=approved',
Hdc_ThauPhu: '/coming-soon?stage=4&t=01',
Hdc_GiaoKhoan: '/coming-soon?stage=4&t=02',
Hdc_NhaCungCap: '/coming-soon?stage=4&t=03',
Hdc_DichVu: '/coming-soon?stage=4&t=04',
Hdc_MuaBan: '/coming-soon?stage=4&t=05',
Hdc_NguyenTacNcc: '/coming-soon?stage=4&t=06',
Hdc_NguyenTacDv: '/coming-soon?stage=4&t=07',
Dashboard: '/dashboard', Dashboard: '/dashboard',
Contracts: '/my-contracts', Contracts: '/my-contracts',
PurchaseEvaluations: '/purchase-evaluations', PurchaseEvaluations: '/purchase-evaluations',

View File

@ -0,0 +1,54 @@
// ComingSoonPage — trang đích cho các mục menu skeleton GĐ2/GĐ4 (module chưa build).
// [S159 2026-07-29] Owner: menu hiển thị đủ 4 giai đoạn + phân list như Duyệt NCC
// NGAY TỪ BÂY GIỜ để mọi người góp ý — leaf chưa có module trỏ về đây thay vì
// link chết. MIRROR ×2 app BYTE-IDENTICAL (nút quay lại dùng navigate(-1) để
// không phụ thuộc route riêng từng app).
import { useNavigate, useSearchParams } from 'react-router-dom'
import { ArrowLeft, Hourglass } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { PePipelineStrip } from '@/components/pe/PePipelineStrip'
const STAGE_INFO: Record<string, { name: string; steps: string; desc: string }> = {
'2': {
name: 'Kế hoạch ký kết HĐ',
steps: 'Bước 7 → 12',
desc: 'Phiếu nối tiếp từ phiếu Duyệt NCC đã duyệt — duyệt mẫu/shopdrawing, so sánh giá và chốt giá trị ký kết hợp đồng.',
},
'4': {
name: 'Hợp đồng cứng',
steps: 'Bước 19 → 21',
desc: 'Lưu bản hợp đồng đã ký cứng (tải lên bản scan sau khi ký, đóng dấu và phát hành).',
},
}
export function ComingSoonPage() {
const navigate = useNavigate()
const [params] = useSearchParams()
const stage = params.get('stage') ?? '2'
const info = STAGE_INFO[stage] ?? STAGE_INFO['2']
return (
<div className="p-6">
<div className="mx-auto max-w-3xl rounded-lg border border-slate-200 bg-white shadow-sm">
<PePipelineStrip current={Number(stage)} />
<div className="flex flex-col items-center px-6 py-10 text-center">
<span className="icon-chip mb-3">
<Hourglass className="h-4 w-4" />
</span>
<h2 className="text-lg font-semibold text-slate-900">{info.name}</h2>
<div className="mt-1 text-xs font-medium text-slate-500">
Giai đoạn {stage} · {info.steps}
</div>
<p className="mt-3 max-w-md text-sm leading-relaxed text-slate-600">{info.desc}</p>
<p className="mt-2 rounded-md bg-amber-50 px-3 py-1.5 text-xs font-medium text-amber-700">
Phân hệ đang đưc triển khai cấu trúc menu hiển thị trước đ mọi người góp ý.
</p>
<Button variant="outline" size="sm" className="mt-5" onClick={() => navigate(-1)}>
<ArrowLeft className="h-3.5 w-3.5" />
Quay lại
</Button>
</div>
</div>
</div>
)
}

View File

@ -1773,6 +1773,14 @@ public static class DbInitializer
// FE staticMap trỏ /dashboard. Prod đã INSERT trực tiếp cùng ngày (idempotent ở đây). // FE staticMap trỏ /dashboard. Prod đã INSERT trực tiếp cùng ngày (idempotent ở đây).
(MenuKeys.KeHoachKyKet, "Kế hoạch ký kết HĐ", null, 26, "FileSignature"), (MenuKeys.KeHoachKyKet, "Kế hoạch ký kết HĐ", null, 26, "FileSignature"),
(MenuKeys.HopDongCung, "Hợp đồng cứng", null, 32, "Stamp"), (MenuKeys.HopDongCung, "Hợp đồng cứng", null, 32, "Stamp"),
// [S159-đợt3] skeleton menu GĐ2/GĐ4 (owner vẽ: "viết hoa + phân list như
// Duyệt NCC" / "thêm Sub đánh số như Duyệt Hợp đồng"). Root CÓ CON ⇒ render
// MenuGroup uppercase tự động. Leaves → /coming-soon (staticMap ×2 app).
("Khkk_G1", "1. Kế hoạch ký kết HĐ (NCC-TP)", MenuKeys.KeHoachKyKet, 1, "FileCheck"),
("Khkk_List", "Danh sách", "Khkk_G1", 1, "List"),
("Khkk_Create", "Thao tác", "Khkk_G1", 2, "Plus"),
("Khkk_Pending", "Đang duyệt", "Khkk_G1", 3, "CheckCircle2"),
("Khkk_Approved", "Đã duyệt", "Khkk_G1", 4, "CheckCheck"),
(MenuKeys.Forms, "Biểu mẫu", null, 40, "FileSpreadsheet"), (MenuKeys.Forms, "Biểu mẫu", null, 40, "FileSpreadsheet"),
(MenuKeys.Reports, "Báo cáo", null, 50, "BarChart3"), (MenuKeys.Reports, "Báo cáo", null, 50, "BarChart3"),
(MenuKeys.System, "Hệ thống", null, 90, "Settings"), (MenuKeys.System, "Hệ thống", null, 90, "Settings"),
@ -1847,9 +1855,13 @@ public static class DbInitializer
// Per-type sub-menu under Contracts: 1 group + 3 leaves each // Per-type sub-menu under Contracts: 1 group + 3 leaves each
// (Danh sách / Thao tác / Duyệt). // (Danh sách / Thao tác / Duyệt).
var order = 31; var order = 31;
var hdcOrder = 1;
foreach (var code in MenuKeys.ContractTypeCodes) foreach (var code in MenuKeys.ContractTypeCodes)
{ {
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
// đúng nhãn đã đánh số 01.-07. (leaf → /coming-soon?stage=4).
tree.Add(("Hdc_" + code, label, MenuKeys.HopDongCung, hdcOrder++, "FileText"));
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"));
@ -2115,6 +2127,31 @@ public static class DbInitializer
// → "PeWf_*"/"PeWorkflows" KHÔNG match (loại admin Designer). // → "PeWf_*"/"PeWorkflows" KHÔNG match (loại admin Designer).
static bool IsPeKey(string key) => key.StartsWith("Pe_"); static bool IsPeKey(string key) => key.StartsWith("Pe_");
// [S159] các bộ key grant CanRead-only mọi role (xem chú thích tại reviewKeys).
static IEnumerable<string> ContractMenuKeys()
{
yield return MenuKeys.Contracts;
foreach (var c in MenuKeys.ContractTypeCodes)
{
yield return MenuKeys.ContractTypeGroup(c);
yield return MenuKeys.ContractTypeList(c);
yield return MenuKeys.ContractTypeCreate(c);
yield return MenuKeys.ContractTypePending(c);
}
}
static IEnumerable<string> MasterMenuKeys() => new[]
{
MenuKeys.Master, MenuKeys.Catalogs, MenuKeys.CatalogUnits, MenuKeys.CatalogMaterials,
MenuKeys.CatalogServices, MenuKeys.CatalogWorkItems, MenuKeys.Departments,
MenuKeys.Projects, MenuKeys.Suppliers,
};
static IEnumerable<string> KhkkKeys() => new[]
{
"Khkk_G1", "Khkk_List", "Khkk_Create", "Khkk_Pending", "Khkk_Approved",
};
static IEnumerable<string> HdcKeys()
=> MenuKeys.ContractTypeCodes.Select(c => "Hdc_" + c);
// MenuKeys.All chứa root PurchaseEvaluations nhưng KHÔNG chứa Pe_* leaf // MenuKeys.All chứa root PurchaseEvaluations nhưng KHÔNG chứa Pe_* leaf
// (sinh động qua factory). Build leaf giống SeedPurchaseEvaluationPermissionDefaultsAsync // (sinh động qua factory). Build leaf giống SeedPurchaseEvaluationPermissionDefaultsAsync
// để upgrade đúng row Pe_* thật trong DB (1 root + 5 leaf × 2 type). // để upgrade đúng row Pe_* thật trong DB (1 root + 5 leaf × 2 type).
@ -2137,6 +2174,14 @@ public static class DbInitializer
// [S159] 2 root placeholder GĐ2/GĐ4 — CanRead-only mọi role (IsPeKey=false // [S159] 2 root placeholder GĐ2/GĐ4 — CanRead-only mọi role (IsPeKey=false
// ⇒ nhánh read-only). Prod đã INSERT trực tiếp; đây giữ fresh-DB hội tụ. // ⇒ nhánh read-only). Prod đã INSERT trực tiếp; đây giữ fresh-DB hội tụ.
.Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung }) .Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung })
// [S159] skeleton GĐ2 (Khkk_*) + GĐ4 (Hdc_*) — leaves trỏ /coming-soon.
.Concat(KhkkKeys()).Concat(HdcKeys())
// [S159] owner "hiển thị hết": HĐ (Contracts+Ct_*) + DANH MỤC (Master-set)
// CanRead-only mọi role — cặp với việc GỠ nhánh [S92] khỏi revoker ở trên.
// Lưu ý: nhánh grant key-thường là skip-existing ⇒ trên prod row false
// KHÔNG được nâng ở đây — re-grant prod đã chạy SQL trực tiếp cùng ngày;
// đây là đường hội tụ cho fresh-DB/env khác.
.Concat(ContractMenuKeys()).Concat(MasterMenuKeys())
.Distinct() .Distinct()
.ToArray(); .ToArray();
var roles = await roleManager.Roles.ToListAsync(); var roles = await roleManager.Roles.ToListAsync();
@ -2214,14 +2259,16 @@ public static class DbInitializer
var rows = await db.Permissions var rows = await db.Permissions
.Where(p => p.RoleId != adminRoleId .Where(p => p.RoleId != adminRoleId
&& (p.MenuKey.StartsWith("Hrm") || p.MenuKey.StartsWith("Off") && (p.MenuKey.StartsWith("Hrm") || p.MenuKey.StartsWith("Off")
|| p.MenuKey == MenuKeys.Personal || p.MenuKey == MenuKeys.Personal)
// [S92 2026-06-29] +Hợp đồng (Contracts + Ct_*) + Danh mục (Master + // [S159 2026-07-29] 🔴 GỠ nhánh [S92] (Contracts+Ct_* / Master+Suppliers+
// Suppliers + Projects + Departments + Catalog*) — anh chốt "chỉ Admin // Projects+Departments+Catalog*) — owner SUPERSEDE chính quyết định S92
// thấy". Pe_* (Duyệt NCC) KHÔNG nằm đây → module PE giữ nguyên cho user. // "chỉ Admin thấy": nay "đang phát triển → cho hiển thị hết để mọi người
|| p.MenuKey == MenuKeys.Contracts || p.MenuKey.StartsWith("Ct_") // góp ý; Duyệt NCC để như cũ". Bài trả giá cùng ngày: grant 38 key bằng
|| p.MenuKey == MenuKeys.Master || p.MenuKey == MenuKeys.Suppliers // SQL tay bị CHÍNH revoker này lật lại ở app-restart kế (Run #423 —
|| p.MenuKey == MenuKeys.Projects || p.MenuKey == MenuKeys.Departments // 447/494 row rơi, 11/13 role về 0; cicd-verify-1a47a61.md mục 9).
|| p.MenuKey.StartsWith("Catalog")) // Đúng chiều-ngược gotcha #75/#76: seeder ungated thắng data-change tay
// ⇒ đổi trạng-thái bền = đổi CODE, nghiệm thu = restart THẬT.
// Hrm*/Off*/Personal GIỮ trong revoker (vùng đang-làm, ý ❸ owner).
&& (p.CanRead || p.CanCreate || p.CanUpdate || p.CanDelete)) && (p.CanRead || p.CanCreate || p.CanUpdate || p.CanDelete))
.ToListAsync(); .ToListAsync();

View File

@ -32,26 +32,37 @@ namespace SolutionErp.Infrastructure.Tests.Application;
// (Cascade) → PHẢI seed MenuItem rows + Role TRƯỚC khi seed Permission (nếu không SQLite FK Error 19). // (Cascade) → PHẢI seed MenuItem rows + Role TRƯỚC khi seed Permission (nếu không SQLite FK Error 19).
public class AdminOnlyModulesRevokeTests public class AdminOnlyModulesRevokeTests
{ {
// Đại diện 5 nhóm anh chốt admin-only — sau revoke PHẢI CanRead=false (non-Admin). // [S159 2026-07-29] Owner SUPERSEDE S92: "đang phát triển → cho hiển thị hết để mọi
// Dùng const/factory MenuKeys (KHÔNG hardcode string) để bắt đồng-bộ nếu key đổi. // người góp ý; Duyệt NCC để như cũ" ⇒ nhánh Contracts/Ct_*/Master-set GỠ khỏi revoker.
private static readonly string[] S92HiddenKeys = // Tập ẩn CÒN LẠI = 3 nhóm đang-làm (Hrm/Off/Personal — ý ❸ owner cùng ngày).
private static readonly string[] StillHiddenKeys =
{
MenuKeys.Hrm, // "Hrm" — match StartsWith("Hrm")
MenuKeys.Off, // "Off" — match StartsWith("Off")
MenuKeys.Personal, // "Personal" — match == Personal
};
// [S159] Bộ key S92 cũ nay MỞ LẠI — revoker KHÔNG ĐƯỢC chạm nữa. Giữ danh sách này
// làm REGRESSION-GUARD: bài trả giá 2026-07-29 = grant 38 key bằng SQL tay bị chính
// revoker lật ở app-restart kế (Run #423, 447/494 row rơi, 11/13 role về 0 —
// cicd-verify-1a47a61.md mục 9). Ai re-add nhánh S92 vào predicate ⇒ test này ĐỎ.
private static readonly string[] ReopenedKeys =
{ {
// HỢP ĐỒNG (Contracts + Ct_*) // HỢP ĐỒNG (Contracts + Ct_*)
MenuKeys.Contracts, MenuKeys.Contracts,
MenuKeys.ContractTypeGroup("ThauPhu"), // "Ct_ThauPhu" — match StartsWith("Ct_") MenuKeys.ContractTypeGroup("ThauPhu"), // "Ct_ThauPhu"
// DANH MỤC (Master + 3 leaf + Catalog*) // DANH MỤC (Master + 3 leaf + Catalog*)
MenuKeys.Master, MenuKeys.Master,
MenuKeys.Suppliers, MenuKeys.Suppliers,
MenuKeys.Projects, MenuKeys.Projects,
MenuKeys.Departments, MenuKeys.Departments,
MenuKeys.Catalogs, // "Catalogs" — match StartsWith("Catalog") MenuKeys.Catalogs,
MenuKeys.CatalogWorkItems, // "CatalogWorkItems" — match StartsWith("Catalog") MenuKeys.CatalogWorkItems,
// Nhóm cũ S58 (regression — vẫn phải ẩn)
MenuKeys.Hrm, // "Hrm" — match StartsWith("Hrm")
MenuKeys.Off, // "Off" — match StartsWith("Off")
MenuKeys.Personal, // "Personal" — match == Personal
}; };
private static readonly string[] S92HiddenKeys =
StillHiddenKeys.Concat(ReopenedKeys).ToArray(); // giữ tên cho seed-helper cũ
// Pe_* (Duyệt NCC) + root PurchaseEvaluations — KHÔNG bị revoke (giữ cho user thường). // Pe_* (Duyệt NCC) + root PurchaseEvaluations — KHÔNG bị revoke (giữ cho user thường).
private static readonly string[] PeKeptKeys = private static readonly string[] PeKeptKeys =
{ {
@ -130,15 +141,22 @@ public class AdminOnlyModulesRevokeTests
await InvokeRevokeAsync(fix); await InvokeRevokeAsync(fix);
foreach (var key in S92HiddenKeys) // Vế 1 — 3 nhóm đang-làm VẪN bị revoke (ý ❸ giữ).
foreach (var key in StillHiddenKeys)
{ {
var row = await GetPermAsync(db, nonAdminId, key); var row = await GetPermAsync(db, nonAdminId, key);
row.Should().NotBeNull($"{key} vẫn có row (revoke chỉ hạ cờ, KHÔNG xóa — giữ vết flip lại)"); row.Should().NotBeNull($"{key} vẫn có row (revoke chỉ hạ cờ, KHÔNG xóa — giữ vết flip lại)");
row!.CanRead.Should().BeFalse($"{key} bị revoke CanRead — menu ẩn khỏi non-Admin (chỉ Admin thấy)"); row!.CanRead.Should().BeFalse($"{key} bị revoke CanRead — nhóm đang-làm ẩn khỏi non-Admin");
row.CanCreate.Should().BeFalse($"{key} revoke CanCreate"); row.CanCreate.Should().BeFalse($"{key} revoke CanCreate");
row.CanUpdate.Should().BeFalse($"{key} revoke CanUpdate"); row.CanUpdate.Should().BeFalse($"{key} revoke CanUpdate");
row.CanDelete.Should().BeFalse($"{key} revoke CanDelete"); row.CanDelete.Should().BeFalse($"{key} revoke CanDelete");
} }
// Vế 2 — ⭐ REGRESSION-GUARD S159: bộ key MỞ LẠI phải GIỮ NGUYÊN CanRead=true
// sau revoke (revoker không còn nhánh S92; re-add là test này đỏ ngay).
foreach (var key in ReopenedKeys)
(await CanReadAsync(db, nonAdminId, key)).Should().BeTrue(
$"{key} — owner S159 mở lại cho mọi role; revoker KHÔNG được lật grant này nữa");
} }
// Admin loại trừ khỏi revoke → giữ nguyên CanRead=true (vẫn thấy 5 nhóm). // Admin loại trừ khỏi revoke → giữ nguyên CanRead=true (vẫn thấy 5 nhóm).

View File

@ -189,12 +189,14 @@ public class ProcurementMasterAccessSeedTests
await InvokeRevokeAsync(fix); // revoke CẢ HAI role về false await InvokeRevokeAsync(fix); // revoke CẢ HAI role về false
await InvokeSeedProcurementAsync(fix); // CHỈ nâng lại Procurement await InvokeSeedProcurementAsync(fix); // CHỈ nâng lại Procurement
// ⭐ ISOLATION: DeptManager row TỒN TẠI (revoke giữ vết) NHƯNG mọi cờ = false — seeder // ⭐ ISOLATION (đọc lại theo S159): revoker KHÔNG còn chạm Master-set ⇒ CanRead=true
// Procurement KHÔNG chạm role khác. Assert cả Master + Suppliers (2 key task chốt) + 7 key kia. // pre-grant của DeptManager GIỮ NGUYÊN (chính sách all-role read — owner 2026-07-29
// supersede S92). Isolation của Procurement-seed nay đo bằng CỜ CAO: seeder chỉ nâng
// CanCreate/Update cho Procurement, KHÔNG rò cờ cao sang role khác.
foreach (var key in AllGrantKeys) foreach (var key in AllGrantKeys)
await AssertFlagsAsync(db, otherId, key, await AssertFlagsAsync(db, otherId, key,
r: false, c: false, u: false, d: false, r: true, c: false, u: false, d: false,
"DeptManager KHÔNG được Procurement-grant chạm — S92 'chỉ Admin thấy' giữ nguyên role khác"); "S159 all-role read GIỮ; cờ cao KHÔNG rò sang DeptManager từ Procurement-grant");
// CONTRAST (chống vacuous-pass): Procurement THẬT SỰ được nâng → chứng seeder CÓ chạy, // CONTRAST (chống vacuous-pass): Procurement THẬT SỰ được nâng → chứng seeder CÓ chạy,
// nên isolation ở trên KHÔNG phải "false vì seeder no-op". // nên isolation ở trên KHÔNG phải "false vì seeder no-op".
@ -246,10 +248,10 @@ public class ProcurementMasterAccessSeedTests
var procCount = await db.Permissions.AsNoTracking().CountAsync(p => p.RoleId == procurementId); var procCount = await db.Permissions.AsNoTracking().CountAsync(p => p.RoleId == procurementId);
procCount.Should().Be(AllGrantKeys.Length, "idempotent — chạy lại KHÔNG tạo row trùng"); procCount.Should().Be(AllGrantKeys.Length, "idempotent — chạy lại KHÔNG tạo row trùng");
// Role khác vẫn bị ẩn sau lần 2 (isolation bền vững). // Isolation bền vững sau lần 2 (S159: CanRead all-role GIỮ true; cờ cao vẫn false).
foreach (var key in AllGrantKeys) foreach (var key in AllGrantKeys)
await AssertFlagsAsync(db, otherId, key, await AssertFlagsAsync(db, otherId, key,
r: false, c: false, u: false, d: false, r: true, c: false, u: false, d: false,
"idempotent — lần seed thứ 2 KHÔNG rò sang DeptManager"); "idempotent — lần seed thứ 2 KHÔNG rò CỜ CAO sang DeptManager (CanRead all-role S159 giữ)");
} }
} }