Compare commits
3 Commits
316a82f96d
...
fc1b8d9642
| Author | SHA1 | Date | |
|---|---|---|---|
| fc1b8d9642 | |||
| b128f30a35 | |||
| a19d6eab37 |
@ -14,7 +14,7 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve
|
||||
|
||||
## ⚠️ 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 — verdict delivered OK.**
|
||||
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)+S117 (#387) cicd-lane CLEAN-return no-garble ×3 — verdict delivered OK.**
|
||||
|
||||
---
|
||||
|
||||
@ -38,7 +38,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 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 3 Test gate:** baseline **495 PASS** (45 Domain + 450 Infra; S116 #386 `88368fd` — 486+9 PE submit-guard/finalize/budget tests [PeSubmitGuardAndBypass +166L / PeCcmThresholdFinalize +118L / PeMultiWinner +72L / PeWorkItemBudget +51L]; prior 486 [S114 #384 `d436af2` +9 multi-NCC / S115 #385], 477, 458, 440, 436, 434, 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 **501 PASS** (45 Domain + 456 Infra; S117 #387 `316a82f` — 495+6 PE approved-price-range tests [NEW PeApprovedPriceFinalizeTests + PeFinalizeProjectionTests mod]; Infra 450→456, Domain frozen 45; prior 495 [S116 #386 `88368fd` 486+9 submit-guard/finalize/budget], 486, 477, 458, 440, 436, 434, 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 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.
|
||||
- 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.
|
||||
@ -56,9 +56,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.
|
||||
- **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.
|
||||
- **SSH→PS quoting (S42):** nested bash→ssh→powershell 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:** **495 PASS** (45 Domain + 450 Infra · 0 fail/skip; S116 #386 `88368fd`; prior 486/477/458/440/436/434/421). Phase 9 UAT skip OK. 🔑 **Concrete count via anon log `.../actions/runs/{run_number}/jobs/0/logs` (~18KB) → per-project `Passed! - Failed: 0, Passed: N, Skipped: 0` (NOT just status=success; `/api/v1/.../tasks/{id}/logs` variant 404s — use web-UI runs path).**
|
||||
- **Mig latest repo: Mig 65 `BackfillPeQuoteIsSelectedAndWinnerInvariant`** (S114 `d436af2`; DATA-ONLY no-schema 2-dir backfill, INVARIANT `Supplier.IsWinner ⟺ Any(Quote.IsSelected)`; Down=no-op, NO ModelSnapshot change [2 mig-file = correct data-only], SQL-Server-only [SQLite EnsureCreated skips]; VERIFIED-APPLIED-PROD history-top + violations=0 + sys.tables=89). Prev Mig 64 `AddSupplierPublishState` / 63 / 62 `ExpandSupplierFields` (see docs/CLAUDE.md migration table). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC`. ⚠️ **`sys.tables`(is_ms_shipped=0)=89** — AddColumn/data-only don't change count; commit touching no new table ⟹ 89 correct, don't FAIL. enum-additive = int NO-mig (history NOT-advance + empty `git diff -- '*Migrations*'`).
|
||||
- **Bundle hash live:** current ship-baseline = S116 #386 (admin js `Dd55jBpj`/css `CfunQHbd` · user js `DVDr1UQ4`/css `DGqqHQXS`; size 1,656,224/1,562,572b; LM 10:12:47/10:13:41 GMT 07-13) — JS-rotate/CSS-frozen from S115 (D51OYyGV/BVdssm5S). See S116 recent-run entry for markers. ⚠️ id≠run_number (head_sha-keyed). PER #69 TWO-MODE hash is SNAPSHOT-per-run NOT stable-baseline (rotate on real-src OR benign-0-src; freeze on 0-src) — ALWAYS re-snapshot LIVE post-success; ship-proof = LM-in-window + real-size (vs 919b SPA-trap) + marker-grep, NOT hash-delta. js-rotate/css-frozen = FE-logic-no-style fingerprint.
|
||||
- **Tests baseline:** **501 PASS** (45 Domain + 456 Infra · 0 fail/skip; S117 #387 `316a82f`; prior 495/486/477/458/440/436/434/421). Phase 9 UAT skip OK. 🔑 **Concrete count via web-UI log path `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{run_number}/jobs/0/logs` (~18KB, NO `/api/v1/` prefix) → per-project `Passed! - Failed: 0, Passed: N, Skipped: 0`. ⚠️ S117 REFINE: `/api/v1/…/actions/runs/{n}/jobs/0/logs` AND `/api/v1/…/tasks/{id}/logs` BOTH now 404 (Gitea) — ONLY the bare web-UI runs path (no api/v1) returns log body; status=success alone still ⟹ gate-passed (tests run pre-deploy).**
|
||||
- **Mig latest repo: Mig 66 `AddPeApprovedPriceRange`** (`20260714035636`, S117 `316a82f`; single AddColumn `PurchaseEvaluations.ApprovedPriceMaxAmount decimal(18,2) nullable` — cột giá-chốt MAX của "duyệt theo khoảng Min–Max" src ProMinMax; VERIFIED-APPLIED-PROD `COL_LENGTH=9` + history-top `20260714035636_AddPeApprovedPriceRange` + sys.tables **89 giữ** [AddColumn no-new-table]). Prev Mig 65 `BackfillPeQuoteIsSelectedAndWinnerInvariant` / 64 `AddSupplierPublishState` / 62 `ExpandSupplierFields` (see docs/CLAUDE.md migration table). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC`. ⚠️ **`sys.tables`(is_ms_shipped=0)=89** — AddColumn/data-only don't change count; commit touching no new table ⟹ 89 correct, don't FAIL. enum-additive = int NO-mig (history NOT-advance + empty `git diff -- '*Migrations*'`).
|
||||
- **Bundle hash live:** current ship-baseline = S117 #387 (admin js `cFI7ih4a`/css `CfunQHbd` · user js `MxgooVZw`/css `DGqqHQXS`; size 1,656,696/1,563,044b; LM 04:18:21/04:19:16 GMT = 11:18/11:19+07 07-14) — JS-rotate ×2/CSS-frozen from S116 (Dd55jBpj/DVDr1UQ4). See S117 recent-run entry for markers. ⚠️ id≠run_number (head_sha-keyed). PER #69 TWO-MODE hash is SNAPSHOT-per-run NOT stable-baseline (rotate on real-src OR benign-0-src; freeze on 0-src) — ALWAYS re-snapshot LIVE post-success; ship-proof = LM-in-window + real-size (vs 919b SPA-trap) + marker-grep, NOT hash-delta. js-rotate/css-frozen = FE-logic-no-style fingerprint.
|
||||
- **Bearer:** admin `admin@solutions.com.vn/Admin@123456` (full) · UAT `nv.test@solutions.com.vn/TestUser@123456` (Drafter CCM, gotcha #44 check).
|
||||
|
||||
## 🔑 Critical config (flag commit nếu tái xuất)
|
||||
@ -76,9 +76,8 @@ BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code /
|
||||
|
||||
## 📅 Recent runs (compressed — verbatim → archive via `_INDEX.md`)
|
||||
|
||||
- **🟢 S116 #386 (rn 386, id 500) `88368fd` PE-nhập-giá-ÂM ô báo giá + submit-guard all-zero GO 5m28s (17:08:42→17:14:10+07 2026-07-13; 7-file: FE PeDetailTabs.tsx ×2 mirror [112 dòng/app] + BE PurchaseEvaluationWorkflowService.cs [:206 `selectedThanhTien.Sum()<=0`→`.All(x=>x==0)` — cho giá ÂM=phát-sinh-giảm/hoàn-tiền NCC=giá THẬT] + 4 test-file; NO migration → tables 89 giữ):** status=success · **gate CONCRETE `runs/386/jobs/0/logs`: Domain 45 + Infra 450 = 495** (+9 vs S114/S115 486; Infra 441→450, Domain frozen 45) · **FE JS-ROTATE ×2 / CSS-frozen** admin js `D51OYyGV`→`Dd55jBpj` (css `CfunQHbd` frozen) · user js `BVdssm5S`→`DVDr1UQ4` (css `DGqqHQXS` frozen) = FE-logic-no-style fingerprint (khớp PeDetailTabs.tsx logic-only) · byte-verify #77 NEW bundle: `Đã nhập giá chào thầu`=1 + `hoàn tiền NCC`=1 + `Đơn vị được chọn chưa có giá chào thầu`=1 + `phát sinh giảm`(toggle)=1 PRESENT (đều =0 ở old-frozen D51OYyGV pre-deploy — chứng NEW-code ship; `Có giá là được`=0 minify-diff non-load-bearing) · size real 1,656,224/1,562,572b (vs 919b SPA-trap) · LM admin 10:12:47 / user 10:13:41 GMT = 17:12/17:13+07 IN-window · smoke ALL: api ready+live 200, admin+eoffice 200, PE 401 alive+secured · 🔑 **PRE-DEPLOY frozen-bundle-at-S115 TRAP-avoided**: poll til status=success TRƯỚC re-snapshot (grep-sớm khi status=running → false-FAIL frozen; xác nhận CI running 17:11 → success 17:14 → bundle rotate). cicd-lane CLEAN-return no-garble.
|
||||
- **🟢 S115 #385 (rn 385, id 499) `81b59f0` GOVERNANCE/TOOLING-ONLY GO 5m23s (12:25:31→12:30:54+07 2026-07-13, harness adopt presence-not-age + hmw.js STOP-HARD; 20-file: 0 src/fe/mig/test — non-ignored trigger = `.claude/workflows/hmw.js` + `scripts/memory-archive-gate.ps1` [`.ps1`∉`scripts/**.md` ⟹ CI-run ĐÚNG #41; rest .md/.json under docs/agent-memory/workflows-runs/broadcasts IGNORED]):** status=success · **gate CONCRETE `runs/385/jobs/0/logs`: Domain 45 + Infra 441 = 486/0-fail/0-skip = S114 baseline EXACT** (0 src/tests diff → count frozen ✓) · **FE bundle FROZEN ×4** admin js `D51OYyGV`/css `CfunQHbd` · user js `BVdssm5S`/css `DGqqHQXS` = S114 verbatim (NO rotate) — deploy physically re-copied (LM 05:29:32/05:30:26 GMT = 12:29:32/12:30:26+07 IN-window) BUT byte-identical (size 1,655,105/1,561,453b = S114 EXACT) ⟹ #69-REFINE 0-fe-src=identical-Vite=same-hash+size+fresh-LM · SPA-trap ctrl fake-hash 919b text/html · Mig FROZEN 65 (0 migration-file diff; API health/ready 200=startup OK) · smoke 4/4 200 (api ready+live, admin, eoffice). 🔑 GOVERNANCE-ONLY PASS: non-.md-in-.claude (hmw.js/.ps1) STILL trigger CI (#41 LIVE post-#495 D10); FROZEN-hash+fresh-LM+identical-size = no-accidental-rotation proof. cicd-lane clean-return no-garble.
|
||||
- **🟢 S114 #384 (rn 384, id 498) `d436af2` GO 5m17s — PE multi-NCC per hạng mục + Mig 65 `BackfillPeQuoteIsSelectedAndWinnerInvariant` APPLIED-PROD (data-only 2-dir backfill, invariant-violations=0 [IsWinner⟺Any(IsSelected)], sys.tables 89) · gate 486 (45D+441I) · FE js-ROTATE×2/css-frozen admin `acprMqfB`→`D51OYyGV`/user `Ck3BTDdA`→`BVdssm5S` (= CURRENT ship-baseline, size 1,655,105/1,561,453b) · byte-verify `trúng thầu theo từng hạng mục` · select-winner WIRED 401 · smoke ALL 200 · 🔑 js-rotate+css-frozen = FE-logic-no-style fingerprint — verbatim → `archive/2026-07.md` via `_INDEX`.**
|
||||
- **🟢 S117 #387 (rn 387, id 501) `316a82f` PE endsBeforeCeo-pointer-aware + duyệt-theo-khoảng-Min–Max + Mig 66 `AddPeApprovedPriceRange` APPLIED-PROD GO 5m36s (11:14:09→11:19:45+07 2026-07-14; 19-file: BE 7 [Domain/Config/Features/Service/IService/Dtos/Controller] + FE 6 [PeDetailTabs+PeWorkflowPanel+types ×2-app] + Mig 3 + test 2 [NEW PeApprovedPriceFinalizeTests + PeFinalizeProjectionTests mod]):** status=success · **gate CONCRETE web-UI-path `runs/387/jobs/0/logs` (NO /api/v1/): Domain 45 + Infra 456 = 501** (+6 vs S116 495; Infra 450→456, Domain frozen 45; 0-fail/0-skip) · **Mig 66 APPLIED-PROD** sqlcmd `COL_LENGTH('PurchaseEvaluations','ApprovedPriceMaxAmount')=9` + history-top `20260714035636_AddPeApprovedPriceRange` + sys.tables **89 giữ** (AddColumn decimal(18,2) no-new-table) · **FE JS-ROTATE ×2/CSS-frozen** admin js `Dd55jBpj`→`cFI7ih4a` (css `CfunQHbd` frozen) · user js `DVDr1UQ4`→`MxgooVZw` (css `DGqqHQXS` frozen) = FE-logic-no-style fingerprint · byte-verify #77 BOTH-app NEW bundle: `cả Min và Max`=1 + `duyệt theo khoảng`=1 + `ProMinMax`=2 + `Min–Max`(en-dash)=1 PRESENT (all=0 in OLD DVDr1UQ4 captured pre-deploy — NEW-code ship proof; ⚠️ `PRO Min`=1 in OLD = pre-existing label `ProMin:'PRO Min'` = FALSE-discriminator) · size real 1,656,696/1,563,044b (vs 919b SPA-trap) · LM admin 04:18:21/user 04:19:16 GMT = 11:18/11:19+07 IN-window · smoke ALL 200 (api ready+live, admin, eoffice) · PE 401-secured: GET-list no-token+bogus→401, POST /transitions bogus+no-token→401, /select-winner bogus→401 (no 500) · 🔑 pre-deploy bundle-snapshot BEFORE poll-success (captured OLD-hash+OLD-feature=0) → post-success re-snapshot = clean rotation proof. cicd-lane CLEAN-return.
|
||||
- _(**S116 #386 `88368fd`** PE-nhập-giá-ÂM ô-báo-giá + submit-guard all-zero [gate 495=45+450, js-rotate×2 D51OYyGV→Dd55jBpj/BVdssm5S→DVDr1UQ4 css-frozen, byte-verify 4-str PRESENT, 🔑 poll-til-success TRƯỚC re-snapshot tránh frozen-trap] **+ S115 #385 `81b59f0`** GOVERNANCE hmw.js+.ps1 non-ignored [gate 486 frozen, bundle FROZEN×4 fresh-LM+identical-size = #69-REFINE 0-src proof, #41 non-.md-in-.claude STILL triggers] **+ S114 #384 `d436af2`** PE multi-NCC per-hạng-mục + Mig 65 backfill-invariant APPLIED-PROD [gate 486, js-rotate×2 acprMqfB→D51OYyGV/Ck3BTDdA→BVdssm5S, byte `trúng thầu theo từng hạng mục`, select-winner 401] — all GO ~5m, FE real-ship, smoke ALL 200 · full verbatim → `archive/2026-07.md` via `_INDEX`)_
|
||||
- _(**S111 #495 `8aa3869` H22-WAL-adopt** [deploy.yml SELF-change +2 paths-ignore, run parsed+ran-full = workflow-self-change PASS; bundle FROZEN ×6-streak; 🔑 **AS-6 baseline-shift**: post-#495 memory-push (budget.json/agent-memory/run-trace) HẾT trigger CI, phân-loại giờ soi file ngoài 7-glob] **+ S112 #496 `e100ef0`** PE-workflow+Supplier-import-Phase-B+Mig63 `AddSupplierImportSourceFields` [gate 458, 🔑 411-refine: bodyless-POST→411 ANY-route → re-probe WITH-body → 401/415-matched vs 404-missing] **+ S113 #383 `5fa11b5`** Supplier-import-v2+Mig64 `AddSupplierPublishState`-APPLIED-PROD [IsPublic 37/37, gate 477] — all GO ~5m, FE real-ship, Mig-applied-prod, smoke ALL 200 · full verbatim → `archive/2026-07.md` via `_INDEX`)_
|
||||
- _(**S107-closeout→S110-spot #490-#494** GOVERNANCE/MEMORY-push GO ×5 [`d74f67f`·`69957fd`·`f5a5c3e`·`6ee60cd`·`ac3f122` — trigger #41 non-.md-in-push: budget.json ×2 + hmw.js ×3, pattern DEAD từ D10 #495] — bundle FROZEN-streak ×5 sau ROTATE #489 [#69 TWO-MODE; LM-in-window + real-size 1,644,931/1,551,280b = signal duy nhất] · Mig 62 + sys.tables 89 frozen · gate 440 · smoke 3×200 ALL · 🔑 #494 spot-detection: LM-prod-newer-than-last-verified-window = missed-run detector · full verbatim → `archive/2026-07.md` via `_INDEX`)_
|
||||
- _(**S107 #489 `21387cd` GOVERNANCE-ONLY (+office-document 12th sub) GO 5m33s** — JS ROTATED-benign `ChSNAJBK`→`BVyH-FY7` / `Bn2w1znH`→`EDsZebvn` CSS-frozen, 🔑 #69 TWO-MODE CONFIRMED (0-fe-src ROTATE #312/#489 hoặc FROZEN #333/#485-487 — ship-proof = LM-in-window + real-size, hash-delta phụ) · **S104 #488 `778fc98` FULL-STACK Supplier 9→27 + Mig 62 `ExpandSupplierFields` APPLIED-PROD** — AddColumn×18-exact + sys.tables-89-invariant + un-cram 4-NCC Note=NULL/Pkg/Bank/LegalRep-populated-EXACT + DTO-18-new-keys-live + JS-rotate-real-src + 🔑 FULL-STACK-SCHEMA-ADD+DATA-UN-CRAM PASS-pattern — full verbatim → `archive/2026-07.md` via `_INDEX`)_
|
||||
|
||||
@ -25,3 +25,6 @@
|
||||
- **🟢 S113 #383 (rn 383, id 497) `5fa11b5` GO 5m14s** — Supplier import v2 + Mig 64 `AddSupplierPublishState` APPLIED-PROD (IsPublic backfill 37/37, sys.tables 89) · gate 477 (45D+432I) · FE real-ship js+css both apps admin `acprMqfB`/`CfunQHbd` · user `Ck3BTDdA`/`DGqqHQXS` (byte-verify `Tải file mẫu`/`Đã công bố`/`Nháp (ẩn)`) · endpoints `/suppliers/import/template`+`?published=true` WIRED 401 · smoke ALL 200 — verbatim → git `8304d08` + `_INDEX`.
|
||||
- **🟢 S112 #496 `e100ef0` GO 5m25s** — PE workflow + Supplier Excel-import Phase B + Mig 63 `AddSupplierImportSourceFields` APPLIED-PROD (2 AddColumn, sys.tables 89) · gate 458 (45D+413I) · FE admin `Cixlt_MA`/user `BhBOG1dn` · 🔑 411-refine: bodyless-POST→411 ANY route → re-probe WITH body → 401/415 (matched) vs 404 (missing) — verbatim → `2026-07.md`/`_INDEX`.
|
||||
- **🟢 S111 #495 (rn 381, id 495) `8aa3869` GO 5m11s (11:45:21→11:50:32+07 2026-07-12, iter6) — H22 WAL adopt 30-file +767 (0 src/BE/FE/mig; non-ignored: settings.json + wal-flush.ps1 + hmw.js + deploy.yml + governance-detectors.ps1 + .gitattributes → CI-run ĐÚNG thiết kế):** status=success (gate 440) · **deploy.yml TỰ-ĐỔI trong chính commit (D10 +2 paths-ignore `.claude/agent-memory/**` + `.claude/workflows/runs/**`) — run created+parse+chạy trọn 5m11s = YAML hợp lệ, workflow-self-change PASS** · Mig FROZEN 62 history-top `ExpandSupplierFields` + sys.tables=89 ✓ · smoke 3×200 (health/ready + 2 FE roots) · **bundle FROZEN ×6-streak** (admin `BVyH-FY7`/`Bbtesa6B` · user `EDsZebvn`/`BmuPz_Dy` = #490→#494) + LM-in-window 11:49:07/11:50:00+07 + size 1,644,931/1,551,280b byte-identical #494 vs fake-919b = ship-proof. 🔑 **AS-6 BASELINE SHIFT từ #495: paths-ignore 7-entry — memory-budget.json/agent-memory/run-trace push HẾT trigger (trigger-pattern #490/#492 DEAD); phân loại trigger giờ soi file ngoài 7-glob.**
|
||||
|
||||
- **🟢 S116 #386 (rn 386, id 500) `88368fd` PE-nhập-giá-ÂM ô báo giá + submit-guard all-zero GO 5m28s (17:08:42→17:14:10+07 2026-07-13; 7-file: FE PeDetailTabs.tsx ×2 mirror [112 dòng/app] + BE PurchaseEvaluationWorkflowService.cs [:206 `selectedThanhTien.Sum()<=0`→`.All(x=>x==0)` — cho giá ÂM=phát-sinh-giảm/hoàn-tiền NCC=giá THẬT] + 4 test-file; NO migration → tables 89 giữ):** status=success · **gate CONCRETE `runs/386/jobs/0/logs`: Domain 45 + Infra 450 = 495** (+9 vs S114/S115 486; Infra 441→450, Domain frozen 45) · **FE JS-ROTATE ×2 / CSS-frozen** admin js `D51OYyGV`→`Dd55jBpj` (css `CfunQHbd` frozen) · user js `BVdssm5S`→`DVDr1UQ4` (css `DGqqHQXS` frozen) = FE-logic-no-style fingerprint (khớp PeDetailTabs.tsx logic-only) · byte-verify #77 NEW bundle: `Đã nhập giá chào thầu`=1 + `hoàn tiền NCC`=1 + `Đơn vị được chọn chưa có giá chào thầu`=1 + `phát sinh giảm`(toggle)=1 PRESENT (đều =0 ở old-frozen D51OYyGV pre-deploy — chứng NEW-code ship; `Có giá là được`=0 minify-diff non-load-bearing) · size real 1,656,224/1,562,572b (vs 919b SPA-trap) · LM admin 10:12:47 / user 10:13:41 GMT = 17:12/17:13+07 IN-window · smoke ALL: api ready+live 200, admin+eoffice 200, PE 401 alive+secured · 🔑 **PRE-DEPLOY frozen-bundle-at-S115 TRAP-avoided**: poll til status=success TRƯỚC re-snapshot (grep-sớm khi status=running → false-FAIL frozen; xác nhận CI running 17:11 → success 17:14 → bundle rotate). cicd-lane CLEAN-return no-garble.
|
||||
- **🟢 S115 #385 (rn 385, id 499) `81b59f0` GOVERNANCE/TOOLING-ONLY GO 5m23s (12:25:31→12:30:54+07 2026-07-13, harness adopt presence-not-age + hmw.js STOP-HARD; 20-file: 0 src/fe/mig/test — non-ignored trigger = `.claude/workflows/hmw.js` + `scripts/memory-archive-gate.ps1` [`.ps1`∉`scripts/**.md` ⟹ CI-run ĐÚNG #41; rest .md/.json under docs/agent-memory/workflows-runs/broadcasts IGNORED]):** status=success · **gate CONCRETE `runs/385/jobs/0/logs`: Domain 45 + Infra 441 = 486/0-fail/0-skip = S114 baseline EXACT** (0 src/tests diff → count frozen ✓) · **FE bundle FROZEN ×4** admin js `D51OYyGV`/css `CfunQHbd` · user js `BVdssm5S`/css `DGqqHQXS` = S114 verbatim (NO rotate) — deploy physically re-copied (LM 05:29:32/05:30:26 GMT = 12:29:32/12:30:26+07 IN-window) BUT byte-identical (size 1,655,105/1,561,453b = S114 EXACT) ⟹ #69-REFINE 0-fe-src=identical-Vite=same-hash+size+fresh-LM · SPA-trap ctrl fake-hash 919b text/html · Mig FROZEN 65 (0 migration-file diff; API health/ready 200=startup OK) · smoke 4/4 200 (api ready+live, admin, eoffice). 🔑 GOVERNANCE-ONLY PASS: non-.md-in-.claude (hmw.js/.ps1) STILL trigger CI (#41 LIVE post-#495 D10); FROZEN-hash+fresh-LM+identical-size = no-accidental-rotation proof. cicd-lane clean-return no-garble.
|
||||
|
||||
@ -127,3 +127,5 @@
|
||||
- 2026-07-12 · S112 #496 `e100ef0` PE workflow + Supplier import Phase B + Mig63 AddSupplierImportSourceFields GO — 411-refine, gate 458 · PASS · `2026-07.md` · substring:"S112 #496 `e100ef0`"
|
||||
- 2026-07-12 · S113 #383 `5fa11b5` Supplier import v2 + Mig64 AddSupplierPublishState APPLIED-PROD GO — IsPublic 37/37, gate 477 · PASS · `2026-07.md` · substring:"S113 #383 (rn 383, id 497)"
|
||||
- 2026-07-13 · S114 #384 `d436af2` PE multi-NCC per hang muc + Mig65 backfill invariant APPLIED-PROD GO — js-rotate x2/css-frozen D51OYyGV/BVdssm5S, gate 486 · PASS · `2026-07.md` · substring:"S114 #384 (rn 384, id 498)"
|
||||
- 2026-07-13 · S115 #385 `81b59f0` GOVERNANCE/TOOLING-ONLY (hmw.js+memory-archive-gate.ps1) GO — FROZEN x4 D51OYyGV/BVdssm5S fresh-LM identical-size #69-REFINE, gate 486 · PASS · `2026-07.md` · substring:"S115 #385 (rn 385, id 499)"
|
||||
- 2026-07-13 · S116 #386 `88368fd` PE nhap-gia-AM + submit-guard all-zero GO — js-rotate x2/css-frozen Dd55jBpj/DVDr1UQ4, gate 495 (45+450), byte-verify 4-str PRESENT · PASS · `2026-07.md` · substring:"S116 #386 (rn 386, id 500)"
|
||||
|
||||
@ -6,6 +6,13 @@
|
||||
|
||||
---
|
||||
|
||||
## 🆕 S117 (2026-07-14) — PE "duyệt theo KHOẢNG" radio-5 ProMinMax ×2 app (anh Kiệt FDC; BE landed contract). Build PASS ×2, SHA-pair Panel+DetailTabs
|
||||
- **Feature:** ô "CHỌN GIÁ CHỐT" cấp cuối +radio-5 "PRO — cả Min và Max (duyệt theo khoảng)" (source=ProMinMax) CHỈ khi `proSuggestedMinPrice!=null&&proSuggestedMaxPrice!=null`. Chọn=chốt dải [Min,Max]. GIỮ radio single-select.
|
||||
- **types (DIVERGE, edit RIÊNG KHÔNG cp):** `PeDetailBundle`(L433) +`approvedPriceMaxAmount:number|null` SAU approvedPriceSource. Anchor 3-dòng approvedPriceAmount/Source/canEditPro same-string 2 file, khác line (admin488/user490).
|
||||
- **PeWorkflowPanel `011ee8cf`:** priceCandidates +`amountMax?`; ProMinMax entry qua `...(hasBothProPrices?[{amount:min,amountMax:max}]:[])` spread TRƯỚC `as {...;amountMax?:number|null}[]` cast; filter type-guard `amountMax?:number` (subtype OK). `selectedCandidate` 1-find → selectedPriceAmount+selectedPriceAmountMax. Submit +`approvedPriceMaxAmount:sendPrice?(selectedPriceAmountMax??undefined):undefined` (undefined→axios bỏ key; nguồn đơn selectedMax=null→undefined; BE reject source-đơn-kèm-Max). Render span ternary `min đ – max đ`. Gate priceMissing GIỮ (ProMinMax có amount=min). NO new state (max derived) → onSuccess reset KHÔNG đổi.
|
||||
- **PeDetailTabs `617b0aa0`:** label-map +`ProMinMax:'PRO Min–Max'`; "Giá chốt duyệt" value ternary `approvedPriceMaxAmount!=null? fmtVnd(amt) – fmtVnd(max) : fmtVnd(amt)`.
|
||||
- **LESSON: conditional array entry = spread `...(cond?[obj]:[])` TRƯỚC `as T[]` cast (né inference lỗi); `??undefined`=omit-key axios chuẩn cho "source đơn không kèm Max"; Max KHÔNG cần state riêng (derive từ selectedCandidate) → 0 reset-site.** Build ×2 PASS (user1939mod `index-DuAp5TWx` 17.8s / admin1950mod `index-D6YhgGH-` 7.2s, 0 TS err). Warn pre-existing @import+>500KB+realtime. NO BE/test touch. 6 file (3/app) diff-stat symmetric. Full precedent, NO ambiguity. Tag `[s117, pe-approve-by-range, prominmax-5th-radio, spread-conditional-before-as-cast, undefined-omit-axios-key, derived-max-no-state, sha256-2pair, type-diverge-edit-same]`
|
||||
|
||||
## 🆕 S116 (2026-07-13) — PE cho nhập SỐ ÂM báo giá "Số tiền báo giá cho hạng mục" ×2 app (owner anh Kiệt; BE+DB nhận âm sẵn 0 validator). Build PASS ×2, SHA-pair
|
||||
PeDetailTabs.tsx BYTE-IDENTICAL, cp fe-user→fe-admin. Âm = phát-sinh-giảm/hoàn tiền NCC = giá THẬT (KHÔNG lỗi). 5 change.
|
||||
- **Sign-toggle 2 dialog (AddSupplier :2379 + QuoteDialog :2948):** nút +/− pattern VndInlineEdit:1003. `neg`=useState RIÊNG (QuoteDialog `setForm({thanhTien})` thay CẢ object → nuốt neg nếu nhét vào form). 🔴 2 dialog POST commit form.thanhTien THẲNG (:2247/:2906, no nút "Lưu") → toggle PHẢI ghi giá-CÓ-DẤU NGAY `setForm({...,thanhTien:-form.thanhTien})`, chỉ flip cờ = MẤT DẤU thầm lặng. Display `formatVndInput(Math.abs(tt))`; onChange `mag=parseVnd();tt=neg?-mag:mag`. QuoteDialog init `neg=(existing?.thanhTien??0)<0`; AddSupplier `false`.
|
||||
@ -22,48 +29,13 @@ PeDetailTabs.tsx BYTE-IDENTICAL, cp fe-user→fe-admin. Âm = phát-sinh-giảm/
|
||||
- **NO 4-place mirror** (dialog+page trên page ĐÃ routed → no route/menuKey/Layout). **NO BE/test touch** (src/Backend/** = impl-backend ∥, tôi Read-only DTO). Build: admin 1950mod `index-CWBEeKi5.js` 8.48s / user 1939mod `index-JpoDnukv.js` 1.54s, 0 TS err; pre-existing warn @import+>500KB+realtime dynamic.
|
||||
- **DEVIATION flagged:** "Tải file mẫu" đặt CHỈ trong dialog (② canonical + ③ acceptance "ImportDialog nút mẫu"; ③ SuppliersPage= badge+Công bố+filter+guard, KHÔNG list nút mẫu) — parent 2.d loosely list page-btn nữa → 1-line ask nếu muốn thêm page-level template btn. Tag `[s113, supplier-publish-draft, isPublic, template-blob-download, ma-editable-grid, mst-missing-warn, publish-not-update, sha256-2pair, type-diverge-edit-identical]`.
|
||||
|
||||
## 🆕 2026-07-12 — Supplier import Excel FE (Phase B Approach A) ×2 app byte-identical (lead chốt UX; BE done+450 tests)
|
||||
NEW `components/master/SupplierImportDialog.tsx` (folder `master/` MỚI cả 2 app) + wire `pages/master/SuppliersPage.tsx` (nút "Import Excel NCC" outline cạnh "Thêm NCC" trong PageHeader actions flex + render dialog). KHÔNG 4-place mirror — dialog trên page ĐÃ routed → no new route/menuKey/Layout.
|
||||
- **Flow:** .xlsx → POST `/suppliers/import/preview` multipart field `file` → preview. `layoutValid===false`→alert đỏ+warnings, ẩn nút xác nhận. Else: badge counts (New/Update/Skip/Error) + warnings amber + `<table>` raw (Dòng·StatusBadge·Mã·Tên·Loại·Thông báo). "Xác nhận nhập" disabled `errorCount>0` → POST `/import/confirm` body `{rows}` = echo `preview.rows` VERBATIM. committed→toast+`invalidateQueries(['suppliers'])`+close; !committed→alert `errors[]`.
|
||||
- **BE contract (Read-only DTO):** enum serialize = **INT** (chỉ UtcDateTimeJsonConverter đăng ký, KHÔNG JsonStringEnumConverter) → `type` 1-6 reuse `SupplierTypeLabel`, `status` RowImportStatus 0-3 const-object; camelCase wire (default web JSON); confirm bind positional record `(IReadOnlyList Rows)`. Round-trip 30 field verbatim → TS type partial vẫn OK runtime.
|
||||
- **Mirror lesson (tái xác nhận S97-badge):** Dialog (fe-user gradient title bar) + PageHeader + types/master (khác MỖI `Department.parentId` admin-only S65) ĐỀU DIVERGED 2 app — NHƯNG import alias-path `@/…` + symbol-signature giống → new file VẪN byte-identical. Byte-identical SuppliersPage cũ (build 2 app) = proof mọi symbol tương thích. UI: KHÔNG shadcn Badge/Alert/Table primitive (chỉ PhaseBadge) → inline Tailwind badge/alert + `<table>`. Multipart = pattern `ContractAttachmentsSection` (FormData + `Content-Type: multipart/form-data`).
|
||||
- **SHA256 ×2 pair:** SupplierImportDialog `6313529c…`, SuppliersPage `e9143d93…`. Build PASS ×2 (admin 1950mod `index-DyOk34HV.js` 19.5s / user 1939mod `index-DXK9ebaz.js` 8.75s, 0 TS err). Pre-existing warn: @import CSS-order + >500KB + realtime.ts dynamic. NO BE touch. Full precedent. Tag `[supplier-import-excel, multipart-preview-confirm, enum-int-wire, byte-identical-2app, no-4place-dialog-on-routed-page]`.
|
||||
|
||||
## 🆕 S97-badge (2026-07-01) — PE chip "✅ Đã được CEO duyệt" khi phiếu ĐÃ DUYỆT đi hết lên CEO (anh Kiệt FDC UAT, em-main chốt)
|
||||
FE-ONLY, PROD LIVE. Đối xứng fix endsBeforeCeo (Mig 60). EDIT1 mở rộng `PeFinalizeChip.tsx` (2 app) + EDIT2 wire `phase={p.phase}` ở 6 call-site.
|
||||
- **EDIT1 component:** +prop `phase?` + nhánh: `if(!endsBeforeCeo){ if(phase===DaDuyet) return emerald "✅ Đã được CEO duyệt"; return null }` else giữ violet "⚑ Kết thúc·X". Import `PurchaseEvaluationPhase` từ `@/types/purchaseEvaluation` — **cùng alias 2 app** → byte-identical (KHÔNG ngoại-lệ import-path). SHA256 IDENTICAL ×2 = `da9c3ed9…`.
|
||||
- **⚠️ GOTCHA TYPE (đã trải):** spec ghi prop `phase?: PurchaseEvaluationPhase` (narrow union `1|…|99`) NHƯNG `PeListItem.phase: number` (types :113) → `phase={p.phase}` = **TS2322 "number not assignable to PurchaseEvaluationPhase"** (fail 2 site). FIX: prop → `phase?: number` (literal `DaDuyet`=7 so-khớp OK; import vẫn dùng trong `=== …DaDuyet` → no unused). LESSON: list-DTO field mirror-BE = `number` rộng → prop nhận `number`, KHÔNG narrow-union; check kiểu item trước khi type prop.
|
||||
- **EDIT2 6 site (grep `PeFinalizeChip endsBeforeCeo` = 6, hết):** admin{PeListPanel:162, ListPage:492} + user{PeListPanel:162, ListPage:492, InboxPage:292, UserDashboardPage:286}. MỌI item = `p` có `.phase` (đã dùng `getPeDisplayStatus(p.phase)`) → KHÔNG bịa. Grep-confirm 6/6 có `phase=`, 0 sót.
|
||||
- Build PASS ×2 (admin 1948mod `index-CScNAtL0.js` / user 1937mod `index-W2A6gg7n.js`, 0 TS err). Pre-existing warn: >500KB (#69) + realtime.ts dynamic. NO BE/mig/test touch. Full precedent (S89 tạo chip). Tag `[s97-badge, ceo-approved-chip, prop-type-number-not-union, ts2322-gotcha, 6-callsite, sha256-2pair]`.
|
||||
|
||||
## 🆕 S97-Part2 (2026-07-01) — FE gate endsBeforeCeo runtime (hook 1-dòng, on-behalf H2-proposed @S98)
|
||||
- 2-app mirror `PeWorkflowPanel`: gate `finalizeFlowStep = endsBeforeCeo ? find(...) : null` + banner ~:278 (làm-mờ Bước CEO theo cờ RUNTIME Mig 60), type `endsBeforeCeo: boolean` sẵn 2 DTO, SHA256 IDENTICAL `b7abbc2f…`. Substance đã được cover third-party: reviewer MEMORY entry S97-bis + session-log S97 Part-2. Tag `[s97-part2, endsbeforeceo-gate, hook-only]`.
|
||||
|
||||
## 🆕 S97 (2026-07-01) — PE finalize wording + đảo default checkbox opt-out→opt-in ×2 app (anh Kiệt FDC UAT, em-main chốt)
|
||||
4 chuỗi-edit `PeWorkflowPanel.tsx` × {admin,user} = 8 hunk. PROD LIVE UAT (anh Kiệt xem trực tiếp). File 2 app byte-identical trước+sau → SHA-pair đúng nghĩa.
|
||||
- **E1 default checkbox:** `useState(true)`→`useState(false)` (~:57). Đảo S96 opt-out→opt-in: giờ default KHÔNG kết thúc, phải TÍCH mới finalize.
|
||||
- **E2 reset onSuccess:** `setApplyLevelFinalize(true)`→`(false)` (~:221). Anchor 3-dòng (`setFinalizeByCcm(false)`+X+`setApprovedPriceSource(null)`) để KHÔNG đụng `setApplyLevelFinalize(e.target.checked)` handler (:664).
|
||||
- **E3 helper text ternary** (~:669-671): reword theo default mới — nhánh true "Đã tích — …KẾT THÚC tại cấp này…", nhánh false "Mặc định: …TRÌNH TIẾP lên CEO… TÍCH vào nếu muốn KẾT THÚC…".
|
||||
- **E4a title** (~:329): static string → template literal `` `Quy trình kết thúc tại ${finalizeLevelName ?? finalizeStepName ?? 'cấp duyệt thay CEO'}…` ``. **E4b JSX text** (~:331): "không trình tới" → "Kết thúc tại {finalizeLevelName ?? finalizeStepName ?? 'cấp trên'}". `finalizeLevelName/finalizeStepName` ĐÃ trong scope (dùng banner :280-287) — no new prop/DTO.
|
||||
- **GOTCHA "không trình tới" 2 chỗ:** comment :121 (KHÔNG đụng) vs JSX :331 (đổi). Dùng ngữ-cảnh `>`…`</span>` cho E4b unique → grep-confirm :121 comment còn nguyên.
|
||||
- **SHA256 IDENTICAL ×2 = `bcf812fd…`** (git diff --no-index exit 0; diff 2 file byte-identical incl. before/after index hash `a92c9db..d97a597`). git diff = đúng 8 hunk (4/file, 12ins/12del), no thừa. Build PASS ×2 (admin 1948mod `index-l_kwCZIF.js` 22.0s / user 1937mod `index-DxUomy4C.js` 8.2s, 0 TS err). Pre-existing warn: @import CSS-order + >500KB + realtime.ts dynamic (#69). NO BE/test/other-FE touch. Full precedent, NO ambiguity. **LESSON: đảo default state = sửa CẢ init `useState` LẪN reset-onSuccess (2 chỗ dễ sót 1); bare-string xuất hiện 2 chỗ (comment+JSX) → anchor context phân-biệt.** Tag `[s97, pe-finalize-opt-in, invert-default-checkbox, wording-reword, sha256-2pair, dual-string-anchor]`.
|
||||
|
||||
## 🆕 S88 D2 (2026-06-25) — PE multi-winner OUTWARD FE ×2 app (BE đã đổi: create→LIST, list DTO winner names)
|
||||
3 edit OUTWARD, em-main chốt FE behavior, coord BE (impl-backend đã viết). 2 SHA-pair + 1 type-DIVERGE.
|
||||
- **E1 CreateContractDialog (PeDetailTabs ~:2144):** mut type `{contractId:string}`→**`{contractIds:string[]}`** (BE tạo 1 HĐ/mỗi IsWinner). onSuccess: toast `` `Đã tạo ${res.data.contractIds.length} hợp đồng từ phiếu.` `` + `navigate(/contracts/${res.data.contractIds[0]})` (giữ destination `/contracts/${...}` của bản single). Dialog GIỮ joined winner-names (:2174) + 1 ContractType-cho-mọi-winner. Bản gốc KHÔNG invalidate → nothing to preserve.
|
||||
- **E2 PeListPanel label (~:168):** `{p.selectedSupplierName && …✓…}` → prefer `winnerSupplierNames.join(', ')` khi `winnerSupplierNames?.length>0`, else `selectedSupplierName`. Giữ ✓ + `text-emerald-600`. Ternary lặp 2 lần (guard + render) — optional-chain `?.length` an-toàn undefined.
|
||||
- **E3 type (DIVERGE 2 file):** `PeListItem` +`winnerSupplierNames:string[]` sau urgent-flags (anchor `isUrgentByPro/Ccm`+`}` byte-identical 2 app → Edit same string ×2, grep-confirm KHÔNG vào inter-app diff, stat giữ 30ins/26del). Create-contract response = **inline generic** trong PeDetailTabs (KHÔNG named type ở types file) → E3b no-op; `contractId:string|null` :121/:443 = link-HĐ-đã-gắn field KHÁC, GIỮ.
|
||||
- **SHA256 ×2 pair:** PeDetailTabs `9d8120b6…`, PeListPanel `c97e133d…` (cp admin→user, git-diff --no-index IDENTICAL). Build PASS ×2 (admin `index-C3j9HaFh.js` 2.18s / user `index-Ct2cpC7h.js` 2.23s, 0 TS err). Pre-existing warn: >500KB + realtime.ts dynamic. NO BE/test touch. **LESSON: OUTWARD multi-winner = winnerSupplierNames[] (BE null selectedSupplierName khi ≥2) → FE prefer-list-fallback-single; type DIVERGED ≠ cp → edit-same + verify-not-in-diff (S87+ confirm).** Tag `[s88, pe-multi-winner-outward, contractIds-list, winner-names-fallback, sha256-2pair, type-diverge-edit-same]`.
|
||||
|
||||
## 🆕 S87+ (2026-06-25) — FROZEN PE behavior-preserving cleanup ×2 app (em-main chốt, coord BE null-safe)
|
||||
4 edit `PeDetailTabs.tsx` (SHA-pair) + 2 wording `types/purchaseEvaluation.ts` (DIVERGED — KHÔNG SHA-pair, edit identical 2 file).
|
||||
- **E1 HoSoLink clear:** `HoSoLinkRow.save` (~:2020) `hoSoLink: hoSoLink || null` → **`hoSoLink.trim()`** (state đã string nên `??""`=no-op; trim ra `""` khi xóa). Coord BE `UpdatePeDraftHandler` null-safe: `""`=CLEAR, null/omit=PRESERVE, val=set (fix data-loss xóa link khi edit header). **GUARD: InfoTab.save (:817) + `PeHeaderForm` PUT body GIỮ OMIT hoSoLink** (omit=preserve = đúng fix) — verify KHÔNG thêm.
|
||||
- **E2 #70 stale-echo guard:** 2 ô PRO `adjustMut` (row3 NS-kỳ-này :1548 + row8 GT-còn-lại :1649) chỉ `saving={adjustMut.isPending}` → `|| peFetching` (`replace_all` đúng 2 occ; `peFetching` computed :1268). Sibling ccmPeriod/proPrice/ccmPrice/proMut/ccmMut đã có. Chống fast-double-save clobber stale `ev.*` (2 ô share 1 absolute-set mut cross-echo).
|
||||
- **E3 stale comment :2507:** "AddItemDialog giữ (dead)…" → đã xóa (grep chỉ comment match; live = `DetailDialog`). Sửa thành "nút đã bỏ (dialog = DetailDialog)".
|
||||
- **E4 type wording (2 file):** `previousSelectedTotal`/`currentProposalTotal` inline cmt "của SelectedSupplier" → "NCC trúng (IsWinner)" (BE giờ multi-winner). Block 7-dòng byte-identical 2 app → edit same → KHÔNG new divergence (grep-confirm my-line ∉ diff).
|
||||
- **SHA256 PeDetailTabs ×2 = `36292924…`** (cp admin→user, git-diff empty). PeHeaderForm untouched `987291c1…` identical. Build PASS ×2 (admin 1946mod `index-CqiQWR8e.js` / user 1935mod `index-Cp0DuKFQ.js`, 0 TS err). Pre-existing warn: @import CSS + >500KB + realtime.ts. NO BE/test touch. **LESSON: type files DIVERGED (S76/S79 color+prose) ≠ SHA-pair → mirror = edit-identical-block + verify-not-in-diff, KHÔNG cp.** Tag `[s87plus, pe-frozen-cleanup, hosolink-clear-empty, gotcha70-guard, type-diverge-edit-identical]`.
|
||||
|
||||
## 📦 Archived detail → `archive/2026-H1.md` (read on demand)
|
||||
- **2026-07-12** Supplier import Excel FE Phase B ×2 app byte-identical — NEW SupplierImportDialog + wire SuppliersPage ("Import Excel NCC"); multipart preview→confirm (echo rows verbatim); enum serialize INT; NO 4-place (dialog trên page routed). `[supplier-import-excel, enum-int-wire, byte-identical-2app]`
|
||||
- **S97-badge** (07-01) PE chip "✅ Đã được CEO duyệt" khi `!endsBeforeCeo && phase=DaDuyet` — PeFinalizeChip +prop `phase?:number` (KHÔNG narrow-union → TS2322) wire 6 call-site. SHA-pair `da9c3ed9`. `[s97-badge, prop-number-not-union]`
|
||||
- **S97-Part2** (07-01) PE gate endsBeforeCeo runtime hook-only PeWorkflowPanel (làm-mờ Bước CEO, Mig 60). SHA-pair `b7abbc2f`. `[s97-part2, endsbeforeceo-gate]`
|
||||
- **S97** (07-01) PE finalize wording + đảo default checkbox opt-out→opt-in — sửa CẢ init `useState(false)` LẪN reset-onSuccess; bare-string 2 chỗ (comment+JSX) anchor-context. SHA-pair `bcf812fd`. `[s97, invert-default-checkbox]`
|
||||
- **S88 D2** (06-25) PE multi-winner OUTWARD — CreateContractDialog `{contractIds:string[]}`; PeListPanel `winnerSupplierNames` prefer-fallback-single. SHA-pair + type-diverge-edit-same. `[s88, multi-winner-outward, cardinality-grep-consumers]`
|
||||
- **S87+** (06-25) FROZEN PE cleanup — HoSoLink clear `.trim()` (""=CLEAR, BE null-safe); #70 stale-echo `||peFetching` 2 ô PRO; type wording DIVERGE edit-same. SHA-pair `36292924`. `[s87plus, hosolink-clear-empty, gotcha70-guard]`
|
||||
- **S79** (2026-06-19) — PE live thousand-sep (`formatVndInput(parseVnd())` on-keystroke 2 comp) + ghi chú giá PRO/CCM (`note` 3-field absolute-set) + typo "Bảng so sánh giá". SHA-pair. `[s79, type-diverge-2app]`
|
||||
- **S78** (2026-06-19) — PE "focus mode" overlay redesign mirror fe-user→admin CONVERGE. PeWorkflowPanel +`onApproved` prop; ListPage `fixed inset-0 z-50` slide overlay a11y. SHA-pair. `[s78, mirror-step, converge]`
|
||||
- **S76** (2026-06-19) — PE budget ma trận 3 cột (DỰ ÁN/PRO/CCM) → `<table border-collapse>` grid + badge `✎ NS PRO/CCM`. LESSON: flex+gap ≠ bảng, phải `<table>`. cwd-misland `fe-*/.claude/` gitignore-guard. `[s76]`
|
||||
|
||||
@ -5,6 +5,47 @@
|
||||
|
||||
---
|
||||
|
||||
## 🆕 2026-07-12 — Supplier import Excel FE (Phase B Approach A) ×2 app byte-identical (lead chốt UX; BE done+450 tests)
|
||||
NEW `components/master/SupplierImportDialog.tsx` (folder `master/` MỚI cả 2 app) + wire `pages/master/SuppliersPage.tsx` (nút "Import Excel NCC" outline cạnh "Thêm NCC" trong PageHeader actions flex + render dialog). KHÔNG 4-place mirror — dialog trên page ĐÃ routed → no new route/menuKey/Layout.
|
||||
- **Flow:** .xlsx → POST `/suppliers/import/preview` multipart field `file` → preview. `layoutValid===false`→alert đỏ+warnings, ẩn nút xác nhận. Else: badge counts (New/Update/Skip/Error) + warnings amber + `<table>` raw (Dòng·StatusBadge·Mã·Tên·Loại·Thông báo). "Xác nhận nhập" disabled `errorCount>0` → POST `/import/confirm` body `{rows}` = echo `preview.rows` VERBATIM. committed→toast+`invalidateQueries(['suppliers'])`+close; !committed→alert `errors[]`.
|
||||
- **BE contract (Read-only DTO):** enum serialize = **INT** (chỉ UtcDateTimeJsonConverter đăng ký, KHÔNG JsonStringEnumConverter) → `type` 1-6 reuse `SupplierTypeLabel`, `status` RowImportStatus 0-3 const-object; camelCase wire (default web JSON); confirm bind positional record `(IReadOnlyList Rows)`. Round-trip 30 field verbatim → TS type partial vẫn OK runtime.
|
||||
- **Mirror lesson (tái xác nhận S97-badge):** Dialog (fe-user gradient title bar) + PageHeader + types/master (khác MỖI `Department.parentId` admin-only S65) ĐỀU DIVERGED 2 app — NHƯNG import alias-path `@/…` + symbol-signature giống → new file VẪN byte-identical. Byte-identical SuppliersPage cũ (build 2 app) = proof mọi symbol tương thích. UI: KHÔNG shadcn Badge/Alert/Table primitive (chỉ PhaseBadge) → inline Tailwind badge/alert + `<table>`. Multipart = pattern `ContractAttachmentsSection` (FormData + `Content-Type: multipart/form-data`).
|
||||
- **SHA256 ×2 pair:** SupplierImportDialog `6313529c…`, SuppliersPage `e9143d93…`. Build PASS ×2 (admin 1950mod `index-DyOk34HV.js` 19.5s / user 1939mod `index-DXK9ebaz.js` 8.75s, 0 TS err). Pre-existing warn: @import CSS-order + >500KB + realtime.ts dynamic. NO BE touch. Full precedent. Tag `[supplier-import-excel, multipart-preview-confirm, enum-int-wire, byte-identical-2app, no-4place-dialog-on-routed-page]`.
|
||||
|
||||
## 🆕 S97-badge (2026-07-01) — PE chip "✅ Đã được CEO duyệt" khi phiếu ĐÃ DUYỆT đi hết lên CEO (anh Kiệt FDC UAT, em-main chốt)
|
||||
FE-ONLY, PROD LIVE. Đối xứng fix endsBeforeCeo (Mig 60). EDIT1 mở rộng `PeFinalizeChip.tsx` (2 app) + EDIT2 wire `phase={p.phase}` ở 6 call-site.
|
||||
- **EDIT1 component:** +prop `phase?` + nhánh: `if(!endsBeforeCeo){ if(phase===DaDuyet) return emerald "✅ Đã được CEO duyệt"; return null }` else giữ violet "⚑ Kết thúc·X". Import `PurchaseEvaluationPhase` từ `@/types/purchaseEvaluation` — **cùng alias 2 app** → byte-identical (KHÔNG ngoại-lệ import-path). SHA256 IDENTICAL ×2 = `da9c3ed9…`.
|
||||
- **⚠️ GOTCHA TYPE (đã trải):** spec ghi prop `phase?: PurchaseEvaluationPhase` (narrow union `1|…|99`) NHƯNG `PeListItem.phase: number` (types :113) → `phase={p.phase}` = **TS2322 "number not assignable to PurchaseEvaluationPhase"** (fail 2 site). FIX: prop → `phase?: number` (literal `DaDuyet`=7 so-khớp OK; import vẫn dùng trong `=== …DaDuyet` → no unused). LESSON: list-DTO field mirror-BE = `number` rộng → prop nhận `number`, KHÔNG narrow-union; check kiểu item trước khi type prop.
|
||||
- **EDIT2 6 site (grep `PeFinalizeChip endsBeforeCeo` = 6, hết):** admin{PeListPanel:162, ListPage:492} + user{PeListPanel:162, ListPage:492, InboxPage:292, UserDashboardPage:286}. MỌI item = `p` có `.phase` (đã dùng `getPeDisplayStatus(p.phase)`) → KHÔNG bịa. Grep-confirm 6/6 có `phase=`, 0 sót.
|
||||
- Build PASS ×2 (admin 1948mod `index-CScNAtL0.js` / user 1937mod `index-W2A6gg7n.js`, 0 TS err). Pre-existing warn: >500KB (#69) + realtime.ts dynamic. NO BE/mig/test touch. Full precedent (S89 tạo chip). Tag `[s97-badge, ceo-approved-chip, prop-type-number-not-union, ts2322-gotcha, 6-callsite, sha256-2pair]`.
|
||||
|
||||
## 🆕 S97-Part2 (2026-07-01) — FE gate endsBeforeCeo runtime (hook 1-dòng, on-behalf H2-proposed @S98)
|
||||
- 2-app mirror `PeWorkflowPanel`: gate `finalizeFlowStep = endsBeforeCeo ? find(...) : null` + banner ~:278 (làm-mờ Bước CEO theo cờ RUNTIME Mig 60), type `endsBeforeCeo: boolean` sẵn 2 DTO, SHA256 IDENTICAL `b7abbc2f…`. Substance đã được cover third-party: reviewer MEMORY entry S97-bis + session-log S97 Part-2. Tag `[s97-part2, endsbeforeceo-gate, hook-only]`.
|
||||
|
||||
## 🆕 S97 (2026-07-01) — PE finalize wording + đảo default checkbox opt-out→opt-in ×2 app (anh Kiệt FDC UAT, em-main chốt)
|
||||
4 chuỗi-edit `PeWorkflowPanel.tsx` × {admin,user} = 8 hunk. PROD LIVE UAT (anh Kiệt xem trực tiếp). File 2 app byte-identical trước+sau → SHA-pair đúng nghĩa.
|
||||
- **E1 default checkbox:** `useState(true)`→`useState(false)` (~:57). Đảo S96 opt-out→opt-in: giờ default KHÔNG kết thúc, phải TÍCH mới finalize.
|
||||
- **E2 reset onSuccess:** `setApplyLevelFinalize(true)`→`(false)` (~:221). Anchor 3-dòng (`setFinalizeByCcm(false)`+X+`setApprovedPriceSource(null)`) để KHÔNG đụng `setApplyLevelFinalize(e.target.checked)` handler (:664).
|
||||
- **E3 helper text ternary** (~:669-671): reword theo default mới — nhánh true "Đã tích — …KẾT THÚC tại cấp này…", nhánh false "Mặc định: …TRÌNH TIẾP lên CEO… TÍCH vào nếu muốn KẾT THÚC…".
|
||||
- **E4a title** (~:329): static string → template literal `` `Quy trình kết thúc tại ${finalizeLevelName ?? finalizeStepName ?? 'cấp duyệt thay CEO'}…` ``. **E4b JSX text** (~:331): "không trình tới" → "Kết thúc tại {finalizeLevelName ?? finalizeStepName ?? 'cấp trên'}". `finalizeLevelName/finalizeStepName` ĐÃ trong scope (dùng banner :280-287) — no new prop/DTO.
|
||||
- **GOTCHA "không trình tới" 2 chỗ:** comment :121 (KHÔNG đụng) vs JSX :331 (đổi). Dùng ngữ-cảnh `>`…`</span>` cho E4b unique → grep-confirm :121 comment còn nguyên.
|
||||
- **SHA256 IDENTICAL ×2 = `bcf812fd…`** (git diff --no-index exit 0; diff 2 file byte-identical incl. before/after index hash `a92c9db..d97a597`). git diff = đúng 8 hunk (4/file, 12ins/12del), no thừa. Build PASS ×2 (admin 1948mod `index-l_kwCZIF.js` 22.0s / user 1937mod `index-DxUomy4C.js` 8.2s, 0 TS err). Pre-existing warn: @import CSS-order + >500KB + realtime.ts dynamic (#69). NO BE/test/other-FE touch. Full precedent, NO ambiguity. **LESSON: đảo default state = sửa CẢ init `useState` LẪN reset-onSuccess (2 chỗ dễ sót 1); bare-string xuất hiện 2 chỗ (comment+JSX) → anchor context phân-biệt.** Tag `[s97, pe-finalize-opt-in, invert-default-checkbox, wording-reword, sha256-2pair, dual-string-anchor]`.
|
||||
|
||||
## 🆕 S88 D2 (2026-06-25) — PE multi-winner OUTWARD FE ×2 app (BE đã đổi: create→LIST, list DTO winner names)
|
||||
3 edit OUTWARD, em-main chốt FE behavior, coord BE (impl-backend đã viết). 2 SHA-pair + 1 type-DIVERGE.
|
||||
- **E1 CreateContractDialog (PeDetailTabs ~:2144):** mut type `{contractId:string}`→**`{contractIds:string[]}`** (BE tạo 1 HĐ/mỗi IsWinner). onSuccess: toast `` `Đã tạo ${res.data.contractIds.length} hợp đồng từ phiếu.` `` + `navigate(/contracts/${res.data.contractIds[0]})` (giữ destination `/contracts/${...}` của bản single). Dialog GIỮ joined winner-names (:2174) + 1 ContractType-cho-mọi-winner. Bản gốc KHÔNG invalidate → nothing to preserve.
|
||||
- **E2 PeListPanel label (~:168):** `{p.selectedSupplierName && …✓…}` → prefer `winnerSupplierNames.join(', ')` khi `winnerSupplierNames?.length>0`, else `selectedSupplierName`. Giữ ✓ + `text-emerald-600`. Ternary lặp 2 lần (guard + render) — optional-chain `?.length` an-toàn undefined.
|
||||
- **E3 type (DIVERGE 2 file):** `PeListItem` +`winnerSupplierNames:string[]` sau urgent-flags (anchor `isUrgentByPro/Ccm`+`}` byte-identical 2 app → Edit same string ×2, grep-confirm KHÔNG vào inter-app diff, stat giữ 30ins/26del). Create-contract response = **inline generic** trong PeDetailTabs (KHÔNG named type ở types file) → E3b no-op; `contractId:string|null` :121/:443 = link-HĐ-đã-gắn field KHÁC, GIỮ.
|
||||
- **SHA256 ×2 pair:** PeDetailTabs `9d8120b6…`, PeListPanel `c97e133d…` (cp admin→user, git-diff --no-index IDENTICAL). Build PASS ×2 (admin `index-C3j9HaFh.js` 2.18s / user `index-Ct2cpC7h.js` 2.23s, 0 TS err). Pre-existing warn: >500KB + realtime.ts dynamic. NO BE/test touch. **LESSON: OUTWARD multi-winner = winnerSupplierNames[] (BE null selectedSupplierName khi ≥2) → FE prefer-list-fallback-single; type DIVERGED ≠ cp → edit-same + verify-not-in-diff (S87+ confirm).** Tag `[s88, pe-multi-winner-outward, contractIds-list, winner-names-fallback, sha256-2pair, type-diverge-edit-same]`.
|
||||
|
||||
## 🆕 S87+ (2026-06-25) — FROZEN PE behavior-preserving cleanup ×2 app (em-main chốt, coord BE null-safe)
|
||||
4 edit `PeDetailTabs.tsx` (SHA-pair) + 2 wording `types/purchaseEvaluation.ts` (DIVERGED — KHÔNG SHA-pair, edit identical 2 file).
|
||||
- **E1 HoSoLink clear:** `HoSoLinkRow.save` (~:2020) `hoSoLink: hoSoLink || null` → **`hoSoLink.trim()`** (state đã string nên `??""`=no-op; trim ra `""` khi xóa). Coord BE `UpdatePeDraftHandler` null-safe: `""`=CLEAR, null/omit=PRESERVE, val=set (fix data-loss xóa link khi edit header). **GUARD: InfoTab.save (:817) + `PeHeaderForm` PUT body GIỮ OMIT hoSoLink** (omit=preserve = đúng fix) — verify KHÔNG thêm.
|
||||
- **E2 #70 stale-echo guard:** 2 ô PRO `adjustMut` (row3 NS-kỳ-này :1548 + row8 GT-còn-lại :1649) chỉ `saving={adjustMut.isPending}` → `|| peFetching` (`replace_all` đúng 2 occ; `peFetching` computed :1268). Sibling ccmPeriod/proPrice/ccmPrice/proMut/ccmMut đã có. Chống fast-double-save clobber stale `ev.*` (2 ô share 1 absolute-set mut cross-echo).
|
||||
- **E3 stale comment :2507:** "AddItemDialog giữ (dead)…" → đã xóa (grep chỉ comment match; live = `DetailDialog`). Sửa thành "nút đã bỏ (dialog = DetailDialog)".
|
||||
- **E4 type wording (2 file):** `previousSelectedTotal`/`currentProposalTotal` inline cmt "của SelectedSupplier" → "NCC trúng (IsWinner)" (BE giờ multi-winner). Block 7-dòng byte-identical 2 app → edit same → KHÔNG new divergence (grep-confirm my-line ∉ diff).
|
||||
- **SHA256 PeDetailTabs ×2 = `36292924…`** (cp admin→user, git-diff empty). PeHeaderForm untouched `987291c1…` identical. Build PASS ×2 (admin 1946mod `index-CqiQWR8e.js` / user 1935mod `index-Cp0DuKFQ.js`, 0 TS err). Pre-existing warn: @import CSS + >500KB + realtime.ts. NO BE/test touch. **LESSON: type files DIVERGED (S76/S79 color+prose) ≠ SHA-pair → mirror = edit-identical-block + verify-not-in-diff, KHÔNG cp.** Tag `[s87plus, pe-frozen-cleanup, hosolink-clear-empty, gotcha70-guard, type-diverge-edit-identical]`.
|
||||
|
||||
## S79 (2026-06-19) — PE detail: live thousand-sep + ghi chú giá PRO/CCM + typo (×2 app)
|
||||
3 task presentation+wiring `PeDetailTabs.tsx` (+`PeWorkspaceCreateView.tsx` typo). BE thêm `note` body song song; FE chốt UX rồi.
|
||||
- **A live phân cách:** chỉ **2 comp** thiếu sep — `VndInlineEdit` (~990) + `BudgetCell` (~1100): đổi `onChange` `value.replace(/[^\d.]/g,'')` → **`formatVndInput(parseVnd(e.target.value))`** (format-on-keystroke, reuse helper module-level :51/52). Dialog money inputs (2111/2555/2645) ĐÃ dùng `formatVndInput`/`parseVnd` sẵn → no-op. KHÔNG đụng phone(tel)/%/qty.
|
||||
|
||||
@ -85,8 +85,9 @@ when-to-use:
|
||||
| **63** | **`AddSupplierImportSourceFields`** | **🎯 S112 — Supplier +`SourceUpdatedAt datetime2?` + `SourceUpdatedBy nvarchar(200)?` (import-provenance cột 29/30 file Excel "Database NCC" — Phase B Excel-import Phase B). 2 AddColumn nullable + 2 DropColumn Down (reversible). **No new table (89 giữ)**. Prod-verified Run #496 PASS `__EFMigrationsHistory` top + 2/2-col present; commit `e100ef0`.** |
|
||||
| **64** | **`AddSupplierPublishState`** | **🎯 S113 — Supplier +`IsPublic` bit NOT NULL default 0 (cổng công-bố "thiếu Mã NCC → nháp/ẩn" R4): AddColumn + `Sql("UPDATE Suppliers SET IsPublic=1")` backfill 37/37 prod→public + DropIndex→CreateIndex đổi Code filtered-unique `WHERE [IsDeleted]=0 AND [Code]<>''` (cho phép nhiều nháp Code=''). **No new table (89 giữ)**. Reversible Down (⚠️ edge: rollback sau khi có ≥2 nháp Code='' sẽ kẹt CreateIndex filter cũ). Prod-verified Run #497; commit `5fa11b5`. Kèm gotcha #80 (null-Code NRE NewSupplier).** |
|
||||
| **65** | **`BackfillPeQuoteIsSelectedAndWinnerInvariant`** | **🎯 S114 — DATA-ONLY no-schema (multi-NCC per hạng mục anh Kiệt): backfill **2-CHIỀU** thiết lập bất-biến `Supplier.IsWinner == Any(Quote.IsSelected)`. CHIỀU-1 `IsWinner→IsSelected` (phiếu cũ whole-supplier winner Mig58 → set IsSelected mọi quote NCC, **preserve tổng** winnerQuoteTotal) + CHIỀU-2 `IsSelected→IsWinner` (dormant/seed EXISTS). Idempotent (`WHERE ...=0`), SQL-Server-only (SQLite EnsureCreated bỏ qua migration), Down **no-op** (data backfill không reversible, mirror Mig 61). **No new table (89 giữ)** — snapshot UNCHANGED (0 CreateColumn). Prod-verified bundle rotate ×2 `D51OYyGV`/`BVdssm5S`; commit `d436af2`. Kèm **gotcha #81** (delete-writer sót re-derive IsWinner → phantom HĐ giaTri=0 — verify-bắt).** |
|
||||
| **66** | **`AddPeApprovedPriceRange`** | **🎯 S117 — PE +`ApprovedPriceMaxAmount decimal(18,2)?` (feature "duyệt theo khoảng Min–Max" anh Kiệt FDC "chọn cả 2 min và max"): source `"ProMinMax"` = Amount(Min)+MaxAmount(Max), bất-biến `MaxAmount != null ⟺ Source=="ProMinMax"` + Min≤Max (guard service `ApplyApprovedPriceOnFinalize` + FluentValidation). 1 AddColumn nullable, **no backfill** (phiếu cũ Source-đơn Max=null nguyên vẹn), **no new table (89 giữ)**, reversible Down (DropColumn). HĐ.GiaTri KHÔNG đọc giá chốt (=SUM báo giá NCC được chọn) → 0 tác động HĐ (range = record-of-decision hiển thị). Prod-verified Run #387 (`ApprovedPriceMaxAmount` col decimal(18,2) + `__EFMigrationsHistory` top); commit `316a82f`.** |
|
||||
|
||||
Total: **89 bảng** `sys.tables` (= 88 dbo-app + `__EFMigrationsHistory`; re-ground S97 cicd Run #482 LIVE — số canonical → `docs/STATUS.md`; last schema-changing Mig 50 net 93→88-app, Mig 51-64 đều AddColumn/index/data-only/schema-expand không đổi số bảng). Xem `docs/database/schema-diagram.md` migration table + §11-15 module ERD (§16+ Mig 27-64 chi tiết pending).
|
||||
Total: **89 bảng** `sys.tables` (= 88 dbo-app + `__EFMigrationsHistory`; re-ground S97 cicd Run #482 LIVE — số canonical → `docs/STATUS.md`; last schema-changing Mig 50 net 93→88-app, Mig 51-66 đều AddColumn/index/data-only/schema-expand không đổi số bảng). Xem `docs/database/schema-diagram.md` migration table + §11-15 module ERD (§16+ Mig 27-64 chi tiết pending).
|
||||
|
||||
## N-stage workflow pattern (Mig 18-20 — Session 12-13)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,44 @@
|
||||
# S117 — PE lũy-kế display-fix + duyệt theo khoảng Min–Max (2026-07-14)
|
||||
|
||||
**Commit:** `316a82f` · **CI Run #387** PASS 5m36s · **State:** Mig 66 · 89 bảng · 501 test · gotcha 81 · menu 54 · bundle admin `cFI7ih4a`/user `MxgooVZw` · prod 37 NCC/17 user.
|
||||
**Mode:** HMW-ON · lead Opus 4.8 · em-main-led · 2× `/fable-real investigator-codebase` engine-đắt (Fable) + 4 spawn (test-specialist · implementer-frontend · cicd-monitor) · 1 commit.
|
||||
|
||||
Buổi UAT anh Kiệt FDC (Zalo, ảnh) đưa 2 việc PE — anh chỉ-định `/fable-real` điều-tra rồi fix.
|
||||
|
||||
## (1) Fix hiển thị lũy-kế "duyệt rồi không bắt" — root cause = DISPLAY-LIE, KHÔNG phải lũy-kế bug
|
||||
|
||||
**Triệu chứng (anh Kiệt/Kenny/Bích Phượng):** phiếu A/018 "8 Mat Gạch xây…" (MAT-8) anh Chương duyệt rồi nhưng phiếu mới A/028 cùng hạng mục hiện Row "2. Ký trước đã chọn thầu = Chưa chọn" → "duyệt rồi mà không bắt lũy kế", xóa/tạo lại vô ích. Nguyên tắc Kenny: "duyệt rồi mới bắc lũy kế".
|
||||
|
||||
**Điều tra (`/fable-real invest`, wf `ab2657ee`):** ⚠️ #53-garble lần 1 → **SendMessage-resume** recover (verdict đầy đủ lần 2) + em-main verify prod-DB độc-lập. Kết quả **lật tiền-đề**: A/018 **CHƯA DaDuyet** — prod `Phase=ChoDuyet, EndedByLevelFinalize=0, pointer Step2/Lvl1 = đang chờ CEO (Bước 3)`. Anh Chương duyệt cấp finalize (AllowApproverFinalize=1) NHƯNG **không tick "Cấp này KẾT THÚC quy trình"** (S97 đảo default = bỏ-tick/opt-in) → phiếu **advance lên CEO** thay vì finalize. `CeoApprovalThreshold=NULL` → nhánh ccm-delegation không dùng được. Lũy-kế filter Row 2 (`PurchaseEvaluationFeatures.cs:906` `Phase==DaDuyet && Any(IsWinner)`) loại A/018 **ĐÚNG** (chưa duyệt xong). IsWinner A/018 = có (winner Thảo Anh). Tính LIVE → xóa/tạo lại A/028 vô ích (lỗi ở phiếu-nguồn).
|
||||
|
||||
**Bug thật = display-lie (gotcha #78 nửa-còn-lại):** `endsBeforeCeo` cho phiếu non-DaDuyet trả config `true` mù (`PurchaseEvaluationFeatures.cs:1152` literal `: true`; list/inbox `.Any()`) → sơ đồ mờ Bước 3 "Kết thúc tại Cấp 2" NGAY KHI phiếu đang nằm inbox CEO → user tưởng đã xong. S97 mới sửa cho phiếu ĐÃ DaDuyet (runtime `EndedByLevelFinalize`), bỏ sót nhánh ChoDuyet.
|
||||
|
||||
**Owner AskUser → "Sửa hiển thị + giữ opt-in".** Fix **POINTER-AWARE 3 site**: non-DaDuyet chỉ `endsBeforeCeo=true` khi CÒN cấp finalize ở-hoặc-SAU con-trỏ hiện tại (phiếu còn có thể finalize). Đã đi QUA → false. Detail `:1152` (in-memory, robust index `steps.IndexOf` vs currentIdx) + list `:647` + inbox `:767` (EF-subquery, rank cấp = `Step.Order-1` — Order tuần-tự s+1 seed/Designer). **BE-only** (FE là consumer thuần: `PeFinalizeChip` + `PeWorkflowPanel:124` gate cờ). +1 test repro `PeFinalizeProjectionTests.ListPe_EndsBeforeCeo_ChoDuyet_PointerAware_PastFinalizeIsFalse` (RED trước → GREEN sau). A/018 lên lũy-kế = việc vận-hành (CEO duyệt / re-approve tick).
|
||||
|
||||
## (2) Feature "duyệt theo khoảng Min–Max"
|
||||
|
||||
**Yêu cầu (anh Kiệt):** ô "CHỌN GIÁ CHỐT" khi duyệt cuối chọn 1 radio (Ncc/ProMin/ProMax/Ccm). Khi PRO có cả Min & Max → cho **"chọn cả 2 min và max"** = duyệt theo khoảng [Min,Max].
|
||||
|
||||
**Điều tra (`/fable-real invest`, wf `abbbb86d`, 0-garble clean verdict) → Model A:** +cột `ApprovedPriceMaxAmount` + source token `"ProMinMax"` radio thứ 5 (chỉ khi cả 2 giá PRO ≠ null). Amount=Min, MaxAmount=Max. Bất-biến `MaxAmount != null ⟺ Source=="ProMinMax"` + Min≤Max. **Linchpin verify độc-lập:** `CreateContractFromEvaluationFeatures.cs:88` GiaTri = SUM báo giá NCC được chọn, `grep ApprovedPrice` = 0-match → **HĐ KHÔNG đọc giá chốt** → range thuần record-of-decision, **0 tác động HĐ**.
|
||||
|
||||
**Implement (anh "làm theo hướng dẫn nó luôn"):**
|
||||
- BE **em-main-solo** (financial + signature ripple): entity + EF config + Mig 66 (AddColumn nullable no-backfill, reversible) + service (`ValidApprovedPriceSources` +ProMinMax · `ApplyApprovedPriceOnFinalize` +param maxAmount + validate · 3 call-site :858/:896/:928 + 2 method sig) + command/validator/handler + controller body + DTO + projection + interface.
|
||||
- FE **implementer-frontend** 2-app SHA-mirror `011ee8cf`/`617b0aa0` (Panel radio thứ 5 + submit `approvedPriceMaxAmount` chỉ khi ProMinMax → axios omit key với source đơn = khớp BE reject · DetailTabs "min – max") + types ×2 (không mirror).
|
||||
- Test **test-specialist** +5 (happy/min==max-boundary/thiếu-Max-Conflict/min>max-Conflict/single+Max-wire-bug + regression). Harness fix: `InvokeApply` reflection 4→5 args (optional param không auto-bind `MethodInfo.Invoke` → tránh `TargetParameterCountException` phá 3 test cũ).
|
||||
- 3 product-decision investigator flag → resolve theo khuyến nghị (HĐ giữ nguyên · range dùng chung 3 finalize-path · 1 radio khớp "chọn cả 2").
|
||||
|
||||
## Verify + deploy
|
||||
- BE build 0-err · full suite **495→501** (Domain 45 + Infra 456) · Mig 66 applied Dev (+ caught up Mig 65 unapplied-local drift) · FE 2-app build sạch · SHA-mirror khớp · submit logic review đúng contract · diff 18 file (pre-existing "M" = CRLF, verify all-mine).
|
||||
- Commit `316a82f` push main → **cicd Run #387 PASS**: test gate 501 · Mig 66 prod (`ApprovedPriceMaxAmount` decimal(18,2) + history top + 89 bảng) · bundle rotate ×2 (CSS frozen=logic-only) · byte-level `ProMinMax`/`duyệt theo khoảng`/`cả Min và Max`/`Min–Max` PRESENT · smoke 200×3 · PE 401.
|
||||
- ⚠️ **#53-garble ×2** (invest-lane-1 + cicd-monitor) → SendMessage-resume + **em-main curl độc-lập cross-verify** (`cFI7ih4a`/`MxgooVZw` khớp verdict agent) = verdict tin-cậy dù agent-return hỏng.
|
||||
|
||||
## Lessons
|
||||
- **premise-flip → surface-before-code:** báo-lỗi "lũy kế không bắt" hóa ra là ĐÚNG-behavior (phiếu chưa duyệt xong); điều-tra lật tiền-đề, hỏi owner TRƯỚC khi code (tránh sửa nhầm filter/đảo S97). Real bug = display-lie.
|
||||
- **HĐ-independent-verify-before-model:** grep ApprovedPrice=0 ở HĐ-gen = bằng chứng range an-toàn → chọn Model A (chỉ record-of-decision) không lo tác động tài-chính.
|
||||
- **#53-garble WRITE-lane ≠ mất-code** (code trên disk) + **cicd cross-verify em-main-curl** (2 nguồn độc-lập khớp) = pattern verify đáng-tin.
|
||||
- **signature-ripple financial = em-main-solo** (3 call-site + 2 method), delegate mechanical FE-mirror + test.
|
||||
- reflection-test `MethodInfo.Invoke` không auto-bind optional param → đổi arity helper phải cập-nhật (test-specialist bắt).
|
||||
|
||||
## NEXT
|
||||
**Anh (UAT prod INCOGNITO — #77):** (1) phiếu chờ CEO → sơ đồ hết mờ "Kết thúc tại Cấp 2"; (2) đưa A/018 lên Đã duyệt → A/028 hiện lũy kế 749.564.000đ; (3) phiếu PRO Min–Max → radio "PRO — cả Min và Max" → chi tiết "min – max"; (4) carry BVAAU.
|
||||
**Em:** commit closeout · watch AI_INFRA 3 email · inv-cb L1 curate · monthly re-tier 2026-08-01.
|
||||
Reference in New Issue
Block a user