From 661f8595f852877bf8787a861e0a734bdb7a8565 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 22 Apr 2026 23:44:24 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20Skill:=20th=C3=AAm=203=20skill=20ops?= =?UTF-8?q?=20project-specific?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .claude/skills/README.md | 68 +++- .claude/skills/dependency-audit-erp/SKILL.md | 155 +++++++++ .claude/skills/ef-core-migration/SKILL.md | 205 ++++++++++++ .claude/skills/iis-deploy-runbook/SKILL.md | 332 +++++++++++++++++++ CLAUDE.md | 2 +- docs/CLAUDE.md | 2 +- 6 files changed, 746 insertions(+), 18 deletions(-) create mode 100644 .claude/skills/dependency-audit-erp/SKILL.md create mode 100644 .claude/skills/ef-core-migration/SKILL.md create mode 100644 .claude/skills/iis-deploy-runbook/SKILL.md diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 92c773c..e84397b 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -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.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//` 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 ` +4. Update `docs/CLAUDE.md` (dòng skill count) +5. Commit `[CLAUDE] Skill: add ` + +## 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] : ...` + - 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 diff --git a/.claude/skills/dependency-audit-erp/SKILL.md b/.claude/skills/dependency-audit-erp/SKILL.md new file mode 100644 index 0000000..858c826 --- /dev/null +++ b/.claude/skills/dependency-audit-erp/SKILL.md @@ -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 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 diff --git a/.claude/skills/ef-core-migration/SKILL.md b/.claude/skills/ef-core-migration/SKILL.md new file mode 100644 index 0000000..bf4ebfc --- /dev/null +++ b/.claude/skills/ef-core-migration/SKILL.md @@ -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 ` + --project src/Backend/SolutionErp.Infrastructure ` + --startup-project src/Backend/SolutionErp.Api ` + --output-dir Persistence/Migrations +``` + +**Naming convention:** PascalCase, verb prefix: +- `Add` — thêm entity / bảng +- `Update` — thay đổi schema +- `Remove` — xóa column / table +- `Rename` — đổ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'`. + +**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`: +```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 AuditLogs => Set(); +4. Application/Common/Interfaces/IApplicationDbContext.cs + - DbSet 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 --idempotent --output migrate.sql +# Review migrate.sql → chạy qua sqlcmd: +sqlcmd -S .\SQLEXPRESS -d SolutionErp -U vrapp -P -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 diff --git a/.claude/skills/iis-deploy-runbook/SKILL.md b/.claude/skills/iis-deploy-runbook/SKILL.md new file mode 100644 index 0000000..b206f1e --- /dev/null +++ b/.claude/skills/iis-deploy-runbook/SKILL.md @@ -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\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 + + + + + + + + +``` + +## 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 ` + --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 -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_.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 diff --git a/CLAUDE.md b/CLAUDE.md index ce5fb00..9b67df1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 2f1dcaa..fb1dba1 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -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