feat(zigbee): import to pending section with edge persistence

Coordinator auto-approves to a canvas Node; routers/end devices land in
pending_devices keyed by IEEE. Discovered parent->child edges are stored
in pending_device_links so that approving a pending device later
auto-creates the Edge once both endpoints exist as canvas Nodes.

- new POST /api/v1/zigbee/import-pending (default mode in modal)
- new pending_device_links table; ieee_address on nodes + pending_devices
- pending_devices.ip migrated to nullable (table rebuild on existing DBs)
- approve / bulk-approve return auto-created edges; sidebar pushes them
  into the canvas store with bottom -> top-t handles
- ZigbeeImportModal: radio toggle pending vs canvas; reset on close
- PendingDeviceModal: zigbee badge, IEEE/LQI/vendor/model rows, services
  hidden for zigbee
- Sidebar pending row: ZIG source badge, LQI badge, friendly_name fallback
- SearchBar: null-safe IP, also searches friendly_name and ieee_address
- Tooltip trigger uses asChild to avoid nested-button hydration error
This commit is contained in:
Pouzor
2026-05-07 23:02:03 +02:00
parent 5e567d4628
commit 3ae159d8d6
15 changed files with 988 additions and 40 deletions
+5 -3
View File
@@ -57,8 +57,10 @@ export function SearchBar({ onOpenPending }: SearchBarProps) {
const pendingResults = q
? pendingDevices.filter((d) =>
d.ip.toLowerCase().includes(q) ||
d.ip?.toLowerCase().includes(q) ||
d.hostname?.toLowerCase().includes(q) ||
d.friendly_name?.toLowerCase().includes(q) ||
d.ieee_address?.toLowerCase().includes(q) ||
d.services.some((s) =>
s.service_name?.toLowerCase().includes(q) ||
s.category?.toLowerCase().includes(q)
@@ -196,10 +198,10 @@ export function SearchBar({ onOpenPending }: SearchBarProps) {
>
<span style={{ fontSize: 10, color: '#e3b341', fontFamily: 'JetBrains Mono, monospace', flexShrink: 0 }}>pending</span>
<span style={{ fontSize: 12, fontWeight: 600, color: '#e6edf3', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{d.hostname ?? d.ip}
{d.friendly_name ?? d.hostname ?? d.ip ?? d.ieee_address ?? 'device'}
</span>
<span style={{ fontSize: 11, color: '#8b949e', fontFamily: 'JetBrains Mono, monospace', flexShrink: 0 }}>
{serviceName ?? d.ip}
{serviceName ?? d.ip ?? d.ieee_address ?? ''}
</span>
</button>
)