diff --git a/backend/app/api/routes/scan.py b/backend/app/api/routes/scan.py index 67156ea..4178e15 100644 --- a/backend/app/api/routes/scan.py +++ b/backend/app/api/routes/scan.py @@ -225,18 +225,18 @@ async def approve_device( if device.status != "pending": raise HTTPException(status_code=409, detail="Device already processed") device.status = "approved" + _zigbee_types = {"zigbee_coordinator", "zigbee_router", "zigbee_enddevice"} + _is_zigbee = node_data.type in _zigbee_types node = Node( label=node_data.label, type=node_data.type, ip=node_data.ip, hostname=node_data.hostname, - status=node_data.status, + status="online" if _is_zigbee else node_data.status, services=node_data.services or [], ieee_address=device.ieee_address, - # Honour caller-supplied check_method, else default to ping when an IP exists - # so the scheduler doesn't silently skip the new node. - check_method=node_data.check_method or ("ping" if node_data.ip else None), - check_target=node_data.check_target, + check_method="none" if _is_zigbee else (node_data.check_method or ("ping" if node_data.ip else None)), + check_target=None if _is_zigbee else node_data.check_target, ) db.add(node) await db.flush() diff --git a/backend/app/api/routes/zigbee.py b/backend/app/api/routes/zigbee.py index af8c9c9..2afbec3 100644 --- a/backend/app/api/routes/zigbee.py +++ b/backend/app/api/routes/zigbee.py @@ -157,7 +157,8 @@ async def _persist_pending_import( node = Node( label=label, type=n.get("type") or "zigbee_coordinator", - status="unknown", + status="online", + check_method="none", ieee_address=ieee, services=[], ) diff --git a/frontend/src/components/modals/NodeModal.tsx b/frontend/src/components/modals/NodeModal.tsx index da28e01..7e479d1 100644 --- a/frontend/src/components/modals/NodeModal.tsx +++ b/frontend/src/components/modals/NodeModal.tsx @@ -22,6 +22,7 @@ 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 CHECK_METHOD_LABELS: Record = { none: 'None', @@ -59,7 +60,9 @@ interface NodeModalProps { // NodeModal is always mounted with a key that changes on open/edit, so useState // initial value is enough - no need for a reset effect. export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node', parentContainerNodes = [] }: NodeModalProps) { - const [form, setForm] = useState>({ ...DEFAULT_DATA, ...initial }) + const merged = { ...DEFAULT_DATA, ...initial } + if (ZIGBEE_TYPES.includes((merged.type ?? '') as NodeType)) merged.check_method = 'none' + const [form, setForm] = useState>(merged) const [iconSearch, setIconSearch] = useState('') const [iconPickerOpen, setIconPickerOpen] = useState(false) const [iconTab, setIconTab] = useState<'generic' | 'brand'>(isBrandIconKey(initial?.custom_icon) ? 'brand' : 'generic') @@ -107,7 +110,10 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' {/* Type + Icon on the same row */}
- { + const t = v as NodeType + setForm((f) => ({ ...f, type: t, ...(ZIGBEE_TYPES.includes(t) ? { check_method: 'none' as CheckMethod } : {}) })) + }}> {NODE_TYPE_LABELS[(form.type ?? 'server') as NodeType]} @@ -289,31 +295,35 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' comma-separated
- {/* Check method */} -
- - -
+ {/* Check method — hidden for zigbee nodes (always none/online) */} + {!ZIGBEE_TYPES.includes((form.type ?? '') as NodeType) && ( +
+ + +
+ )} - {/* Check target */} -
- - set('check_target', e.target.value)} - placeholder="http://..." - className={`bg-[#21262d] border-[#30363d] font-mono text-sm h-8 ${modalStyles['modal-radius']}`} - /> -
+ {/* Check target — hidden for zigbee nodes */} + {!ZIGBEE_TYPES.includes((form.type ?? '') as NodeType) && ( +
+ + set('check_target', e.target.value)} + placeholder="http://..." + className={`bg-[#21262d] border-[#30363d] font-mono text-sm h-8 ${modalStyles['modal-radius']}`} + /> +
+ )} {/* Parent container */} {form.type !== 'groupRect' && form.type !== 'group' && filteredParentNodes.length > 0 && ( diff --git a/frontend/src/components/modals/__tests__/NodeModal.test.tsx b/frontend/src/components/modals/__tests__/NodeModal.test.tsx index ef32c0d..bcc958c 100644 --- a/frontend/src/components/modals/__tests__/NodeModal.test.tsx +++ b/frontend/src/components/modals/__tests__/NodeModal.test.tsx @@ -433,4 +433,24 @@ describe('NodeModal', () => { const slider = screen.getByLabelText('Bottom connection points slider') as HTMLInputElement expect(slider.value).toBe('48') }) + + // ── Zigbee nodes ────────────────────────────────────────────────────── + + const zigbeeTypes = ['zigbee_coordinator', 'zigbee_router', 'zigbee_enddevice'] as const + + it.each(zigbeeTypes)('hides Check Method for %s type', (type) => { + renderModal({ initial: { ...BASE, type } }) + expect(screen.queryByText('Check Method')).toBeNull() + }) + + it.each(zigbeeTypes)('hides Check Target for %s type', (type) => { + renderModal({ initial: { ...BASE, type } }) + expect(screen.queryByText('Check Target')).toBeNull() + }) + + it.each(zigbeeTypes)('submits check_method=none for %s type', (type) => { + const { onSubmit } = renderModal({ initial: { ...BASE, type, label: 'Zigbee Node' } }) + fireEvent.click(screen.getByRole('button', { name: 'Add' })) + expect((onSubmit.mock.calls[0][0] as Partial).check_method).toBe('none') + }) })