# Gotchas — SOLUTION_ERP > Bẫy/pitfall đã gặp + cách xử lý. Đọc trước khi debug tương tự để không mất thời gian. Cập nhật liên tục khi gặp bug mới. ## Tech stack constraints (.NET 10 + TS 6 + Vite 8) ### 1. MediatR 14.x không tương thích → pin 12.4.1 **Triệu chứng:** `Unable to resolve service for type 'MediatR.IMediator'` — `AddMediatR` vẫn chạy nhưng không register IMediator. **Fix:** Pin `MediatR 12.4.1`. Khi đó `RequestHandlerDelegate` là delegate không tham số (v14 có thêm CancellationToken). ### 2. Swashbuckle 10.x + Microsoft.OpenApi 2.x breaking change **Triệu chứng:** Build fail `The type or namespace 'Models' does not exist in 'Microsoft.OpenApi'`. Swagger 404. **Fix:** - Remove `Microsoft.AspNetCore.OpenApi` khỏi Api - Downgrade Swashbuckle về `6.9.0` ### 3. TypeScript 6 `erasableSyntaxOnly` cấm `enum` **Fix:** Dùng `const + as const + typeof[keyof]` pattern: ```ts export const SupplierType = { NhaCungCap: 1 } as const export type SupplierType = typeof SupplierType[keyof typeof SupplierType] ``` ### 4. TypeScript 6 deprecate `baseUrl` **Fix:** Bỏ `baseUrl` trong tsconfig, chỉ giữ `paths`. Paths resolve relative tsconfig location. ### 5. Node 22 local vs CI pin 20 **Bài học NamGroup:** CI build fail trên Node latest. **Fix:** - `package.json` engines: `">=20"` (min, không upper) - `.nvmrc` = `20` cho CI - GitHub/Gitea Actions: `actions/setup-node@v4` với `node-version: '20.x'` ## EF Core 10 ### 6. Expression tree không support switch expression **Triệu chứng:** `CS8514: An expression tree may not contain a switch expression`. **Fix:** Tách switch ra ngoài LINQ: ```csharp var hasPermission = action switch { "Read" => await query.AnyAsync(p => p.CanRead), "Create" => await query.AnyAsync(p => p.CanCreate), _ => false, }; ``` ### 7. Design-time DbContext resolve fail **Triệu chứng:** `dotnet ef migrations add` → `Unable to resolve service for type 'DbContextOptions'`. **Fix:** Tạo `IDesignTimeDbContextFactory` trong Infrastructure. ### 8. `AddDefaultTokenProviders()` không có trong `AddIdentityCore` **Fix:** Bỏ call nếu chưa cần password reset. Khi cần, chuyển `AddIdentity` hoặc add package `Microsoft.AspNetCore.Identity.UI`. ## OpenXml / ClosedXML ### 9. `SpaceProcessingModeValues` namespace **Fix:** Full path + wrap `EnumValue<>`: ```csharp textElement.Space = new DocumentFormat.OpenXml.EnumValue< DocumentFormat.OpenXml.SpaceProcessingModeValues>( DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve); ``` ### 10. Placeholder `{{field}}` bị split runs **Vấn đề:** Word hay split text thành nhiều `` — placeholder miss khi regex replace. **Fix:** Iterate Paragraph, gom text tất cả `` → replace → gán lại text đầu + clear rest. Đã implement trong `DocxRenderer`. ### 11. Word COM `SaveAs` PowerShell type conversion **Fix:** Dùng `SaveAs2`: ```powershell $doc.SaveAs2($outPath, 16) # 16 = wdFormatDocumentDefault ``` ### 12. Word COM stuck **Fix:** - `$word.DisplayAlerts = 0` - Nếu stuck → `Get-Process WINWORD | Stop-Process -Force` - Fallback: LibreOffice headless `soffice --headless --convert-to docx` ## System.Text.Json ### 13. Record deserialization fail với Unicode qua CLI **Triệu chứng:** POST JSON tiếng Việt từ Windows bash/curl → 400 "JSON value could not be converted". **Fix:** Dùng `curl --data-binary @file.json` (file UTF-8). API handle đúng qua axios/Swagger. ## File operations ### 14. Dropbox sync có thể revert file đang edit **Triệu chứng:** Write thành công, build pass, runtime chạy code cũ. **Fix:** Sau Write quan trọng → Read lại verify. Nếu revert → Write lại. ### 15. `.gitignore` wwwroot rules - `wwwroot/uploads/` → **ignore** (user files) - `wwwroot/templates/` → **commit** (source of truth) - `wwwroot/exports/` → ignore (temp) ## Dev workflow ### 16. Port conflict khi restart dev server **Fix:** `TaskStop` task cũ, hoặc `netstat -ano | findstr :8082` → `taskkill /F /PID `. ### 17. EF migration 3-file rule Mỗi migration tạo: `{name}.cs` + `{name}.Designer.cs` + `ApplicationDbContextModelSnapshot.cs`. Commit đủ 3. ## Claude Code harness quirks ### 18. Edit tool "File not read" sau system-reminder **Triệu chứng:** Edit file vừa Read, lỗi "File has not been read yet". **Nguyên nhân:** System reminder interrupt reset read-cache. **Fix:** Read lại file rồi Write/Edit. Hoặc dùng Write (ghi đè full) thay Edit. ### 19. Build pass nhưng DI thiếu registration **Triệu chứng:** `dotnet build` → 0 errors nhưng runtime throw `Unable to resolve service`. **Nguyên nhân:** C# compiler chỉ check type, không check DI graph. **Fix:** Sau thêm interface mới + impl → luôn add `services.AddScoped()` trong `DependencyInjection.cs`. Test API start up là OK check. ## Contract workflow ### 20. Mã HĐ gen 2 lần sau reject → approve lại **Fix:** Check `if (contract.MaHopDong is null)` trước khi gen. Đã implement trong `ContractWorkflowService.TransitionAsync`. ### 21. ~~BE adjacency vs FE NEXT_PHASES sync~~ (RESOLVED) **Đã xử lý:** FE không còn hardcode `NEXT_PHASES` nữa. BE expose `contract.workflow.nextPhases` trong `ContractDetailDto` từ `WorkflowPolicyRegistry.ForContract(contract)`. FE render dynamic từ đó — single source of truth. Nếu đổi policy BE: chỉ cần update `WorkflowPolicies.Standard` hoặc `WorkflowPolicies.SkipCcm` trong `Domain/Contracts/WorkflowPolicy.cs`. FE tự reflect. ### 22. Race condition gen mã HĐ khi 2 user cùng transition tới DangDongDau **Fix:** `IsolationLevel.Serializable` transaction trong `ContractCodeGenerator`. Không skip. ### 42. Dual schema workflow V1 vs V2 — Service phải branch theo pin field (Session 17) **Symptom:** Phiếu PE pin V2 (`ApprovalWorkflowId` set qua workspace Select) nhưng Service vẫn match approver theo schema cũ (`Dept+PositionLevel`). Approver V2 không duyệt được, button Duyệt báo Forbidden. **Root cause:** Sau Mig 23-24 entity PE có 2 field workflow pin: - `WorkflowDefinitionId` (Mig 21 V1 legacy) — pin schema flat cũ - `ApprovalWorkflowId` (Mig 23 V2 mới) — pin schema 3-table mới Service trước đó chỉ đọc `WorkflowDefinitionId` → bỏ qua V2. **Fix (`b41484b`):** `PurchaseEvaluationWorkflowService.TransitionAsync` branch: ```csharp if (evaluation.ApprovalWorkflowId is Guid awId) await ApproveV2Async(evaluation, awId, ...); // iterate ApprovalWorkflowSteps + Levels match ApproverUserId else await ApproveV1LegacyAsync(evaluation, ...); // iterate WorkflowSteps match Dept+PositionLevel ``` **Pattern reusable** khi wire schema mới song song schema cũ (Contract V2 sắp tới): pin field flag để rẽ logic, KHÔNG drop legacy ngay (giữ backward compat phiếu cũ). ### 43. Step.Order ≠ index 0-based — không thể EF query trực tiếp (Session 17) **Symptom:** Implement `ResolveV2InboxIdsAsync` (V2-aware Inbox) bằng EF query thẳng: ```csharp .Where(s => s.Order == e.CurrentWorkflowStepIndex.Value + 1) // FAIL — Step.Order là logical, không phải position ``` Logic sai: `WorkflowStep.Order` không phải position 0-based mà là số sort thứ tự (vd 5, 10, 20). `Steps.OrderBy(s => s.Order).ToList()[idx]` mới đúng. **Fix:** Precompute candidates EF query → in-memory sort by Order → array index access: ```csharp var candidates = await db.PE.Where(...).ToListAsync(ct); // Step 1: EF lấy phiếu V2 pending var workflows = await db.AW.Include(...).ToDictionaryAsync(w => w.Id, ct); foreach (var c in candidates) { var steps = wf.Steps.OrderBy(s => s.Order).ToList(); // Step 2: in-memory sort var step = steps[c.CurrentWorkflowStepIndex.Value]; // Step 3: array index ... } ``` Trade-off: scalable đến vài trăm phiếu pending, không ngon cho >10k. Optimize sau nếu cần. ## Permission matrix ### 23. Permission update không real-time **Triệu chứng:** Admin tick permission cho role X → user X vẫn thấy menu cũ. **Nguyên nhân:** FE cache menu trong `localStorage`, không auto refetch. **Fix:** User phải logout/login. Phase 3 iteration 2 có thể thêm SignalR push "permission-changed" → FE tự refetch `/menus/me`. ### 24. MenuKey typo — không check type **Fix:** Luôn dùng `MenuKeys.Contracts` const (BE) + `MenuKeys.Contracts` (FE `menuKeys.ts`). Không hardcode string. ## IIS / Windows Server ### 25. `Install-WindowsFeature Web-WebSockets` khóa section `` ở applicationHost **Triệu chứng:** Sau khi install WebSocket feature → TẤT CẢ IIS site có `` trong web.config trả về HTTP 500.19 với error code `0x80070021` "configuration section cannot be used at this path" — kể cả site khác project không liên quan. **Nguyên nhân:** Feature install thêm `` section vào `applicationHost.config` với `overrideModeDefault="Deny"`. Site web.config override section đó → fail. **Fix:** Unlock section ở server level: ```powershell & "$env:SystemRoot\system32\inetsrv\appcmd.exe" unlock config -section:system.webServer/webSocket ``` Tương tự khi dùng URL Rewrite `` cần unlock `system.webServer/rewrite/allowedServerVariables`. **Cảnh báo co-existence:** Trên VPS shared với project khác, enable feature mới qua `Install-WindowsFeature` có thể làm sập site project khác. Luôn test all site sau mỗi enable. ## SignalR / Realtime ### 26. SignalR WebSocket không cho custom Authorization header **Triệu chứng:** `new HubConnectionBuilder().withUrl('/hubs/x', { headers: { Authorization: ... } })` — WebSocket transport vẫn 401. **Nguyên nhân:** Browser WebSocket API không cho set custom headers cho handshake. Chỉ 2 transport khác (SSE / LongPolling) mới dùng headers. **Fix:** - FE: dùng `accessTokenFactory: () => token` — SignalR client tự append `?access_token=` query cho WebSocket - BE: Wire JWT bearer `OnMessageReceived` để đọc token từ query khi path matches `/hubs/*`: ```csharp options.Events = new JwtBearerEvents { OnMessageReceived = ctx => { var accessToken = ctx.Request.Query["access_token"]; var path = ctx.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) ctx.Token = accessToken; return Task.CompletedTask; } }; ``` ### 27. SignalR SaveChangesInterceptor — capture Added ở SavingChanges, push ở SavedChanges **Lý do:** SavedChanges chỉ có entries sau commit thành công. Nhưng ở SavedChanges thì `EntityEntry.State` đã về `Unchanged` → không thể filter `Added`. **Fix:** 2-phase pattern: ```csharp public override ValueTask> SavingChangesAsync(...) { _pending = eventData.Context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Added) .Select(e => e.Entity).ToList(); return base.SavingChangesAsync(...); } public override async ValueTask SavedChangesAsync(..., int result, ...) { foreach (var n in _pending) await _realtimeNotifier.PushAsync(n); _pending.Clear(); return result; } ``` ## DevOps / CI/CD ### 28. LibreOffice download URL 404 khi pin wrong version **Triệu chứng:** `Invoke-WebRequest https://download.documentfoundation.org/libreoffice/stable/25.2.7/...` → 404. **Nguyên nhân:** LibreOffice mirror chỉ giữ vài version mới nhất. 25.2.7, 24.8.7 không có. Chỉ 25.8.6 tồn tại tại thời điểm cài. **Fix:** Check mirror URL trước khi pin. Dùng `Invoke-WebRequest -Method Head` verify trước download thật. ### 29. PowerShell 5.1 `>> $GITHUB_PATH` ghi UTF-16 → NUL byte crash Gitea Actions **Triệu chứng:** Gitea Actions job fail với "NUL byte in PATH". `echo "C:\\dotnet" >> $env:GITHUB_PATH`. **Nguyên nhân:** PS 5.1 default encoding UTF-16 LE BOM khi redirect `>>`. Gitea reads PATH as UTF-8 → NUL byte xuất hiện sau mỗi ASCII char. **Fix:** Dùng `Out-File -Encoding utf8 -Append`: ```powershell "C:\dotnet" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append ``` Hoặc drop step GITHUB_PATH hoàn toàn nếu NSSM PATH đã có sẵn dotnet+node. ### 30. PS 5.1 scripts với Vietnamese diacritics → parser error **Triệu chứng:** `Cannot parse script: Unexpected character` khi chạy PS script có text tiếng Việt inline. **Nguyên nhân:** PS 5.1 đọc file script với ANSI codepage (Windows-1258 hoặc default 1252), không phải UTF-8. **Fix (1):** Save script với BOM UTF-8 (Write-Host có dấu vẫn work): ```powershell [System.IO.File]::WriteAllText($path, $content, [System.Text.Encoding]::UTF8) ``` **Fix (2, safer):** Rewrite script ASCII-only. Text tiếng Việt nằm trong log messages thay dùng code: ```powershell Write-Host "Setup IIS sites done" # thay vi "Hoan tat" ``` ## TypeScript / FE ### 31. Dialog `size="xl"` TS2322 nếu variant không khai báo **Triệu chứng:** `` → `Type '"xl"' is not assignable to type '"sm" | "md" | "lg"'`. **Fix:** Sửa usage về `"lg"`, hoặc add `"xl"` vào `DialogSize` type union trong `components/ui/Dialog.tsx`. Đừng lazy `as any`. ## FE architecture ### 32. NavLink `end` prop cho query-param URL variants **Triệu chứng:** `/contracts?type=1` highlight cả `/contracts` lẫn `/contracts?type=2` cùng lúc. **Nguyên nhân:** Default NavLink `startsWith` match. Query string không parse distinct paths. **Fix:** `end={path.includes('?')}` trong resolvePath để query-variants match exact: ```tsx ``` ## IIS / Windows Server (continued) ### 33. IPv4/IPv6 port hijack trên VPS shared (G-084) **Triệu chứng:** `git.baocaogiaoduc.vn` trả về homepage Next.js của VietReport thay vì Gitea UI. Headers lộ `x-nextjs-cache: HIT` + `X-Powered-By: ARR/3.0` (request đã qua IIS ARR proxy rồi mới hit Next.js). **Root cause:** Next.js app (NSSM service) được deploy lên VPS shared với Gitea, ignore env `PORT=3001 HOSTNAME=127.0.0.1` và bind `0.0.0.0:3000`. Gitea bind `0.0.0.0:3000` trước đó bị Windows fallback xuống IPv6-only `[::]:3000` (default `IPV6_V6ONLY=1`). IIS ARR rewrite `http://localhost:3000` → Windows DNS resolve IPv4 first → hit Next.js → leak homepage cho TẤT CẢ subdomain có ARR proxy về `:3000`. **Fix (VietReport applied):** 1. Next.js NSSM env `PORT=3001 HOSTNAME=127.0.0.1` — bind loopback IPv4 2. Gitea `HTTP_ADDR=127.0.0.1` — bind loopback IPv4 explicit 3. IIS `web.config` rewrite URL dùng `127.0.0.1` thay `localhost` 4. NSSM `DependOnService=gitea` — boot order tránh race **3 rules rút ra — áp dụng mọi service trên VPS shared:** - Reverse-proxy luôn **IP literal `127.0.0.1`**, KHÔNG dùng `localhost` - Backend services bind **loopback IPv4 explicit**, KHÔNG `0.0.0.0` - Service dependency cho boot order khi nhiều service cùng port family **SOLUTION_ERP relevance:** - API host trong IIS app pool out-of-process (ANCM tự quản lý port Kestrel ephemeral) → risk THẤP - FE gọi trực tiếp `https://api.solutions.com.vn` (không ARR proxy) → risk THẤP - **NHƯNG** nếu tương lai thêm ARR reverse proxy (fe-admin/user `/api` proxy) hoặc deploy Kestrel standalone qua NSSM → PHẢI apply 3 rules trên - Scripts + skill doc đã update `localhost` → `127.0.0.1` để đồng bộ ## FE routing + state (Phase 6) ### 34. React Router NavLink `isActive` chỉ match pathname, không query string **Triệu chứng:** 2 NavLink cùng pathname (`/purchase-evaluations?type=2` vs `/purchase-evaluations?type=2&pendingMe=1`) cùng highlight khi URL là một trong 2. User thấy menu "Danh sách" + "Duyệt" active đồng thời. **Nguyên nhân:** React Router v6 `NavLink`'s built-in `isActive` chỉ so pathname. `end` prop chỉ thêm exact-match cho pathname segment, không check query string. **Fix:** Custom `isActive` với `queryMatches` helper (URLSearchParams set equality). Xem `Layout.tsx` cả 2 FE: ```tsx function queryMatches(current: string, target: string): boolean { const a = new URLSearchParams(current) const b = new URLSearchParams(target) const aKeys = [...a.keys()].sort() const bKeys = [...b.keys()].sort() if (aKeys.length !== bKeys.length) return false return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k)) } function MenuLeaf({ node }: { node: MenuNode }) { const location = useLocation() const path = resolvePath(node.key) const [targetPath, targetQuery = ''] = path.split('?') const isActive = location.pathname === targetPath && queryMatches(location.search.replace(/^\?/, ''), targetQuery) return ... } ``` ### 35. Menu tree inheritance phải extend khi thêm root mới **Triệu chứng:** Admin/role đã grant `PurchaseEvaluations.Read` (inherit parent) nhưng menu children `Pe_DuyetNcc_List` / `Pe_DuyetNcc_Create` không hiển thị. Chỉ thấy root `PurchaseEvaluations` ở Layout sidebar. **Nguyên nhân:** `GetMyMenuTreeQuery` hardcode 2 inherit root: `Contracts` và `Workflows`. Descendant Ct_*/Wf_* auto-inherit CRUD flags từ parent qua switch statement. Khi thêm root mới (`PurchaseEvaluations`, `PeWorkflows`) — không có trong switch → children mặc định (false,false,false,false) → filter `HasAccess` hide children. **Fix:** Extend switch + `nextInherit` propagation: ```csharp var contractsFlags = GetFlags(MenuKeys.Contracts); var workflowsFlags = GetFlags(MenuKeys.Workflows); var peFlags = GetFlags(MenuKeys.PurchaseEvaluations); // NEW var peWorkflowsFlags = GetFlags(MenuKeys.PeWorkflows); // NEW // Trong BuildChildren: if (inheritFromKey is not null && !resolved.ContainsKey(m.Key)) { flags = inheritFromKey switch { var k when k == MenuKeys.Contracts => contractsFlags, var k when k == MenuKeys.Workflows => workflowsFlags, var k when k == MenuKeys.PurchaseEvaluations => peFlags, // NEW var k when k == MenuKeys.PeWorkflows => peWorkflowsFlags, // NEW _ => flags, }; } var nextInherit = inheritFromKey ?? (m.Key == MenuKeys.Contracts ? MenuKeys.Contracts : m.Key == MenuKeys.Workflows ? MenuKeys.Workflows : m.Key == MenuKeys.PurchaseEvaluations ? MenuKeys.PurchaseEvaluations : m.Key == MenuKeys.PeWorkflows ? MenuKeys.PeWorkflows : null); ``` **Rule:** Khi thêm 1 root mới có child leaves (vd `PeWorkflows` → `PeWf_*`) — PHẢI update cả 3 chỗ: (1) MenuKeys.All, (2) GetMyMenuTreeQuery GetFlags + switch, (3) nextInherit propagation. ### 36. Vite env var embed compile-time — đổi `.env.production` phải rebuild FE **Triệu chứng:** Đổi `VITE_API_BASE_URL=...` trong `.env.production` nhưng FE vẫn gọi URL cũ. Hot reload không giúp. **Nguyên nhân:** Vite inline `import.meta.env.VITE_*` tại build time vào JS bundle (minified). File `.env*` chỉ đọc khi `vite build` — không runtime. **Fix:** Sau đổi env: 1. Rebuild: `cd fe-admin ; npm run build` 2. Deploy dist mới lên IIS 3. Clear CDN/browser cache (Ctrl+Shift+R) Verify bundle có URL mới: `curl dist/assets/index-*.js | grep -oE 'https://[^"]+api'`. ## Deploy / Production (continued) ### 37. PowerShell 5.1 diacritics trong script — gotcha #30 tái phát **Triệu chứng (bis):** `migrate-domains.ps1` viết với "Phương Án", "→", em-dash → PS 5.1 parser fail `Missing closing ''`, `Unexpected character`. **Fix:** Luôn ASCII-only cho .ps1 — rule lặp lại gotcha #30. Cách phát hiện: grep file cho UTF-8 multi-byte chars trước khi deploy: ```bash grep -P '[\x80-\xff]' scripts/*.ps1 # Nếu có match → rewrite ASCII-only ``` ## Email / Users ### 38. Email rename Identity user — 4 field cần update đồng thời **Triệu chứng:** Đổi `user.Email` xong login với email mới vẫn 401. Hoặc UserManager.FindByEmail trả null. **Nguyên nhân:** Identity lookup qua `NormalizedEmail` (uppercase), không `Email`. Username cũng dùng email. 4 field phải sync: ```csharp u.Email = newEmail; u.NormalizedEmail = newEmail.ToUpperInvariant(); u.UserName = newEmail; u.NormalizedUserName = newEmail.ToUpperInvariant(); await userManager.UpdateAsync(u); ``` **Bonus:** Check conflict trước khi rename (user khác đã có email mới) → skip để tránh duplicate. ### 39. act_runner v0.2.13 fetch `actions/checkout` từ github.com timeout 21s **Triệu chứng:** Run #108/#109 fail trong **22s** với: ``` Get "https://github.com/actions/checkout/info/refs?service=git-upload-pack": dial tcp 20.205.243.166:443: connectex: A connection attempt failed because the connected party did not properly respond... ``` Test gate (Domain + Infra) chưa kịp chạy. Build/deploy không tới. **Nguyên nhân:** act_runner mỗi run đều `git fetch` action source code từ github.com (kiểm tra update `actions/checkout@v4`). Khi VPS → github.com TCP có vấn đề (intermittent firewall/network), 21s timeout → toàn job fail TRƯỚC step nào của workflow chạy. **Fix:** Thay `uses: actions/checkout@v4` bằng manual git checkout từ Gitea internal — bypass github.com hoàn toàn. ```yaml - name: Checkout (manual git, bypass github.com) shell: powershell run: | git config --global --add safe.directory '*' git init -q git remote add origin "https://gitea-actions:${{ github.token }}@git.baocaogiaoduc.vn/${{ github.repository }}.git" $ref = "${{ github.ref }}" if ($ref -like "refs/heads/*") { $ref = $ref.Substring(11) } git fetch --depth=30 origin $ref git checkout --quiet "${{ github.sha }}" ``` Tương tự với `actions/upload-artifact@v4` — bỏ vì cũng phụ thuộc github.com. TRX file vẫn save local trong `test-results/` cho debug. **Long-term option:** config `github_mirror` trong gitea-runner config.yaml mirror github.com → Gitea internal repo. Hoặc pre-cache `.cache/act//` manually 1 lần. **Reference:** Run #108 commit `52999f3` fail, run #110 commit `14b7d18` fix pass. ### 40. npm junction cache `tsc not found` sau Move-Item — chưa xác định root cause **Triệu chứng:** Implement npm cache strategy bằng junction (mklink /J) + Move-Item node_modules → cache dir → fail `'tsc' is not recognized` ở step `npm run build`. Log NO Write-Host "cache MISS" output, NO npm install output. Timing 1.6s từ end-of-BE-build → start-of-fe-admin npm run build (impossible cho npm install 49s). **Hypothesis:** - (A) Move-Item của `node_modules` chứa nested junctions/symlinks → .bin/ relative paths broken sau move - (B) act_runner PowerShell stream capture có quirk với cache MISS branch → output bị silenced - (C) `Test-Path` trả về stale TRUE từ một state khác **Workaround tạm:** Rollback về fresh `npm install` mỗi run (49s + 33s = 82s). Path filter docs-only skip CI là alternative win lớn hơn. **TODO khi debug session sau:** - Thử `robocopy /MIR` thay `Move-Item` (handle symlinks tốt hơn) - Hoặc Copy-Item với `-Force -Recurse` (slower nhưng safer) - Hoặc dùng act_runner built-in `cache.host` server (có sẵn trong config.yaml) **Reference:** Run #111 commit `29eb5d9` fail, rollback ở `a21790d`. ### 41. Gitea Actions `paths-ignore` — workflow file change vẫn trigger **Triệu chứng:** Setup `paths-ignore: ['docs/**', '**/*.md']` để skip CI khi commit MD-only. Tự nhiên commit `.gitea/workflows/deploy.yml` (chính workflow file) cũng bị skip → không thể test workflow change. **Nguyên nhân:** `paths-ignore` evaluate set của file thay đổi. Nếu TẤT CẢ file thay đổi match patterns → skip. Workflow file `.gitea/workflows/**` không trong list ignore → trigger normal. **OK behavior.** **Edge case ngược:** commit thay đổi cả `docs/STATUS.md` + `src/Backend/...cs` → NOT skip vì có file ngoài ignore patterns. **Cũng OK.** **Verify:** Commit chỉ touch `docs/STATUS.md` → check Gitea Actions UI → phải KHÔNG có run mới trigger. Test với `curl /api/v1/.../runs/` trả `Not found` cho run-id tiếp theo. **Pattern hiện áp dụng:** ```yaml on: push: branches: [main] paths-ignore: - 'docs/**' - '**/*.md' - '.claude/skills/**' - '.gitignore' - 'scripts/**.md' ``` KHÔNG ignore: `.gitea/workflows/**`, `*.cs`, `*.tsx`, `*.ts`, `*.csproj`, `*.json`, `*.slnx`, `tests/**`. **Saving:** ~196s/commit cho ~30% commit thuộc loại docs-only (chốt MD, session log, etc). **Reference:** Commit `29eb5d9` add filter, verify ở commit `512880c` (docs-only) → Gitea NO trigger run #113. ### 44. Silent 403 từ class-level `[Authorize(Policy = ...)]` quá strict (Session 18) **Triệu chứng:** UAT 2026-05-08 — Drafter `nv.test` Workspace tạo phiếu B, dropdown "Quy trình duyệt" empty silent. Admin Designer cùng URL endpoint thấy data đầy đủ. Không có toast error / network panel hint. **Root cause:** `ApprovalWorkflowsV2Controller` class-level `[Authorize(Policy = "Workflows.Read")]` → non-admin role (Drafter chỉ có `PurchaseEvaluations.Read`) bị 403 Forbidden khi GET `/api/approval-workflows-v2`. TanStack Query catch HTTP error trả `data=undefined`, FE component render dropdown empty không có "loading" / "error" state visible. **Fix (`f77ea38`):** Tách policy theo action — class-level `[Authorize]` only (any authenticated), action-level chỉ POST/DELETE giữ `[Authorize(Policy = "Workflows.Create")]`: ```csharp [ApiController] [Route("api/approval-workflows-v2")] [Authorize] // ← any authenticated, không hardcode policy public class ApprovalWorkflowsV2Controller(IMediator mediator) : ControllerBase { [HttpGet] // ← Drafter pick workflow lúc create — read-only OK public async Task<...> Overview(...) { ... } [HttpPost] [Authorize(Policy = "Workflows.Create")] // ← admin Designer public async Task<...> Create(...) { ... } } ``` **Pattern reusable:** Endpoint dùng cho nhiều use case (admin Designer + user list-pick) — split policy per action thay vì class-level uniform. Read-only list workflow KHÔNG nhạy cảm (chỉ là cấu hình quy trình, không expose business data). **Phòng tránh tương lai:** Khi controller class-level `[Authorize(Policy)]`, audit role nào cần access từng action. Nếu GET cần broader role hơn POST → split policy. Default `[Authorize]` (any authenticated) cho list-pick endpoint. **FE diagnostic improvement:** TanStack Query error nên hiển thị warning UI (toast hoặc banner) thay vì silent. Hiện tại `useQuery` catch silent → debug khó. Future: wire `onError` handler global show generic error toast. ### 45. PE "Trả về nhưng hệ thống vẫn duyệt" — FE button label vs decision payload mismatch (Session 21 turn 3) **Triệu chứng:** UAT 2026-05-12 — User bro screenshot button labeled `← Trả lại` trong PE Workflow Panel (menu "Duyệt"), nhấn vào nhưng phiếu KHÔNG về phase TraLai — ngược lại tiến qua Cấp tiếp theo (hệ thống ghi nhận approve). User mô tả hành vi: "Trả về nhưng hệ thống vẫn duyệt". **Root cause:** `PeWorkflowPanel.tsx` có 3 chỗ check transition type với logic KHÔNG sync giữa nhau: - **L205-207** `isSendBack` (button label color): include cả `DangSoanThao` lẫn `TraLai` từ phase trung gian → label hiển thị `← Trả lại` đúng. - **L64-66** `isReject` (payload `decision` gửi BE): CHỈ check `DangSoanThao`, **thiếu `TraLai`** → khi target=TraLai (98), `isReject=false` → payload `decision: 1` (Approve) thay vì `2` (Reject). - **L247-248** dialog `isSendBack` (title + warning): CHỈ check `DangSoanThao`, **thiếu `TraLai`** → dialog title fallback `'✓ Duyệt → Trả lại'` (sai semantic) + KHÔNG hiển thị amber warning "Phiếu sẽ về Đang soạn thảo". BE `PurchaseEvaluationWorkflowService.TransitionAsync`: - L51 `if (decision == Reject)` branch → set Phase=TraLai correctly khi decision=Reject. - L97 `APPROVE STEP` branch khi decision=Approve + fromPhase=ChoDuyet → `ApproveV2Async` UPSERT opinion = "đã duyệt" + advance Cấp. - → Khi FE gửi `decision=1` (do bug `isReject`), BE đi vào nhánh APPROVE thay vì REJECT → phiếu được ghi nhận approve dù user định trả lại. **Severity:** 🔴 CRITICAL — data integrity issue. NV nhấn "Trả lại" sẽ vô tình "duyệt" phiếu sang Cấp tiếp theo + UPSERT opinion vĩnh viễn vào `PurchaseEvaluationLevelOpinions` (Mig 26). Khó rollback vì BE đã `SaveChangesAsync`. **Fix Chunk A (`de00887` BE defense-in-depth):** ```csharp // PurchaseEvaluationWorkflowService.cs sau set isAdmin/isSystem (L48), trước REJECT branch (L51) if ((targetPhase == PurchaseEvaluationPhase.TraLai || targetPhase == PurchaseEvaluationPhase.TuChoi) && decision != ApprovalDecision.Reject) { throw new ConflictException( $"Transition tới {targetPhase} BẮT BUỘC decision=Reject (nhận {decision}). " + "Báo lỗi caller — payload mismatch giữa target phase và decision."); } ``` Boundary protection cho mọi caller tương lai (API client / mobile / cron retry). 3 regression test: - `TransitionAsync_TargetTraLai_WithApproveDecision_Throws_AndDoesNotMutateState` (bug reproduce) - `TransitionAsync_TargetTuChoi_WithApproveDecision_Throws_AndDoesNotMutateState` (consistency cover) - `TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai` (happy path control) **Fix Chunk B (`4b29d00` FE mirror 2 app):** ```typescript // PeWorkflowPanel.tsx (fe-user + fe-admin) — 3 chỗ × 2 app // Chỗ 1: isReject payload (line 64-66) const isReject = target === PurchaseEvaluationPhase.TuChoi || (target === PurchaseEvaluationPhase.DangSoanThao && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao) || (target === PurchaseEvaluationPhase.TraLai // ← THÊM && evaluation.phase !== PurchaseEvaluationPhase.TraLai) // Chỗ 2: dialog isSendBack (line 247-248) const isSendBack = (target === PurchaseEvaluationPhase.DangSoanThao || target === PurchaseEvaluationPhase.TraLai) // ← THÊM && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao && evaluation.phase !== PurchaseEvaluationPhase.TraLai // ← THÊM ``` Chỗ 3 (button label `isSendBack` L205-207) đã đúng từ S17, KHÔNG đụng. **Pattern reusable — invariant check khi viết FE workflow transition:** 1. Button label condition (visual) phải SYNC với payload decision (semantic). 2. Dialog title/warning condition phải SYNC với button label + payload. 3. Tốt nhất: extract `isReject(target, currentPhase)` thành 1 helper FE + BE share semantic — KHÔNG duplicate logic giữa 3 chỗ. **Phòng tránh tương lai:** - Khi spec mới có thêm phase terminal/intermediate (vd Session 17 thêm TraLai làm Phase RIÊNG thay vì DangSoanThao revert), audit grep TOÀN BỘ logic check `=== DangSoanThao` để xem chỗ nào cần thêm `|| === NewPhase`. - BE guard early invariant `(targetPhase ∈ terminalSet) ⇔ (decision == Reject)` thay vì trust FE payload. - Test-before bug fix BẮT BUỘC §7 — 3 test cover bug reproduce + consistency + happy path. **References:** - Commit fix: `de00887` (BE Chunk A) + `4b29d00` (FE Chunk B) - Spec Session 17: `feedback_n_stage_workflow_pattern` DEPRECATED + spec mới trong `PurchaseEvaluationWorkflowService.cs` comment L15-19 - State machine 5 trạng thái: Nháp / Đã gửi duyệt / **Trả lại (98) — Phase RIÊNG** / Từ chối / Đã duyệt ### 46. Gitea Actions API path `/tasks` not `/runs` + cache stale ~2 min (CICD Monitor S21 t4 discovery) **Triệu chứng:** CICD Monitor sub-agent S21 t4 run đầu poll Gitea Actions runs sau push → `GET https://git.baocaogiaoduc.vn/api/v1/repos/vietreport-admin/solution-erp/actions/runs?limit=5` → **404 Not Found**. Tưởng repo không có Actions enabled hoặc API endpoint sai. Debug 10 phút retry path/auth/header trước khi tìm ra đúng path. **Root cause:** Gitea API v1 spec dùng `/actions/tasks` (NOT `/actions/runs`). Naming khác GitHub Actions API (GitHub: `/actions/runs`). Public no-auth read OK cho repo public. **Endpoint đúng:** ```bash GET https://git.baocaogiaoduc.vn/api/v1/repos/vietreport-admin/solution-erp/actions/tasks?limit=5 ``` **Response fields:** - `id` (task internal ID) - `run_number` (display number Run #N) - `head_sha` (commit triggered task) - `status` (queued / running / success / failure / cancelled) - `conclusion` (final state when status=success/failure) - `created_at`, `updated_at` - `display_title` (commit message summary) **Match task to commit:** Filter `head_sha == $commitSha` thay vì rely on order. **Bonus gotcha — cache stale:** `updated_at` field caches ~2 phút sau khi deploy thật xong (act_runner ghi log final, Gitea API chưa update DB ngay). Cross-check VPS file timestamps khi cần time-sensitive verify: ```powershell ssh vietreport-vps "Get-Item C:\inetpub\solution-erp\admin\index.html | Select LastWriteTime" ssh vietreport-vps "Get-Item C:\inetpub\solution-erp\api\SolutionErp.Api.dll | Select LastWriteTime" ``` File LastWriteTime VPS = thực sự deploy completion time (NSSM copy + IIS recycle xong). **Phòng tránh tương lai:** - CICD Monitor system prompt + Bash command preset đã update sang `/actions/tasks` (saved trong MEMORY S21 t4). - Khi setup automation script với Gitea API → đọc rõ Gitea API spec v1.20+ (`https://docs.gitea.com/api/`) thay vì assume GitHub naming. - Time-sensitive verify (vd "deploy xong chưa, có an toàn trigger task tiếp không"): KHÔNG trust API status timestamp đơn lẻ → cross với VPS file mtime hoặc curl bundle hash live. ### 47. `.claude/agent-memory/**` paths-ignore — hypothesis disproven, preventive note cho non-.md state files (S22 discovery + S22 chốt revise) **Triệu chứng ban đầu (em main HYPOTHESIS S22):** Cuối session flush 3-4 sub-agent MEMORY.md drift patch + commit dạng `[CLAUDE] Docs: chốt S22 ...` — em main đoán push trigger Gitea Actions full deploy ~3.5min waste vì `.claude/agent-memory/**` thiếu trong paths-ignore. **Verify thực tế (CICD Monitor Run #193 S22 chốt — 2026-05-13 23:16):** Hypothesis **DISPROVEN**. `paths-ignore` của workflow file đã có pattern `**/*.md` — glob này match **mọi `.md` file ở mọi độ sâu** (kể cả `.claude/agent-memory/{investigator,implementer,reviewer,cicd-monitor}/MEMORY.md`). Push `cc8a7d3` (Docs S22 chốt + 4 agent MEMORY flush, 8 files tất cả là `.md`) → CI **correctly SKIPPED**. Run #193 cuối cùng cho code change là `b04a11a` (Mig 30 BE+FE), KHÔNG phải `cc8a7d3` Docs. **Cross-check:** `git show --name-only cc8a7d3` → 8 files với extension `.md` only → match `**/*.md` glob → SKIPPED đúng. **Preventive note cho future:** Nếu tương lai add **non-`.md` state files** vào `.claude/agent-memory/` (vd `archive.json`, `metrics.log`, `cache.bin`) — sẽ trigger CI vì KHÔNG match `**/*.md`. Khi đó add `.claude/agent-memory/**` vào paths-ignore explicit: ```yaml paths-ignore: - 'docs/**' - '**/*.md' - '.claude/skills/**' - '.claude/agent-memory/**' # ← preventive nếu add non-.md state files ``` **Severity:** Informational (hiện tại KHÔNG có vấn đề thật vì all MEMORY files là `.md`). KHÔNG cần fix `.gitea/workflows/deploy.yml`. **Cross-ref:** Gotcha #41 paths-ignore docs-only skip pattern. **Lesson:** Verify hypothesis qua actual Gitea API task list TRƯỚC KHI claim CI waste. Em main S22 đoán nhầm — CICD Monitor catch sai. **References:** - CICD Monitor Run #186 (S21 t4 2026-05-13 19:13) — first discovery - CICD Monitor Run #187 (S21 t5 2026-05-13 20:12) — confirmed pattern + cache stale bonus - Memory `feedback_multi_agent_setup` Plan G Trial Week 1 evidence ### 48. Multi-Changelog.Add() trong cùng SaveChangesAsync → SQLite frozen-clock tie-break → tests `OrderByDescending(CreatedAt).First()` non-deterministic (Session 25 Plan AB + Run #215 catch) **Triệu chứng:** Plan AB Chunk A `cdfd542` add SECOND Changelog.Add() entry vào `ApplyReturnModeAsync` (cover Bug 2 — Return mode log) end-of-function. Caller `TransitionAsync:100` đã có sẵn `LogTransitionAsync` add FIRST Changelog entry (Action=Transition + ContextNote=comment chứa "không lùi được"). 2 entries cùng `SaveChangesAsync` transaction → SQLite test fixture frozen clock → CreatedAt **identical microseconds** cho cả 2 rows. Plan M edge case tests (S23 t3) query `.OrderByDescending(c => c.CreatedAt).FirstAsync()` assert `ContextNote.Contains("không lùi được")` — sau Plan AB, SQLite tie-break non-deterministic, pick Plan AB row (EntityType=Workflow, Action=Update, ContextNote=null) → `Expected ContextNote not to be ` FAIL. **CI Run #215 sha=cdfd542** test_infra FAIL 2/53 (51 PASS, 2 FAIL): - `ApplyReturnMode_OneStep_AtStep1_ResetsToBuoc1Cap1_KeepsChoDuyet` (line 350) - `ApplyReturnMode_OneLevel_AtStep1Level1_ResetsToBuoc1Cap1_KeepsChoDuyet` (line 308) Test gate caught regression → deploy never reached → prod spared broken state. **Fix Option A (chốt):** Test query filter by `Summary.Contains("Chuyển phase")` để pick đúng LogTransition entry. Plan AB BE code stays clean. ```csharp // Trước Plan AB Chunk A2: var changelog = await db.PurchaseEvaluationChangelogs .Where(c => c.PurchaseEvaluationId == pe.Id) .OrderByDescending(c => c.CreatedAt) .FirstAsync(); // Sau Plan AB Chunk A2 fix (commit 8c05947): var changelog = await db.PurchaseEvaluationChangelogs .Where(c => c.PurchaseEvaluationId == pe.Id && c.Summary!.Contains("Chuyển phase")) .OrderByDescending(c => c.CreatedAt) .FirstAsync(); ``` **Pattern reusable:** Khi handler/service add NEW Changelog row trong existing flow đã có LogTransition row, tests query audit table MUST filter EntityType / Action / Summary keyword **discriminator** thay vì raw OrderByDescending timestamp. Cross-ref Contract V2 test setup tương lai. **Severity:** Major — caught by CI before prod ship, no user impact. Lesson reinforced UAT mode `feedback_uat_skip_verify` skip `dotnet test` per chunk RISK khi BE refactor > 100 LOC + signature change → em main resumed local test verify post Plan AB Chunk A2. **References:** - Plan AB Chunk A commit `cdfd542` (Run #215 FAIL) - Plan AB Chunk A2 fix commit `8c05947` (Run #216 PASS) - Memory `feedback_uat_skip_verify` lesson reinforced S25 - File: `tests/SolutionErp.Infrastructure.Tests/Services/PurchaseEvaluationWorkflowServiceReturnModeTests.cs:304-310, 346-352` ### 49. UI dual-phase badge `fromPhase → toPhase` gây nhầm khi 3/4 Reject mode giữ Phase=ChoDuyet (Session 25 Plan AD) **Triệu chứng:** Bro UAT 2026-05-19 Plan AC deploy: panel "Lịch sử duyệt" 6 entries TẤT CẢ hiện `Đã gửi duyệt → Đã gửi duyệt` (vì 3 mode Return OneLevel/OneStep/Assignee giữ Phase=ChoDuyet sau Mig 28 — chỉ Drafter mode set TraLai). Reject entry visually IDENTICAL Approve entry → user nhầm "Đã trả lại nhưng vẫn hiện đã duyệt". **Fix Plan AD (commit `0aaf2df`):** 1. **Drop fromPhase → toPhase badges entirely** trong ApprovalsTab (cả `fe-user` + `fe-admin` mirror §3.9). Visual confusion gỡ bỏ. 2. **Thay bằng next-target hint parse từ comment** via helper `extractNextTargetHint(decision, toPhase, comment)`: - **Approve:** Summary "sang Cấp X" → "→ Cấp X", "sang Bước Y" → "→ Bước Y (Cấp 1)", "Duyệt vượt cấp" → "→ Vượt cấp tới Cấp cuối", toPhase=DaDuyet(20) → "→ Đã duyệt hoàn tất" - **Reject:** ContextNote "Người chỉ định" → "→ Trả về Người chỉ định (Bước X Cấp Y)" parse regex, "Người soạn thảo"/"Drafter" → "→ Trả về Người soạn thảo", "không lùi được" → "→ Không lùi được", "Trả về 1 Cấp"/"Trả về Cấp X" → "→ Lùi về Cấp X", toPhase=TuChoi(99) → "→ Từ chối hoàn toàn" 3. **Decision badge** (Plan AC `a734bf2` đã add): Duyệt emerald / Trả lại amber / Từ chối rose — phân biệt Action level KHÔNG dựa vào phase. **Pattern reusable cross-project:** UI audit history KHÔNG nên render dual-phase badge khi state machine self-loop (e.g. ChoDuyet → ChoDuyet là advance pointer trong cùng phase, KHÔNG phải transition). Thay bằng Decision badge + semantic next-target hint parse từ structured comment. **Severity:** UX confusion (KHÔNG functional bug). Bro UAT phản hồi sau Plan AC deploy. **References:** - Plan AD commit `0aaf2df` (Run #219 PASS) - File: `fe-user/src/components/pe/PeDetailTabs.tsx:1995-2070` (ApprovalsTab + decisionBadge + extractNextTargetHint) - Mirror: `fe-admin/src/components/pe/PeDetailTabs.tsx` ### 51. INFRASTRUCTURE seed vs DEMO seed phân biệt — `DemoSeed:Disabled` flag gate trap (Session 29 Plan B Chunk A2 Hotfix CICD) **Triệu chứng:** Plan B Chunk A2 Implementer scaffold `SeedSampleContractWorkflowV2Async` mirror PE `SeedSampleApprovalWorkflowsV2Async` pattern — nested inside `if (!demoSeedDisabled)` branch trong `DbInitializer.cs:105-111`. Prod has `DemoSeed:Disabled=true` (Plan T S23 t10 — UAT permanent clean slate). Run #231 PASS deploy NHƯNG `QT-HD-V2-001` workflow KHÔNG seed prod. CICD Monitor (agentId a2ea2e3a) verify Stage 4 catch smoking gun log: `"DemoSeed:Disabled=true → skip workflow + contracts + PE + sample V2 seed (Plan T S23 t10 + Plan B Chunk A2 Contract V2)"`. → Drafter Workspace dropdown V2 EMPTY → V2 contract path **BLOCKED end-to-end UAT**. **Root cause:** Implementer mirror PE pattern (which IS gated for valid reason — PE workflow A/B already seeded V1 historically) → áp dụng nguyên xi cho Contract V2 (no V1 sample existed) → V2 path không có default workflow để Drafter pick. **Fix Hotfix CICD (`38f1c4d`):** PROMOTE `SeedSampleContractWorkflowV2Async` ra ngoài `DemoSeed` gate: ```csharp if (!demoSeedDisabled) { await SeedDemoContractsAsync(...); await SeedDemoPurchaseEvaluationsAsync(...); await SeedSampleApprovalWorkflowsV2Async(...); // PE: gated (historical V1 sample existed) } // [Plan B S29 2026-05-22 Hotfix CICD] OUT of gate await SeedSampleContractWorkflowV2Async(...); // INFRASTRUCTURE — V2 path requires ``` **Pattern reusable — Seed classification table:** | Category | Examples | Gate? | |---|---|---| | **INFRASTRUCTURE always run** | Roles, Departments, Catalogs, MenuTree, AdminPermissions, ContractTemplates, **SampleWorkflowV2 (V2 path requires)** | NO gate | | **DEMO gated** | DemoUsers (30 sample, Plan T disabled prod), DemoContracts ([DEMO]), DemoPE ([DEMO]), SampleApprovalWorkflowsV2 (historical PE A/B legacy) | `if (!demoSeedDisabled)` | **Decision tree khi add new Seed method:** 1. Production có cần seed này để work end-to-end không? - YES → INFRASTRUCTURE (no gate) - NO → DEMO (gated) 2. Có safe để admin edit/delete/disable không? - YES → INFRASTRUCTURE OK (admin sửa qua Designer khi cần) - NO → architect re-think (immutable seed = anti-pattern) **Bonus — Smart Friend ROI:** CICD Monitor catch BEFORE bro UAT 401/empty experience. Cumulative pattern proven 4× S22 #44 + S25 #48 + S29 Plan B Reviewer #ApplicableType + S29 Plan B CICD #DemoSeed. **Phòng tránh tương lai:** Khi Implementer mirror PE V2 pattern cho new module (Budget V2, Notification V2), explicit check question: "PE pattern gated/ungated, có applicable cho new module không?" — KHÔNG copy nguyên xi DemoSeed gate placement. **References:** - Hotfix CICD commit: `38f1c4d` (Run #232 PASS 3m33s) - File: `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs:105-122` - Cross-ref: Plan T S23 t10 DemoSeed flag (gotcha implicit) - Memory cross-ref: `feedback_demo_seed_flag_disable.md` (Plan T pattern) ### 50. Page move cross-app — `Layout.tsx` `resolvePath` staticMap missed mirror → silent sidebar drop (Session 29 Plan CA Hotfix 1) **Triệu chứng:** Plan CA move 4 master pages từ fe-admin → fe-user (commit `06a441c` Implementer Case 2). BE `/api/menus/me` return tree đầy đủ (Master + 4 children + Catalogs sub-tree 4 children, ALL CanRead=true). Admin login eoffice → sidebar group "DANH MỤC" expand chỉ thấy "Danh mục chi tiết" (Catalogs), **3 leaf Suppliers/Projects/Departments + 4 sub-catalogs biến mất silent**. **Root cause:** `fe-user/src/components/Layout.tsx:55-94` `resolvePath(key)` staticMap chỉ có 7 entries cho `Dashboard/Contracts/PE/Budgets/Bg_*`. THIẾU 7 entries cho master/catalog leaf mới move: - `Suppliers`, `Projects`, `Departments` - `CatalogUnits`, `CatalogMaterials`, `CatalogServices`, `CatalogWorkItems` Component `MenuLeaf` line 238: `if (!path) return null` → khi `resolvePath` trả null, component returns null → React render nothing → **silent drop khỏi DOM**, không có error toast/console warn. **Fix Hotfix 1 (`e55d96b`):** Thêm 7 entries vào fe-user `staticMap` mirror EXACT từ fe-admin `Layout.tsx:33-47`: ```typescript const staticMap: Record = { Dashboard: '/dashboard', // ... existing 6 entries Suppliers: '/master/suppliers', Projects: '/master/projects', Departments: '/master/departments', CatalogUnits: '/master/catalogs/units', CatalogMaterials: '/master/catalogs/materials', CatalogServices: '/master/catalogs/services', CatalogWorkItems: '/master/catalogs/work-items', } ``` **Pattern reusable — 4-place mirror checklist khi Implementer Case 2 cookie-cutter copy page cross-app:** 1. ✅ Page file (`pages//*.tsx`) — copy nguyên content 2. ✅ `App.tsx` Routes — add `` 3. ✅ `lib/menuKeys.ts` constants — mirror BE `MenuKeys.cs` 4. ⚠️ **`components/Layout.tsx` `resolvePath` staticMap** — KEY mapping → route path. **DỄ MISS** vì khác file scope với pages directory. **Bonus:** fe-admin có `staticMap` đầy đủ từ trước Plan CA → mirror dễ. fe-user trước Plan CA KHÔNG có master keys → Implementer cookie-cutter copy page nhưng quên check staticMap diff. **Phòng tránh tương lai:** Implementer Case 2 cross-app page move task prompt MUST list 4 places explicit. Reviewer Cat 1 wire claim verify SHOULD include: "Sidebar menu visible end-to-end test post-build". **References:** - Commit Hotfix 1: `e55d96b` (Run #230 PASS, fe-user bundle `DVBLmZlt` → `Dgn1iU9E`) - File: `fe-user/src/components/Layout.tsx:55-94` (resolvePath staticMap + line 238 MenuLeaf null guard) - Original Plan CA Chunk B commit: `06a441c` (Implementer missed point 4) - Mirror: `fe-admin/src/components/Layout.tsx:33-53` ### 52. `qdrant-client` 1.18 xóa `search()` API — `except Exception: continue` nuốt lỗi silent → vector search luôn trả `[]` (Session 31 RAG eval diagnosis) **Triệu chứng:** `search_memory` MCP tool chỉ trả kết quả cho queries có BM25 exact-match tốt (có tất cả token trong cùng 1 chunk). Queries dùng ngữ nghĩa / multi-hop concept → **0 results** dù Qdrant `points_count=2949` green. **Root cause:** `qdrant-client 1.18.0` removed `QdrantClient.search()` method hoàn toàn. `retrieval.py` vẫn gọi `_qdrant.search(...)`: ```python # OLD (broken in 1.18): hits = _qdrant.search( collection_name=c, query_vector=query_vector, limit=top_k, with_payload=True, ) # AttributeError: 'QdrantClient' object has no attribute 'search' ``` Exception bị `except Exception: continue` nuốt silent → `vec_results = []` mọi lúc → pipeline chỉ có BM25. **Kết quả trước fix:** RAG eval v1.1 recall@5 = 0.455 (chỉ BM25 queries). v1.0 = 0.455 (same reason). BM25 strict AND-match: ALL tokens phải cùng chunk → multi-token query 8+ tokens fail. **Fix (retrieval.py):** ```python # NEW (qdrant-client 1.12+ query_points API): resp = _qdrant.query_points( collection_name=c, query=query_vector, # ← param renamed: query_vector → query limit=top_k, with_payload=True, ) for h in resp.points: # ← .points không phải iterable trực tiếp ``` **Kết quả sau fix:** recall@5 = 1.000 (11/11), avg rerank = 0.847. PASS gate. **Phòng tránh:** Pin `qdrant-client` version trong deps (`==1.x.y`). Hoặc thêm health-check startup: `assert hasattr(_qdrant, 'query_points'), "upgrade qdrant-client"`. **KHÔNG dùng `except Exception: continue` che-mờ lỗi API** — ít nhất log warning. ### 53. Sub-agent truncation / stall pattern khi heavy MEMORY update phase end-of-task — Reviewer + CICD bị cut mid-sentence ở Update MEMORY.md step (Session 35 × 3 occurrence) **Triệu chứng:** Sub-agent (Reviewer + CICD spawn ~100K token budget, ~30+ tool uses) chạy adversarial checks / smoke verify hoàn chỉnh, returning verdict PASS qua snippet visible, NHƯNG output bị truncate ở final "Update MEMORY.md BEFORE stop" step. Em main không nhận structured verdict đầy đủ. **Pattern empirical S35:** - Reviewer FE forms (1200 LOC + 60 mutation): Cat 1 "wire BE PERFECT" + 33 tool uses → truncated mid-MEMORY append - Reviewer BE CRUD (576 LOC + 16 endpoint): "MEMORY size warning 24.6KB exceeds 24.4KB. Let me append concise entry but trim verbose..." → truncated mid-trim - CICD Run #244 verify (FE Admin deploy): "VPS mtime cross-check confirms ship at 10:05" → stalled 600s watchdog timeout **Root cause hypothesis:** 1. Sub-agent context window approaches limit khi cumulative tool output (Read MEMORY ~25KB initial + Read references + Bash output + grep results) + 100K spawn budget 2. MEMORY.md size ~25-31KB borderline triggers Edit/Write large operation late-stage → token overflow during streaming 3. Stream watchdog 600s timeout không recover (CICD case) — process hung internally **Mitigation S35 verified:** - **A. Tight brief scope** — Reviewer FE Admin spawn (~5K token brief, 4 cat tight, "concise findings only") → PASS clean 5K return không truncated. Pattern: brief budget < 8K + scope ≤ 4 cat + explicit "DO NOT curate MEMORY heavy — short append only this time" - **B. Em main manual verify post-truncation** — Cat 2-6 grep-based verify (SHA256 diff exit code + grep count match) takes ~5 phút em main, faster than re-spawn - **C. Curate MEMORY pre-spawn nếu > 25KB** — agent MEMORY > threshold trigger truncation risk. Em main curate proxy archive q3.md trước spawn heavy. - **D. Avoid forcing MEMORY heavy update in agent spec** — phase "Update MEMORY.md BEFORE stop with detailed findings" → switch to "short append 1 entry FIFO most-recent-first, KHÔNG curate old" **Cumulative occurrences S35:** 3/8 sub-agent spawn (Reviewer × 2 + CICD × 1 = 37.5% truncation rate at borderline ~25-31KB MEMORY). Heavy task + large MEMORY = correlation point. **References:** - S33 Implementer truncation pattern 2/3 (memory `feedback_implementer_truncation_mitigation` user-level — heavy scaffold ≥30 file) - S35 Reviewer FE forms (Session 35 push #1 verify): output cut after Cat 1 PERFECT statement - S35 Reviewer BE CRUD (Session 35 push #2 verify): cut mid-MEMORY trim - S35 CICD Run #244 (Session 35 push #3 verify): stalled 600s watchdog after VPS mtime cross-check **Phòng tránh:** 1. Tight brief scope ≤ 8K tokens cho Reviewer/CICD nếu task verifiable qua grep/diff em main 2. MEMORY pre-spawn audit: nếu > 25KB → curate proxy archive trước spawn 3. Agent spec ghi rõ "short append MEMORY only, NO curate", remove "BEFORE stop with detailed" directive khi MEMORY borderline 4. Em main backup verify Cat 2-6 manual grep nếu Reviewer truncated mid-verdict **References:** - AI_INFRA: `claude-rag/lib/retrieval.py` `vector_search()` function — fixed 2026-05-26 S31 - Eval run: `eval/runs/2026-05-26-baseline-v1.1-final.json` - Diagnosis: Qdrant REST `/collections/proj_solution_erp` green (2949 points), `bm25.db` 2949 chunks → pipeline broken, not data. Confirmed via `python -c "from qdrant_client import QdrantClient; client.search(...)"` → AttributeError. --- ### 54. Anthropic API 529 Overloaded transient khi spawn sub-agent → 0 token fail (Session 37 Plan G-O3 + Session 29 cumulative) **Triệu chứng:** Spawn Implementer/Reviewer/CICD qua Agent tool → completed status nhưng result = `API Error: 529 Overloaded` + `subagent_tokens=0` + `tool_uses=0`. Agent KHÔNG chạy gì, 0 token billed. Khác hẳn truncation (#53 — agent chạy nhưng cut output). **Pattern empirical:** S37 FE Proposal spawn fail 529 (0 token, 218s duration = pure wait) + S29 Plan CA CICD verify fail 529 × 2 (transient Anthropic API overload window). Recurring khi Anthropic API load cao (peak hours / model release window). **Phân biệt 529 vs #53 truncation:** - 529 Overload: `tokens=0`, agent KHÔNG start → retry-able HOẶC em main solo fallback - #53 truncation: agent chạy đầy đủ (~100-150K token) nhưng cut output mid-MEMORY/mid-exploration → KHÔNG retry (đã tốn token), em main grep verify manual **Mitigation verified S37:** - **A. Em main solo fallback** — KHÔNG retry loop (529 transient nhưng spawn lại có thể fail tiếp). Em main viết code trực tiếp reliable hơn (S37: BE 700 LOC + FE 4 file × 2 app solo sau 2 spawn fail). Proven faster than wait-retry. - **B. Critical-path task KHÔNG để 1 agent block** — nếu task on critical path (cần ship trong session) → em main có sẵn fallback plan solo, KHÔNG block chờ agent. - **C. Off-peak spawn** — heavy parallel spawn (3-4 agent) tránh giờ peak nếu không critical. **Cumulative occurrence:** S29 × 2 (Plan CA CICD) + S37 × 1 (FE Proposal) = 3× across project. ~5-10% spawn fail rate observed at peak. ### 55. Sub-agent truncation mid-EXPLORATION phase (extend #53 — Session 37 Implementer BE) **Triệu chứng:** Khác #53 (truncate mid-MEMORY end-of-task), S37 Implementer BE Proposal truncate NGAY ĐẦU ở exploration phase — return `"Now I need to look at Common Models... ICurrentUser, IDateTime..."` sau 30 tool uses, CHƯA write file nào. 150K token wasted (đọc reference + diagnose compile error mid-research). **Root cause:** Heavy spec brief (~10K token) + agent đọc nhiều reference file (PE WorkflowService + Features + CodeSequence + Common Models) → context bloat trước khi bắt đầu write → truncate giữa research. **Mitigation:** - Brief WRITE agent ≤ 8K (gotcha #53 rule A reinforced — heavy spec ~10K = quá rủi ro) - Pre-supply reference snippets trong brief (em main đọc + paste shape thay vì để agent đọc full) → agent KHÔNG cần exploration phase tốn token - HOẶC em main solo cho task spec phức tạp cần đọc > 4 reference file (S37 lesson: BE Proposal mirror PE = nhiều reference → em main solo reliable) **References:** S37 Implementer BE spawn `a3afd177` (truncate mid-exploration) + memory `feedback_implementer_truncation_mitigation` (heavy scaffold ≥30 file pattern). Cumulative truncation S35 × 3 (mid-MEMORY) + S37 × 1 (mid-exploration) = 4× extend #53. --- ## Checklist debug bug mới 1. Build pass không? → fail → check using + package version compat 2. DI register đủ? → runtime error "Unable to resolve" → add `AddScoped/Singleton` 3. API log startup có error ẩn? → `tail` output file 4. File đã persist đúng chưa? → `head -5` verify sau Write 5. Nếu package exotic → thử downgrade về stable trước 6. Nếu TS error → check `erasableSyntaxOnly`, `verbatimModuleSyntax` 7. Nếu EF expression tree → tách logic ra ngoài query 8. Nếu Unicode CLI → dùng file payload 9. Nếu workflow 403 → check FE `workflow.nextPhases` sync từ BE pinned policy 10. Nếu SignalR 401 → dùng `accessTokenFactory` + BE OnMessageReceived hook (#26) 11. Nếu PS 5.1 script fail → check encoding UTF-8 / BOM / ASCII-only (#30) 12. Nếu subdomain trả sai content / bị hijack → check IPv4/IPv6 port collision trên VPS shared (#33) 13. Nếu 2 NavLink cùng active / không đúng highlight → custom isActive match query string (#34) 14. Nếu menu item có quyền nhưng không hiện → check GetMyMenuTreeQuery inheritance extend (#35) 15. Nếu FE gọi API sai URL sau đổi env → rebuild + clear bundle cache (#36) 16. Nếu .ps1 fail parser trên PS 5.1 → ASCII-only, grep multi-byte chars (#30, #37) 17. Nếu rename email Identity vẫn 401 → update 4 field NormalizedEmail/UserName (#38) 18. Nếu CI fail TCP timeout 21s ở "Set up job" → bypass github.com, manual checkout từ Gitea (#39) 19. Nếu npm install caching fail `tsc not found` → KHÔNG dùng junction Move-Item, thử robocopy/Copy-Item (#40) 20. Nếu CI vẫn trigger khi commit MD-only → paths-ignore trong on:push không match patterns đúng (#41) 21. Nếu user phàn nàn "feature work cho admin nhưng user empty/403 silent" → check class-level Authorize policy có over-restrict cho non-admin không, split per action (#44) 22. Nếu button workflow label nói "Trả lại" nhưng phiếu vẫn tiến approve → audit FE `isReject` payload condition vs button `isSendBack` label condition vs dialog `isSendBack` warning condition — phải sync 3 chỗ với CÙNG set target phase. BE thêm guard `(target ∈ terminalSet) ⇔ (decision=Reject)` chặn caller mismatch (#45) 23. Nếu Gitea Actions API trả 404 trên `/actions/runs` → đúng path là `/actions/tasks` (Gitea naming khác GitHub). Cache `updated_at` stale ~2 min → cross-check VPS file LastWriteTime cho time-sensitive verify (#46) 24. Nếu test `OrderByDescending(CreatedAt).First()` query audit table fail sau add Changelog mới → SQLite frozen-clock tie-break, MUST filter Summary/EntityType discriminator (#48) 25. Nếu page move cross-app (Implementer Case 2) nhưng menu leaf KHÔNG hiện sidebar dù BE trả permission OK → check `Layout.tsx` `resolvePath` staticMap miss key mapping → MenuLeaf null guard silent drop (#50). 4-place mirror checklist: page + Routes + menuKeys.ts + Layout.tsx staticMap 26. Nếu new Seed method KHÔNG chạy prod dù dotnet build PASS + deploy SUCCESS → check nested inside `if (!demoSeedDisabled)` gate (Plan T S23 t10 flag enabled prod) → INFRASTRUCTURE seed phải PROMOTE OUT of DemoSeed gate (#51). Decision tree: production cần để work end-to-end? YES → ungate 25. Nếu UI audit list show `Đã gửi duyệt → Đã gửi duyệt` lặp gây nhầm → drop dual-phase badge khi state machine self-loop, thay Decision badge + next-target hint parse từ comment (#49) 27. Nếu RAG `search_memory` trả 0 results dù Qdrant green + BM25 có data → `qdrant-client` upgrade xóa `search()` method, bị nuốt silent. Test: `python -c "from qdrant_client import QdrantClient; c=QdrantClient(url='http://127.0.0.1:6333'); c.search"`. Fix: dùng `query_points(query=...).points` (#52) 28. Nếu sub-agent (Reviewer/CICD) return PASS verdict bị cut mid-sentence ở "Update MEMORY.md" step → MEMORY > 25KB triggers truncation risk. Mitigation: tight brief ≤ 8K + em main grep verify manual + curate MEMORY pre-spawn nếu > 25KB (#53) 29. Nếu spawn sub-agent trả `API Error: 529 Overloaded` + `tokens=0` → Anthropic API transient overload, agent KHÔNG chạy. KHÔNG retry loop → em main solo fallback reliable (#54). Phân biệt với #53 truncation (agent chạy đủ token nhưng cut output) 30. Nếu sub-agent WRITE truncate NGAY ĐẦU exploration phase (chưa write file, đọc > 4 reference) → heavy spec ~10K + context bloat. Mitigation: brief ≤ 8K + pre-supply reference snippet trong brief HOẶC em main solo nếu cần đọc > 4 reference file (#55)