Compare commits

..

7 Commits

Author SHA1 Message Date
Pouzor 8541922386 bump: version 2.0.3 2026-05-13 11:37:06 +02:00
Rémy 3ed9cb0d4f Merge pull request #146 from Pouzor/fix/zigbee-no-check-method
fix(zigbee): hide check method in modal, force none/online for zigbee nodes
2026-05-13 11:35:10 +02:00
Pouzor fff11a4b6a fix(zigbee): hide check method in modal, force none/online for zigbee nodes
Zigbee nodes have no IP-based check — hide Check Method and Check Target
fields in NodeModal for all three zigbee types. Default and force
check_method='none' so the scheduler marks them always online.
Backend approve and zigbee import routes also set status='online' and
check_method='none' for zigbee node types.
2026-05-13 11:14:35 +02:00
Pouzor 0680566081 bump: 2.0.2 2026-05-11 19:55:21 +02:00
Remy 2f5a90a00e Merge pull request #142 from Pouzor/fix/visual
Node modal polish + Smart Home and Brand icon pickers
2026-05-11 19:53:48 +02:00
Pouzor 928f63df0f fix(icons): narrow ICON_MAP lookup type for strict build
tsc -b (used in npm run build) flagged TS2774 because LucideIcon is a
function and therefore always truthy. Cast the lookup result to
LucideIcon | undefined so the falsy branch becomes meaningful.
2026-05-11 19:44:20 +02:00
Pouzor e84a4e0eb3 merge feat/brand-icons into fix/visual 2026-05-11 19:20:10 +02:00
8 changed files with 68 additions and 37 deletions
+1 -1
View File
@@ -1 +1 @@
2.0.1
2.0.3
+5 -5
View File
@@ -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()
+2 -1
View File
@@ -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=[],
)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "frontend",
"version": "2.0.1",
"version": "2.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frontend",
"version": "1.13.0",
"version": "2.0.3",
"dependencies": {
"@base-ui/react": "^1.2.0",
"@dagrejs/dagre": "^2.0.4",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "2.0.1",
"version": "2.0.3",
"type": "module",
"scripts": {
"dev": "vite",
+36 -26
View File
@@ -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<CheckMethod, string> = {
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<Partial<NodeData>>({ ...DEFAULT_DATA, ...initial })
const merged = { ...DEFAULT_DATA, ...initial }
if (ZIGBEE_TYPES.includes((merged.type ?? '') as NodeType)) merged.check_method = 'none'
const [form, setForm] = useState<Partial<NodeData>>(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 */}
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Type</Label>
<Select value={form.type} onValueChange={(v) => set('type', v as NodeType)}>
<Select value={form.type} onValueChange={(v) => {
const t = v as NodeType
setForm((f) => ({ ...f, type: t, ...(ZIGBEE_TYPES.includes(t) ? { check_method: 'none' as CheckMethod } : {}) }))
}}>
<SelectTrigger className={`bg-[#21262d] border-[#30363d] text-sm h-8 w-full cursor-pointer ${modalStyles['modal-interactive']} ${modalStyles['modal-radius']}`} aria-label="Node type selector">
<SelectValue>{NODE_TYPE_LABELS[(form.type ?? 'server') as NodeType]}</SelectValue>
</SelectTrigger>
@@ -289,31 +295,35 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
<span className="text-[10px] text-muted-foreground/50">comma-separated</span>
</div>
{/* Check method */}
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Check Method</Label>
<Select value={form.check_method ?? 'ping'} onValueChange={(v) => set('check_method', v as CheckMethod)}>
<SelectTrigger className={`bg-[#21262d] border-[#30363d] text-sm h-8 cursor-pointer ${modalStyles['modal-interactive']} ${modalStyles['modal-radius']}`} aria-label="Check method selector">
<SelectValue>{CHECK_METHOD_LABELS[(form.check_method ?? 'ping') as CheckMethod]}</SelectValue>
</SelectTrigger>
<SelectContent className="bg-[#21262d] border-[#30363d]">
{CHECK_METHODS.map((m) => (
<SelectItem key={m} value={m} className="text-sm">{CHECK_METHOD_LABELS[m]}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Check method — hidden for zigbee nodes (always none/online) */}
{!ZIGBEE_TYPES.includes((form.type ?? '') as NodeType) && (
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Check Method</Label>
<Select value={form.check_method ?? 'ping'} onValueChange={(v) => set('check_method', v as CheckMethod)}>
<SelectTrigger className={`bg-[#21262d] border-[#30363d] text-sm h-8 cursor-pointer ${modalStyles['modal-interactive']} ${modalStyles['modal-radius']}`} aria-label="Check method selector">
<SelectValue>{CHECK_METHOD_LABELS[(form.check_method ?? 'ping') as CheckMethod]}</SelectValue>
</SelectTrigger>
<SelectContent className="bg-[#21262d] border-[#30363d]">
{CHECK_METHODS.map((m) => (
<SelectItem key={m} value={m} className="text-sm">{CHECK_METHOD_LABELS[m]}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* Check target */}
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Check Target</Label>
<Input
value={form.check_target ?? ''}
onChange={(e) => set('check_target', e.target.value)}
placeholder="http://..."
className={`bg-[#21262d] border-[#30363d] font-mono text-sm h-8 ${modalStyles['modal-radius']}`}
/>
</div>
{/* Check target — hidden for zigbee nodes */}
{!ZIGBEE_TYPES.includes((form.type ?? '') as NodeType) && (
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Check Target</Label>
<Input
value={form.check_target ?? ''}
onChange={(e) => set('check_target', e.target.value)}
placeholder="http://..."
className={`bg-[#21262d] border-[#30363d] font-mono text-sm h-8 ${modalStyles['modal-radius']}`}
/>
</div>
)}
{/* Parent container */}
{form.type !== 'groupRect' && form.type !== 'group' && filteredParentNodes.length > 0 && (
@@ -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<NodeData>).check_method).toBe('none')
})
})
+1 -1
View File
@@ -218,6 +218,6 @@ export function resolveCustomIcon(customIconKey?: string): ResolvedIcon | null {
const slug = brandIconSlug(customIconKey)
return { kind: 'brand', slug, url: brandIconUrl(slug) }
}
const icon = ICON_MAP[customIconKey]
const icon = ICON_MAP[customIconKey] as LucideIcon | undefined
return icon ? { kind: 'lucide', icon } : null
}