Files
ubi-dan-itam/CLAUDE.md
T
toanhoangandClaude Sonnet 5 f325abf22c docs: resolve Assignment open questions, unblock Lane C
Stakeholder decided: createAssignment() blocks if AssetItem.status
isn't in_stock, and update routes use PATCH /api/<entity>/[id]
(applies to both Assignment and PurchaseOrder).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-02 19:22:43 -04:00

36 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 docs/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 docs/asset-license-management.md)
  • DB access: Prisma (ORM + migrations, đã chốt 2026-07-30 — lý do trong docs/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 docs/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.

Git / commit convention

  • Message theo Conventional Commits: feat:, fix:, docs:, chore:, test:, refactor: + mô tả ngắn gọn ở thì hiện tại (vd feat: add Employee Prisma model + migration).
  • Chia nhỏ commit theo lớp kỹ thuật khi build 1 entity/feature mới — không gộp hết vào 1 commit lớn khó review. Thứ tự chuẩn cho 1 entity mới:
    1. feat: add <Entity> Prisma model + migration
    2. feat: add <Entity> validation + CRUD (src/lib/<entity>.ts)
    3. feat: add <Entity> API routes
    4. test: add <Entity> API tests
    • Nếu 1 lớp cần sửa lại (bug do lớp sau phát hiện), thêm commit fix: riêng, không gộp ngược vào commit cũ (đã push thì không amend).
  • Vẫn chạy xuyên suốt cả entity trong 1 lượt (feature-implementer → test-runner) rồi mới hỏi/báo cáo — không dừng lại xin review sau từng commit nhỏ. User sẽ tự xem lại qua git log/diff từng commit sau khi xong.
  • Lưu ý PowerShell trên máy này: đừng để dấu " (ngoặc kép) lồng bên trong message nhiều dòng khi truyền qua git commit -m — dễ bị PowerShell tách nhầm argument, khiến git báo lỗi pathspec (đã gặp thật, xem cách né ở lịch sử commit). Cách an toàn: gán message vào biến trước ($msg = @'...'@; git commit -m $msg), tránh dấu " trong nội dung, hoặc dùng git commit -F <file> nếu bắt buộc phải có ".

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 (ba/pm/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), các 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: stakeholder (user) chỉ cần đưa ra mục tiêu/ưu tiên ở mức nghiệp vụ — không cần tự chia nhỏ task hay chọn giữa các phương án kỹ thuật. Đồng thời 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 mục tiêu nghiệp vụ stakeholder đưa ra (vd "thêm tracking seat cho License", "cho phép reject Request"):

  1. ba — chuyển yêu cầu ở mức business thành spec cụ thể (field, rule, edge case, acceptance criteria), bám theo docs/asset-license-management.md, và note lại spec/quyết định đã chốt vào chính doc đó. Nếu có quyết định nghiệp vụ chưa rõ (ai được approve, xử lý Assignment khi License bị retire...) → dừng lại hỏi thẳng stakeholder, không tự đoán. Bỏ qua bước này nếu yêu cầu đã đủ nhỏ/rõ (vd 1 bug fix cụ thể).
  2. pm — nhận spec từ ba (hoặc thẳng backlog đã rõ), chia thành danh sách task cụ thể theo đúng thứ tự lớp kỹ thuật chuẩn (model+migration → lib → API → test), sắp theo phụ thuộc, map mỗi task tới đúng 1 commit theo convention ở "Git / commit convention" — tách càng cụ thể càng tốt. Nếu spec thiếu chi tiết kỹ thuật mà việc tự quyết có thể đổi hành vi nghiệp vụ, pm hỏi ngược lại ba để bổ sung spec trước khi chốt task, thay vì tự đoán rồi chia task luôn. Không tự code, không tự quyết nghiệp vụ.
  3. feature-implementer — nhận đúng 1 task từ pm, 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.
  4. 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.
  5. 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 2–3 tới khi xanh hết.
  6. 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

  • ba/pm luôn chạy trước khi có task cụ thể cho feature-implementer, trừ khi task đã đủ nhỏ/rõ ràng (bug fix cụ thể, không phải feature mới) — trường hợp đó có thể giao thẳng cho feature-implementer.
  • 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.
  • ba/pm không tự quyết nghiệp vụ mơ hồ — luôn đẩy ngược lại stakeholder thay vì đoán.
  • pm gặp gap kỹ thuật ảnh hưởng nghiệp vụ (field/rule/quan hệ chưa nêu trong spec) → hỏi ngược lại ba (không tự bịa), ba trả lời bằng cách tra docs/asset-license-management.md hoặc escalate tiếp lên stakeholder nếu doc cũng chưa có câu trả lời.

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.
  • Git repo khởi tạo + push lên GitHub xong (2026-07-30): https://github.com/toandev96/ubi-dan-itam.git (private), remote origin/branch master. Business context (docs/asset-license-management.md) đã move từ ubisoft/processes/ vào trong repo này cùng ngày, để travel theo code khi clone ở máy khác (vd VM Linux ở nhà qua Proxmox) — không còn phụ thuộc thư mục processes/ ở workspace ngoài repo nữa cho riêng phần business context của app này.

  • Auth (login nội bộ, không phải SSO công ty) xong (2026-07-31): User Prisma model (prisma/schema.prisma), src/lib/users.ts (Zod + createUser() + verifyUserCredentials(), bcryptjs hash), scripts/create-user.ts (tạo account bằng CLI, không có self-registration), Auth.js v5 (next-auth@^5.0.0-beta.32, Credentials provider, JWT session, không dùng @auth/prisma-adapter vì không cần OAuth/DB session) ở src/auth.ts + src/app/api/auth/[...nextauth]/route.ts, trang login src/app/login/page.tsx. Permission model flat (bất kỳ User nào login cũng làm mọi thứ được — quyết định đã chốt, không có role). CONFIRMED qua verify độc lập (test-runner + security review): 16/16 test pass (6 cũ + 10 mới) trên Postgres thật, tsc/lint/build sạch, không lộ password, bcrypt compare đúng chuẩn constant-time, AUTH_SECRET fail-closed thật khi thiếu (đã test thực tế, không fallback giá trị đoán được).

    • Finding nhỏ (chưa block, cần fix sau): timing side-channel trong verifyUserCredentials — trả về sớm khi email không tồn tại (bỏ qua bcrypt compare), nên request với email sai vs password sai có thời gian phản hồi khác nhau chút, có thể dò được email nào tồn tại. Fix: luôn chạy bcrypt.compare() kể cả khi không tìm thấy user (so với 1 hash giả cố định).
    • Finding nhỏ (chấp nhận được, cần track): scripts/create-user.ts --password <giá trị> lộ password vào shell history/process list — chấp nhận vì chỉ IT admin tự chạy tay, không phải self-registration công khai, nhưng nên cải thiện sau (đọc từ prompt/stdin thay vì argv).
    • Note triển khai: chạy next start (production) cần thêm AUTH_TRUST_HOST=true hoặc AUTH_URL trong env, nếu không sẽ gặp lỗi UntrustedHost — đã thêm vào .env.example/.env khi deploy thật (xem bullet deploy bên dưới).
  • Route-gating xong (2026-07-31): src/middleware.ts (protect mọi page route theo default-deny, redirect /login nếu chưa đăng nhập, trừ /login chính nó — tránh vòng lặp redirect), src/auth.config.ts (config Auth.js edge-safe, tách ra khỏi src/auth.ts — không có Credentials provider vì authorize() gọi verifyUserCredentials() kéo theo bcryptjs, là Node API không chạy được trên Edge Runtime mà middleware.ts dùng; src/middleware.ts tự tạo instance NextAuth(authConfig) riêng, nhẹ, chỉ để decode JWT session cookie — xác nhận qua build thật: trước khi tách, next build báo warning bcryptjs/jose bundle vào Edge Runtime (middleware 115kB); sau khi tách, build sạch, middleware còn 78.9kB). src/auth.ts giờ spread authConfig + thêm Credentials provider thật, dùng cho API route/Server Component (chạy Node runtime).

    • config.matcher của middleware loại trừ toàn bộ /api (không chỉ /api/auth) — API route tự check auth() lấy session ngay trong handler (không dựa hoàn toàn vào middleware, theo đúng khuyến nghị chính thức của Auth.js: verify session càng gần chỗ fetch data càng tốt). /api/requests (GET/POST) đã thêm check auth(), trả 401 {"error":"Unauthorized"} nếu chưa login — xem src/app/api/requests/route.ts.
    • / (home page placeholder) có yêu cầu login — chọn theo nguyên tắc "mặc định chặn hết, allowlist rõ ràng cho cái không cần" thay vì opt-in dễ quên route mới.
    • Test: tests/requests.test.ts mock @/auth (vi.mock) thay vì gọi auth() thật, vì auth() không tham số đọc cookie qua next/headers — chỉ hoạt động khi chạy trong request-handling thật của Next.js, không phải khi test import gọi thẳng GET()/POST() (bypass hoàn toàn Next). Test mặc định mock session hợp lệ (beforeEach) để không phá test nghiệp vụ cũ, có riêng 2 test set mock về null để xác nhận 401. middleware.ts bản thân không có test tự động (chạy Edge Runtime, không test được qua vitest theo cách hiện tại) — đã verify tay qua next build + next start thật (curl xác nhận: GET / → 307 → /login, GET /login → 200 không loop, GET/POST /api/requests chưa login → 401 JSON, GET /api/auth/session → 200 không bị chặn). CONFIRMED: npx tsc --noEmit, npm run lint, npm test (18/18, gồm 2 test 401 mới), npm run build đều sạch.
  • Employee entity xong + Request.employee_id retrofit sang FK thật (2026-07-31): Employee Prisma model (prisma/schema.prisma — name, department (string đơn giản, chưa tách entity riêng), managerId self-relation nullable), src/lib/employees.ts (Zod + CRUD, theo đúng pattern chuẩn ở "Coding conventions"), src/app/api/employees/route.ts (POST/GET, gate auth() như /api/requests), tests/employees.test.ts (11 test, Postgres thật). Request.employeeId — trước đây là String tự do, không có ràng buộc — giờ là FK thật tới Employee (@relation, migration 20260731075828_relate_request_to_employee). Đây là thay đổi hành vi có chủ ý tới API Request đã ship trước đó: employee_id trong body POST /api/requests giờ bắt buộc là UUID hợp lệ (Zod .uuid(), không còn nhận string tuỳ ý như "emp-1"), và phải trỏ tới một Employee có thật — nếu không, createRequest() bắt lỗi FK violation của Prisma (P2003) và trả 400 {"error": "employee_id does not reference an existing employee"} thay vì lỗi 500 thô. tests/requests.test.ts đã cập nhật: mọi test tạo Request giờ tạo Employee thật trước qua helper createTestEmployee() rồi dùng .id thật, có thêm 2 test mới cho đúng 2 case bị FK chặn (UUID sai format, UUID đúng format nhưng không tồn tại).

    • Bug hạ tầng test phát hiện + fix trong lượt này: tests/setup.ts tạo DB Postgres tạm riêng cho mỗi file test bằng cách ghi đè process.env.DATABASE_URL trong beforeAll — cách này không an toàn khi Vitest chạy nhiều file test song song (mặc định): timing giữa beforeAll/query của 2 file có thể chồng nhau khiến cả 2 cùng thao tác nhầm lên 1 DB tạm. Trước đây không lộ ra vì chưa có 2 file test nào cùng đụng bảng có FK với nhau; lộ ra ngay khi Request có FK thật tới Employee (tests/employees.test.ts và tests/requests.test.ts xoá/đọc nhầm dữ liệu của nhau, lặp lại 100% mỗi lần chạy). Fix: vitest.config.ts thêm fileParallelism: false (chạy tuần tự từng file) — CONFIRMED: 31/31 test pass ổn định qua npm test (11 employees + 10 requests + 10 auth), npx tsc --noEmit/npm run lint/npm run build đều sạch.
  • AssetItem entity xong (2026-07-31): AssetItem Prisma model + enum AssetItemStatus (in_stock/assigned/repair/retired, default in_stock) — serialNumber (unique), model, purchaseDate, warrantyEndDate (nullable — không phải asset nào cũng biết hạn bảo hành lúc nhập), migration 20260731090347_add_asset_item. src/lib/assets.ts (Zod + CRUD, theo đúng pattern chuẩn ở "Coding conventions", bắt lỗi Prisma P2002 khi serialNumber trùng và trả 400 {"error": "serialNumber already exists"} thay vì lỗi 500 thô), src/app/api/assets/route.ts (POST/GET, gate auth() như /api/employees), tests/assets.test.ts (11 test, Postgres thật). Asset mới luôn tạo ở status in_stock (không nhận status từ client lúc tạo, như Request luôn tạo Draft) — các state khác (assigned/repair/retired) sẽ set qua thao tác sau, chưa có endpoint riêng. Chưa có quan hệ tới Vendor/Assignment (2 entity đó chưa build). CONFIRMED qua test-runner độc lập: 42/42 test pass (11 assets + 11 employees + 10 requests + 10 auth) trên Postgres thật, npx tsc --noEmit/npm run lint/npm run build đều sạch.

  • Deploy lên homelab xong (2026-08-02): Dockerfile (single-stage node:20-alpine, npm ci + npm run build + npm start) và service app mới trong docker-compose.yml (port 3002:3000, cùng network asset-license-mgmt với db để resolve hostname db). Gotcha đã gặp + fix: node:20-alpine không có libssl sẵn → Prisma không detect được OpenSSL lúc npm ci (postinstall chạy prisma generate), tự chọn nhầm engine binary (openssl-1.1.x) — build vẫn xanh (route API là dynamic, không chạm Prisma lúc build) nhưng crash lúc runtime khi query thật. Fix: RUN apk add --no-cache openssl trong Dockerfile trước npm ci. Đã verify bằng cách gọi prisma.employee.count() thật bên trong container sau khi rebuild — pass. Expose ra internet qua ubi.toanhoang.work theo đúng flow add-service skill ở /opt/homelab/CLAUDE.md: subdomain qua cloudflare-ddns-ubi, NPM proxy host (10.8.2.3:3002, SSL forced) + Let's Encrypt, Unbound Host Override cho LAN. AUTH_TRUST_HOST=true thêm vào .env/.env.example (đóng TODO cũ). Chưa có user nào trong DB — chạy docker compose exec app npm run user:create để tạo account đầu tiên trước khi ai có thể login thật.

  • License entity xong (2026-08-02): License Prisma model + enum LicenseType (perpetual/subscription) — name, vendor (string đơn giản, chưa tách entity Vendor riêng, theo đúng precedent Employee.department), seatsTotal (bắt buộc > 0), seatsUsed (default 0, client không set được lúc tạo — giống AssetItem.status luôn tạo in_stock), expiryDate (nullable — perpetual không hết hạn), migration 20260802123955_add_license. src/lib/licenses.ts (Zod + CRUD), src/app/api/licenses/route.ts (POST/GET, gate auth() như các entity khác), tests/licenses.test.ts (27 test, cả lib-level lẫn route-level, có thêm test malformed JSON/non-object body — đóng luôn 1 phần gap coverage cũ, nhưng chỉ cho licenses, chưa retrofit lại cho assets/employees/requests). CONFIRMED qua test-runner độc lập: 69/69 test pass, tsc/lint sạch. Review qua code-reviewer (trigger: seat-count logic): không có bug chặn, nhưng có finding cần nhớ — createLicense() trả Promise<LicenseRecord> trần (không theo pattern {ok,value}|{ok,error} như createAssetItem()); vô hại bây giờ vì License chưa có unique constraint nào, nhưng nếu sau này thêm @@unique (vd trên name+vendor) mà không đồng thời thêm xử lý lỗi P2002 ở createLicense(), request trùng sẽ trả 500 thô thay vì 400 sạch như các entity khác. Chưa fix vì hiện tại xử lý lỗi cho constraint không tồn tại là dư thừa — cần nhớ làm cùng lúc nếu/khi thêm unique constraint.

Việc còn dở

  • fileParallelism: false (fix race condition test-infra) là fix đúng nhưng "cùn" — chặn song song toàn bộ suite thay vì chỉ fix gốc rễ (tests/setup.ts ghi đè process.env.DATABASE_URL toàn cục thay vì truyền connection string riêng cho từng Prisma Client instance). Test suite sẽ chạy chậm dần tuyến tính khi thêm entity mới (AssetItem, License...). Cân nhắc refactor tests/setup.ts để mỗi file tự có Prisma Client riêng trỏ đúng DB tạm của mình, rồi bật lại parallelism — chưa gấp ở quy mô hiện tại (3 file, ~15s).
  • Fix timing side-channel ở verifyUserCredentials (xem note ở "Trạng thái hiện tại").
  • Cải thiện scripts/create-user.ts để password không lộ qua CLI arg (đọc từ stdin/prompt thay vì --password).
  • 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).
  • Button (shadcn) không forwardRef, React đang pin 18 — dormant risk, xem note ở "Trạng thái hiện tại".
  • Vendor/PurchaseOrder(gộp Receipt)/Assignment/AssetCategoryAttributeDef (2026-08-02): spec đầy đủ ở docs/asset-license-management.md, đã có task list phụ thuộc-đúng do pm-pass sequencing (2026-08-02), sẵn sàng giao feature-implementer theo đúng thứ tự dưới đây (3 lane độc lập, có thể xen kẽ):
    • Lane A (Vendor → License.vendorId → PurchaseOrder → purchaseOrderId x3): (1-4) Vendor entity chuẩn 4 commit (model+migration/lib/API/test), không phụ thuộc gì. (5) Retrofit License.vendor (string) → License.vendorId (FK) — 1 commit gộp (schema+migration+lib+test), theo đúng tiền lệ retrofit Request.employeeId (commit 49d96b7, không tách lớp như entity mới vì tách ra sẽ để lại state nửa vời không chạy được) — tiện thể fix luôn finding cũ của code-reviewer: createLicense() chưa theo pattern {ok,value}|{ok,error}, giờ bắt buộc phải thêm vì vendorId là FK mới (cần bắt P2003). Phụ thuộc (1). (6-9) PurchaseOrder entity chuẩn 4 commit (vendorId FK, receivedBy FK→Employee, Zod .refine() cho ràng buộc receivedAt/receivedBy cùng null hoặc cùng có giá trị, bắt P2003 cho cả 2 FK, theo đúng pattern {ok,error} ngay từ đầu) — phụ thuộc (1) Vendor + Employee (đã có). (10) AssetItem.purchaseOrderId — 1 commit gộp (nullable FK). (11) License.purchaseOrderId — 1 commit gộp riêng với task (5) dù cùng file, vì trigger phụ thuộc khác nhau (Vendor vs PurchaseOrder) — gộp chung sẽ vi phạm nguyên tắc 1-commit-1-thay-đổi-nghiệp-vụ. (12) Request.purchaseOrderId — 1 commit gộp. (10)(11)(12) đều chỉ phụ thuộc (6), độc lập lẫn nhau, thứ tự giữa 3 task này không quan trọng.
    • Lane B (AssetCategory → AssetCategoryAttributeDef + AssetModel → wire dynamic validation → AssetItem.modelId): gap phát hiện khi sequencing — AssetCategory/AssetModel/ConsumableItem đã có spec từ round BA trước (2026-08-02, xem "Phân loại tài sản" trong doc) nhưng CHƯA build (không có trong prisma/schema.prisma hiện tại, AssetItem.model vẫn là string tự do) — phải làm trước thì AssetCategoryAttributeDef mới có ý nghĩa. (13-16) AssetCategory entity chuẩn 4 commit (name, valueGroup enum high_value/low_value), không phụ thuộc gì. (17-20) AssetCategoryAttributeDef entity chuẩn 4 commit (categoryId FK, key, label, dataType enum, required, sortOrder, unique (categoryId,key) → bắt P2002) — phụ thuộc (13), độc lập với AssetModel. (21-24) AssetModel entity chuẩn 4 commit (categoryId FK + business rule "category phải valueGroup=high_value" — cần lookup DB, không phải Zod thuần; manufacturer, name, specs JSONB — ở task này specs chỉ validate lỏng là object hợp lệ, CHƯA khớp động theo AssetCategoryAttributeDef) — phụ thuộc (13), độc lập với (17-20). (25) Wire dynamic specs validation vào AssetModel create/update — 1 commit riêng, không gộp vào (22), vì đây là logic mới/rủi ro (build Zod schema runtime từ AssetCategoryAttributeDef, map dataType→z.string()/number()/boolean()/coerce.date(), từ chối key lạ, bắt buộc key required) — phụ thuộc (18) và (22). (26) Retrofit AssetItem.model (string) → AssetItem.modelId (FK) — 1 commit gộp, phụ thuộc (21) (không cần đợi (25)).
    • Lane C (Assignment, độc lập hoàn toàn với Lane A/B — chỉ cần Employee+AssetItem+License đã có sẵn): (27) model+migration (employeeId FK, assetItemId/licenseId nullable FK, assignedAt, returnedAt nullable, signedAt nullable). (28) lib validation+CRUD — rủi ro nhất trong cả 4 entity: Zod .refine() XOR assetItemId/licenseId, chặn >1 Assignment active/AssetItem, transaction tăng/giảm License.seatsUsed kèm chặn overbook (seatsUsed >= seatsTotal), đồng bộ AssetItem.status (assigned/in_stock), helper transfer (đóng cũ+mở mới cùng 1 transaction). (29) API routes — cần quyết định shape mới: đây là entity đầu tiên cần thao tác "update" (return/transfer) trong khi codebase hiện tại mọi entity đều chỉ có POST(create)+GET(list), chưa có route update nào — đề xuất mặc định POST /api/assignments (assign/transfer) + PATCH /api/assignments/[id] (return, nhận {returnedAt}}), cần xác nhận lại khi giao task cho pm/feature-implementer vì không có tiền lệ. (30) tests. (31) Bắt buộc code-reviewer pass riêng cho Assignment (diff 27-30) trước khi coi là xong — đúng rule ở "Multi-agent dev workflow" (state-mutation-với-invariant, seat-count) — không phải 1 commit, là 1 gate.
    • 2 câu hỏi mở đã được stakeholder trả lời (2026-08-02, xem "Assignment" trong docs/asset-license-management.md) — Lane C hết bị chặn: (a) createAssignment() chặn nếu AssetItem.status khác in_stock (400). (b) Route update dùng PATCH /api/<entity>/[id] (áp dụng cho cả Assignment và PurchaseOrder), không dùng action-based sub-route.
    • ConsumableItem (phần catalog, KHÔNG phải luồng "xin+nhận" — luồng đó vẫn BỊ CHẶN chờ 7 câu hỏi mở, xem doc): độc lập với toàn bộ 3 lane trên (chỉ cần AssetCategory), sẵn sàng cho pm chia task riêng nếu stakeholder muốn gộp vào cùng đợt — không đưa vào sequencing này vì nằm ngoài phạm vi 4 việc được giao lần này.
  • 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-08-02 (deploy lên homelab ubi.toanhoang.work, License entity, task sequencing cho Vendor/PurchaseOrder/Assignment/AssetCategoryAttributeDef)