Super Admin Device Creation Follow-Up Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Refine the Super Admin quick-create flow on /system/devices so a system user can choose a device type, review backend-generated name/serial suggestions, preserve manual overrides, and save a minimal inventory device payload.
Architecture: This is a dependent follow-up plan. It assumes docs/superpowers/plans/2026-05-02-super-admin-system-device-inventory.md has already been implemented, including explicit device:*:system RBAC, the /system/devices vertical slice, nullable system inventory ownership, and the base device-identity preview utility. This follow-up does not reintroduce the tenant /devices flow for Super Admin. Instead, it keeps the older plan’s quick-create sequence and re-targets it to SystemDeviceInventoryController, SystemDeviceInventoryService, and SystemDeviceInventoryPage.
Tech Stack: NestJS, TypeORM, Jest, React 19, Ant Design Form, TanStack Query, Vitest, Testing Library, Bun, Biome
Prerequisite: Stop immediately if /system/devices, SystemDeviceInventoryService, SystemDeviceInventoryPage, or the system inventory migration/RBAC work from the broader plan are not already present. This plan is intentionally not a substitute for the system inventory foundation.
Assumption: The broader inventory plan already created CreateSystemDeviceDto, GenerateSystemDeviceIdentityQueryDto, systemDeviceInventoryService.generateIdentity(), and the /system/devices route surface. This plan is allowed to tighten those contracts, add missing conflict handling, and simplify the create drawer UX, but it should not fork the route model or revive /devices as the Super Admin entry point.
File Structure
Modify
backend/src/devices/device-identity.util.ts
Keep the shared generation helpers aligned with the quick-create expectations if the broader plan left gaps.backend/src/devices/device-identity.util.spec.ts
Lock preview prefix, parsing, and next-sequence behavior to the exact quick-create rules.backend/src/devices/system-device-inventory.service.ts
Ensure the system inventory service supports minimal quick-create payloads and conflict-safe saves.backend/src/devices/system-device-inventory.service.spec.ts
Add focused coverage for preview generation and minimal create behavior.backend/src/devices/controllers/system-device-inventory.controller.ts
Confirm the preview route is documented and stays on/system/devices/generate-identity.backend/src/devices/controllers/system-device-inventory.controller.spec.ts
Verify the controller delegates preview generation to the system inventory service.frontend/src/types/system-device.types.ts
Align create and preview types with the quick-create drawer payload.frontend/src/services/system-device-inventory.service.ts
Keep the preview-fetch and create calls typed against the relaxed quick-create contract.frontend/src/hooks/useSystemDeviceInventory.ts
Expose or adjust the preview/create hooks used by the drawer flow.frontend/src/pages/system-devices/SystemDeviceInventoryPage.tsx
Wire the create drawer to system preview generation, preserve manual overrides, and submit a minimal create payload.frontend/src/pages/system-devices/SystemDeviceInventoryPage.spec.tsx
Verify autofill, manual override protection, and minimal create payload behavior.
Task 1: Lock The Shared Device Identity Rules
Files:
Modify:
backend/src/devices/device-identity.util.tsModify:
backend/src/devices/device-identity.util.spec.tsStep 1: Add the failing utility expectations
Update backend/src/devices/device-identity.util.spec.ts to keep the shared helper behavior explicit:
import {
buildGeneratedDeviceIdentity,
getNextGeneratedSequence,
parseGeneratedSequence,
} from '@/devices/device-identity.util';
describe('device identity util', () => {
it('builds the expected default name and serial for supported system inventory types', () => {
expect(buildGeneratedDeviceIdentity('sensor', 1)).toEqual({
type: 'sensor',
sequence: 1,
name: 'Sensor 001',
serialNumber: 'SNS-001',
});
expect(buildGeneratedDeviceIdentity('gateway', 14)).toEqual({
type: 'gateway',
sequence: 14,
name: 'Gateway 014',
serialNumber: 'GTW-014',
});
});
it('parses only serials that match the default pattern for the same type', () => {
expect(parseGeneratedSequence('sensor', 'SNS-007')).toBe(7);
expect(parseGeneratedSequence('sensor', 'GTW-007')).toBeNull();
expect(parseGeneratedSequence('sensor', 'SNS-CUSTOM')).toBeNull();
});
it('computes the next sequence by ignoring malformed custom serials', () => {
expect(getNextGeneratedSequence('sensor', ['SNS-001', 'SNS-004', 'CUSTOM-01'])).toBe(5);
expect(getNextGeneratedSequence('feeder', [])).toBe(1);
});
});
- Step 2: Run the utility test to verify the current foundation still matches
Run: cd backend && bun run test -- src/devices/device-identity.util.spec.ts --runInBand
Expected: FAIL only if the broader plan implemented different prefixes, parsing, or next-sequence behavior than the quick-create flow requires.
- Step 3: Tighten the utility implementation if needed
If the current implementation does not already match, update backend/src/devices/device-identity.util.ts to this shape:
export const DEVICE_IDENTITY_RULES = {
sensor: { namePrefix: 'Sensor', serialPrefix: 'SNS' },
gateway: { namePrefix: 'Gateway', serialPrefix: 'GTW' },
inverter: { namePrefix: 'Inverter', serialPrefix: 'INV' },
feeder: { namePrefix: 'Feeder', serialPrefix: 'FDR' },
} as const;
type DeviceIdentityType = keyof typeof DEVICE_IDENTITY_RULES;
const formatSequence = (sequence: number) => sequence.toString().padStart(3, '0');
export function buildGeneratedDeviceIdentity(type: DeviceIdentityType, sequence: number) {
const rule = DEVICE_IDENTITY_RULES[type];
const suffix = formatSequence(sequence);
return {
type,
sequence,
name: `${rule.namePrefix} ${suffix}`,
serialNumber: `${rule.serialPrefix}-${suffix}`,
};
}
export function parseGeneratedSequence(
type: DeviceIdentityType,
serialNumber: string,
): number | null {
const { serialPrefix } = DEVICE_IDENTITY_RULES[type];
const match = new RegExp(`^${serialPrefix}-(\\d{3})$`).exec(serialNumber);
return match ? Number(match[1]) : null;
}
export function getNextGeneratedSequence(
type: DeviceIdentityType,
serialNumbers: string[],
): number {
const maxSequence = serialNumbers.reduce((max, current) => {
const parsed = parseGeneratedSequence(type, current);
return parsed && parsed > max ? parsed : max;
}, 0);
return maxSequence + 1;
}
- Step 4: Run the utility test to verify it passes
Run: cd backend && bun run test -- src/devices/device-identity.util.spec.ts --runInBand
Expected: PASS with 3 passing tests in device-identity.util.spec.ts.
- Step 5: Commit
git add backend/src/devices/device-identity.util.ts backend/src/devices/device-identity.util.spec.ts
git commit -m "test: lock system device identity preview rules"
Task 2: Tighten System Inventory Preview And Minimal Create
Files:
Modify:
backend/src/devices/system-device-inventory.service.tsModify:
backend/src/devices/system-device-inventory.service.spec.tsModify:
backend/src/devices/controllers/system-device-inventory.controller.tsModify:
backend/src/devices/controllers/system-device-inventory.controller.spec.tsStep 1: Add the failing backend tests
Add these cases to backend/src/devices/system-device-inventory.service.spec.ts:
it('generates the next preview identity for a device type', async () => {
deviceRepository.find.mockResolvedValueOnce([
{ serialNumber: 'SNS-001' },
{ serialNumber: 'SNS-003' },
{ serialNumber: 'CUSTOM-XYZ' },
] as Device[]);
const result = await service.generateIdentity('sensor');
expect(deviceRepository.find).toHaveBeenCalledWith({
where: { type: 'sensor' },
select: { serialNumber: true },
});
expect(result).toEqual({
type: 'sensor',
sequence: 4,
name: 'Sensor 004',
serialNumber: 'SNS-004',
});
});
it('creates an inventory device from the minimal quick-create payload', async () => {
deviceRepository.findOne.mockResolvedValueOnce(null);
mockManager.save.mockImplementationOnce(async (device: Device) => ({
...device,
id: 'device-1',
inventoryStatus: DeviceInventoryStatus.IN_INVENTORY,
organizationId: null,
}));
const result = await service.create(
{
serialNumber: 'SNS-004',
name: 'Sensor 004',
type: 'sensor',
},
systemUser,
);
expect(result.serialNumber).toBe('SNS-004');
expect(result.name).toBe('Sensor 004');
expect(result.organizationId).toBeNull();
expect(mockManager.create).toHaveBeenCalledWith(
Device,
expect.objectContaining({
serialNumber: 'SNS-004',
name: 'Sensor 004',
type: 'sensor',
organizationId: null,
inventoryStatus: DeviceInventoryStatus.IN_INVENTORY,
}),
);
});
it('translates a unique-index save failure into ConflictException', async () => {
deviceRepository.findOne.mockResolvedValueOnce(null);
mockManager.save.mockRejectedValueOnce({
driverError: { code: '23505' },
});
await expect(
service.create(
{
serialNumber: 'SNS-004',
name: 'Sensor 004',
type: 'sensor',
},
systemUser,
),
).rejects.toThrow(ConflictException);
});
Add this case to backend/src/devices/controllers/system-device-inventory.controller.spec.ts:
it('delegates preview identity generation to the system inventory service', async () => {
service.generateIdentity.mockResolvedValue({
type: 'sensor',
sequence: 1,
name: 'Sensor 001',
serialNumber: 'SNS-001',
});
await expect(controller.generateIdentity({ type: 'sensor' })).resolves.toEqual({
type: 'sensor',
sequence: 1,
name: 'Sensor 001',
serialNumber: 'SNS-001',
});
expect(service.generateIdentity).toHaveBeenCalledWith('sensor');
});
- Step 2: Run the backend tests to verify they fail
Run: cd backend && bun run test -- src/devices/system-device-inventory.service.spec.ts src/devices/controllers/system-device-inventory.controller.spec.ts --runInBand
Expected: FAIL if the broader plan’s base implementation does not yet support minimal quick-create defaults, conflict translation, or the exact preview contract.
- Step 3: Implement or tighten the system inventory backend behavior
Update backend/src/devices/system-device-inventory.service.ts so preview generation and create defaults are explicit:
import { ConflictException } from '@nestjs/common';
import {
buildGeneratedDeviceIdentity,
getNextGeneratedSequence,
} from '@/devices/device-identity.util';
async generateIdentity(type: Device['type']) {
const devices = await this.deviceRepository.find({
where: { type },
select: { serialNumber: true },
});
const sequence = getNextGeneratedSequence(
type,
devices.map((device) => device.serialNumber),
);
return buildGeneratedDeviceIdentity(type, sequence);
}
async create(dto: CreateSystemDeviceDto, currentUser: CurrentUserData) {
this.assertSystemScope(currentUser, 'create');
const existingDevice = await this.deviceRepository.findOne({
where: { serialNumber: dto.serialNumber },
});
if (existingDevice) {
throw new ConflictException('Device with this serial number already exists');
}
return this.dataSource.transaction(async (manager) => {
const device = manager.create(Device, {
serialNumber: dto.serialNumber,
name: dto.name,
type: dto.type,
model: dto.model ?? null,
capabilities: dto.capabilities ?? null,
organizationId: null,
inventoryStatus: DeviceInventoryStatus.IN_INVENTORY,
status: 'offline',
pondId: null,
areaId: null,
assignedAt: null,
retiredAt: null,
warrantyMonths: dto.warrantyMonths ?? 12,
ownershipType: dto.ownershipType ?? 'sold',
accessToken: this.generateAccessToken(),
secret: this.generateSecret(),
mqttTopic: `hmpiot/devices/${dto.serialNumber}/telemetry`,
isActive: true,
});
try {
return this.toResponse(await manager.save(device));
} catch (error) {
const driverError = error as { driverError?: { code?: string } };
if (driverError.driverError?.code === '23505') {
throw new ConflictException('Device with this serial number already exists');
}
throw error;
}
});
}
Update backend/src/devices/controllers/system-device-inventory.controller.ts if the preview route or Swagger contract is missing:
@Get('generate-identity')
@ApiOperation({ summary: 'Generate a preview identity for a new system inventory device' })
@ApiQuery({
name: 'type',
required: true,
enum: ['sensor', 'gateway', 'inverter', 'feeder'],
})
@ApiResponse({ status: 200, description: 'Preview identity generated successfully' })
@RequirePermissions('device:read:system')
generateIdentity(@Query() query: GenerateSystemDeviceIdentityQueryDto) {
return this.service.generateIdentity(query.type);
}
@ApiResponse({ status: 409, description: 'Serial number already exists or preview was consumed' })
- Step 4: Run the backend tests to verify they pass
Run: cd backend && bun run test -- src/devices/system-device-inventory.service.spec.ts src/devices/controllers/system-device-inventory.controller.spec.ts --runInBand
Expected: PASS with preview generation, minimal create, and conflict-safe create behavior covered.
- Step 5: Commit
git add backend/src/devices/system-device-inventory.service.ts backend/src/devices/system-device-inventory.service.spec.ts backend/src/devices/controllers/system-device-inventory.controller.ts backend/src/devices/controllers/system-device-inventory.controller.spec.ts
git commit -m "feat: tighten system device quick-create backend flow"
Task 3: Align Frontend System Device Contracts With Quick-Create
Files:
Modify:
frontend/src/types/system-device.types.tsModify:
frontend/src/services/system-device-inventory.service.tsModify:
frontend/src/hooks/useSystemDeviceInventory.tsStep 1: Add the contract changes required by the drawer flow
Update frontend/src/types/system-device.types.ts so the create DTO matches the minimal payload emitted by the quick-create drawer:
export interface CreateSystemDeviceDto {
serialNumber: string;
name: string;
type: SystemDeviceType;
model?: string;
capabilities?: Record<string, unknown>;
warrantyMonths?: number;
ownershipType?: 'sold' | 'leased';
}
export interface GenerateSystemDeviceIdentityResponse {
type: SystemDeviceType;
sequence: number;
name: string;
serialNumber: string;
}
Update frontend/src/services/system-device-inventory.service.ts to keep preview and create typed against those contracts:
import type {
CreateSystemDeviceDto,
GenerateSystemDeviceIdentityResponse,
SystemDeviceType,
} from '@/types/system-device.types';
create(data: CreateSystemDeviceDto) {
return api.post('/system/devices', data).then((response) => response.data);
},
generateIdentity(type: SystemDeviceType): Promise<GenerateSystemDeviceIdentityResponse> {
return api
.get('/system/devices/generate-identity', { params: { type } })
.then((response) => response.data);
},
Update frontend/src/hooks/useSystemDeviceInventory.ts so the page can use explicit preview/create hooks:
export const useCreateSystemDevice = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: CreateSystemDeviceDto) => systemDeviceInventoryService.create(dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['system-device-inventory'] });
},
});
};
export const useGenerateSystemDeviceIdentity = () =>
useMutation({
mutationFn: (type: SystemDeviceType) => systemDeviceInventoryService.generateIdentity(type),
});
- Step 2: Verify imports and compile-time type usage locally
Run: cd frontend && bun run test -- src/pages/system-devices/SystemDeviceInventoryPage.spec.tsx --runInBand
Expected: FAIL until the page test and page component are updated to use the relaxed quick-create contract.
- Step 3: Commit
git add frontend/src/types/system-device.types.ts frontend/src/services/system-device-inventory.service.ts frontend/src/hooks/useSystemDeviceInventory.ts
git commit -m "refactor: align system device frontend contracts with quick create"
Task 4: Wire The /system/devices Drawer Autofill And Create Flow
Files:
Modify:
frontend/src/pages/system-devices/SystemDeviceInventoryPage.tsxModify:
frontend/src/pages/system-devices/SystemDeviceInventoryPage.spec.tsxStep 1: Add the failing drawer tests
Update frontend/src/pages/system-devices/SystemDeviceInventoryPage.spec.tsx with focused quick-create coverage:
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SystemDeviceInventoryPage } from '@/pages/system-devices/SystemDeviceInventoryPage';
import { renderWithProviders } from '@/test/renderWithProviders';
const generateIdentity = vi.fn();
const createDevice = vi.fn();
vi.mock('@/hooks/useSystemDeviceInventory', () => ({
useSystemDeviceInventory: () => ({
data: { data: [], meta: { total: 0, page: 1, limit: 20, totalPages: 0 } },
isLoading: false,
}),
useCreateSystemDevice: () => ({ mutateAsync: createDevice, isPending: false }),
useUpdateSystemDevice: () => ({ mutateAsync: vi.fn(), isPending: false }),
useGenerateSystemDeviceIdentity: () => ({ mutateAsync: generateIdentity, isPending: false }),
useAssignSystemDeviceOrganization: () => ({ mutateAsync: vi.fn(), isPending: false }),
useUnassignSystemDeviceOrganization: () => ({ mutateAsync: vi.fn(), isPending: false }),
useUpdateSystemDeviceInventoryStatus: () => ({ mutateAsync: vi.fn(), isPending: false }),
useDeleteSystemDevice: () => ({ mutateAsync: vi.fn(), isPending: false }),
}));
describe('SystemDeviceInventoryPage quick create', () => {
beforeEach(() => {
generateIdentity.mockReset();
createDevice.mockReset();
});
it('autofills name and serial after selecting a type', async () => {
generateIdentity.mockResolvedValue({
type: 'sensor',
sequence: 1,
name: 'Sensor 001',
serialNumber: 'SNS-001',
});
renderWithProviders(<SystemDeviceInventoryPage />, {
initialEntries: ['/system/devices'],
});
fireEvent.click(screen.getByRole('button', { name: /thêm thiết bị/i }));
fireEvent.mouseDown(screen.getByLabelText(/loại thiết bị/i));
fireEvent.click(await screen.findByText(/sensor/i));
await waitFor(() => {
expect(screen.getByDisplayValue('Sensor 001')).toBeInTheDocument();
expect(screen.getByDisplayValue('SNS-001')).toBeInTheDocument();
});
});
it('preserves a manually edited name when the type changes again', async () => {
generateIdentity
.mockResolvedValueOnce({
type: 'sensor',
sequence: 1,
name: 'Sensor 001',
serialNumber: 'SNS-001',
})
.mockResolvedValueOnce({
type: 'gateway',
sequence: 1,
name: 'Gateway 001',
serialNumber: 'GTW-001',
});
renderWithProviders(<SystemDeviceInventoryPage />, {
initialEntries: ['/system/devices'],
});
fireEvent.click(screen.getByRole('button', { name: /thêm thiết bị/i }));
fireEvent.mouseDown(screen.getByLabelText(/loại thiết bị/i));
fireEvent.click(await screen.findByText(/sensor/i));
fireEvent.change(await screen.findByLabelText(/tên thiết bị/i), {
target: { value: 'Sensor ao A' },
});
fireEvent.mouseDown(screen.getByLabelText(/loại thiết bị/i));
fireEvent.click(await screen.findByText(/gateway/i));
await waitFor(() => {
expect(screen.getByDisplayValue('Sensor ao A')).toBeInTheDocument();
expect(screen.getByDisplayValue('GTW-001')).toBeInTheDocument();
});
});
it('submits a minimal create payload without model', async () => {
generateIdentity.mockResolvedValue({
type: 'sensor',
sequence: 1,
name: 'Sensor 001',
serialNumber: 'SNS-001',
});
createDevice.mockResolvedValue({ id: 'device-1' });
renderWithProviders(<SystemDeviceInventoryPage />, {
initialEntries: ['/system/devices'],
});
fireEvent.click(screen.getByRole('button', { name: /thêm thiết bị/i }));
fireEvent.mouseDown(screen.getByLabelText(/loại thiết bị/i));
fireEvent.click(await screen.findByText(/sensor/i));
fireEvent.click(screen.getByRole('button', { name: /lưu/i }));
await waitFor(() =>
expect(createDevice).toHaveBeenCalledWith({
serialNumber: 'SNS-001',
name: 'Sensor 001',
type: 'sensor',
}),
);
});
});
- Step 2: Run the page test to verify it fails
Run: cd frontend && bun run test -- src/pages/system-devices/SystemDeviceInventoryPage.spec.tsx
Expected: FAIL because the page either does not preserve manual overrides, does not submit the minimal payload, or still couples the create drawer to broader edit/inventory concerns.
- Step 3: Implement the drawer behavior in
SystemDeviceInventoryPage.tsx
Use a real Ant Design form instance, the system inventory hooks, and “last suggested value” tracking so only untouched fields are replaced:
type DeviceFormValues = Pick<CreateSystemDeviceDto, 'name' | 'serialNumber' | 'type' | 'model'>;
const [form] = Form.useForm<DeviceFormValues>();
const [drawerOpen, setDrawerOpen] = useState(false);
const [editingDevice, setEditingDevice] = useState<SystemDeviceRow | null>(null);
const [lastSuggestedName, setLastSuggestedName] = useState<string | null>(null);
const [lastSuggestedSerial, setLastSuggestedSerial] = useState<string | null>(null);
const { mutateAsync: createDevice, isPending: isCreating } = useCreateSystemDevice();
const { mutateAsync: updateDevice, isPending: isUpdating } = useUpdateSystemDevice();
const { mutateAsync: generateIdentity, isPending: isGeneratingIdentity } =
useGenerateSystemDeviceIdentity();
const resetCreateState = () => {
form.resetFields();
setEditingDevice(null);
setLastSuggestedName(null);
setLastSuggestedSerial(null);
};
const openCreateDrawer = () => {
resetCreateState();
setDrawerOpen(true);
};
const closeDrawer = () => {
setDrawerOpen(false);
resetCreateState();
};
const applyGeneratedIdentity = async (type: SystemDeviceType) => {
const generated = await generateIdentity(type);
const currentName = form.getFieldValue('name');
const currentSerial = form.getFieldValue('serialNumber');
const nextValues: Partial<DeviceFormValues> = { type };
if (!currentName || currentName === lastSuggestedName) {
nextValues.name = generated.name;
setLastSuggestedName(generated.name);
}
if (!currentSerial || currentSerial === lastSuggestedSerial) {
nextValues.serialNumber = generated.serialNumber;
setLastSuggestedSerial(generated.serialNumber);
}
form.setFieldsValue(nextValues);
};
const handleCreate = async (values: DeviceFormValues) => {
const payload: CreateSystemDeviceDto = {
type: values.type,
name: values.name.trim(),
serialNumber: values.serialNumber.trim(),
...(values.model?.trim() ? { model: values.model.trim() } : {}),
};
if (editingDevice) {
await updateDevice({ id: editingDevice.id, dto: payload });
} else {
await createDevice(payload);
}
closeDrawer();
};
Keep the create drawer fields narrow and ordered around the quick-create flow:
<Button type="primary" onClick={openCreateDrawer}>
Thêm thiết bị
</Button>
<Drawer
open={drawerOpen}
onClose={closeDrawer}
title={editingDevice ? 'Chỉnh sửa thiết bị' : 'Thêm thiết bị'}
>
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Form.Item label="Loại thiết bị" name="type" rules={[{ required: true }]}>
<Select
placeholder="Chọn loại thiết bị"
loading={isGeneratingIdentity}
onChange={(value: SystemDeviceType) => void applyGeneratedIdentity(value)}
>
<Select.Option value="sensor">Sensor</Select.Option>
<Select.Option value="gateway">Gateway</Select.Option>
<Select.Option value="inverter">Inverter</Select.Option>
<Select.Option value="feeder">Feeder</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Tên thiết bị" name="name" rules={[{ required: true }]}>
<Input placeholder="Nhập tên thiết bị" />
</Form.Item>
<Form.Item label="Mã serial" name="serialNumber" rules={[{ required: true }]}>
<Input placeholder="Nhập mã serial" />
</Form.Item>
<Form.Item label="Mô hình" name="model">
<Input placeholder="Nhập mô hình thiết bị (không bắt buộc)" />
</Form.Item>
<Form.Item>
<Space className="w-full justify-end">
<Button onClick={closeDrawer}>Hủy</Button>
<Button type="primary" htmlType="submit" loading={isCreating || isUpdating}>
Lưu
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
Use openCreateDrawer only for the quick-create action. Keep edit-specific flows separate so system inventory CRUD features from the broader plan do not leak into the create path.
- Step 4: Run the page test to verify it passes
Run: cd frontend && bun run test -- src/pages/system-devices/SystemDeviceInventoryPage.spec.tsx
Expected: PASS with autofill, manual override preservation, and minimal create payload coverage green.
- Step 5: Commit
git add frontend/src/pages/system-devices/SystemDeviceInventoryPage.tsx frontend/src/pages/system-devices/SystemDeviceInventoryPage.spec.tsx
git commit -m "feat: refine system device quick create drawer"
Task 5: Verify The Follow-Up Change Set
Files:
Modify only if verification reveals formatting or import-order fixes in already-touched files.
Step 1: Run the focused backend tests
Run: cd backend && bun run test -- src/devices/device-identity.util.spec.ts src/devices/system-device-inventory.service.spec.ts src/devices/controllers/system-device-inventory.controller.spec.ts --runInBand
Expected: PASS for all shared identity and system quick-create backend cases.
- Step 2: Run the focused frontend test
Run: cd frontend && bun run test -- src/pages/system-devices/SystemDeviceInventoryPage.spec.tsx
Expected: PASS for preview autofill, manual override protection, and minimal create payload behavior.
- Step 3: Run repository checks
Run: bun run check
Expected: PASS with no Biome diagnostics in touched files.
- Step 4: Smoke test the actual UI in dev mode
Run: bun run frontend:start:dev
Expected: Vite dev server starts and serves the app at http://localhost:5173.
Manual verification in the browser:
1. Open /system/devices as a Super Admin.
2. Click "Thêm thiết bị".
3. Confirm "Loại thiết bị" is the first field.
4. Choose "Sensor" and verify the form prefills "Sensor 001" / "SNS-001" or the next available sequence.
5. Change the name manually, switch the type, and confirm only untouched fields regenerate.
6. Leave "Mô hình" empty and save.
7. Confirm the success path closes the drawer and refreshes the inventory list.
- Step 5: Commit any verification-only fixes
git add backend/src/devices/device-identity.util.ts backend/src/devices/device-identity.util.spec.ts backend/src/devices/system-device-inventory.service.ts backend/src/devices/system-device-inventory.service.spec.ts backend/src/devices/controllers/system-device-inventory.controller.ts backend/src/devices/controllers/system-device-inventory.controller.spec.ts frontend/src/types/system-device.types.ts frontend/src/services/system-device-inventory.service.ts frontend/src/hooks/useSystemDeviceInventory.ts frontend/src/pages/system-devices/SystemDeviceInventoryPage.tsx frontend/src/pages/system-devices/SystemDeviceInventoryPage.spec.tsx
git commit -m "chore: verify system device quick create flow"
Self-Review
- Spec coverage:
- Dependent-on-inventory-slice constraint: header and prerequisite sections.
- Shared backend preview rules: Task 1.
/system/devices/generate-identityand conflict-safe create: Task 2.- Relaxed minimal frontend create contract: Task 3.
- Drawer autofill/manual override protection/minimal payload: Task 4.
- Focused verification on the follow-up scope: Task 5.
- Placeholder scan:
- No
TODO,TBD, or “handle appropriately” placeholders remain.
- No
- Type consistency:
- Backend and frontend preview types stay on
'sensor' | 'gateway' | 'inverter' | 'feeder'. - The create payload remains minimal and system-scoped throughout the plan.
- Backend and frontend preview types stay on