diff --git a/frontend/src/components/modals/NodeModal.tsx b/frontend/src/components/modals/NodeModal.tsx index f7e6b95..246a314 100644 --- a/frontend/src/components/modals/NodeModal.tsx +++ b/frontend/src/components/modals/NodeModal.tsx @@ -18,6 +18,7 @@ const NODE_TYPE_GROUPS: { label: string; types: NodeType[] }[] = [ { label: 'Virtualization', types: ['proxmox', 'vm', 'lxc', 'docker_host', 'docker_container'] }, { label: 'IoT', types: ['iot', 'camera', 'cpl'] }, { label: 'Zigbee', types: ['zigbee_coordinator', 'zigbee_router', 'zigbee_enddevice'] }, + { label: 'Z-Wave', types: ['zwave_coordinator', 'zwave_router', 'zwave_enddevice'] }, { label: 'Personal', types: ['computer', 'laptop', 'mobile'] }, { label: 'Electrical', types: ['grid', 'ups', 'battery', 'generator', 'solar_panel', 'inverter', 'circuit_breaker', 'contactor', 'electrical_switch', 'socket', 'light', 'meter', 'transformer', 'load'] }, { label: 'Generic', types: ['generic', 'groupRect'] }, @@ -26,6 +27,9 @@ const NODE_TYPE_GROUPS: { label: string; types: NodeType[] }[] = [ const CHECK_METHODS: CheckMethod[] = ['none', 'ping', 'http', 'https', 'tcp', 'ssh', 'prometheus', 'health'] const CONTAINER_MODE_TYPES: NodeType[] = ['proxmox', 'vm', 'lxc', 'docker_host'] const ZIGBEE_TYPES: NodeType[] = ['zigbee_coordinator', 'zigbee_router', 'zigbee_enddevice'] +const ZWAVE_TYPES: NodeType[] = ['zwave_coordinator', 'zwave_router', 'zwave_enddevice'] +// Mesh radio devices aren't IP-reachable, so they default to no status check. +const MESH_TYPES: NodeType[] = [...ZIGBEE_TYPES, ...ZWAVE_TYPES] const CHECK_METHOD_LABELS: Record = { none: 'None', @@ -73,7 +77,7 @@ interface NodeModalProps { // initial value is enough - no need for a reset effect. export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node', parentCandidates = [], currentNodeId }: NodeModalProps) { const merged = { ...DEFAULT_DATA, ...initial } - if (ZIGBEE_TYPES.includes((merged.type ?? '') as NodeType)) merged.check_method = 'none' + if (MESH_TYPES.includes((merged.type ?? '') as NodeType)) merged.check_method = 'none' const [form, setForm] = useState>(merged) const [iconSearch, setIconSearch] = useState('') const [iconPickerOpen, setIconPickerOpen] = useState(false) @@ -133,7 +137,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' const t = v as NodeType setForm((f) => { const next: Partial = { ...f, type: t } - if (ZIGBEE_TYPES.includes(t)) next.check_method = 'none' as CheckMethod + if (MESH_TYPES.includes(t)) next.check_method = 'none' as CheckMethod // Drop the parent only if it's no longer a valid target for the // new type — keep container-mode parents (any node can nest). const parent = parentCandidates.find((n) => n.id === f.parent_id) diff --git a/frontend/src/components/modals/__tests__/NodeModal.test.tsx b/frontend/src/components/modals/__tests__/NodeModal.test.tsx index 9f187ba..be4c81c 100644 --- a/frontend/src/components/modals/__tests__/NodeModal.test.tsx +++ b/frontend/src/components/modals/__tests__/NodeModal.test.tsx @@ -188,6 +188,20 @@ describe('NodeModal', () => { expect((onSubmit.mock.calls[0][0] as Partial).type).toBe('nas') }) + it('offers Z-Wave node types and submits one', () => { + const { onSubmit } = renderModal({ initial: BASE }) + fireEvent.change(selects()[0], { target: { value: 'zwave_enddevice' } }) + fireEvent.click(screen.getByRole('button', { name: 'Add' })) + expect((onSubmit.mock.calls[0][0] as Partial).type).toBe('zwave_enddevice') + }) + + it('forces check_method to none when a Z-Wave type is selected', () => { + const { onSubmit } = renderModal({ initial: { ...BASE, check_method: 'ping' } }) + fireEvent.change(selects()[0], { target: { value: 'zwave_coordinator' } }) + fireEvent.click(screen.getByRole('button', { name: 'Add' })) + expect((onSubmit.mock.calls[0][0] as Partial).check_method).toBe('none') + }) + // ── Check method ────────────────────────────────────────────────────── it('pre-fills check_method from initial', () => {