feat: show Z-Wave scan runs in Scan History

Scan History now recognises kind=zwave: dedicated Z-Wave filter chip,
badge (RadioTower, orange) and import-done toast, instead of falling
back to the generic IP type.

ha-relevant: yes
This commit is contained in:
Pouzor
2026-06-26 13:10:12 +02:00
parent 8faf5c1c79
commit 9b8f15bec3
2 changed files with 45 additions and 11 deletions
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { RefreshCw, X, Loader2, StopCircle, Clock, ScanLine, Network, Inbox } from 'lucide-react'
import { RefreshCw, X, Loader2, StopCircle, Clock, ScanLine, Network, RadioTower, Inbox } from 'lucide-react'
import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { scanApi } from '@/api/client'
@@ -22,7 +22,18 @@ interface ScanHistoryModalProps {
onClose: () => void
}
type KindFilter = 'all' | 'ip' | 'zigbee'
type KindFilter = 'all' | 'ip' | 'zigbee' | 'zwave'
/** Normalise a ScanRun.kind into one of the known display kinds. */
function runKind(kind: string | undefined): 'ip' | 'zigbee' | 'zwave' {
return kind === 'zigbee' ? 'zigbee' : kind === 'zwave' ? 'zwave' : 'ip'
}
const KIND_META = {
ip: { label: 'IP', color: '#a855f7' },
zigbee: { label: 'Zigbee', color: '#00d4ff' },
zwave: { label: 'Z-Wave', color: '#ff6e00' },
} as const
type StatusFilter = 'all' | 'running' | 'done' | 'error' | 'cancelled'
const STATUS_FILTERS: { key: StatusFilter; label: string }[] = [
@@ -37,6 +48,7 @@ const KIND_FILTERS: { key: KindFilter; label: string }[] = [
{ key: 'all', label: 'All' },
{ key: 'ip', label: 'IP' },
{ key: 'zigbee', label: 'Zigbee' },
{ key: 'zwave', label: 'Z-Wave' },
]
function statusColor(s: string): string {
@@ -90,8 +102,9 @@ export function ScanHistoryModal({ open, onClose }: ScanHistoryModalProps) {
toast.error(`Scan failed: ${run.error ?? 'unknown error'}`)
}
if (prev?.status === 'running' && run.status === 'done') {
if (run.kind === 'zigbee') {
toast.success(`Zigbee import done — ${run.devices_found} device${run.devices_found !== 1 ? 's' : ''}`)
if (run.kind === 'zigbee' || run.kind === 'zwave') {
const label = run.kind === 'zwave' ? 'Z-Wave' : 'Zigbee'
toast.success(`${label} import done — ${run.devices_found} device${run.devices_found !== 1 ? 's' : ''}`)
}
useCanvasStore.getState().notifyScanDeviceFound()
}
@@ -143,7 +156,7 @@ export function ScanHistoryModal({ open, onClose }: ScanHistoryModalProps) {
}
const filtered = runs.filter((r) => {
const k = r.kind === 'zigbee' ? 'zigbee' : 'ip'
const k = runKind(r.kind)
if (kindFilter !== 'all' && k !== kindFilter) return false
if (statusFilter !== 'all' && r.status !== statusFilter) return false
return true
@@ -216,7 +229,9 @@ export function ScanHistoryModal({ open, onClose }: ScanHistoryModalProps) {
</div>
)}
{filtered.map((r) => {
const isZigbee = r.kind === 'zigbee'
const kind = runKind(r.kind)
const meta = KIND_META[kind]
const KindIcon = kind === 'zigbee' ? Network : kind === 'zwave' ? RadioTower : ScanLine
return (
<div key={r.id} className="rounded-lg border border-border bg-[#161b22] p-3">
<div className="flex items-center gap-2">
@@ -225,12 +240,10 @@ export function ScanHistoryModal({ open, onClose }: ScanHistoryModalProps) {
{r.status === 'running' && <Loader2 size={12} className="animate-spin text-[#e3b341]" />}
<span
className="inline-flex items-center gap-1 text-[10px] font-mono px-1.5 py-0.5 rounded uppercase tracking-wider"
style={isZigbee
? { background: '#00d4ff22', color: '#00d4ff' }
: { background: '#a855f722', color: '#a855f7' }}
style={{ background: `${meta.color}22`, color: meta.color }}
>
{isZigbee ? <Network size={10} /> : <ScanLine size={10} />}
{isZigbee ? 'Zigbee' : 'IP'}
<KindIcon size={10} />
{meta.label}
</span>
<span className="ml-auto text-xs text-muted-foreground font-mono">
{r.devices_found} found
@@ -61,6 +61,17 @@ const ZIGBEE_RUN = {
error: null,
}
const ZWAVE_RUN = {
id: 'run-5',
status: 'done',
kind: 'zwave',
ranges: [],
devices_found: 5,
started_at: new Date().toISOString(),
finished_at: new Date().toISOString(),
error: null,
}
function renderModal() {
return render(
<TooltipProvider>
@@ -155,4 +166,14 @@ describe('ScanHistoryModal', () => {
expect(screen.getByText('7 found')).toBeDefined()
expect(screen.queryByText('3 found')).toBeNull()
})
it('filters by zwave kind', async () => {
vi.mocked(scanApi.runs).mockResolvedValue({ data: [DONE_RUN, ZWAVE_RUN] } as never)
renderModal()
await waitFor(() => expect(screen.getAllByText('done').length).toBe(2))
fireEvent.click(screen.getByRole('button', { name: 'Z-Wave' }))
// Only the zwave run (5 found) remains
expect(screen.getByText('5 found')).toBeDefined()
expect(screen.queryByText('3 found')).toBeNull()
})
})