feat: non-blocking scan with progressive device discovery
Backend:
- asyncio.to_thread(_nmap_scan) — nmap no longer blocks the event loop
- Commit each discovered device immediately (previously one bulk commit at end)
- Update ScanRun.devices_found after each device so history panel shows live count
- broadcast_scan_update() pushes {type: scan_device_found} WS event per device
- Refactor broadcast_status to shared _broadcast() helper, adds type: "status" field
Frontend:
- useStatusPolling handles both WS message types (status / scan_device_found)
- canvasStore: scanEventTs + notifyScanDeviceFound() action
- PendingDevicesPanel auto-refreshes when WS scan event is received
This commit is contained in:
@@ -150,7 +150,7 @@ export function Sidebar({ onAddNode, onScan, onSave }: SidebarProps) {
|
||||
function PendingDevicesPanel() {
|
||||
const [devices, setDevices] = useState<PendingDevice[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { addNode } = useCanvasStore()
|
||||
const { addNode, scanEventTs } = useCanvasStore()
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
@@ -165,7 +165,12 @@ function PendingDevicesPanel() {
|
||||
}, [])
|
||||
|
||||
// Load on mount
|
||||
useState(() => { load() })
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
// Auto-refresh when the backend pushes a scan_device_found event
|
||||
useEffect(() => {
|
||||
if (scanEventTs > 0) load()
|
||||
}, [scanEventTs, load])
|
||||
|
||||
const handleApprove = async (device: PendingDevice) => {
|
||||
try {
|
||||
|
||||
@@ -3,15 +3,18 @@ import { useCanvasStore } from '@/stores/canvasStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
interface StatusMessage {
|
||||
node_id: string
|
||||
status: 'online' | 'offline' | 'pending' | 'unknown'
|
||||
checked_at: string
|
||||
response_time_ms: number | null
|
||||
type?: string
|
||||
node_id?: string
|
||||
status?: 'online' | 'offline' | 'pending' | 'unknown'
|
||||
checked_at?: string
|
||||
response_time_ms?: number | null
|
||||
run_id?: string
|
||||
devices_found?: number
|
||||
}
|
||||
|
||||
export function useStatusPolling() {
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const { updateNode } = useCanvasStore()
|
||||
const { updateNode, notifyScanDeviceFound } = useCanvasStore()
|
||||
const { isAuthenticated, token } = useAuthStore()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,11 +30,15 @@ export function useStatusPolling() {
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg: StatusMessage = JSON.parse(event.data)
|
||||
updateNode(msg.node_id, {
|
||||
status: msg.status,
|
||||
response_time_ms: msg.response_time_ms ?? undefined,
|
||||
last_seen: msg.status === 'online' ? msg.checked_at : undefined,
|
||||
})
|
||||
if (msg.type === 'scan_device_found') {
|
||||
notifyScanDeviceFound()
|
||||
} else if (msg.node_id && msg.status) {
|
||||
updateNode(msg.node_id, {
|
||||
status: msg.status,
|
||||
response_time_ms: msg.response_time_ms ?? undefined,
|
||||
last_seen: msg.status === 'online' ? msg.checked_at : undefined,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed messages
|
||||
}
|
||||
@@ -45,5 +52,5 @@ export function useStatusPolling() {
|
||||
ws.close()
|
||||
wsRef.current = null
|
||||
}
|
||||
}, [isAuthenticated, token, updateNode])
|
||||
}, [isAuthenticated, token, updateNode, notifyScanDeviceFound])
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface CanvasState {
|
||||
edges: Edge<EdgeData>[]
|
||||
hasUnsavedChanges: boolean
|
||||
selectedNodeId: string | null
|
||||
scanEventTs: number
|
||||
|
||||
onNodesChange: (changes: NodeChange<Node<NodeData>>[]) => void
|
||||
onEdgesChange: (changes: EdgeChange<Edge<EdgeData>>[]) => void
|
||||
@@ -29,6 +30,7 @@ interface CanvasState {
|
||||
setProxmoxContainerMode: (proxmoxId: string, enabled: boolean) => void
|
||||
markSaved: () => void
|
||||
loadCanvas: (nodes: Node<NodeData>[], edges: Edge<EdgeData>[]) => void
|
||||
notifyScanDeviceFound: () => void
|
||||
}
|
||||
|
||||
export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
@@ -36,6 +38,7 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
edges: [],
|
||||
hasUnsavedChanges: false,
|
||||
selectedNodeId: null,
|
||||
scanEventTs: 0,
|
||||
|
||||
onNodesChange: (changes) =>
|
||||
set((state) => ({
|
||||
@@ -126,6 +129,8 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
|
||||
markSaved: () => set({ hasUnsavedChanges: false }),
|
||||
|
||||
notifyScanDeviceFound: () => set({ scanEventTs: Date.now() }),
|
||||
|
||||
loadCanvas: (nodes, edges) => {
|
||||
// React Flow requires parents before children in the array
|
||||
const parents = nodes.filter((n) => !n.parentId)
|
||||
|
||||
Reference in New Issue
Block a user