Đơn giản hoá form Tạo thiết bị + Hoàn thiện Device Profile Catalog
Date: 2026-07-05
Status: Approved (conversation-level), pending file review gate
1) Goal
Đơn giản hoá màn “Thêm thiết bị mới” (Thêm thiết bị mới) ở trang quản lý thiết bị của tổ chức (/devices), theo yêu cầu:
- Form chỉ còn
Loại thiết bịlà input chính người dùng chọn. Tên thiết bịtự động sinh bằng cách ghép Loại thiết bị + số tự tăng.Mã serialkhông còn là input — hệ thống tự sinh.Mô hìnhkhông xoá hẳn (vẫn có giá trị tham khảo/quản lý tài sản) nhưng chuyển thành lựa chọn từ danh mục có thể cấu hình (vì model do chính nền tảng/nhà sản xuất kiểm soát), và không bắt buộc.
Vì Device là một entity dùng chung cho nhiều luồng (tạo mới của tổ chức, chỉnh sửa, và System Device Inventory của Super Admin), thay đổi phải áp dụng nhất quán cho cả ba nơi, và hạ tầng “danh mục Mô hình” (Device Profile) hiện có nhưng làm dở phải được hoàn thiện thay vì chắp vá thêm một cơ chế song song.
2) Current State
- Form Create ở
frontend/src/pages/devices/DeviceList.tsx(Drawer, dòng ~437–479) có 4 field:Tên thiết bị(Input, bắt buộc),Mã serial(Input, bắt buộc),Loại thiết bị(Select, bắt buộc),Mô hình(Input, bắt buộc). - Backend
CreateDeviceDto(backend/src/devices/dto/create-device.dto.ts):serialNumberbắt buộc + unique ở DB,namebắt buộc,typebắt buộc enum,modeloptional. serialNumberkhông mang ý nghĩa nghiệp vụ cho người dùng — nó là khoá kỹ thuật nội bộ: dùng làmnamekhi provision thiết bị lên ThingsBoard, và là khoá join để route telemetry ngược về đúng thiết bị (telemetry.service.ts, so khớpserialNumber === dto.deviceName). Firmware không biết đến khái niệm này.- Đã có sẵn cơ chế tự sinh
name/serialNumbertheo mẫu<Prefix>-<sequence>(backend/src/devices/device-identity.util.ts,DEVICE_IDENTITY_RULEScho sensor/gateway/inverter/feeder) nhưng scope global, hiện chỉ dùng cho luồng System Device Inventory (system-device-inventory.service.ts, endpointGET /system/devices/generate-identity). modellà free-text, không có logic nghiệp vụ nào phụ thuộc vào giá trị của nó ở backend, nhưng được hiển thị ở nhiều nơi:DeviceList.tsx(bảng + form),DeviceDetail.tsx,PondDetail.tsx,DeviceDrawer.tsx(trang Tổ chức),SystemDeviceInventoryPage.tsx.- Đã tồn tại một catalog “Device Profile” làm dở dang:
- Entity
DeviceProfile(backend/src/devices/entities/device-profile.entity.ts) cócode,name,deviceTypeId,manufacturer,model,firmwareFamily,supportedFunctions,isActive. Deviceentity đã có sẵn cộtdevice_profile_id(FK tớiDeviceProfile) — hạ tầng liên kết đã có, chỉ là chưa được dùng ở luồng org-facing.- API hiện chỉ có
GET /device-profilesvàPOST /device-profiles, quyền giới hạndevice:read:system/device:create:system— org user không gọi được. - Trang admin
frontend/src/pages/system-devices/DeviceProfilesPage.tsxchỉ liệt kê (read-only), nút “Tạo profile” chưa gắn xử lý. - Bảng
device_typesđược seed vớigateway, sensor, feeder, pump_controller— lệch với 4 giá trịDevice.typeđang dùng thực tế (sensor, gateway, inverter, feeder).pump_controllerkhông được tham chiếu ở bất kỳ đâu khác ngoài migration seed của nó. SystemDeviceInventoryPage.tsxđã dùnguseDeviceProfiles()để chọndeviceProfileId, nhưng đồng thời vẫn có một ô “Mô hình” nhập tay tự do riêng biệt — hai khái niệm trùng lặp cùng tồn tại.
- Entity
3) Options Considered
Option A: Chỉ sửa form Create của tổ chức, giữ nguyên mọi nơi khác
Pros: Phạm vi nhỏ, nhanh.
Cons: Tạo ra 2 hành vi khác nhau cho cùng field trên cùng entity (Create tự sinh serial/chọn model qua danh mục, Edit vẫn cho gõ tay serial → có thể phá unique constraint và khoá join telemetry; model ở Edit và ở form admin vẫn là text tự do trong khi Create dùng danh mục → dữ liệu không nhất quán, không đúng tinh thần “config được” mà người dùng yêu cầu).
Option B: Xây catalog Model mới, độc lập với DeviceProfile có sẵn
Pros: Không phải dọn dẹp taxonomy cũ (pump_controller), không phải mở quyền đọc cho endpoint hiện tại.
Cons: Tạo ra catalog thứ hai song song với DeviceProfile vốn đã có đúng cấu trúc cần thiết (đã có deviceTypeId, model, manufacturer, isActive, và FK device_profile_id sẵn trên Device) — lãng phí, và System Device Inventory vẫn tồn đọng một danh mục nửa vời cần dọn.
Option C (chọn): Hoàn thiện DeviceProfile thành danh mục dùng chung, đồng bộ cả 3 luồng
Hoàn thiện CRUD + quyền đọc + UI quản lý cho DeviceProfile hiện có, đối soát lại device_types cho khớp 4 loại thiết bị đang dùng, và dùng deviceProfileId làm nguồn “Mô hình” thống nhất ở cả Create, Edit (tổ chức) và System Device Inventory (Super Admin).
Pros: Một nguồn sự thật duy nhất cho “Mô hình”, không có 2 cơ chế song song, giải quyết dứt điểm phần dở dang có sẵn thay vì né tránh nó.
Cons: Khối lượng việc lớn hơn: 1 migration dữ liệu, API CRUD đầy đủ, mở quyền đọc có kiểm soát, UI quản lý catalog mới, và sửa đồng thời 3 form.
Người dùng đã xác nhận rõ ưu tiên “bản ship hoàn chỉnh, chuẩn chỉ” hơn là giới hạn phạm vi — chọn Option C.
4) Selected Design
4.1 Sinh Tên thiết bị tự động (Create, org-facing)
- Format:
${TypeLabel}-${sequence}(ví dụSensor-3), dùngnamePrefixcó sẵn trongDEVICE_IDENTITY_RULES(Sensor/Gateway/Inverter/Feeder) làmTypeLabel. sequenceđược tính theo tổ chức hiện tại + loại thiết bị: quét cácnamehiện có của tổ chức đó khớp pattern${TypeLabel}-\d+, lấy số lớn nhất + 1 (không chỉ đếm số lượng, để không bị lặp số khi thiết bị cũ đã bị xoá).sequencehiển thị dạng số nguyên thường, không zero-pad (Sensor-3, không phảiSensor-003) — khác vớiserialNumber(mục 4.2) vốn giữ nguyên format zero-pad 3 chữ số đã có sẵn (SNS-003), vì đây là hai cơ chế sinh số độc lập phục vụ hai mục đích khác nhau.- Đây là scope generation mới, tách biệt với cơ chế global sẵn có trong
device-identity.util.ts(dùng cho System Device Inventory) — không sửa cơ chế cũ, chỉ thêm hàm mới scoped theoorganizationId + type. - Form hiển thị tên sẽ được tạo dưới dạng preview read-only (không cho sửa tay), cập nhật lại mỗi khi đổi
Loại thiết bị, theo đúng UX patternapplyGeneratedIdentityđã có ởSystemDeviceInventoryPage.tsx.
4.2 Sinh Mã serial tự động (Create, org-facing)
- Ẩn hoàn toàn khỏi form Create. Backend tự sinh bằng cách tái sử dụng nguyên trạng
buildGeneratedDeviceIdentity/getNextGeneratedSequence(device-identity.util.ts), giữ nguyên scope global toàn hệ thống (không theo tổ chức) vì vai trò của nó là khoá kỹ thuật join telemetry, cần unique tuyệt đối bất kể tổ chức nào tạo. DevicesService.create()bỏ qua mọi giá trịname/serialNumberdo client gửi lên (nếu có) — luôn tự sinh phía server. Client chỉ gửitype+deviceProfileId(optional).- Race condition: bọc việc sinh sequence + insert trong transaction, retry có giới hạn (ví dụ tối đa 5 lần) khi vi phạm unique constraint, để tránh 2 request đồng thời sinh trùng
serialNumberhoặc trùngnametrong cùng tổ chức+loại.
4.3 Hoàn thiện Device Profile Catalog
Đối soát device_types: migration data thay pump_controller bằng inverter để khớp đúng 4 giá trị Device.type đang dùng (sensor, gateway, inverter, feeder). Đã xác nhận pump_controller không được tham chiếu ở đâu khác — an toàn.
API bổ sung (backend/src/devices/controllers/device-profiles.controller.ts):
PATCH /device-profiles/:id— cập nhậtname,manufacturer,model,firmwareFamily,supportedFunctions,isActive. Quyềndevice:update:system.DELETE /device-profiles/:id— soft-delete (setisActive = false), không xoá cứng vì có thể đã cóDevice.device_profile_idtham chiếu. Quyềndevice:delete:system.- Mở quyền đọc cho org user:
GET /device-profileschấp nhận thêmdevice:read:ownbên cạnhdevice:read:system(scope-aware, theo đúng mô hình RBACresource:action:scopeđã dùng trong hệ thống); khi gọi ở scopeown, chỉ trả về profile cóisActive = true. Hỗ trợ filter theodeviceTypequa query param.
UI quản lý (frontend/src/pages/system-devices/DeviceProfilesPage.tsx): hoàn thiện thật — gắn nút “Tạo profile” vào form/modal tạo mới (dùng hook useCreateDeviceProfile đã có sẵn nhưng chưa được UI nào gọi), thêm action Sửa (useUpdateDeviceProfile mới) và Xoá/Vô hiệu hoá (useDeleteDeviceProfile mới, gọi endpoint soft-delete).
4.4 “Mô hình” trở thành nguồn sự thật duy nhất qua deviceProfileId
- Form Create/Edit (
DeviceList.tsx) và form System Device Inventory (SystemDeviceInventoryPage.tsx) đều đổi field “Mô hình” thành Select chọndeviceProfileId, optional, lọc theoLoại thiết bịđã chọn, dùng chung một hook fetch danh mục (useDeviceProfilesmở rộng nhận filterdeviceType). - Xoá ô “Mô hình” nhập tay tự do đang trùng lặp ở
SystemDeviceInventoryPage.tsx. - Hiển thị “Mô hình” ở mọi nơi đọc (
DeviceDetail.tsx, bảngDeviceList.tsx,PondDetail.tsx) đổi sang ưu tiêndevice.deviceProfile?.model ?? device.model ?? '—'— vẫn hiển thị được giá trịmodeltự do cũ của thiết bị legacy (tạo trước khi có thay đổi này), không cần data migration cho các bản ghi cũ. - Cột
model(free-text) trênDevicekhông bị xoá khỏi DB (theo đúng nguyên tắc không xoá chỉ vì “có trong DB” khi vẫn còn giá trị tham khảo cho dữ liệu cũ), nhưng không còn là input trực tiếp ở bất kỳ form nào từ nay — chỉ còn đọc để backward-compat hiển thị.
4.5 Đồng bộ form Edit (org-facing, DeviceList.tsx)
Tên thiết bị: vẫn cho sửa tay bình thường (đổi tên gợi nhớ là nhu cầu hợp lệ, khác bản chất với serial).Mã serial: chuyển sang hiển thị read-only (không còn là input có thể sửa) — chỉ xem, tránh người dùng tự gõ đè phá vỡ unique constraint/khoá join telemetry.Loại thiết bị: giữ nguyên Select như hiện tại.Mô hình: đổi thành Select danh mục như mô tả ở 4.4.
4.6 DTO changes
CreateDeviceDto(org-facing): bỏname,serialNumber,model(free text) khỏi input hợp lệ; chỉ nhậntype(bắt buộc) +deviceProfileId(optional, uuid) + các field optional khác vốn đã có (pondId,installationDate,warrantyMonths,ownershipType,capabilities).UpdateDeviceDto: nhậnname(optional, cho phép đổi tên tay),deviceProfileId(optional). Không nhậnserialNumber(không cho sửa qua API). Giữ khả năng đọcmodelcũ nhưng không phải input mới.- DTO cho System Device Inventory (
create-system-device.dto.ts) đồng bộ tương tự: bỏ ômodeltự do, chỉ dùngdeviceProfileId.
5) File-Level Scope
- Backend:
backend/src/devices/device-identity.util.ts: thêm hàm sinh sequence theoorganizationId + type(không sửa hàm cũ).backend/src/devices/devices.service.ts: sửacreate()tự sinhname/serialNumber, bỏ qua input client, thêm retry khi conflict; sửaupdate()không cho sửaserialNumber.backend/src/devices/dto/create-device.dto.ts,update-device.dto.ts: bỏ field theo 4.6.backend/src/devices/dto/create-system-device.dto.ts,system-device-inventory.service.ts: bỏ ômodeltự do, dùngdeviceProfileId.backend/src/devices/controllers/device-profiles.controller.ts,device-profile.service.ts: thêmupdate, soft-delete, mở quyền đọcownscope, filter theodeviceType.- Migration mới: data-fix
device_types(pump_controller→inverter). backend/src/telemetry/telemetry.service.ts: không đổi logic, chỉ xác nhận vẫn hoạt động đúng với serial tự sinh (không có thay đổi cần thiết, chỉ cần test lại).
- Frontend:
frontend/src/pages/devices/DeviceList.tsx: sửa Drawer Create (bỏ Tên/Serial input, thêm preview tên, đổi Mô hình thành Select) và Drawer Edit (serial read-only, Mô hình Select).frontend/src/pages/system-devices/SystemDeviceInventoryPage.tsx: bỏ ô Mô hình tự do, dùng chung Select danh mục.frontend/src/pages/system-devices/DeviceProfilesPage.tsx: thêm modal/form tạo, sửa, xoá.frontend/src/hooks/useSystemDeviceInventory.tshoặc hook riêng cho device profiles: thêmuseUpdateDeviceProfile,useDeleteDeviceProfile, mở rộnguseDeviceProfiles(filter?: { deviceType }).frontend/src/pages/devices/DeviceDetail.tsx,frontend/src/pages/farm/PondDetail.tsx: đổi cách hiển thịmodeltheo 4.4.frontend/src/types/device.types.ts,frontend/src/schemas/device.schema.ts: cập nhật type/schema khớp DTO mới.
6) Acceptance Criteria
- Form “Thêm thiết bị mới” (tổ chức) chỉ yêu cầu chọn
Loại thiết bị;Mô hìnhoptional dạng Select; không còn inputTên thiết bị/Mã serial. - Thiết bị mới tạo có
namedạng${TypeLabel}-${sequence}đúng theo tổ chức + loại thiết bị, không trùng với thiết bị hiện có cùng tổ chức+loại. - Thiết bị mới tạo có
serialNumbertự sinh, unique toàn hệ thống, theo đúng pattern hiện có (SNS-/GTW-/INV-/FDR-). - Form Edit (tổ chức):
Mã serialchỉ đọc,Mô hìnhlà Select cùng danh mục với Create. - Form System Device Inventory: không còn ô “Mô hình” tự do, chỉ còn Select danh mục dùng chung.
GET /device-profilesgọi được bởi org user (scopeown), chỉ trả profileisActive = true, filter được theodeviceType.- Có thể tạo, sửa, và vô hiệu hoá (soft-delete) một Device Profile qua UI trang
/system/device-profiles. device_typeskhông cònpump_controller, có đủ 4 loại khớp vớiDevice.type.- Thiết bị cũ (tạo trước thay đổi này) vẫn hiển thị đúng giá trị
modelcũ ở mọi trang đọc, không bị mất dữ liệu. - Telemetry route đúng thiết bị khi
serialNumberlà giá trị tự sinh (không có regression).
7) Out of Scope
- Cho phép org user tự tạo/sửa Device Profile — chỉ Super Admin quản lý catalog (đúng tinh thần “nhà mình sản xuất thì phải config được” — config tập trung).
- Data migration gán
deviceProfileIdngược cho thiết bị cũ dựa trên giá trịmodelfree-text hiện có (không tự động suy đoán match). - Đổi cơ chế authentication/access token của thiết bị.
- Thay đổi luồng assign/unassign organization, inventory lifecycle của System Device Inventory (giữ nguyên spec đã duyệt trước đó).
8) Risks and Mitigations
- Risk: Data-fix
device_typeschạy migration trên môi trường đã có dữ liệu thật dùngpump_controller.
Mitigation: Đã xác nhận bằng grep toàn repo không có tham chiếu nào ngoài migration seed; kiểm tra thêm ở DB thực tế trước khi chạy migration ở production. - Risk: Mở quyền đọc
GET /device-profilescho org user có thể lộ thông tin không cần thiết (manufacturer,firmwareFamily).
Mitigation: Các trường này không nhạy cảm (đã là thông tin công khai về sản phẩm), chấp nhận trả về nguyên vẹn cho scopeown, chỉ lọc theoisActive. - Risk: Race condition khi 2 request tạo thiết bị đồng thời trong cùng tổ chức+loại sinh trùng sequence.
Mitigation: Retry có giới hạn trong transaction khi vi phạm unique constraint. - Risk: Xoá ô nhập
Mô hìnhtự do ở System Device Inventory làm Super Admin không thể ghi model cho thiết bị chưa có trong catalog.
Mitigation: Chấp nhận đánh đổi này theo đúng yêu cầu “phải config được” — Super Admin luôn có quyền tạo profile mới trước khi gán, không mất khả năng, chỉ đổi luồng thao tác.