From 6bfe3db5e26826eb7953a5b1c0a425fecbf79b21 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 25 Jun 2026 10:58:15 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20Docs:=20S88=20closeout=20=E2=80=94?= =?UTF-8?q?=20PE=20stability=20audit=20+=20D1/D2=20prod-verified?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit STATUS/HANDOFF S88 prepend (State THAT: Mig 59 · 88 bang · 419 test (45D+374I) · gotcha 73 · bundle admin BQq2eTP6/user dHa64I59) + session log + agent-memory harvest (6 sub: cicd/impl-backend/impl-frontend/inv-codebase/reviewer/test-spec). D1/D2 production already shipped: 73cce1f (Run #347 PASS, +11 test, the 6 fixes) + 1aa3bdb (Run success, +6 test, create-contract 1->N + list winner names). Both prod-verified (bundle rotation + Gitea run=success; cicd-monitor verdict truncated #53 -> curl-self-verify). Curate-debt: test-specialist L1 34.8KB OVER-SOFT (root-caused #53 x3 this session) DEFERRED next session as #1 NEXT-em (proper byte-exact L1->L2 + _INDEX, not rushed). Co-Authored-By: Claude Opus 4.8 --- .claude/agent-memory/cicd-monitor/MEMORY.md | 16 ++--- .../implementer-backend/MEMORY.md | 1 + .../implementer-frontend/MEMORY.md | 15 +++++ .../investigator-codebase/MEMORY.md | 6 ++ .claude/agent-memory/reviewer/MEMORY.md | 5 ++ .../agent-memory/test-specialist/MEMORY.md | 2 +- docs/HANDOFF.md | 2 +- docs/STATUS.md | 2 +- ...2026-06-25-S88-pe-stability-audit-d1-d2.md | 58 +++++++++++++++++++ 9 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 docs/changelog/sessions/2026-06-25-S88-pe-stability-audit-d1-d2.md diff --git a/.claude/agent-memory/cicd-monitor/MEMORY.md b/.claude/agent-memory/cicd-monitor/MEMORY.md index 88782e2..523b8d6 100644 --- a/.claude/agent-memory/cicd-monitor/MEMORY.md +++ b/.claude/agent-memory/cicd-monitor/MEMORY.md @@ -32,7 +32,7 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve - **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 **402 PASS** (45 Domain + 357 Infra; S88 #345 — 395 + 7 new `PeCcmBudgetPeriodSetterAuthzTests`; prior S87 #344 395, S86 377). 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 **413 PASS** (45 Domain + 368 Infra; S88-quad #347 — 402 + 11 D1 [PeHoSoLinkTests+PeWorkItemBudgetTests EXTEND + new PeUrgentNotifyDirectorTests]; prior S88 #345 402, S87 #344 395). 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. @@ -48,11 +48,11 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve ## 🧠 SOLUTION_ERP CI/CD essentials (S80 verified — re-ground từ docs/STATUS.md canonical) - **Gitea:** `git.baocaogiaoduc.vn/vietreport-admin/solution-erp` · workflow `.gitea/workflows/deploy.yml` · paths-ignore `['docs/**','**/*.md','.claude/skills/**']`. 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`)**. pw `vrapp/buKL3TGBkD0wDDbYVw65QeX9` read from prod `appsettings.Production.json` 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. - **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:** **402 PASS** (45 Domain + 357 Infra · 0 fail/skip; S88 Run #345 sha 9b1ee0c — 395 + 7 CCM-budget-period setter authz; prior S87 #344 395). Phase 9 UAT skip per chunk OK. +- **Tests baseline:** **419 PASS** (45 Domain + 374 Infra · 0 fail/skip; S89 Run #348 sha 1aa3bdb — 413 + 6 D2 multi-winner [CreateContractFromEvaluationMultiWinnerTests + PeListWinnerNamesProjectionTests]; prior S88-quad #347 413). Phase 9 UAT skip per chunk OK. - **Mig latest repo:** **Mig 59 `20260624084608_AddCcmBudgetPeriodToPurchaseEvaluation`** (S88 #345; PE +1 AddColumn `CcmBudgetPeriodAmount` decimal(18,2) NULL no-backfill — VERIFIED-APPLIED-PROD history-HEAD + col-exists 0-nonnull). Prev Mig 58 `AddPeMultiWinnerAndLevelFinalize` (S87 #344). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`. ⚠️ **`sys.tables` (is_ms_shipped=0) = 88** (S61 Budget-replace DROPPED 93→88 — narrative-93 STALE; when commit touches no schema, 88 correct, don't FAIL on 88. Always cross-ref COMMIT scope vs ambient count). enum-additive (e.g. `ApprovalAttachment=5`) = int-stored NO-mig → expect history NOT-advance + empty `git diff -- '*Migrations*'`. -- **Bundle hash live (S88-bis Run #346):** admin js `BABhPh2s`/css `Pltrs5ku` · user js `DLulO6uP`/css `Foiw55B6` (BOTH js+css ROTATED from #345 `C8lWxEQK`/`BFfMO5l8` + `Kz5cSW2u`/`B11h2RoV` by real fe-src PE "4. Thông tin chọn thầu" layout restructure — css rotated = new utility classes for highlighted block). ⚠️ **PER #69: hash MAY rotate per deploy OR stay frozen if 0 fe-src diff (byte-identical) — SNAPSHOT for THIS run, NOT a frozen baseline.** Real FE ship = diff `fe-*/src` in commit/range + real-size-vs-fake (1.6/1.5MB vs ~919b SPA-trap), NOT hash delta. ASYMMETRIC-deploy framing (S66) RETIRED by #69. +- **Bundle hash live (S89 Run #348):** admin js `BQq2eTP6`/css `Pltrs5ku` · user js `dHa64I59`/css `Foiw55B6` (JS BOTH ROTATED from #347 `CP_iKhTu`/`DUe0fndl` by real fe-src D2 PeDetailTabs+PeListPanel; **css FROZEN `Pltrs5ku`/`Foiw55B6` = #347/#346 — correct, D2 FE logic-only no new utility class**). ⚠️ **PER #69: hash MAY rotate per deploy OR stay frozen if 0 fe-src diff (byte-identical) — SNAPSHOT for THIS run, NOT a frozen baseline.** Real FE ship = diff `fe-*/src` in commit/range + real-size-vs-fake (1.6/1.5MB vs ~919b SPA-trap), NOT hash delta. ASYMMETRIC-deploy framing (S66) RETIRED by #69. - **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) @@ -70,16 +70,16 @@ BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code / ## 📅 Recent runs (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`) -- **S88-bis #346 (id=460) `0d3c0cf` PASS ~5m (FE-ONLY PE layout, anh Kiệt FDC)** — restructure PeDetailTabs: tách c/d/e → khối nổi bật "4. Thông tin chọn thầu". Files=2 ONLY: `fe-admin/src/components/pe/PeDetailTabs.tsx` + `fe-user/...` — ZERO BE, ZERO `*Migrations*`/`*Persistence*` (verified empty diff), ZERO test. status=success ⟹ test gate passed (**baseline 402 unchanged**, FE-only). **Bundle BOTH js+css ROTATE (real fe-src layout ⟹ new content-hash incl new utility classes): admin js `C8lWxEQK`→`BABhPh2s` css `BFfMO5l8`→`Pltrs5ku`, user js `Kz5cSW2u`→`DLulO6uP` css `B11h2RoV`→`Foiw55B6`** (#345 baseline). Real-ship proven: admin js 1,628,719b Last-Modified 10:12:55Z=17:12+07 in-window (run 17:09→17:14) · user js 1,532,620b 10:13:52Z in-window · SPA-trap control `ZZfakehash0.js`→200 but 919b text/html (size-check meaningful). Smoke api/health ready+live 200, admin+eoffice root 200, GET /api/purchase-evaluations + /api/menus both 401 unauth (wired NOT 404/500). **Mig FROZEN 59** (prod history HEAD==`AddCcmBudgetPeriodToPurchaseEvaluation`==repo, no-pending) + sys.tables 88 (89 incl __EFMigrationsHistory, 0 schema change). gotcha #69-inverse re-confirmed (real fe-src ⟹ hash rotates). conn key `ConnectionStrings.Default`, prod path `C:\inetpub\solution-erp\api`. ⚠️ SSH→PS table-count `<>'literal'` mangled via base64/CLIXML → use `NOT LIKE` ASCII-escape or count-incl-history−1. -- **S88 #345 `9b1ee0c` PASS ~4m (FULL-STACK PE Section-B 3-col + Mig 59, anh Kiệt FDC/CCM)** — Section B (THỰC HIỆN) rewrite 1-col-list → 3-col table (per-col kỳ-này) + new setter endpoint `PATCH /purchase-evaluations/{id}/budget/ccm-period` (`SetPeCcmBudgetPeriodCommand`, fail-closed Admin|CostControl else Forbidden) + DTO field `ccmBudgetPeriodAmount`. Files 14: BE 4 (Controller/DTO/Domain/Config) + 2 App-features (PeSuggestedPriceFeatures + PurchaseEvaluationFeatures) + FE **both apps** 4 (PeDetailTabs + types ×2) + Mig 3-file + 1 new test. **Mig 59 `20260624084608_AddCcmBudgetPeriodToPurchaseEvaluation`** = single AddColumn `PurchaseEvaluations.CcmBudgetPeriodAmount` decimal(18,2) NULL **no backfill** — VERIFIED-APPLIED-PROD (history top==`AddCcmBudgetPeriod`==repo HEAD; sys.columns COL=1 type `decimal(18,2) null=1`; NONNULLrows=0 as designed). Test gate=success ⟹ passed (**baseline 402** = 45 Domain + 357 Infra = 395 + **7 `PeCcmBudgetPeriodSetterAuthzTests`**: Admin-set/CostControl-set/Drafter-forbidden/Procurement-forbidden/Zero-valid/Null-clears/Unknown-NotFound). **Bundle BOTH js+css ROTATE (real fe-src Section-B rewrite): admin js `CXCKlNay`→`C8lWxEQK` css `BKB2NsgQ`→`BFfMO5l8`, user js `Di40ewrF`→`Kz5cSW2u` css `BaqeI4iG`→`B11h2RoV`** (css rotated = new table-grid utility classes, NOT logic-only). Real-ship proven: admin js 1,627,505b Last-Modified 09:10:47Z=16:10+07 in-window (16:08→16:12) · user js 1,531,406b 09:11:40Z in-window · SPA-trap control `ZZfake0.js`→200 but 919b text/html (size-check meaningful). Endpoint live: `PATCH .../budget/ccm-period` unauth→**401** bodyless AND with-json-body (wired: NOT 404=route-exists, NOT 500=no-crash; PATCH auth short-circuits before IIS Length-check so 401 not 411). Health api ready/live + admin + eoffice 4×200. GET /api/purchase-evaluations + /api/menus both 401. ⚠️ ambient sys.tables=88 (NOT 89 as run-spec hedged — sqlcmd direct COUNT WHERE is_ms_shipped=0 AND name<>'__EFMigrationsHistory' = 88; Mig 59 AddColumn-only 0-new-table, consistent w/ S62 re-ground baseline). conn key `ConnectionStrings.Default`, prod path `C:\inetpub\solution-erp\api`. gotcha #69 inverse re-confirmed (real fe-src ⟹ hash rotates). +- **S89 #348 (id=462) `1aa3bdb` PASS ~5m10s (FULL-STACK PE D2 multi-winner outward, NO-MIG)** — create-contract 1→N per winner (command→List, multi-winner handler in CreateContractFromEvaluationFeatures, Controller returns `{contractIds}`) + list-card winner-names projection (PurchaseEvaluationFeatures + Dtos WinnerSupplierNames). Files 12: BE 4 (Controller/CreateContractFromEvaluationFeatures/Dtos/PurchaseEvaluationFeatures) + FE **both apps** 6 (PeDetailTabs+PeListPanel+types ×2) + 2 test (CreateContractFromEvaluationMultiWinnerTests + PeListWinnerNamesProjectionTests). **NO `*Migrations*`/`*Persistence*` in diff** (git diff EMPTY — logic-only, no schema). status=success ⟹ test gate passed (**baseline 419** = 45 Domain + 374 Infra = 413 +6; logs 404 anon so count not log-scraped — trusted gate+baseline). **Bundle JS BOTH ROTATE (real fe-src PeDetailTabs+PeListPanel ⟹ new content-hash): admin js `CP_iKhTu`→`BQq2eTP6` · user js `DUe0fndl`→`dHa64I59`** (#347 baseline); **css FROZEN admin `Pltrs5ku` user `Foiw55B6` = #347/#346 — correct, D2 FE logic-only no new utility class** (gotcha #69-inverse). Real-ship proven (SPA-trap defeated): admin js 1,628,901b Last-Modified 03:34:13Z=10:34+07 in-window (run 10:30:30→10:35:40 = 03:30:30→03:35:40Z) · user js 1,532,802b 03:35:07Z in-window · control fake `ZZfakehash00.js`→200 but 919b text/html (size-check meaningful). **BE dll ALL 4 fresh in-window: Api 03:33:00Z (Controller `{contractIds}` live) · Application 03:31:04Z (CreateContractFromEvaluation multi-winner handler + PE winner-names projection live) · Infrastructure 03:31:28Z · Domain 03:30:47Z.** **Mig FROZEN 59** (repo MIGCOUNT=59 + prod history HEAD==`AddCcmBudgetPeriodToPurchaseEvaluation`==repo + HistCount=59 no-pending) + sys.tables 89 incl __EFMigrationsHistory = 88 business (no schema change). **Endpoint live: `POST /api/purchase-evaluations/{id}/create-contract` bodyless→411 (IIS Length-Required pre-auth), +json-body→401 (wired: NOT 404=route-exists, NOT 500=no-crash; `{contractIds}` multi-winner contract live).** Health api ready/live + admin + eoffice 4×200. GET /api/purchase-evaluations + /api/menus + /api/approval-workflows-v2 all 401 unauth (wired). gotcha #69-inverse re-confirmed (real fe-src ⟹ js rotates, css frozen when logic-only). ⚠️ **DB pw = `buKL3TGBkD0wDDbYVw65QeX9` (NOT `vrapp/buKL3...` — `vrapp/` was user/pw-combined-notation in memory essentials, actual Password field has no prefix; read from prod `appsettings.Production.json` ConnectionStrings.Default).** SSH→PS `$_.LastWriteTimeUtc` mangled in nested double-quote → base64 EncodedCommand worked; sqlcmd `-Q` quoting mangled → scp .sql + `-i` file worked cleanest. conn key `ConnectionStrings.Default`, prod path `C:\inetpub\solution-erp\api`. +- **S88-quad #347 (id=461) `73cce1f` PASS ~5m04s (FULL-STACK PE D1 stability, NO-MIG)** — Block-B multi-winner financials re-key (PurchaseEvaluationFeatures) + CEO-notify SaveChanges-persist (PurchaseEvaluationUrgentFeatures +5 lines) + setter RELOCATE PeSuggestedPriceFeatures→PeWorkItemBudgetFeatures (−56/+57) + HoSoLink trim + #70 guard + Dtos comment. Files 12: BE 5 (Dtos+4 features) + FE **both apps** 4 (PeDetailTabs+types ×2) + 3 test (PeHoSoLinkTests+PeWorkItemBudgetTests EXTEND, **new PeUrgentNotifyDirectorTests +237 lines**). **NO `*Migrations*`/`*Persistence*` in diff** (logic-only re-key, no schema). status=success ⟹ test gate passed (**baseline 413** = 45 Domain + 368 Infra = 402 +11; logs 404 anon so count not log-scraped — trusted gate+baseline). **Bundle JS BOTH ROTATE (real fe-src #70-guard+HoSoLink-trim ⟹ new content-hash): admin js `BABhPh2s`→`CP_iKhTu` · user js `DLulO6uP`→`DUe0fndl`** (#346 baseline); **css FROZEN admin `Pltrs5ku` user `Foiw55B6` — correct, D1 FE logic-only no new utility class**. Real-ship proven: admin js 1,628,726b Last-Modified 02:57:57Z=09:57+07 in-window (run 09:54:15→09:59:19 = 02:54:15→02:59:19Z) · user js 1,532,627b 02:58:50Z in-window · SPA-trap control `ZZfakehash00.js`→200 but 919b text/html (size-check meaningful). **BE dll ALL 4 fresh in-window: Api 02:56:42Z · Infrastructure 02:55:16Z (PeWorkItemBudgetFeatures setter-relocate live) · Application 02:54:51Z (PurchaseEvaluationFeatures Block-B + UrgentFeatures CEO-persist live) · Domain 02:54:36Z.** **Mig FROZEN 59** (MIGCOUNT=59, prod history HEAD==`AddCcmBudgetPeriodToPurchaseEvaluation`==repo, no-pending; correct NO-MIG D1) + sys.tables 88 (89 incl __EFMigrationsHistory). Health api ready/live + admin + eoffice 4×200. GET /api/purchase-evaluations + /api/menus + /api/approval-workflows-v2 all 401 unauth (wired NOT 404/500). gotcha #69-REFINE inverse re-confirmed (real fe-src ⟹ js rotates, css frozen when logic-only). conn key `ConnectionStrings.Default`, prod path `C:\inetpub\solution-erp\api` (skill-doc `\apps\...` STALE). ⚠️ SSH→PS table-count `<>'literal'` mangled via base64/CLIXML AGAIN → use count-incl-history−1 (88) workaround. +- _(S88-bis #346 `0d3c0cf` FE-only PE layout "4. Thông tin chọn thầu" 2-files js+css-BOTH-rotate baseline-402 · S88 #345 `9b1ee0c` FULL-STACK Section-B 3-col + **Mig 59 `AddCcmBudgetPeriodToPurchaseEvaluation`** AddColumn-no-backfill VERIFIED-PROD + setter `PATCH .../budget/ccm-period` PATCH-401-not-411 + 7 authz-test → PASS, FIFO-trimmed, full verbatim git + archive `_INDEX`)_ - **S87 #344 (id=458) `f1b10e9` PASS ~5m11s (FULL-STACK PE CCM 3-fix + Mig 58, anh Kiệt FDC)** — multi-winner SUM (SelectWinner→SupplierIds[] array body) + budget=0 valid-submit + per-level auto-finalize (`ApproveV2Async if matchingLevel.AllowApproverFinalize`). Files: BE 13 (Controller/App/Domain/Infra/Service + Mig 3-file) + FE **BOTH apps** 7 (PeDetailTabs/PeWorkflowPanel/types ×2 + fe-admin ApprovalWorkflowsV2Page Designer) + 6 test. **Mig 58 `20260624071509_AddPeMultiWinnerAndLevelFinalize`** = 2 AddColumn bit (`PurchaseEvaluationSuppliers.IsWinner` default0 + `ApprovalWorkflowLevels.AllowApproverFinalize` default0) + 1 backfill `UPDATE ... SET IsWinner=1 WHERE pes.SupplierId=pe.SelectedSupplierId` — VERIFIED-APPLIED-PROD (history top==`AddPeMultiWinnerAndLevelFinalize`==repo HEAD; both cols sys.columns=1; **backfill ran: winners=13 == pe_with_SelectedSupplierId=13 / 30 total supplier rows** — exactly 1 winner per pre-existing selected PE). Test gate=success ⟹ passed (**baseline 395** = 45 Domain + 350 Infra per spec; was 397 −2 because per-level-finalize moved runtime-flag→auto-config, NOT regression). **Bundle BOTH ROTATE (real fe-src ⟹ new content-hash, contrasts #69-REFINE BE-only): admin js `aKuNip19`→`CXCKlNay` (css FROZEN `BKB2NsgQ`, logic-only diff no new chunk), user js `BUsL0HeQ`→`Di40ewrF` (css FROZEN `BaqeI4iG`).** Real-ship proven: admin js 1,621,660b Last-Modified 08:26:32Z=15:26+07 in-window · user js 1,525,569b 08:27:26Z in-window · control fake-hash `ZZfakehash0.js`→200 but 919b text/html (SPA-trap confirms 200-alone meaningless). Endpoint live: `POST /api/purchase-evaluations/{id}/select-winner` bodyless→411 (IIS Length-Required pre-auth), +json-body→401 (wired, NOT 404/400/500; `SelectWinnerBody(IReadOnlyList? SupplierIds)`→`SelectPurchaseEvaluationWinnerCommand` array contract live). Health api ready/live + admin + eoffice 4×200. ⚠️ **ambient sys.tables=89 (was 88) — NOT from Mig 58** (AddColumn-only, 0 new table; newest table `PeWorkItemBudgets` 06-13 Mig 50, none created 06-24); 88 baseline last re-grounded S62, +1 pre-existing drift in Mig 50-57 range, harmless. ⚠️ prod path `C:\inetpub\solution-erp\api` (skill-doc `\apps\SolutionErp\Api` STALE — re-confirmed); conn key `ConnectionStrings.Default`; base64 EncodedCommand + 1 path-retry. gotcha #69-REFINE inverse confirmed (real fe-src ⟹ hash DOES rotate). - **S86 #343 (id=457) `ae4f106` PASS ~5m00s (BE-ONLY notify-filter)** — PE "cần duyệt" notify EXCLUDE drafter + 1 new test (`PeApproverNotifyExcludesDrafterTests`) + STATUS + agent-memory. Only-BE-src: 1 file `PurchaseEvaluationWorkflowService.cs`, ZERO `fe-*/src`, ZERO `*Migrations*`. status=success ⟹ test gate passed (**baseline 377** = 45 Domain + 332 Infra per spec; S85 374 +3; reviewer's 2 transient parallel-collection flake did NOT surface red in CI — gate green). **Ship-proof #69-REFINE (BE-only) CONFIRMED via dll Last-Modified in run-window (16:37:38→16:42:35+07 = 09:37:38→09:42:35Z):** Api.dll 09:40:04Z · **Application.dll 09:38:12Z · Infrastructure.dll 09:38:34Z** (PurchaseEvaluationWorkflowService lives in Infrastructure → recompiled fresh) — all 3 in-window. **FE bundle FROZEN (correct, 0 fe-src diff ⟹ byte-identical): admin `aKuNip19`/`BKB2NsgQ`, user `BUsL0HeQ`/`BaqeI4iG` = S85 baseline.** Mig FROZEN 57 (`AddPeSuggestedPriceNotes`, prod head==repo) + tables 88. Prod 4×200 (api ready/live, admin, eoffice) + PE wired (GET /api/purchase-evaluations + /api/menus both 401 unauth, NOT 404/500). Notify-filter BE-internal no-new-endpoint → test 377 covers behavior (no curl-probe of notify logic). gotcha #69-REFINE reaffirmed (BE-only: dll Last-Modified = sole ship-proof, hash-delta N/A). ⚠️ SSH transient drop ×3 mid-session (`Connection closed by 103.124.94.38`) — base64 EncodedCommand + retry-loop recovered; not a deploy issue. - **S85 #342 `d4f6021` PASS ~5m00s (DOCS+SQL NO-OP, INVERTED criteria)** — 6×.md + `scripts/s85-wipe-uat-data.sql` (only non-ignored path → CI triggers; whole-workflow still builds FE+publish). **PASS-when-FROZEN, NOT rotate.** Verified: status=success ⟹ test gate passed (baseline **374** = 45 Domain + 329 Infra per STATUS.md canonical, logs API 404 anon so count not log-scraped — trusted gate+baseline) + **bundle FROZEN admin `aKuNip19`/user `BUsL0HeQ` (correct: 0 fe-src diff ⟹ byte-identical ⟹ same hash, gotcha #69-REFINE)** but DID ship (admin js Last-Modified 08:56:34Z=15:56+07 IN-window 15:53→15:58, real 1.62MB/user 1.52MB vs SPA-trap 919b, 2nd-fetch byte-stable). Prod 4×200 (ready/live/admin/eoffice) + endpoint smoke 4×200 (PE/awf-v2/menus/projects). **Prior S85 deploys still live: R6 TZ fix `49ac086` → `createdAt:"…7331140Z"` trailing-Z CONFIRMED; R5 UpdateDraft `1263c0d` → both ancestors of d4f6021, shipped via #341 (#340 cancelled but #341 success built later commit incl TZ).** SignalR negotiate 411 (bodyless-POST IIS, wired, notif untouched). gotcha #73 (TZ) + #69-REFINE reaffirmed. - **S83 #333 `fc1f19d` PASS ~4m59s** — BE-only PE-guard (require ApprovalWorkflowId V2 at create-validator + submit-branch ConflictException; close FE-only-validation gap → phiếu reached "Đã gửi duyệt" w/ null workflow = V1-legacy-banner trap). +2 validator tests → **test 356** (gate=success ⟹ pass). NO-mig (frozen 57, prod head `AddPeSuggestedPriceNotes`==repo, no-pending) + ZERO fe-*/src diff. **Bundle hash UNCHANGED (admin `CsJetgZH`/user `BVS0ApIm` = #330) BUT shipped — Last-Modified 17:10:35/17:11:27 in-window** (real js 1.62/1.52MB vs SPA-trap 919b) → **gotcha #69-REFINE: 0-FE-src ⟹ byte-identical ⟹ same-hash; Last-Modified is true ship-proof.** Guard BE-only no-endpoint → no curl-probe (356 tests cover RED→GREEN). Health api/ready+live 200, admin/eoffice 200, api-root 404 (no `/` route, expected). - **S78 #330 `7886fd0` PASS ~4m56s** — cross-stack NO-MIG enum `ApprovalAttachment=5` (attach-on-approve) + FE 2-app PeWorkflowPanel modal-upload. Verified: empty-migrations-diff + Mig-frozen-57 + tables88 + bundle BOTH rotate js+css (real 1.6/1.5MB vs fake-919b) + endpoint bodyless-POST-411→re-probe-body-401-wired + health 4×200. → _INDEX. - **S76 #318 `e33481e` PASS ~4m58s** — full-stack Mig56 ProInitial/ProAdjust VERIFIED-APPLIED-PROD (sys.columns decimal(18,2)) + **5th-axis BACKFILL-verify** 4-rows-0-violation (gotcha #64 prod-data-UPDATE-first-time) + history-advance-55→56 + tables88 + bundle BOTH+css rotate + health 4×200. Pre-deploy DB snapshot (Mig55) → post (Mig56) = unambiguous proof. → _INDEX. -- **S83 #325 `e29391e` PASS ~4m39s** — FE-only tiny budget-subitem indent/dash + bundle BOTH-js-rotate css-FROZEN (utility-reuse, no new css chunk) + Mig-frozen-57 + tables88 + test351. → _INDEX. -- _(S77 #329 `e823694` FE-only banner Trả-lại · #328 `424131d` BE-only notify-block test-gate-KEY · #327 `fa6654b` FE-list-restructure · #326 `b5aa72d` BE-authz asymmetric · #323 #322 #321 #320 PE UX batch → all PASS, FIFO-trimmed, full verbatim git + archive `_INDEX`)_ +- _(S83 #325 `e29391e` FE-only budget-subitem indent js-rotate-css-FROZEN test351 · S77 #329 `e823694` FE-only banner Trả-lại · #328 `424131d` BE-only notify-block test-gate-KEY · #327 `fa6654b` FE-list-restructure · #326 `b5aa72d` BE-authz asymmetric · #323 #322 #321 #320 PE UX batch → all PASS, FIFO-trimmed, full verbatim git + archive `_INDEX`)_ - _(S74-bis #315 `8655ebf` Mig55 CcmNote · S73 #313 `1d86abc` Mig54 5-cols + endpoint-401-not-404-probe · S69b #307 `1f8947e` Office-golive seed-16/16-DB-query-proof → archive `_INDEX` + git)_ - **Older runs (S75 #301 ← S62 #286 ← … → S29 #232) full verbatim → git `d2f52ba` + `archive/2026-06.md`** (incl #291 06-16 FAIL forensic [gotcha #65]); pre-S38 → `archive/2026-05-{runs,q2,q3,q4}.md`. diff --git a/.claude/agent-memory/implementer-backend/MEMORY.md b/.claude/agent-memory/implementer-backend/MEMORY.md index c3a0325..dcf19cf 100644 --- a/.claude/agent-memory/implementer-backend/MEMORY.md +++ b/.claude/agent-memory/implementer-backend/MEMORY.md @@ -74,6 +74,7 @@ UI `disabled={!canX}` + BE helper `EnsureCanXAsync(id, userId)` throw 403 (NOT i ## 📅 Recent activity (FIFO — older → archive/git) +- **2026-06-25 (S88 PE stability cleanup — FROZEN-module mechanical relocation + 1 stale-comment fix BE — NO migration, 3 edit/2 file, em-main spec deterministic 100% file/line/boundary given → ACCEPT Case 1):** Pure mechanical move ZERO behavior-change. **T1 misplaced-command relocate:** `SetPeCcmBudgetPeriodCommand`+Validator+Handler (BUDGET setter writes `pe.CcmBudgetPeriodAmount` Section B "NS kỳ này" CCM, Mig 59) sống NHẦM trong suggested-PRICE file → move về budget file. (a) REMOVE `PeSuggestedPriceFeatures.cs` lines 144-199 (blank + `===== CCM NS kỳ này =====` header → `SetPeCcmBudgetPeriodCommandHandler` closing `}`); file end clean tại line 143 (`UpdatePeSuggestedPriceCcmCommandHandler` `}`), suggested-price PRO/CCM trên giữ nguyên. (b) APPEND `PeWorkItemBudgetFeatures.cs` END sau line 200 (`UpdatePeBudgetCcmCommandHandler` `}`) → cạnh budget setters `UpdatePeBudgetPro/Ccm`. Code BYTE-IDENTICAL (logic/authz Admin\|CostControl fail-closed/validation GTE(0).When(HasValue)/absolute-set null=clear/changelog UNCHANGED); CHỈ prepend 1 comment line `// [S88] Chuyển từ PeSuggestedPriceFeatures.cs về đây (đúng nhóm budget setter).`. Usings 8-header IDENTICAL 2 file → KHÔNG add duplicate. Namespace `SolutionErp.Application.PurchaseEvaluations` SAME → MediatR type-resolution transparent, controller `:125` KHÔNG đụng. **T2 stale-comment fix:** `Dtos/PurchaseEvaluationDtos.cs` ~250-254 comment trên `decimal WinnerQuoteTotal` ghi SUM "của SelectedSupplierId" (OLD single-winner) → verified actual compute `PurchaseEvaluationFeatures.cs:1093-1099` = `Suppliers.Where(s=>s.IsWinner)` (multi, Mig 58) → sửa CHỈ comment-text "MỌI đơn vị IsWinner (multi-winner Mig 58)". Field/code UNCHANGED. **LESSON:** frozen-relocation = read BOTH file FIRST exact boundary + verify usings-superset target + verify stale-comment claim vs real compute-site TRƯỚC sửa (KHÔNG trust comment blind, mirror S35 EF-source-of-truth). Build `SolutionErp.Application.csproj -v q` **0 warn 0 err** (4s). KHÔNG test (test-specialist)/FE/mig/commit. Tag `[s88, frozen-relocation, byte-identical-move, mediatr-type-resolve-transparent, stale-comment-verify-compute-site, no-behavior-change]`. - **2026-06-24 (PE Section B 3-cột THỰC HIỆN — CCM "NS kỳ này" cột riêng BE — Mig 59 `AddCcmBudgetPeriodToPurchaseEvaluation` 3-file, 5 edit/0 new file, em-main CHỐT spec+API-contract 100% → ACCEPT Case 1):** Section B tách 3 cột Dự án|PRO|CCM giống Section A. PRO=`BudgetPeriodAmount` sẵn có; CCM nhập cột riêng độc-lập. COOKIE-CUTTER mirror S77 suggested-price-Ccm + S65b HoSoLink one-column. (1) `PurchaseEvaluation.cs` +`decimal? CcmBudgetPeriodAmount` sau ExpectedRemainingAmount. (2) `PurchaseEvaluationConfiguration.cs` +`HasColumnType("decimal(18,2)")` sau ExpectedRemaining (spec asked literal decimal(18,2); siblings dùng HasPrecision(18,2) NHƯNG emit identical column — tôi follow spec verbatim HasColumnType). (3) Mig: Up=1 AddColumn decimal(18,2) nullable NO table/index, Down=1 DropColumn, snapshot verified — clean mirror Mig 52. (4) DTO `PurchaseEvaluationDetailBundleDto` +`decimal? CcmBudgetPeriodAmount` sau ExpectedRemainingAmount TRƯỚC BudgetSummary + projection PEFeatures.cs:1110 positional-insert đúng order (positional record → projection order BẮT BUỘC khít). (5) Setter `SetPeCcmBudgetPeriodCommand(Guid Id, decimal? CcmBudgetPeriodAmount):IRequest` append `PeSuggestedPriceFeatures.cs` (same concern PE role-gate setters) + Validator `GreaterThanOrEqualTo(0).When(HasValue)` (budget=0 hợp lệ) + Handler fail-closed Forbidden Admin|CostControl TRƯỚC side-effect (S56 #5) + absolute-set null=clear (gotcha #73) + changelog Header (sibling-consistency; spec min=SaveChanges). (6) Controller `[HttpPatch("{id:guid}/budget/ccm-period")]` + `CcmBudgetPeriodBody` (spec EXPLICIT PATCH dù siblings PUT — follow spec). **Call-site verify:** `new PurchaseEvaluationDetailBundleDto(` repo-wide → EXACTLY 1 (PEFeatures:1100, đã sửa) → positional-safe. Submit-guard `BudgetPeriodAmount` PRO KHÔNG đụng. Build SolutionErp.slnx (6 proj incl 2 test, gotcha #65) **0 warn 0 err**. KHÔNG apply DB/FE/test/commit. Route FE/test mirror: DTO field `ccmBudgetPeriodAmount` camelCase + PATCH `/api/purchase-evaluations/{id}/budget/ccm-period` body `{ ccmBudgetPeriodAmount }`. Tag `[pe-section-b-3col, ccm-budget-period, mig59, one-column-no-table, absolute-set-fail-closed, positional-dto-order, patch-per-spec]`. - **2026-06-19 (PE chuông báo approver BE — NO migration, 1 edit/1 file, em-main spec deterministic 100% → ACCEPT Case 1):** Tra Sol (Zalo) "không thấy chuông — việc có hồ sơ cần duyệt": approver Cấp hiện tại KHÔNG nhận notify khi phiếu ENTER/ADVANCE tới ChoDuyet (chỉ drafter báo terminal). FIX = +1 block trong `LogTransitionAsync` (PurchaseEvaluationWorkflowService.cs ~line 1058) NGAY SAU drafter-notify, KHÔNG endpoint mới (Pattern 4 — notify = side-effect của Submit/Approve-advance, cả 2 đã gọi LogTransitionAsync + end ChoDuyet). **Resolution mirror EXACT canonical** `EnsureActorInLevel` (line ~301): `db.ApprovalWorkflows.AsNoTracking().Include(Steps).ThenInclude(Levels)` → `stepsOrdered[CurrentWorkflowStepIndex]` (bounds-check) → `.Levels.Where(Order==CurrentApprovalLevelOrder && ApproverUserId!=Guid.Empty && !=actorUserId).Select(ApproverUserId).Distinct()` → `NotifyManyAsync(ids, Generic, "Phiếu cần bạn duyệt: {MaPhieu??TenGoiThau}", "Có phiếu Duyệt NCC đang chờ bạn duyệt.", /purchase-evaluations/{Id}, refId:Id, ct)`. **Recon:** (1) `ApprovalWorkflowLevel.ApproverUserId` = non-null `Guid` (NOT Guid?) → "exclude null" = `!=Guid.Empty` defensive (OR-of-N rows mỗi 1 NV). (2) `NotifyManyAsync` sig verified = `(IEnumerable, NotificationType, title, string? desc, string? href, Guid? refId, CT)` khớp urgent-feature ref. (3) actor-exclude qua `!= actorUserId` (Guid != Guid? lift OK). **Guard V2-only:** `ApprovalWorkflowId is Guid && CurrentWorkflowStepIndex is int && CurrentApprovalLevelOrder is int` → V1/no-workflow skip. **Best-effort try/catch** nuốt lỗi (phiếu đã SaveChanges; notify fail KHÔNG rollback). Block đọc pointer SAU set → Submit (idx0/lvl1) + Approve-advance (pointer advance rồi LogTransition) đều fire đúng. Build SolutionErp.slnx (2 test project, gotcha #65) **0 warn 0 err**. KHÔNG test/FE/mig/commit. Tag `[pe-approver-bell, notify-many, log-transition-hook, no-mig, v2-only, best-effort-trycatch, pattern4-side-effect]`. - **2026-06-19 (S77 PE +2 ghi-chú giá đề xuất BE — Mig 57 `AddPeSuggestedPriceNotes` 3-file, 6 edit/0 new file, COOKIE-CUTTER S73 suggested-price + S74 CcmNote, em-main CONTRACT-locked names 100% → ACCEPT Case 1):** PRO dải Min/Max + CCM 1 giá (S73) +ô GIẢI THÍCH vì-sao min vs max (anh Kiệt FDC + Tra Sol). 2 cột nullable, no table/index/backfill. (1) `PurchaseEvaluation.cs` +`string? ProSuggestedPriceNote`/`CcmSuggestedPriceNote` ngay sau CcmSuggestedPrice. (2) `PurchaseEvaluationConfiguration.cs` +`HasMaxLength(1000)` ×2 sau ApprovedPriceSource (KHÔNG index — free-text). (3) Mig: Up=2 AddColumn nvarchar(1000) nullable, Down=2 DropColumn, snapshot ×2 verified — clean mirror Mig 55. (4a) `UpdatePeSuggestedPriceProCommand` +trailing `string? Note=null` + validator `MaximumLength(1000)` MATCH-EF (S35) + handler **absolute-set** `pe.ProSuggestedPriceNote=request.Note` (null=clear, gia-đình text-field) + changelog part khi đổi. (4b) `UpdatePeSuggestedPriceCcmCommand` mirror + changelog suffix "(kèm ghi chú)". Role-gate KHÔNG đụng (Note gated y giá: Forbidden fail-closed TRƯỚC side-effect đã có). (5) Controller `SuggestedPriceProBody`/`CcmBody` +`string? Note` + pass `body.Note` vào cmd (4th/3rd positional). (6) `PurchaseEvaluationDetailBundleDto` +`ProSuggestedPriceNote`/`CcmSuggestedPriceNote` sau CcmSuggestedPrice + projection PEFeatures.cs positional-insert ĐÚNG order (DTO positional → order khít projection BẮT BUỘC). **Call-site grep verify (S65b lesson):** `new UpdatePeSuggestedPrice(Pro|Ccm)Command\(` repo-wide → 2 controller (đã sửa) + 18 test named-arg/positional-dừng-trước-Note → trailing-optional fully backward-compat, 0 test edit. FE 0 ref (đúng — implementer-frontend next). Build SolutionErp.slnx (2 test project, gotcha #65) **0 warn 0 err**. KHÔNG apply DB/FE/test/commit. Route: `note` camelCase qua PUT `/suggested-price/{pro,ccm}` body + GET detail bundle. Tag `[s77, pe-suggested-price-note, mig57, two-column-no-table, absolute-set, trailing-optional-callsite-safe, positional-dto-order]`. diff --git a/.claude/agent-memory/implementer-frontend/MEMORY.md b/.claude/agent-memory/implementer-frontend/MEMORY.md index 474fb37..97e10c1 100644 --- a/.claude/agent-memory/implementer-frontend/MEMORY.md +++ b/.claude/agent-memory/implementer-frontend/MEMORY.md @@ -6,6 +6,21 @@ --- +## 🆕 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. diff --git a/.claude/agent-memory/investigator-codebase/MEMORY.md b/.claude/agent-memory/investigator-codebase/MEMORY.md index 5cde2c1..c6edf14 100644 --- a/.claude/agent-memory/investigator-codebase/MEMORY.md +++ b/.claude/agent-memory/investigator-codebase/MEMORY.md @@ -70,8 +70,14 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte ## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`) +- **2026-06-25 PE FROZEN patchwork audit AREA-6 (FE consumers BOTH apps — multi-winner + mirror-drift + dead-code + #70):** ⭐ Post-Mig58 multi-winner (`isWinner` bool) NOW implemented (supersedes 06-24 "SINGLE" recon). **All 6 PE components + 3 PE pages SHA-IDENTICAL fe-admin↔fe-user** (PeDetailTabs/PeHeaderForm/PeUrgentChips/PeListPanel/PeWorkspaceCreateView/PeWorkflowPanel). type files DIFFER but ONLY badge-tint (-700→-800 ink) + comments (intentional per-app UX, struct fields identical incl ccmBudgetPeriodAmount/isWinner/8 Allow flags). **RESIDUAL single-winner BUG (low):** `PeListPanel.tsx:168` renders `p.selectedSupplierName` (list-card winner label) → BLANK when ≥2 joint winners (BE list-DTO `PurchaseEvaluationFeatures.cs:615,706` projects from `SelectedSupplierId` which BE sets null when count≠1 `PurchaseEvaluationDetailFeatures.cs:431`). List DTO has NO isWinner/winner-list field; detail-view multi-winner correct everywhere (8 detail consumers all converted: computeGiaChaoThau:78, missingForApproval:195, submitChecklist:229, ThongTinChonThau:1949/1970, Section-B row4:1578, CreateContractDialog:2171 fallback `||selectedSupplierName`, HangMucCard toggle:2560). **#70 stale-echo ASYMMETRY:** budget Section-B CCM-period editor gates `ccmPeriodMut.isPending||peFetching` (`PeDetailTabs.tsx:1563`) but PRO sibling row3/row8 `adjustMut` gates `.isPending` ONLY (`:1548,:1649`, NO peFetching) — rapid PRO row3→row8 absolute-set pair can echo stale ev.* pre-refetch (same #70 class CCM guards). 3 peFetching sites exist (945/1268/1736). **Stale comment** PeDetailTabs:2507 references `AddItemDialog` "giữ(dead)" but function DELETED (grep 0 def) — misleading comment only, not dead code. PeWorkflowsPage admin-only = by-design (workflow config not mirrored). → _INDEX. +- **2026-06-25 PE FROZEN patchwork audit AREA-2 (budget matrix + Section B 3-col, Mig 56/59):** ⭐ **BAND-AID CONFIRMED** `SetPeCcmBudgetPeriodCommand+Validator+Handler` (`PeSuggestedPriceFeatures.cs:153-199`) is a BUDGET setter (writes `pe.CcmBudgetPeriodAmount`) MISPLACED in a price-feature file; SHOULD live in `PeWorkItemBudgetFeatures.cs` (where UpdatePeBudgetPro/Ccm sit). Test+comments already self-flag (PeCcmBudgetPeriodSetterAuthzTests.cs:14). ⚠️ **Role-gate ASYMMETRY (intentional, NOT a bug — but inconsistent surface):** CCM budget setters = flat role-gate (CostControl|Admin fail-closed): UpdatePeBudgetCcm `PeWorkItemBudgetFeatures.cs:160-165` + SetPeCcmBudgetPeriod `PeSuggestedPriceFeatures.cs:176-181`. BUT the PRO sibling `/budget-adjust`→`AdjustPurchaseEvaluationBudgetCommandHandler` `PurchaseEvaluationFeatures.cs:349-394` = **phase/drafter/workflow-slot gate** (Drafter when Nháp/TraLai; Approver+AllowApproverEditBudget F4 when ChoDuyet) NOT role-PRO. So "PRO col" & "CCM col" of same Section-B row 3 have DIFFERENT authz models (PRO=drafter-flow on `pe.BudgetPeriodAmount`; CCM=role-flow on `pe.CcmBudgetPeriodAmount`). Both per-PE fields (NOT PeWorkItemBudget per-pair — that's the matrix rows Initial/Adjustment). Section-B **"Dự án" col = intentional FE-only "—"** hardcoded (`PeDetailTabs.tsx:1520,1542,1596,1606`; Domain `PeWorkItemBudget.cs:19` "chưa wire BE — sau mới có người dự án nhập") — documented, NOT a stub-bug. Controller class-level `[Authorize]` only (`PurchaseEvaluationsController.cs:15`) → handler gates ARE the boundary. Tests solid (PeWorkItemBudgetTests 30+ cases, PeCcmBudgetPeriodSetterAuthzTests 7). → _INDEX. + - **2026-06-24 PE 3-change recon (Lưu Trần/anh Kiệt CCM — multi-winner + budget=0-valid + per-step terminal):** ⭐ (A1 winner) cardinality=**SINGLE** scalar `PE.SelectedSupplierId Guid?` (`PurchaseEvaluation.cs:28`); cmd `SelectPurchaseEvaluationWinnerCommand(PEId,Guid?)` `PurchaseEvaluationDetailFeatures.cs:392` handler `:398-423` (null=clear S85-C5); ctrl `/select-winner` `PurchaseEvaluationsController.cs:167-172`. winnerQuoteTotal=**DERIVED** SUM `Quote.ThanhTien WHERE SupplierId==Selected` 3 sites: read `PurchaseEvaluationFeatures.cs:1089` + submit-guard `PurchaseEvaluationWorkflowService.cs:191` + ccm-delegate `:868`. ⚠️ Quote ALREADY has `IsSelected bool` (`PurchaseEvaluationQuote.cs:15` per-item winner, UNUSED by total). Multi-winner = NEW junction OR reuse IsSelected + change predicate `==Selected`→`IN selectedSet`. FE single `