docs: consolidate CLAUDE.md status/backlog, add Assignment review findings

Condensed the "Trạng thái hiện tại"/"Việc còn dở" sections — most of the
detailed per-entity narrative was either resolved, superseded by a later
change, or derivable from git log, so it's trimmed to what isn't:
decisions, gotchas, and still-open risks. Added the UI (worktree-ui-basic
merge), the Assignment transfer fix, and the two dormant findings
(XOR defense-in-depth, Assignment FK onDelete) from the code-reviewer gate.
This commit is contained in:
toanhoang
2026-08-03 20:27:52 -04:00
parent d6cea577c9
commit e1ad9da72d
+23 -39
View File
@@ -80,48 +80,32 @@ Với mỗi mục tiêu nghiệp vụ stakeholder đưa ra (vd "thêm tracking s
## 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.
Backend CRUD xong cho toàn bộ entity chốt tới nay: `Request` (chỉ state `Draft`), `User`+Auth, `Employee`, `AssetItem`, `License`, `Vendor`, `PurchaseOrder`, `AssetCategory`, `AssetCategoryAttributeDef`, `AssetModel`, `Assignment`. Chi tiết field/migration từng entity xem `prisma/schema.prisma` + git log — không lặp lại ở đây, chỉ ghi những thứ **không tự derive được từ code** (quyết định, gotcha, rủi ro còn treo).
- **`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.
- Nền tảng (2026-07-30/31): bootstrap Next.js + `Request` (Draft), migrate SQLite→Postgres, ESLint, Tailwind v4 + shadcn `@latest`, Auth.js v5 (Credentials + bcryptjs, flat permission — không role), route-gating qua `src/middleware.ts` (default-deny; `src/auth.config.ts` tách riêng vì Credentials provider dùng bcryptjs không chạy được Edge Runtime; API route tự check `auth()` vì matcher loại trừ `/api`). Deploy lên homelab (2026-08-02) tại `ubi.toanhoang.work` — gotcha `apk add openssl` đã ghi ở `/opt/homelab/CLAUDE.md`, không lặp lại ở đây.
- `Request.employeeId` retrofit string→FK (2026-07-31): thay đổi hành vi API có chủ ý — `employee_id` giờ bắt buộc UUID hợp lệ + phải trỏ Employee có thật, trả `400` sạch thay vì 500 nếu không (P2003).
- `License.vendor` retrofit string→`vendorId` FK (2026-08-02), cùng lúc `PurchaseOrder` (gộp `Receipt`) ra đời với `purchaseOrderId` (nullable FK) ở cả `AssetItem`/`License`/`Request` (3 chiều traceability khác nhau — xem `docs/asset-license-management.md`).
- Phân cấp tài sản (2026-08-03): `AssetCategory` (`valueGroup` high/low_value) → `AssetCategoryAttributeDef` (thuộc tính động theo category, unique `(categoryId,key)`) → `AssetModel` (`specs` JSONB, validate động qua Zod build runtime từ AttributeDef). `AssetItem.model` (string) retrofit sang `AssetItem.modelId` (FK).
- `Assignment` xong + code-reviewer gate PASS (2026-08-03): CRUD chuẩn + `transferAssignment()` (đóng Assignment cũ + tạo mới cùng 1 transaction, cố tình **không** đụng `AssetItem.status`/`License.seatsUsed` vì resource không thực sự rảnh giữa chừng khi transfer) — fix 1 gap HIGH code-reviewer phát hiện (trước đó phải gọi 2 request rời, có race window + không rollback). `PATCH /api/assignments/[id]` dispatch theo body: có `employeeId` → transfer, có `returnedAt` → return, cả hai → 400. CONFIRMED qua test-runner độc lập: 284/284 test, tsc/lint sạch.
- UI cơ bản đã có (nhánh `worktree-ui-basic`, đã merge vào `feat/assignment`, đã push): shared nav + fetch hook + table/select/label/textarea primitives, list+create cho mọi entity, list+create+return cho `Assignment`. Chưa có UI cho transfer (chỉ có API).
## 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)
- `fileParallelism: false` (fix race condition test-infra, xem git blame `vitest.config.ts`) đúng nhưng "cùn" — chặn song song toàn bộ suite thay vì fix gốc rễ (`tests/setup.ts` ghi đè `process.env.DATABASE_URL` toàn cục). Đáng cân nhắc hơn nay đã 11 file test (~40s) — refactor mỗi file tự có Prisma Client riêng rồi bật lại parallelism, chưa gấp.
- Fix timing side-channel ở `verifyUserCredentials`: trả về sớm khi email không tồn tại (bỏ qua bcrypt compare) → thời gian phản hồi khác nhau giữa "email sai" vs "password sai", có thể dò được email 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).
- Cải thiện `scripts/create-user.ts` để password không lộ qua CLI arg (đọc từ stdin/prompt thay vì `--password`) — chấp nhận được vì chỉ IT admin tự chạy tay, không phải self-registration công khai.
- Bump `vitest` lên 4.x (fix advisory critical GHSA-5xrq-8626-4rwp — không khai thác được vì project không chạy `vitest --ui`) — không khẩn cấp.
- Rà lại advisory riêng của `next` (vài high/moderate — SSRF qua WebSocket upgrade, middleware bypass) khi có dịp.
- Test coverage gap: `assets`/`employees`/`requests` chưa có test malformed JSON body / non-object body (`licenses`/`assignments` đã có).
- `Button` (shadcn) không `forwardRef` (registry hiện tại target React 19), project pin React 18.3.1 — dormant risk, chỉ phát sinh nếu sau này có chỗ truyền `ref` vào `<Button>`.
- **Finding từ code-reviewer gate cho `Assignment` (2026-08-03), cả hai đều dormant/chưa gãy vì chưa có delete endpoint nào và `createAssignment()` hiện chỉ được gọi qua route đã validate:**
- XOR (`assetItemId`/`licenseId`) chỉ enforce ở tầng Zod (route boundary), không lặp lại bên trong `createAssignment()` — nếu sau này có caller khác gọi thẳng lib function (script, bulk-import) bỏ qua validate, có thể tạo Assignment vi phạm XOR.
- FK `assetItemId`/`licenseId` trên `Assignment` dùng `ON DELETE SET NULL`, không nhất quán với `employeeId` (`ON DELETE RESTRICT`) — nếu sau này có endpoint xoá `AssetItem`/`License`, các `Assignment` lịch sử tham chiếu tới nó sẽ bị null hoá cả 2 field, xoá mất thông tin đã từng assign gì (và re-vi-phạm XOR retroactively). Nên đổi sang `RESTRICT` như `employeeId` khi/nếu thêm delete endpoint.
- (LOW, chưa cần fix) `returnedAt` không validate `>= assignedAt`; decrement `License.seatsUsed` lúc return không có floor guard (an toàn hiện tại vì luôn đi kèm 1 increment trước đó, nhưng không có defensive floor nếu state drift do can thiệp DB tay).
- `ConsumableItem` (phần catalog, low-value) đã có spec (`docs/asset-license-management.md`) nhưng **chưa build** — sẵn sàng giao `pm` khi cần. Luồng "xin+nhận" consumable riêng (đơn giản hơn state machine `Request`) vẫn **BỊ CHẶN** chờ stakeholder trả lời 7 câu hỏi mở (xem doc).
- Các state tiếp theo của Request: Pending Approval → Approved → Ordered → Received → Delivered → Closed, và nhánh Rejected/Cancelled.
- UI cho các thao tác update/transfer (hiện chỉ Assignment có return UI, chưa có transfer UI) và cho các state Request kế tiếp — chờ built xong phần trên.
- Auth/phân quyền (ai được approve request, ai được xác nhận nhận hàng/bàn giao) — vẫn flat, chưa có role.
---
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)
Cập nhật lần cuối: 2026-08-03 (Assignment transfer fix + code-reviewer gate PASS, UI cơ bản merge từ worktree-ui-basic, dọn gọn "Trạng thái hiện tại"/"Việc còn dở")