Files
ubi-dan-itam/CLAUDE.md
T

15 KiB
Raw Blame History

Asset & License Management — Dev Notes

Web app nội bộ quản lý tài sản IT + license cho Ubisoft VN local IT. Luồng chính: Request → Approval → Procurement → Receiving → Delivery → Tracking → Thu hồi/Retire.

Business context, data model, luồng trạng thái, case đặc biệt: xem ../processes/asset-license-management.md — đó là nguồn brainstorm chính, đừng duplicate nội dung đó ở đây, chỉ link tới.

Stack (đã chốt)

  • Next.js (TypeScript) full-stack
  • DB: Postgres (đã chốt 2026-07-29, xem lý do trong ../processes/asset-license-management.md)
  • DB access: Prisma (ORM + migrations, đã chốt 2026-07-30 — lý do trong ../processes/asset-license-management.md). Không viết raw SQL cho app logic nữa — ngoại lệ duy nhất là hạ tầng test (tests/setup.ts dùng raw pg Client để tạo/xoá throwaway database, đó là test infra chứ không phải data access layer).
  • Validation: Zod (đã chốt 2026-07-30, đi cùng quyết định Prisma)
  • UI/CSS: Tailwind CSS v4 + shadcn/ui (CLI @latest, hiện tại v4.16.0) — nâng cấp từ Tailwind v3 + [email protected] ngày 2026-07-30, xem lý do quyết định trong ../processes/asset-license-management.md. Không còn tailwind.config.ts — theme khai báo thẳng trong CSS qua @theme inline { ... } (src/app/globals.css), postcss.config.js chỉ còn plugin @tailwindcss/postcss (không cần autoprefixer riêng nữa). shadcn config: style new-york-v4, base color neutral, CSS variables bật, tailwind.config: "" (components.json) — đây là style/preset cổ điển (Radix), không phải 8 preset thương hiệu hoá mới của CLI hiện tại (nova/vega/maia/lyra/mira/luma/sera/rhea, chọn qua -p), vốn ép dùng theme "Nova/Vega..." + tuỳ chọn thư viện primitive khác (--base base = Base UI, --base aria = React Aria) — không dùng các preset đó cho project này. Component dùng radix-ui (package hợp nhất, không phải @radix-ui/react-slot riêng lẻ nữa) và tw-animate-css (thay tailwindcss-animate). globals.css còn @import "shadcn/tailwind.css" — 1 file CSS runtime do chính package shadcn (npm) publish kèm nhiều utility (data-state variants, scroll-fade, shimmer...), là dependency thật (không phải chỉ dev tool) vì cần resolve lúc build. Khi thêm component mới: npx shadcn@latest add <component> (đã test non-interactive OK với components.json này qua flag -y --overwrite).
  • Self-hosted (theo nguyên tắc chung của workspace — ưu tiên self-host hơn cloud, xem ../processes/new-office-fitout.md)

Coding conventions

Rút ra từ pattern thật đã dùng cho Request (xem src/lib/requests.ts, src/app/api/requests/route.ts, prisma/schema.prisma) — theo đúng pattern này cho các entity tiếp theo, đừng bịa pattern mới:

  • 1 file schema Prisma (prisma/schema.prisma) cho tất cả model — không tách nhiều file. Field trong model dùng camelCase (employeeId), map sang cột DB snake_case qua @map("employee_id"); tên bảng map qua @@map("requests") (snake_case, số nhiều).
  • 1 file src/lib/<entity>.ts mỗi entity, chứa cả 3 thứ trong cùng file: Zod schema + hàm validate trả về ValidationResult ({ok:true,value}|{ok:false,error}, không throw), các hàm CRUD gọi Prisma Client, và 1 hàm toRecord() map từ shape Prisma (camelCase) sang shape public API (snake_case — vd employee_id, created_at). Ranh giới cố ý: nội bộ TS/Prisma dùng camelCase, API trả ra ngoài dùng snake_case.
  • API route (src/app/api/<entity>/route.ts) giữ mỏng: parse JSON trong try/catch (400 nếu invalid JSON), check typeof body === "object" && body !== null (400 nếu không), gọi hàm validate của lib, gọi hàm CRUD, trả NextResponse.json(...) với status code đúng (400 validation lỗi, 201 tạo mới, 200 list). Không viết business logic trong route handler.
  • Prisma Client: dùng singleton ở src/lib/prisma.ts (global-guard chống hot-reload spawn nhiều instance) — import prisma từ đó, không tự new PrismaClient() ở nơi khác.
  • Enum có tập giá trị cố định gắn với domain (vd type: asset|license) → Prisma enum, không phải String + check tay.
  • Màu/theme Tailwind: từ khi lên Tailwind v4 (2026-07-30), theme khai báo thẳng trong src/app/globals.css qua @theme inline { --color-*: var(--*); ... } — không còn tailwind.config.ts. Biến CSS ở :root/.dark đã là giá trị oklch(...) đầy đủ, @theme inline chỉ alias thẳng qua var(--x), không bọc thêm hsl(...)/rgb(...) (bài học từ bug hsl(oklch(...)) hồi còn Tailwind v3 + tailwind.config.ts, đã fix rồi bỏ luôn file đó khi lên v4).
  • Test: 1 file tests/<entity>.test.ts mirror theo lib file, chạy trên Postgres thật qua DB tạm per-test-file (xem tests/setup.ts) — không mock DB. beforeEach dọn bằng prisma.<model>.deleteMany().
  • Schema mới/đổi schema → tạo migration Prisma thật (prisma migrate dev --name <mô_tả>), không sửa tay SQL.

Môi trường

Project này được dùng từ nhiều máy khác nhau (máy Windows công ty, VM Linux ở nhà qua Proxmox) — luôn kiểm tra host hiện tại trước khi giả định cách chạy npm, đừng áp cứng 1 cách cho mọi nơi.

  • Máy Windows công ty (C:\Users\thoangminh\Desktop\ubisoft\asset-license-mgmt): không có Node.js/npm cài native. Mọi npm install/npm test/tsc phải chạy qua Docker (node:20 container, xem cú pháp bên dưới). Docker Desktop đã confirm hoạt động được trên máy này (dùng để chạy Xibo containers, xem ../xibo/).
  • VM Linux ở nhà (Proxmox): nếu VM có Node.js cài native (kiểm tra bằng node -v/which node trước), chạy npm install/npm test/tsc trực tiếp, không cần wrap qua Docker — chỉ Postgres mới cần Docker (docker compose up -d db), vì đó là quyết định kiến trúc (Postgres qua container), không phải vì thiếu Node. Nếu VM đó cũng không có Node, dùng lại cách wrap Docker như máy Windows.
  • DB luôn là Postgres qua docker-compose.yml (service db, network asset-license-mgmt-net, image postgres:16-alpine) — cái này giống nhau ở mọi máy, không phụ thuộc host có Node native hay không. Trước khi npm test, phải start Postgres: docker compose up -d db (từ thư mục project).
    • Nếu phải wrap qua Docker (máy không có Node native): chạy npm trong container gắn cùng network để resolve được hostname db: docker run --rm --network asset-license-mgmt-net -v "<project dir>:/app" -w /app -e DATABASE_URL="postgres://asset_license_mgmt:change-me-local-dev@db:5432/asset_license_mgmt" node:20 sh -c "npm install && npm test"
    • Nếu Node native: set DATABASE_URL (xem .env/.env.example) rồi chạy thẳng npm install && npm test — Postgres vẫn phải resolve qua db nếu chạy trong Docker network, hoặc localhost/port map nếu compose có publish port (hiện docker-compose.yml không publish port ra host, cần thêm port mapping hoặc chạy npm trong cùng Docker network nếu muốn gọi từ ngoài container).
    • Test tự tạo/xoá một database Postgres tạm (test_<uuid>) riêng cho mỗi lần chạy file test (xem tests/setup.ts), không đụng vào database dev chính.
    • DATABASE_URL mẫu nằm ở .env.example — copy thành .env (đã gitignore) nếu cần chỉnh local, riêng cho từng máy.
  • npm install tự chạy prisma generate qua hook postinstall (xem package.json) — không cần gọi tay trừ khi thay đổi schema.prisma mà không chạy npm install lại.
  • pg vẫn còn trong devDependencies — chỉ dùng cho hạ tầng test (tests/setup.ts tạo/xoá throwaway DB), không phải data access layer của app (đó là Prisma). Đừng nhầm là app vẫn dùng raw SQL.

Lưu ý về việc gọi custom agent (feature-implementer/test-runner/code-reviewer)

Các agent định nghĩa ở .claude/agents/ trong thư mục này (asset-license-mgmt/) chỉ được Claude Code nhận diện làm subagent_type nếu phiên Claude Code được mở với working directory là chính thư mục này (hoặc thư mục con của nó) — Claude Code quét .claude/agents/ từ cwd đi lên tới repo root, không quét xuống thư mục con. Nếu mở Claude Code từ ubisoft/ (thư mục cha, gộp nhiều project), 3 agent này sẽ báo "not found". Cách đúng: cd vào asset-license-mgmt/ rồi mới chạy claude khi làm việc trên project này.

Multi-agent dev workflow

Mục tiêu: không để một agent vừa viết code vừa tự chấm bài mình — tách vai trò để bug bị bắt lại thay vì được tự "cho qua".

Với mỗi task cụ thể (vd "thêm API tạo Request", "màn hình bàn giao asset"):

  1. feature-implementer — nhận đúng 1 task, viết code + test đi kèm trong cùng lượt. Không tự tuyên bố "đã pass" — chỉ báo cáo đã đổi gì, giả định gì, và cái gì cố tình để ngoài scope.
  2. test-runner — spawn riêng, không phải cùng agent vừa code. Chạy toàn bộ test suite + lint + type-check, báo lỗi cụ thể kèm file:line. Không tự sửa code.
  3. Nếu fail: đưa report của test-runner làm input cho một lượt feature-implementer mới để fix. Lặp lại bước 1–2 tới khi xanh hết.
  4. code-reviewer (optional, dùng cho thay đổi lớn/rủi ro — vd đổi state machine của Request, logic seat-count license) — review diff cuối cùng trước khi commit, tìm bug logic/security, không tự sửa.

Quy tắc cứng

  • Implementer và test-runner luôn là 2 lượt spawn riêng biệt cho cùng 1 task — không gộp.
  • test-runner không được tự sửa file, kể cả khi lỗi trông "hiển nhiên".
  • Nếu implementer thấy task mơ hồ, chọn giả định nhỏ nhất hợp lý và ghi rõ trong report thay vì dừng lại hỏi — nhưng nếu ảnh hưởng luồng trạng thái Request/quyền approval thì phải hỏi trước khi code.

Trạng thái hiện tại

  • Task 1 xong: đã bootstrap Next.js (TS, App Router) project thật, entity Request (chỉ state Draft) với POST/GET /api/requests.
  • Migrate SQLite → Postgres xong: CONFIRMED pass qua test-runner độc lập.
  • ESLint setup xong: CONFIRMED pass sạch qua test-runner độc lập.
  • Retrofit Request sang Prisma + Zod xong (2026-07-30): prisma/schema.prisma (model Request, enum RequestType), src/lib/prisma.ts (singleton client), src/lib/requests.ts (Zod schema thay hàm validate tay, Prisma Client thay raw SQL), scripts/migrate.ts (gọi prisma migrate deploy), tests/setup.ts/tests/requests.test.ts (schema áp qua prisma migrate deploy, dọn dữ liệu qua prisma.request.deleteMany()). src/lib/db.ts (raw pg Pool cũ) đã xoá. CONFIRMED qua test-runner độc lập: 6/6 test pass trên Postgres thật, type-check sạch, lint sạch. Migration SQL đối chiếu khớp 1:1 với schema cũ (chỉ khác type từ TEXT+CHECK sang Postgres enum — tương đương semantics).
  • npm audit: 1 "critical" nằm ở vitest (devDependency, CVE cần vitest --ui đang chạy — project không dùng UI mode nên không khai thác được trong cách dùng hiện tại). Rủi ro thực tế thấp, chưa cần fix gấp — xem "Việc còn dở".
  • Tailwind + shadcn/ui setup xong (2026-07-30, ban đầu trên v3): postcss.config.js, src/app/globals.css, components.json, src/lib/utils.ts (cn()), src/components/ui/button.tsx (component mẫu để xác nhận pipeline chạy được). Có 1 bug phát hiện qua verify độc lập (màu token bị bọc hsl() chồng lên giá trị oklch() sẵn có → CSS không hợp lệ, silent fail lúc render dù build/lint/tsc đều xanh) — đã fix và CONFIRMED qua verify độc lập lần 2.
  • Nâng cấp lên Tailwind v4 + shadcn CLI @latest xong (2026-07-30): bỏ tailwind.config.ts (theme giờ khai trong globals.css qua @theme inline), đổi postcss.config.js sang @tailwindcss/postcss, đổi dependency (radix-ui thay @radix-ui/react-slot, tw-animate-css thay tailwindcss-animate, thêm shadcn làm runtime dependency vì globals.css import shadcn/tailwind.css), button.tsx viết lại theo registry new-york-v4 hiện tại. CONFIRMED qua test-runner độc lập: install/build/tsc/lint sạch, 6/6 test Request vẫn pass (không liên quan tới UI), CSS đã compile xác nhận màu sạch (không còn kiểu bug hsl(oklch(...))), không còn gói v3 sót lại.
    • Rủi ro tồn đọng (dormant, chưa gãy): button.tsx mới bỏ React.forwardRef (convention của registry hiện tại là cho React 19), nhưng project đang pin React 18.3.1 — nếu sau này có chỗ truyền ref vào <Button>, ref sẽ không được forward đúng cách. Hiện tại không chỗ nào trong code truyền ref nên chưa phát sinh lỗi — nhưng cần nhớ khi build UI thật, tránh <Button ref={...}> hoặc phải tự thêm lại forwardRef nếu cần.

Việc còn dở

  • Bump vitest lên 4.x (fix advisory critical GHSA-5xrq-8626-4rwp — hiện không khai thác được vì project không chạy vitest --ui, nhưng nên dọn khi có breaking-change window) — không khẩn cấp.
  • Rà lại advisory riêng của next (prod dependency, có vài high/moderate — vd SSRF qua WebSocket upgrade, middleware bypass) khi có dịp, tách biệt khỏi đợt retrofit Prisma/Zod này.
  • Test coverage gaps (chưa block gì, chỉ là follow-up): chưa test malformed JSON body, chưa test JSON body không phải object (array/string).
  • Chưa có git repo cho project này — chưa có version history/rollback cho bất kỳ thay đổi nào ở trên. Nên git init sớm trước khi làm thêm entity mới.
  • Button (shadcn) không forwardRef, React đang pin 18 — dormant risk, xem note ở "Trạng thái hiện tại".
  • Các entity còn lại: Employee, AssetItem, License, PurchaseOrder, Receipt, Assignment
  • Các state tiếp theo của Request: Pending Approval → Approved → Ordered → Received → Delivered → Closed, và nhánh Rejected/Cancelled
  • UI (hiện chỉ có placeholder home page)
  • Auth/phân quyền (ai được approve request, ai được xác nhận nhận hàng/bàn giao)

Cập nhật lần cuối: 2026-07-30