All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m21s
Final close session 5 — bao gồm: ==== Tests Phase 3 mini (NEW) ==== tests/SolutionErp.Infrastructure.Tests/Application/PeWorkflowAdminTests.cs - 6 test CreatePeWorkflowDefinitionCommandHandler: - First version → IsActive=true, Version=1, ActivatedAt set - Second version same Code → auto-increment v2 + deactivate v1 (atomic) - Different EvaluationType (A vs B) → independent active state - Persists steps ordered by Order field - Persists approvers per step - Third version → v1 + v2 deactivate, v3 active Total tests: 71 → 77 pass / ~2s (54 Domain + 23 Infra). Skip Phase 3 full (UpsertOpinion + Budget link validation) — cần Identity UserManager DI helper, defer session sau. ==== 3 gotcha CI mới (#39 #40 #41) ==== - #39 act_runner github.com TCP timeout 21s → manual checkout fix (run #108/#109 fail, #110 pass) - #40 npm junction cache `tsc not found` after Move-Item — rolled back, hypothesis nested junctions trong node_modules disrupt .bin/ paths. TODO debug session sau với robocopy hoặc act_runner cache.host - #41 Gitea Actions paths-ignore behavior — workflow file change vẫn trigger (correct), commit MD-only skip 100% (verify512880c→ no run #113) + Checklist debug bug mới items 18-20 referencing 3 gotcha trên. ==== Doc updates (8 file) ==== - STATUS.md: header Phase 8 update + 3 row Recently Done CI fixes + cumulative test 71→77 - HANDOFF.md: TL;DR + CI optimize section + Phase status + gotcha count 38→41 - migration-todos.md: Phase 8 §E updated với Phase 3 mini done + CI fixes - rules.md §7 Testing: rewrite full — stack + test pyramid + quy tắc bổ sung mỗi feature + workflow user end-of-task (`dotnet test` local trước push) + CI gate behavior - architecture.md §11: update test pyramid + phased priority + CI optimization sub-section (3 fix manual checkout / path filter / npm cache rollback) + tốc độ deploy table - gotchas.md: + #39 #40 #41 đầy đủ (triệu chứng + nguyên nhân + fix + reference) - ef-core-migration SKILL: Phase 8 update note thêm CI fixes + 77 test - CLAUDE.md root: test count 71→77 + folder structure + CI/CD pipeline 3 fix section - memory project_solution_erp.md: session 5 summary + workflow user mới - session log 2026-04-29-2300-chot-final-ci-tests-gotchas.md (NEW — 9 section detail) ==== Skill audit cron ==== `solution-erp-skill-audit-monthly` next fire 2026-05-01 (2 ngày sau). Cron survives across sessions (setup commitb904a25). Khi fire sẽ: - Cross-check 6 skill với STATUS/gotchas/migration-todos - Auto-refresh stale + đề xuất add/archive cho human approve - Log vào docs/changelog/skill-audit-2026-05.md - ABORT nếu repo dirty ==== Verify ==== - dotnet test SolutionErp.slnx → 77 pass / ~2s (54 Domain + 23 Infra) - git status clean sau commit này - CI: commit này chứa code (test + workflow) → trigger CI test gate Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
593 lines
24 KiB
Markdown
593 lines
24 KiB
Markdown
# 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<TResponse>` 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<T>'`.
|
|
|
|
**Fix:** Tạo `IDesignTimeDbContextFactory<ApplicationDbContext>` 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 `<w:t>` — placeholder miss khi regex replace.
|
|
|
|
**Fix:** Iterate Paragraph, gom text tất cả `<w:t>` → 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 <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<IX, X>()` 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 `<webSocket>` ở applicationHost
|
|
|
|
**Triệu chứng:** Sau khi install WebSocket feature → TẤT CẢ IIS site có `<webSocket enabled="true" />` 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 `<webSocket>` 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 `<serverVariables>` 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<InterceptionResult<int>> SavingChangesAsync(...) {
|
|
_pending = eventData.Context.ChangeTracker.Entries<Notification>()
|
|
.Where(e => e.State == EntityState.Added)
|
|
.Select(e => e.Entity).ToList();
|
|
return base.SavingChangesAsync(...);
|
|
}
|
|
|
|
public override async ValueTask<int> 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:** `<Dialog size="xl">` → `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
|
|
<NavLink to={path} end={path.includes('?')}>
|
|
```
|
|
|
|
## 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 <NavLink to={path} className={isActive ? 'active' : ''}>...</NavLink>
|
|
}
|
|
```
|
|
|
|
### 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/<hash>/`
|
|
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/<id>`
|
|
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)
|