fix: approve devices onto the active design + Z-Wave node fields

Approve (single + bulk) created the canvas Node under the first design
instead of the design the user is viewing, so approved devices were
invisible on the active canvas and got wiped on the next save — and a
re-approve returned "0 approved" because the rows were already approved.

- bulk-approve now accepts design_id; the UI sends the active design.
- single approve already honoured design_id; the UI now sends it too.
- generalize the wireless branch (status=online, mesh props, no ICMP
  check) to Z-Wave as well as Zigbee, using build_zwave_properties.

ha-relevant: yes
This commit is contained in:
Pouzor
2026-06-26 21:58:31 +02:00
parent d41896fadf
commit 13420bead8
6 changed files with 127 additions and 22 deletions
@@ -6,6 +6,7 @@ import {
import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { scanApi } from '@/api/client'
import { useCanvasStore } from '@/stores/canvasStore'
import { useDesignStore } from '@/stores/designStore'
import { toast } from 'sonner'
import { PendingDeviceModal, type PendingDevice } from '@/components/modals/PendingDeviceModal'
import type { NodeType, ServiceInfo } from '@/types'
@@ -127,6 +128,7 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
// Optionally restrict to devices that have at least one detected service.
const [withServicesOnly, setWithServicesOnly] = useState(false)
const { addNode, scanEventTs } = useCanvasStore()
const activeDesignId = useDesignStore((s) => s.activeDesignId)
const highlightRef = useRef<HTMLButtonElement>(null)
const load = useCallback(async () => {
@@ -283,6 +285,8 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
status: wireless ? 'online' : 'unknown',
services: (device.services ?? []) as ServiceInfo[],
properties,
// Approve onto the design the user is viewing, not the first design.
design_id: activeDesignId ?? undefined,
}
const res = await scanApi.approve(device.id, nodeData)
const nodeId = res.data.node_id
@@ -327,7 +331,7 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
const ids = [...selectedIds]
if (ids.length === 0) return
try {
const res = await scanApi.bulkApprove(ids)
const res = await scanApi.bulkApprove(ids, activeDesignId)
const deviceToNode: Record<string, string> = {}
res.data.device_ids.forEach((did, i) => { deviceToNode[did] = res.data.node_ids[i] })
const approvedDevices = devices.filter((d) => ids.includes(d.id))
@@ -213,7 +213,7 @@ describe('PendingDevicesModal', () => {
fireEvent.click(screen.getByTestId('pending-card-dev-a'))
fireEvent.click(screen.getByTestId('pending-card-dev-b'))
fireEvent.click(screen.getByRole('button', { name: /Approve \(2\)/ }))
await waitFor(() => expect(mockBulkApprove).toHaveBeenCalledWith(['dev-a', 'dev-b']))
await waitFor(() => expect(mockBulkApprove).toHaveBeenCalledWith(['dev-a', 'dev-b'], null))
})
it('bulk approve carries the scanned MAC onto the canvas node (#168)', async () => {
@@ -290,7 +290,7 @@ describe('PendingDevicesModal', () => {
fireEvent.click(screen.getByRole('button', { name: 'Select mode' }))
fireEvent.click(screen.getByTestId('pending-card-dev-a'))
fireEvent.keyDown(window, { key: 'Enter' })
await waitFor(() => expect(mockBulkApprove).toHaveBeenCalledWith(['dev-a']))
await waitFor(() => expect(mockBulkApprove).toHaveBeenCalledWith(['dev-a'], null))
expect(mockBulkRestore).not.toHaveBeenCalled()
})