[CLAUDE] Skill: thêm 3 skill ops project-specific
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m47s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m47s
Khảo sát alirezarezvani/claude-skills repo — phần lớn skill đã có ở user-level (code-reviewer, sql-database-assistant, focused-fix, senior-frontend, mcp-builder...). Bulk import sẽ trùng + nhiều skill là doc-dump generic không có YAML when-to-use. Thay vào đó: viết 3 skill PROJECT-SPECIFIC encode kiến thức SOLUTION_ERP-only mà generic không thể biết: - dependency-audit-erp: dotnet list --vulnerable + npm audit cho fe-admin/fe-user, respect pin constraint MediatR 12.4.1 + Swashbuckle 6.9.0 + Node 20.x, dẫn chiếu gotchas, output template + CI integration TODO Phase 5.1 - ef-core-migration: 8 migration history + 3-file rule + Design TimeDbContextFactory + 6 pitfalls cụ thể (bao gồm cascade vs restrict cho WorkflowDefinitionId), workflow add entity mới end- to-end, prod apply via idempotent script - iis-deploy-runbook: 3 IIS site topology + win-acme cert + NSSM gitea-runner shared VIETREPORT + LibreOffice 25.8.6 headless, debug playbook 500/502/SignalR/login, deploy steps + manual emergency, rotate creds + backup commands, dẫn chiếu gotcha #25/26/28/29 Skills README cập nhật: 6 skill (3 domain + 3 ops). CLAUDE.md + docs/CLAUDE.md sync count. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -4,11 +4,21 @@ Skill này là tài liệu chuyên biệt để Claude (và developer khác) dù
|
||||
|
||||
## Skills hiện có
|
||||
|
||||
### Domain skills (logic nghiệp vụ)
|
||||
|
||||
| Skill | Mục đích | Trigger ví dụ | Trạng thái |
|
||||
|---|---|---|---|
|
||||
| `contract-workflow` | State machine 9 phase, role × phase guard, SLA timer, auto-approve | "approve contract", "chuyển phase", "auto-approve quá hạn" | 📝 Placeholder (Phase 3) |
|
||||
| `form-engine` | Render template docx/xlsx, parse 8 form, field mapping, PO generator | "export contract as word", "điền form", "render template" | 📝 Placeholder (Phase 2) |
|
||||
| `permission-matrix` | Role × MenuKey × CRUD, seed, reset password, 3-layer resolution | "permission denied", "gán role", "menu không hiện" | 📝 Placeholder (Phase 1) |
|
||||
| `contract-workflow` | State machine 9 phase + versioned workflow per ContractType + role × phase guard + SLA + auto-gen mã HĐ RG-001 | "approve contract", "chuyển phase", "versioned workflow", "HĐ cũ giữ cũ" | ✅ Tier 3 updated |
|
||||
| `form-engine` | Render template docx/xlsx + FieldSpec JSON + DynamicForm + PDF export LibreOffice | "export contract as word", "điền form", "render template", "PDF export" | ✅ Active |
|
||||
| `permission-matrix` | Role × MenuKey × CRUD + seed + 3-layer resolution + inherit Contracts/Workflows | "permission denied", "gán role", "menu không hiện", "inherit permission" | ✅ Active |
|
||||
|
||||
### Ops/infra skills (devops + security + schema)
|
||||
|
||||
| Skill | Mục đích | Trigger ví dụ | Trạng thái |
|
||||
|---|---|---|---|
|
||||
| `dependency-audit-erp` | Scan CVE NuGet + npm 2 FE, respect pin constraint (MediatR 12.4.1, Swashbuckle 6.9.0) | "npm audit", "dotnet vulnerable", "deps scan", "nâng cấp package" | ✅ New Tier 3 |
|
||||
| `ef-core-migration` | Tạo/revert EF Core 10 migration, 3-file rule, DesignTimeDbContextFactory, 8 migration history | "thêm migration", "EF migration", "schema update", "snapshot lỗi" | ✅ New Tier 3 |
|
||||
| `iis-deploy-runbook` | 3 IIS site + win-acme cert + gitea-runner + LibreOffice + debug 500/502/SignalR prod | "prod 500", "IIS fail", "cert hết hạn", "restart app pool", "deploy IIS" | ✅ New Tier 3 |
|
||||
|
||||
## Format chuẩn 1 skill
|
||||
|
||||
@ -17,34 +27,36 @@ Mỗi skill là 1 folder với ít nhất `SKILL.md` + optional `examples/` + `r
|
||||
```
|
||||
.claude/skills/<skill-name>/
|
||||
├── SKILL.md ← Entry point: description, when-to-use, workflow
|
||||
├── examples/ ← Code snippets mẫu
|
||||
├── examples/ ← Code snippets mẫu (optional)
|
||||
│ └── *.cs | *.tsx
|
||||
└── references/ ← Link đến file code thật, docs
|
||||
└── references/ ← Link đến file code thật, docs (optional)
|
||||
```
|
||||
|
||||
**Frontmatter `SKILL.md`:**
|
||||
**Frontmatter `SKILL.md` (BẮT BUỘC `when-to-use` để skill auto-trigger):**
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: contract-workflow
|
||||
description: State machine 9 phase cho hợp đồng — guard rule, SLA, auto-approve
|
||||
name: skill-name-kebab-case
|
||||
description: 1-3 câu mô tả skill làm gì + stack specificity (để embedding match chính xác)
|
||||
when-to-use:
|
||||
- "approve contract"
|
||||
- "state machine bug"
|
||||
- "SLA expired"
|
||||
- "trigger phrase 1"
|
||||
- "trigger phrase 2"
|
||||
- "keyword tiếng Việt"
|
||||
---
|
||||
|
||||
# Contract Workflow Skill
|
||||
# Skill Name
|
||||
|
||||
## Context
|
||||
...
|
||||
|
||||
## Workflow
|
||||
## Workflow / Commands
|
||||
...
|
||||
|
||||
## Pitfalls
|
||||
...
|
||||
|
||||
## Code pointers
|
||||
- `src/Backend/SolutionErp.Domain/Contracts/ContractPhase.cs`
|
||||
- `src/Backend/SolutionErp.Application/Contracts/Commands/TransitionContractCommand.cs`
|
||||
- `path/to/file.cs`
|
||||
```
|
||||
|
||||
## Tạo skill mới — checklist
|
||||
@ -52,4 +64,28 @@ when-to-use:
|
||||
1. Tạo folder `.claude/skills/<kebab-case-name>/`
|
||||
2. Viết `SKILL.md` với frontmatter + sections: Context / Workflow / Code pointers / Common pitfalls
|
||||
3. Add row vào bảng "Skills hiện có" phía trên
|
||||
4. Commit `[CLAUDE] Skill: add <name>`
|
||||
4. Update `docs/CLAUDE.md` (dòng skill count)
|
||||
5. Commit `[CLAUDE] Skill: add <name>`
|
||||
|
||||
## Nguyên tắc design skill
|
||||
|
||||
**PROJECT-SPECIFIC, không clone generic:**
|
||||
- Skill user-level global đã có sẵn (`code-reviewer`, `sql-database-assistant`, `focused-fix`, ...)
|
||||
- Skill project-level phải encode kiến thức SOLUTION_ERP-only mà generic không có:
|
||||
- Commit convention `[CLAUDE] <scope>: ...`
|
||||
- Path pattern `src/Backend/SolutionErp.*/...`
|
||||
- Pin constraint (MediatR 12.4.1, Swashbuckle 6.9.0, TypeScript 6 erasableSyntaxOnly)
|
||||
- Gotcha-referenced (dẫn chiếu `docs/gotchas.md#N`)
|
||||
- Workflow Vietnamese-first
|
||||
|
||||
**Keep it actionable:**
|
||||
- Commands copy-pastable (không pseudocode)
|
||||
- Paths đầy đủ (không `src/...`)
|
||||
- Version pinned (không "latest")
|
||||
- Dẫn chiếu gotcha/migration # cụ thể
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/CLAUDE.md` — quick rules + full stack context
|
||||
- `docs/gotchas.md` — 32 bẫy đã gặp
|
||||
- `docs/changelog/migration-todos.md` — roadmap 5 phase + Tier 3
|
||||
|
||||
155
.claude/skills/dependency-audit-erp/SKILL.md
Normal file
155
.claude/skills/dependency-audit-erp/SKILL.md
Normal file
@ -0,0 +1,155 @@
|
||||
---
|
||||
name: dependency-audit-erp
|
||||
description: Scan lỗ hổng NuGet + npm cho SOLUTION_ERP (.NET 10 + 2 FE React). Chạy dotnet list package --vulnerable + npm audit cho fe-admin/fe-user, report CVE + decide upgrade path. Respect pin constraint (MediatR 12.4.1 cannot upgrade to 14, Swashbuckle 6.9.0 cannot upgrade to 7+, Node 20.x only for CI).
|
||||
when-to-use:
|
||||
- "dependency audit"
|
||||
- "npm audit"
|
||||
- "dotnet vulnerable"
|
||||
- "scan CVE"
|
||||
- "nâng cấp package"
|
||||
- "security dependencies"
|
||||
- "deps scan CI"
|
||||
---
|
||||
|
||||
# Dependency Audit — SOLUTION_ERP
|
||||
|
||||
> **Context:** Phase 5.1 security backlog explicit — "Dependencies scan CI (`dotnet list package --vulnerable --include-transitive`, `npm audit --audit-level=high`)". Chưa hook vào workflow, có thể chạy manual.
|
||||
|
||||
## Commands (chạy từ root repo)
|
||||
|
||||
### Backend .NET
|
||||
|
||||
```powershell
|
||||
# Restore trước khi scan (first time / sau pull)
|
||||
dotnet restore
|
||||
|
||||
# Scan top-level + transitive vulnerable
|
||||
dotnet list src/Backend/SolutionErp.Api/SolutionErp.Api.csproj package --vulnerable --include-transitive
|
||||
|
||||
# Scan cả solution
|
||||
dotnet list SolutionErp.slnx package --vulnerable --include-transitive
|
||||
|
||||
# Outdated check (khác vulnerable — chỉ cảnh báo version cũ)
|
||||
dotnet list SolutionErp.slnx package --outdated
|
||||
```
|
||||
|
||||
**Output interpretation:**
|
||||
- `> High` / `> Critical` severity → fix ngay (pin lên patch/minor version không breaking)
|
||||
- `> Moderate` → plan trong sprint tới
|
||||
- `> Low` → track, không rush
|
||||
|
||||
### Frontend 2 app
|
||||
|
||||
```powershell
|
||||
# fe-admin
|
||||
cd fe-admin
|
||||
npm audit --audit-level=high
|
||||
npm audit --json > ../tmp/fe-admin-audit.json # detail
|
||||
cd ..
|
||||
|
||||
# fe-user
|
||||
cd fe-user
|
||||
npm audit --audit-level=high
|
||||
npm audit --json > ../tmp/fe-user-audit.json
|
||||
cd ..
|
||||
```
|
||||
|
||||
Hoặc chạy song song từ 1 lệnh:
|
||||
```powershell
|
||||
cd fe-admin; npm audit --audit-level=high; cd ..; cd fe-user; npm audit --audit-level=high; cd ..
|
||||
```
|
||||
|
||||
## ⚠️ Pin constraints — KHÔNG auto-upgrade dù có CVE/outdated
|
||||
|
||||
Đọc `docs/gotchas.md` trước khi bấm `npm audit fix` hoặc `dotnet add package`. Những pin sau đây có lý do rõ ràng:
|
||||
|
||||
### Backend pins
|
||||
|
||||
| Package | Current | Lý do không upgrade | Gotcha # |
|
||||
|---|---|---|---|
|
||||
| `MediatR` | **12.4.1** | v14 breaking — `AddMediatR` fail resolve `IMediator` | #1 |
|
||||
| `Swashbuckle.AspNetCore` | **6.9.0** | v10+ yêu cầu `Microsoft.OpenApi` 2.x — breaking `Swashbuckle.AspNetCore.Models` namespace | #2 |
|
||||
| `Microsoft.AspNetCore.OpenApi` | **removed** | Conflict Swashbuckle, dùng riêng 1 trong 2 | #2 |
|
||||
| `Microsoft.EntityFrameworkCore` | **10.x** | Pin theo .NET 10 SDK (`global.json` 10.0.104) | — |
|
||||
| `Microsoft.AspNetCore.Identity` | **10.x** | Pin theo .NET 10 — `AddIdentityCore` không có `AddDefaultTokenProviders` | #8 |
|
||||
|
||||
### Frontend pins
|
||||
|
||||
| Package | Current | Lý do pin |
|
||||
|---|---|---|
|
||||
| `typescript` | **6.x** | `erasableSyntaxOnly` cấm enum → đã rewrite const-object pattern, rollback = rewrite lại (#3) |
|
||||
| `vite` | **8.x** | rolldown native binding cần fresh `node_modules` trên CI (gotcha npm ci fail) |
|
||||
| `@microsoft/signalr` | **8.0.7** | SignalR hub version parity với ASP.NET Core 10 — test kỹ trước khi bump |
|
||||
| `react` | **19** | Auto-scaffolded, đã style-adapt cho |
|
||||
| Node engine | **`>=20`** | CI pin `20.x` qua `.nvmrc` (NamGroup bài học, gotcha #5) |
|
||||
|
||||
## Workflow khi fix vulnerability
|
||||
|
||||
```
|
||||
1. Đọc CVE detail: npm audit (--json) hoặc https://github.com/advisories/GHSA-xxxx
|
||||
2. Check có phải transitive dep không?
|
||||
- Direct → bump trong package.json / .csproj, test build
|
||||
- Transitive → check ancestor package, có newer không
|
||||
3. Nếu ancestor fix available nhưng là major version:
|
||||
- Đánh giá breaking change (đọc CHANGELOG)
|
||||
- Nếu yes → defer + document, dùng npm overrides hoặc dotnet transitive constraint
|
||||
4. Test local:
|
||||
- BE: dotnet build + dotnet run → /health/ready healthy
|
||||
- FE: npm run build + npm run dev → login flow + CRUD smoke test
|
||||
5. Commit [CLAUDE] Infra: bump <package> for CVE-xxxx
|
||||
6. Watch CI xanh
|
||||
```
|
||||
|
||||
## CI integration (TODO — Phase 5.1 backlog)
|
||||
|
||||
Dự kiến thêm vào `.gitea/workflows/deploy.yml` step:
|
||||
|
||||
```yaml
|
||||
- name: Deps audit
|
||||
shell: pwsh
|
||||
run: |
|
||||
dotnet list SolutionErp.slnx package --vulnerable --include-transitive 2>&1 | Tee-Object -Variable nugetOut
|
||||
if ($nugetOut -match 'has the following vulnerable packages') {
|
||||
Write-Error "NuGet vulnerabilities found"
|
||||
exit 1
|
||||
}
|
||||
cd fe-admin; npm audit --audit-level=high; if ($LASTEXITCODE -ne 0) { exit 1 }; cd ..
|
||||
cd fe-user; npm audit --audit-level=high; if ($LASTEXITCODE -ne 0) { exit 1 }; cd ..
|
||||
```
|
||||
|
||||
Gate nên set `continue-on-error: true` lần đầu → monitor 1 tuần → enable blocking.
|
||||
|
||||
## Output template (khi manual chạy)
|
||||
|
||||
```markdown
|
||||
# Deps audit — {YYYY-MM-DD}
|
||||
|
||||
## NuGet
|
||||
- Critical: 0
|
||||
- High: 0
|
||||
- Moderate: 0
|
||||
- Low: 0
|
||||
|
||||
## npm fe-admin
|
||||
- Critical: 0
|
||||
- High: 0
|
||||
- Moderate: N (liệt kê)
|
||||
|
||||
## npm fe-user
|
||||
- Critical: 0
|
||||
- High: 0
|
||||
- ...
|
||||
|
||||
## Action items
|
||||
- [ ] Bump X from a.b.c → a.b.d (patch, safe) — CVE-xxxx
|
||||
- [ ] Defer Y (v3 → v4 breaking, plan Phase N)
|
||||
- [ ] Override Z via npm overrides (transitive, no direct bump)
|
||||
```
|
||||
|
||||
Lưu vào `docs/changelog/deps-audit-{YYYY-MM-DD}.md` nếu có action.
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/gotchas.md` — 26+ bẫy package compat đã gặp
|
||||
- `docs/changelog/migration-todos.md` Phase 5.1 — checklist deps scan CI
|
||||
- `SolutionErp.slnx` + `global.json` — .NET version pin
|
||||
205
.claude/skills/ef-core-migration/SKILL.md
Normal file
205
.claude/skills/ef-core-migration/SKILL.md
Normal file
@ -0,0 +1,205 @@
|
||||
---
|
||||
name: ef-core-migration
|
||||
description: Tạo/sửa/revert EF Core 10 migration cho SOLUTION_ERP. Dùng khi thêm entity mới, thay đổi schema, rollback migration, debug DesignTimeDbContextFactory fail. Đã có 8 migration sẵn (Init → AddVersionedWorkflows). Snapshot + Designer + Migration 3-file rule bắt buộc commit đủ.
|
||||
when-to-use:
|
||||
- "thêm migration"
|
||||
- "EF Core migration"
|
||||
- "dotnet ef migrations add"
|
||||
- "revert migration"
|
||||
- "schema DB update"
|
||||
- "DbContext change"
|
||||
- "snapshot lỗi"
|
||||
- "DesignTimeDbContextFactory"
|
||||
---
|
||||
|
||||
# EF Core Migration — SOLUTION_ERP
|
||||
|
||||
> **Context:** .NET 10 + EF Core 10 + SQL Server. DbContext: `ApplicationDbContext` ở `Infrastructure/Persistence/`. Startup: `SolutionErp.Api`.
|
||||
|
||||
## Migration history (8 migration hiện có)
|
||||
|
||||
| # | Name | Tables added / changed |
|
||||
|---|---|---|
|
||||
| 1 | `Init` | 7 Identity tables (AspNetUsers/Roles/UserRoles/...) |
|
||||
| 2 | `AddMasterData` | Suppliers, Projects, Departments |
|
||||
| 3 | `AddPermissions` | MenuItems, Permissions |
|
||||
| 4 | `AddForms` | ContractTemplates, ContractClauses |
|
||||
| 5 | `AddContractsWorkflow` | Contracts, ContractApprovals, ContractComments, ContractAttachments, ContractCodeSequences |
|
||||
| 6 | `AddNotifications` | Notifications |
|
||||
| 7 | `AddWorkflowTypeAssignments` | WorkflowTypeAssignments (legacy admin override) |
|
||||
| 8 | `AddVersionedWorkflows` | WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers + `Contracts.WorkflowDefinitionId` FK |
|
||||
|
||||
Total: **24 bảng** dbo + `__EFMigrationsHistory`. Xem `docs/database/schema-diagram.md` ERD đầy đủ.
|
||||
|
||||
## Commands (chạy từ root repo)
|
||||
|
||||
### Thêm migration mới
|
||||
|
||||
```powershell
|
||||
# Sau khi chỉnh Domain entity + EF config + DbContext DbSet
|
||||
dotnet ef migrations add <MigrationName> `
|
||||
--project src/Backend/SolutionErp.Infrastructure `
|
||||
--startup-project src/Backend/SolutionErp.Api `
|
||||
--output-dir Persistence/Migrations
|
||||
```
|
||||
|
||||
**Naming convention:** PascalCase, verb prefix:
|
||||
- `Add<Thing>` — thêm entity / bảng
|
||||
- `Update<Thing>` — thay đổi schema
|
||||
- `Remove<Thing>` — xóa column / table
|
||||
- `Rename<Thing>` — đổi tên
|
||||
|
||||
### Apply migration to DB
|
||||
|
||||
```powershell
|
||||
# Dev: LocalDB SolutionErp_Dev
|
||||
dotnet ef database update `
|
||||
--project src/Backend/SolutionErp.Infrastructure `
|
||||
--startup-project src/Backend/SolutionErp.Api
|
||||
|
||||
# Hoặc đơn giản: chạy API → DbInitializer tự Migrate
|
||||
dotnet run --project src/Backend/SolutionErp.Api
|
||||
```
|
||||
|
||||
### Revert 1 migration (rollback)
|
||||
|
||||
```powershell
|
||||
# Rollback về migration trước đó (ví dụ về sau AddWorkflowTypeAssignments)
|
||||
dotnet ef database update AddWorkflowTypeAssignments `
|
||||
--project src/Backend/SolutionErp.Infrastructure `
|
||||
--startup-project src/Backend/SolutionErp.Api
|
||||
|
||||
# Xóa file migration (3 file, xem dưới)
|
||||
dotnet ef migrations remove `
|
||||
--project src/Backend/SolutionErp.Infrastructure `
|
||||
--startup-project src/Backend/SolutionErp.Api
|
||||
```
|
||||
|
||||
### Gen SQL script (review trước khi apply prod)
|
||||
|
||||
```powershell
|
||||
# Script từ 1 migration → latest
|
||||
dotnet ef migrations script AddWorkflowTypeAssignments `
|
||||
--project src/Backend/SolutionErp.Infrastructure `
|
||||
--startup-project src/Backend/SolutionErp.Api `
|
||||
--output tmp/migration-7-to-8.sql
|
||||
|
||||
# Idempotent (check if exists trước mỗi operation)
|
||||
dotnet ef migrations script --idempotent `
|
||||
--project src/Backend/SolutionErp.Infrastructure `
|
||||
--startup-project src/Backend/SolutionErp.Api
|
||||
```
|
||||
|
||||
## 3-file rule (BẮT BUỘC commit đủ)
|
||||
|
||||
Mỗi `migrations add` sinh **3 file** — phải commit đủ:
|
||||
|
||||
```
|
||||
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/
|
||||
├── {Timestamp}_{Name}.cs ← migration logic (Up/Down)
|
||||
├── {Timestamp}_{Name}.Designer.cs ← model snapshot tại thời điểm migration
|
||||
└── ApplicationDbContextModelSnapshot.cs ← current model state (được overwrite mỗi migration)
|
||||
```
|
||||
|
||||
**Gotcha #17:** Thiếu `ModelSnapshot.cs` → `migrations add` kế tiếp sẽ sinh duplicate columns. Thiếu `Designer.cs` → không revert được.
|
||||
|
||||
## Pitfalls thường gặp
|
||||
|
||||
### P1 — DesignTime DbContext resolve fail
|
||||
|
||||
**Triệu chứng:** `dotnet ef migrations add` → `Unable to resolve service for type 'DbContextOptions<ApplicationDbContext>'`.
|
||||
|
||||
**Nguyên nhân:** EF tooling chạy đứng ngoài runtime DI, cần factory riêng.
|
||||
|
||||
**Fix:** Đã có `src/Backend/SolutionErp.Infrastructure/Persistence/DesignTimeDbContextFactory.cs`. Nếu connection string thay đổi → update factory, không chỉ `appsettings.json`.
|
||||
|
||||
### P2 — Table rename / column rename gen ra DROP + CREATE
|
||||
|
||||
**Triệu chứng:** Migration sinh `DropColumn` + `AddColumn` thay vì `RenameColumn` → mất data.
|
||||
|
||||
**Fix:** Sửa migration thủ công sang `migrationBuilder.RenameColumn(...)`. Hoặc dùng `[Column("newname")]` attribute để EF tự detect rename.
|
||||
|
||||
### P3 — Query filter (soft delete) khi thêm FK
|
||||
|
||||
**Triệu chứng:** Warning `The entity type 'X' has a global query filter but referencing entity 'Y' doesn't. This may lead to inconsistent results`.
|
||||
|
||||
**Fix:** Entity reference cũng phải có query filter:
|
||||
```csharp
|
||||
builder.HasQueryFilter(x => !x.IsDeleted);
|
||||
```
|
||||
|
||||
Hoặc dùng `.IgnoreQueryFilters()` cho query cần bypass.
|
||||
|
||||
### P4 — FK cascade/restrict khác với expectation
|
||||
|
||||
**Triệu chứng:** Migration apply OK nhưng DELETE parent → cascade sang child (hoặc ngược lại restrict block).
|
||||
|
||||
**Fix:** Explicit config trong `IEntityTypeConfiguration<T>`:
|
||||
```csharp
|
||||
builder.HasOne(x => x.Contract)
|
||||
.WithMany(c => c.Approvals)
|
||||
.HasForeignKey(x => x.ContractId)
|
||||
.OnDelete(DeleteBehavior.Cascade); // hoặc Restrict, SetNull
|
||||
```
|
||||
|
||||
**Case study quan trọng:** `Contracts.WorkflowDefinitionId` → `WorkflowDefinitions.Id` **PHẢI dùng `Restrict`** để protect HĐ cũ khi admin archive version (xem `workflow-contract.md` invariants).
|
||||
|
||||
### P5 — Nullable reference type gen column NOT NULL
|
||||
|
||||
**Triệu chứng:** Property `public string? Description` → migration gen `NOT NULL`.
|
||||
|
||||
**Fix:** Check `builder.HasQueryFilter` chỗ config, hoặc explicit `builder.Property(x => x.Description).IsRequired(false)`.
|
||||
|
||||
### P6 — `AddVersionedWorkflows` duplicate seed
|
||||
|
||||
**Case study hiện tại:** Nếu thêm migration mới sau `AddVersionedWorkflows`, cẩn thận KHÔNG trigger `DbInitializer.SeedWorkflowDefinitionsAsync` 2 lần. Check `if (!db.WorkflowDefinitions.Any())` trong DbInitializer.
|
||||
|
||||
## Workflow khi thêm entity mới (example: add AuditLog)
|
||||
|
||||
```
|
||||
1. Domain/AuditLogs/AuditLog.cs — tạo entity (BaseEntity hoặc AuditableEntity)
|
||||
2. Infrastructure/Persistence/Configurations/AuditLogConfiguration.cs
|
||||
- ToTable("AuditLogs")
|
||||
- Unique index trên (EntityType, EntityId) nếu cần
|
||||
3. Infrastructure/Persistence/ApplicationDbContext.cs
|
||||
- public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||
4. Application/Common/Interfaces/IApplicationDbContext.cs
|
||||
- DbSet<AuditLog> AuditLogs { get; }
|
||||
5. dotnet ef migrations add AddAuditLogs
|
||||
6. Review file .cs sinh ra → OK
|
||||
7. dotnet ef database update → LocalDB apply
|
||||
8. Test: dotnet run → check bảng được tạo
|
||||
9. Commit [CLAUDE] Domain+Infra: add AuditLog entity + migration
|
||||
10. ⚠️ UPDATE docs/database/schema-diagram.md (ERD + migration table)
|
||||
11. UPDATE docs/STATUS.md nếu là work lớn
|
||||
```
|
||||
|
||||
## Apply prod (VPS)
|
||||
|
||||
```powershell
|
||||
# Trên VPS, sau khi deploy code:
|
||||
# Option 1: auto migrate — DbInitializer.MigrateAsync() chạy khi API startup
|
||||
# Option 2: manual script
|
||||
dotnet ef migrations script <LastApplied> --idempotent --output migrate.sql
|
||||
# Review migrate.sql → chạy qua sqlcmd:
|
||||
sqlcmd -S .\SQLEXPRESS -d SolutionErp -U vrapp -P <pw> -i migrate.sql
|
||||
```
|
||||
|
||||
**Prod safety:**
|
||||
- Backup trước: `scripts/backup-sql.ps1`
|
||||
- Dry-run idempotent script local trước
|
||||
- Test rollback plan: script from-N-to-M-1 sẵn
|
||||
|
||||
## Code pointers
|
||||
|
||||
- `src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs` — 24 DbSet
|
||||
- `src/Backend/SolutionErp.Infrastructure/Persistence/DesignTimeDbContextFactory.cs` — EF tooling factory
|
||||
- `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs` — seed + warn + migrate runtime
|
||||
- `src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/` — 24 IEntityTypeConfiguration
|
||||
- `src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs` — interface Application layer
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/database/database-guide.md` — conventions + migration workflow chi tiết
|
||||
- `docs/database/schema-diagram.md` — ERD 24 bảng
|
||||
- `docs/gotchas.md` #7, #17 — migration pitfalls
|
||||
332
.claude/skills/iis-deploy-runbook/SKILL.md
Normal file
332
.claude/skills/iis-deploy-runbook/SKILL.md
Normal file
@ -0,0 +1,332 @@
|
||||
---
|
||||
name: iis-deploy-runbook
|
||||
description: Ops runbook cho SOLUTION_ERP deploy trên Windows Server IIS — 3 site (api/admin/user.huypham.vn), win-acme Let's Encrypt, NSSM gitea-runner shared với VIETREPORT, LibreOffice soffice headless. Dùng khi debug 500/502 prod, restart site, rotate cert, fix CI/CD runner, troubleshoot WebSocket, thêm site mới.
|
||||
when-to-use:
|
||||
- "prod 500 error"
|
||||
- "IIS site fail"
|
||||
- "cert hết hạn"
|
||||
- "win-acme"
|
||||
- "gitea runner"
|
||||
- "deploy IIS"
|
||||
- "restart app pool"
|
||||
- "webSocket 500"
|
||||
- "reverse proxy FE"
|
||||
- "LibreOffice prod"
|
||||
---
|
||||
|
||||
# IIS Deploy Runbook — SOLUTION_ERP
|
||||
|
||||
> **Context:** VPS Windows Server shared với VIETREPORT project. IIS + URL Rewrite + ARR + WebSockets module + win-acme. Deploy qua Gitea Actions self-hosted runner.
|
||||
|
||||
## Production topology
|
||||
|
||||
```
|
||||
Internet
|
||||
│ 443 (HTTPS)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ IIS (Windows Server VPS) │
|
||||
│ │
|
||||
│ ┌─ api.huypham.vn ─┐ ┌─ admin.huypham.vn ─┐ ┌─ user.huypham.vn ─┐
|
||||
│ │ SolutionErp-Api │ │ SolutionErp-Admin │ │ SolutionErp-User │
|
||||
│ │ → out-of-process │ │ (static SPA, URL │ │ (static SPA, URL │
|
||||
│ │ Kestrel :5443 │ │ Rewrite /api → 5443)│ │ Rewrite...) │
|
||||
│ │ ASP.NET Core 10 │ │ React build/ │ │ React build/ │
|
||||
│ │ │ │ │ │ │
|
||||
│ └────────────────────┘ └────────────────────┘ └────────────────────┘
|
||||
│ │
|
||||
│ Let's Encrypt (win-acme) — 3 cert auto-renew 60d │
|
||||
│ Shared gitea-runner NSSM service (with VIETREPORT) │
|
||||
│ LibreOffice 25.8.6 headless │
|
||||
│ SQL Server 2019 Express (\\.\SQLEXPRESS) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 3 IIS sites
|
||||
|
||||
| Site | Binding | Physical path | Apool | Purpose |
|
||||
|---|---|---|---|---|
|
||||
| `SolutionErp-Api` | `*:443:api.huypham.vn` HTTPS | `C:\inetpub\apps\SolutionErp\Api\` | out-of-process Kestrel | ASP.NET Core 10 API (port 5443 internal) |
|
||||
| `SolutionErp-Admin` | `*:443:admin.huypham.vn` HTTPS + `*:80` redirect | `C:\inetpub\apps\SolutionErp\Admin\` | static (no app pool .NET) | React build fe-admin |
|
||||
| `SolutionErp-User` | `*:443:user.huypham.vn` HTTPS + `*:80` redirect | `C:\inetpub\apps\SolutionErp\User\` | static | React build fe-user |
|
||||
|
||||
**SPA web.config:** 2 FE có `URL Rewrite` rule:
|
||||
1. HTTP → HTTPS redirect (bắt buộc, CORS whitelist chỉ https)
|
||||
2. `/api/* → http://localhost:5443/api/*` (ARR reverse proxy)
|
||||
3. `/hubs/* → http://localhost:5443/hubs/*` (SignalR)
|
||||
4. React Router fallback: `/*` → `/index.html`
|
||||
|
||||
## Quick commands
|
||||
|
||||
### Restart 1 site
|
||||
|
||||
```powershell
|
||||
# PowerShell as Admin
|
||||
Import-Module WebAdministration
|
||||
Stop-WebSite -Name "SolutionErp-Api"
|
||||
Start-WebSite -Name "SolutionErp-Api"
|
||||
|
||||
# Hoặc recycle app pool (API out-of-process):
|
||||
Restart-WebAppPool -Name "SolutionErp-Api"
|
||||
|
||||
# Check site status:
|
||||
Get-Website -Name "SolutionErp-*" | Format-Table Name, State, Bindings
|
||||
```
|
||||
|
||||
### Xem log API
|
||||
|
||||
```powershell
|
||||
# Serilog file rolling daily
|
||||
Get-Content "C:\inetpub\apps\SolutionErp\Api\Logs\log-$(Get-Date -Format 'yyyyMMdd').txt" -Tail 50
|
||||
|
||||
# IIS log
|
||||
Get-Content "C:\inetpub\logs\LogFiles\W3SVC<ID>\u_ex$(Get-Date -Format 'yyMMdd').log" -Tail 30
|
||||
|
||||
# Stdout log khi crash startup
|
||||
Get-Content "C:\inetpub\apps\SolutionErp\Api\Logs\stdout_*.log" -Tail 30
|
||||
```
|
||||
|
||||
### Health check
|
||||
|
||||
```powershell
|
||||
# Từ server
|
||||
curl http://localhost:5443/health/live
|
||||
curl http://localhost:5443/health/ready
|
||||
|
||||
# Từ ngoài
|
||||
curl https://api.huypham.vn/health/ready
|
||||
```
|
||||
|
||||
## Let's Encrypt cert — win-acme
|
||||
|
||||
### Check trạng thái
|
||||
|
||||
```powershell
|
||||
# Mở win-acme interactive
|
||||
& "C:\tools\win-acme\wacs.exe"
|
||||
# Menu > Manage renewals > list — xem 3 cert + next renew date
|
||||
|
||||
# Hoặc file:
|
||||
Get-Content "C:\ProgramData\win-acme\Production\$(hostname)\Renewals\*.renewal.json"
|
||||
```
|
||||
|
||||
### Cert hết hạn emergency
|
||||
|
||||
```powershell
|
||||
# Force renew 1 cert
|
||||
& "C:\tools\win-acme\wacs.exe" --renew --force --id {renewal-id}
|
||||
|
||||
# Full re-issue nếu renewal fail:
|
||||
& "C:\tools\win-acme\wacs.exe" # interactive → 'N' create new
|
||||
# Chọn: HTTP validation, web root = site physical path, auto install IIS
|
||||
```
|
||||
|
||||
**Gotcha:** Shared runner với VIETREPORT → win-acme HTTP challenge cần `.well-known/acme-challenge/` accessible qua HTTP (port 80). Rule HTTP→HTTPS redirect trong web.config PHẢI **exclude** path này:
|
||||
|
||||
```xml
|
||||
<rule name="Redirect to HTTPS" stopProcessing="true">
|
||||
<match url="(.*)" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" />
|
||||
<add input="{REQUEST_URI}" pattern="^/\.well-known/" negate="true" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" />
|
||||
</rule>
|
||||
```
|
||||
|
||||
## Gitea Actions runner (NSSM service)
|
||||
|
||||
### Status
|
||||
|
||||
```powershell
|
||||
# NSSM service name: gitea-runner (shared với VIETREPORT)
|
||||
Get-Service gitea-runner
|
||||
nssm status gitea-runner
|
||||
|
||||
# Restart
|
||||
Restart-Service gitea-runner
|
||||
|
||||
# Log
|
||||
Get-Content "C:\tools\gitea-runner\logs\act_runner.log" -Tail 50
|
||||
```
|
||||
|
||||
### Token rotate (nếu runner disconnected)
|
||||
|
||||
```powershell
|
||||
# Stop service
|
||||
Stop-Service gitea-runner
|
||||
|
||||
# Re-register qua Gitea admin UI → Actions → Runners → get new registration token
|
||||
& "C:\tools\gitea-runner\act_runner.exe" register `
|
||||
--instance https://git.baocaogiaoduc.vn `
|
||||
--token <new-token> `
|
||||
--no-interactive
|
||||
|
||||
# Start lại
|
||||
Start-Service gitea-runner
|
||||
```
|
||||
|
||||
## LibreOffice headless (PDF / docx converter)
|
||||
|
||||
### Check install
|
||||
|
||||
```powershell
|
||||
& "C:\Program Files\LibreOffice\program\soffice.exe" --version
|
||||
# → LibreOffice 25.8.6.x
|
||||
```
|
||||
|
||||
### Test convert manual
|
||||
|
||||
```powershell
|
||||
# Tạo temp dir isolated (mô phỏng per-request pattern của LibreOfficeDocumentConverter)
|
||||
$work = New-Item -ItemType Directory -Path "$env:TEMP\lo-test-$(Get-Random)"
|
||||
$userInst = "$work\userinst"
|
||||
|
||||
& "C:\Program Files\LibreOffice\program\soffice.exe" `
|
||||
--headless `
|
||||
"-env:UserInstallation=file:///$($userInst.Replace('\', '/'))" `
|
||||
--convert-to pdf `
|
||||
--outdir $work `
|
||||
"C:\path\to\test.docx"
|
||||
|
||||
# Output: $work\test.pdf
|
||||
ls $work
|
||||
Remove-Item -Recurse -Force $work
|
||||
```
|
||||
|
||||
### Prod fail patterns
|
||||
|
||||
- **60s timeout** → PDF lớn (>100 page) có thể quá. Xem `LibreOfficeDocumentConverter` — tăng timeout nếu cần
|
||||
- **Locked font fallback** → Be Vietnam Pro missing → text render hỏng. Install font trên server
|
||||
- **Concurrent request lock** → mỗi request 1 `UserInstallation` dir riêng → tránh lock
|
||||
|
||||
## Debug playbook — prod error
|
||||
|
||||
### HTTP 500 all site
|
||||
|
||||
Xem gotcha #25 (docs/gotchas.md):
|
||||
```powershell
|
||||
# Likely config lock:
|
||||
& "$env:SystemRoot\system32\inetsrv\appcmd.exe" list config -section:system.webServer/webSocket
|
||||
# → overrideMode="Deny" → fix:
|
||||
& "$env:SystemRoot\system32\inetsrv\appcmd.exe" unlock config -section:system.webServer/webSocket
|
||||
```
|
||||
|
||||
### HTTP 502 Bad Gateway (Admin/User → API)
|
||||
|
||||
```
|
||||
1. Check API up: curl http://localhost:5443/health/live
|
||||
- Down → restart API site + check stdout log
|
||||
2. Check ARR enabled: IIS Manager > server level > Application Request Routing
|
||||
- "Enable proxy" phải tick
|
||||
3. Check URL Rewrite rule fe web.config
|
||||
- action type="Rewrite" url="http://localhost:5443/{R:0}"
|
||||
```
|
||||
|
||||
### SignalR 401 (WebSocket connect fail)
|
||||
|
||||
Xem gotcha #26:
|
||||
```
|
||||
1. FE console: check ?access_token= query có trong negotiate URL không
|
||||
2. BE log: JwtBearer OnMessageReceived có fire cho /hubs/* không
|
||||
3. IIS WebSocket module: Install-WindowsFeature Web-WebSockets (đã có)
|
||||
4. Section unlock: appcmd unlock config -section:system.webServer/webSocket
|
||||
```
|
||||
|
||||
### Login "Network Error"
|
||||
|
||||
Xem `docs/gotchas.md` CORS + HTTPS redirect:
|
||||
```
|
||||
1. User gõ http://admin.huypham.vn → không redirect → CORS block
|
||||
2. Fix: SPA web.config PHẢI có HTTP→HTTPS rule (đã có)
|
||||
3. Test: curl -I http://admin.huypham.vn → expect 301 Location: https://...
|
||||
```
|
||||
|
||||
### DB connection fail
|
||||
|
||||
```powershell
|
||||
# 1. SQL service up?
|
||||
Get-Service MSSQL*
|
||||
|
||||
# 2. TCP enabled?
|
||||
Import-Module SqlServer
|
||||
# Hoặc check registry:
|
||||
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL*.SQLEXPRESS\MSSQLServer\SuperSocketNetLib\Tcp"
|
||||
|
||||
# 3. vrapp login OK?
|
||||
sqlcmd -S .\SQLEXPRESS -U vrapp -P <pw> -Q "SELECT DB_NAME()"
|
||||
# Expect: SolutionErp
|
||||
|
||||
# 4. appsettings connection string (qua Gitea secrets)
|
||||
# Check C:\inetpub\apps\SolutionErp\Api\appsettings.Production.json có ConnectionStrings:DefaultConnection
|
||||
```
|
||||
|
||||
## Deploy steps (CI/CD xanh)
|
||||
|
||||
Gitea Actions workflow: `.gitea/workflows/deploy.yml`. Flow:
|
||||
|
||||
```
|
||||
Push to main
|
||||
→ Runner pick up job
|
||||
→ checkout repo
|
||||
→ setup .NET 10 + Node 20
|
||||
→ npm ci (fe-admin + fe-user, rolldown native binding OK nếu fresh node_modules)
|
||||
→ dotnet restore + publish
|
||||
→ npm run build (fe-admin + fe-user)
|
||||
→ render appsettings.Production.json từ secrets (JWT_SECRET, DB_CONNECTION)
|
||||
→ stop app pool SolutionErp-Api
|
||||
→ xcopy publish → C:\inetpub\apps\SolutionErp\{Api,Admin,User}
|
||||
→ start app pool
|
||||
→ curl /health/ready → must be 200 trong 30s
|
||||
→ report status
|
||||
```
|
||||
|
||||
### Manual deploy (emergency)
|
||||
|
||||
```powershell
|
||||
# Local build
|
||||
dotnet publish src/Backend/SolutionErp.Api -c Release -o .\publish\api
|
||||
cd fe-admin; npm ci; npm run build; cd ..
|
||||
cd fe-user; npm ci; npm run build; cd ..
|
||||
|
||||
# Scp sang server (cần plink/pscp hoặc rsync)
|
||||
scp -r .\publish\api\* user@server:C:/inetpub/apps/SolutionErp/Api/
|
||||
scp -r .\fe-admin\dist\* user@server:C:/inetpub/apps/SolutionErp/Admin/
|
||||
scp -r .\fe-user\dist\* user@server:C:/inetpub/apps/SolutionErp/User/
|
||||
|
||||
# Trên server:
|
||||
Restart-WebAppPool -Name "SolutionErp-Api"
|
||||
curl http://localhost:5443/health/ready
|
||||
```
|
||||
|
||||
## Backup + recovery
|
||||
|
||||
```powershell
|
||||
# DB backup (script sẵn, chưa schedule):
|
||||
& "C:\inetpub\apps\SolutionErp\scripts\backup-sql.ps1"
|
||||
# Output: backup/SolutionErp_<ts>.bak (compressed + retention 30d)
|
||||
|
||||
# Schedule daily 03:00:
|
||||
schtasks /create /tn "SolutionErp Backup" `
|
||||
/tr "powershell -ExecutionPolicy Bypass -File C:\inetpub\apps\SolutionErp\scripts\backup-sql.ps1" `
|
||||
/sc DAILY /st 03:00 /ru SYSTEM
|
||||
```
|
||||
|
||||
Restore: xem `docs/guides/runbook.md`.
|
||||
|
||||
## Rotate credentials (Phase 5.1 backlog)
|
||||
|
||||
- [ ] SQL `sa` password (rotate)
|
||||
- [ ] SQL `vrapp` password (update Gitea secret `DB_CONNECTION` + appsettings.Production.json)
|
||||
- [ ] JWT secret (update Gitea secret `JWT_SECRET`, next deploy sẽ lan tỏa. Tất cả token cũ invalid)
|
||||
- [ ] Gitea runner registration token (re-register service)
|
||||
- [ ] Admin default `Admin@123456` (đổi qua `/system/users` admin UI ngay sau deploy)
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/guides/deployment-iis.md` — first-time setup
|
||||
- `docs/guides/runbook.md` — operations guide chi tiết
|
||||
- `docs/guides/cicd.md` — CI/CD pipeline
|
||||
- `docs/gotchas.md` — #25 webSocket lock, #26 SignalR, #28 LibreOffice 404, #29 PS 5.1 UTF-16
|
||||
- `scripts/deploy-iis.ps1` · `scripts/backup-sql.ps1` · `scripts/install-libreoffice.ps1`
|
||||
- `.gitea/workflows/deploy.yml` — CI/CD definition
|
||||
@ -75,7 +75,7 @@ Kiến trúc: **.NET 10 Clean Architecture + 2 React FE (admin + user) + SQL Ser
|
||||
| [`docs/database/schema-diagram.md`](docs/database/schema-diagram.md) | ⭐ ERD + luồng DB + data flow 19 table |
|
||||
| [`docs/flows/README.md`](docs/flows/README.md) | Index 6 flow (auth, permission, contract, form, SLA) |
|
||||
| [`docs/gotchas.md`](docs/gotchas.md) | ⭐ 26 bẫy đã gặp — đọc trước khi debug tương tự |
|
||||
| [`.claude/skills/`](.claude/skills/README.md) | 3 skill: contract-workflow, form-engine, permission-matrix |
|
||||
| [`.claude/skills/`](.claude/skills/README.md) | 6 skill: contract-workflow, form-engine, permission-matrix, dependency-audit-erp, ef-core-migration, iis-deploy-runbook |
|
||||
| [`docs/guides/vps-setup.md`](docs/guides/vps-setup.md) | ⭐ Master runbook deploy VPS shared với VIETREPORT |
|
||||
|
||||
## ⚠️ Kết thúc session
|
||||
|
||||
@ -74,7 +74,7 @@ SOLUTION_ERP/
|
||||
│ └── changelog/
|
||||
│ ├── migration-todos.md Roadmap 5 phase
|
||||
│ └── sessions/ YYYY-MM-DD session logs
|
||||
├── .claude/skills/ Skill library (contract-workflow, form-engine, ...)
|
||||
├── .claude/skills/ 6 skill: 3 domain (contract-workflow, form-engine, permission-matrix) + 3 ops (dependency-audit-erp, ef-core-migration, iis-deploy-runbook)
|
||||
├── scripts/ parse_forms.py, parse_workflow.py, seed, deploy
|
||||
├── SolutionErp.slnx Solution file
|
||||
├── global.json .NET 10 SDK pin
|
||||
|
||||
Reference in New Issue
Block a user