# 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. ## 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. ## 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)