import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Plus, RefreshCw, Router, Wifi, Trash2, ChevronRight, Search, Radar, ArrowUpCircle } from 'lucide-react'; import { devicesApi, topologyApi } from '../services/api'; import type { Device } from '../types'; import type { DiscoveredDevice } from '../services/api'; import { useCanWrite } from '../hooks/useCanWrite'; import clsx from 'clsx'; import AddDeviceModal from '../components/devices/AddDeviceModal'; function StatusDot({ status }: { status: Device['status'] }) { return ( {status} ); } function DeviceTypeIcon({ type }: { type: Device['device_type'] }) { if (type === 'wireless_ap') return ; return ; } export default function DevicesPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const canWrite = useCanWrite(); const [showAddModal, setShowAddModal] = useState(false); const [addPrefill, setAddPrefill] = useState<{ name?: string; ip_address?: string } | undefined>(); const [search, setSearch] = useState(''); const [deleteConfirm, setDeleteConfirm] = useState(null); const [searching, setSearching] = useState(false); const { data: devices = [], isLoading } = useQuery({ queryKey: ['devices'], queryFn: () => devicesApi.list().then((r) => r.data), refetchInterval: 30_000, }); const { data: discovered = [] } = useQuery({ queryKey: ['devices-discovered'], queryFn: () => devicesApi.discovered().then((r) => r.data), refetchInterval: 60_000, }); const syncMutation = useMutation({ mutationFn: (id: number) => devicesApi.sync(id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['devices'] }), }); const deleteMutation = useMutation({ mutationFn: (id: number) => devicesApi.delete(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['devices'] }); setDeleteConfirm(null); }, }); const filtered = devices.filter( (d) => !search || d.name.toLowerCase().includes(search.toLowerCase()) || d.ip_address.includes(search) || d.model?.toLowerCase().includes(search.toLowerCase()) ); return (

Devices {devices.length > 0 && ( ({devices.filter((d) => d.status === 'online').length}/{devices.length} online) )}

{canWrite && (
)}
{/* Search */}
setSearch(e.target.value)} />
{/* Device list */} {isLoading ? (
Loading...
) : filtered.length === 0 ? (

{search ? 'No devices match your search' : 'No devices added yet'}

{!search && 'Click "Add Device" to connect your first Mikrotik device'}

) : (
{filtered.map((device) => ( navigate(`/devices/${device.id}`)} > ))}
Device IP Address Model ROS Version Status Last Seen Actions
{device.name}
{{ router: 'Router', switch: 'Switch', wireless_ap: 'Wireless AP', other: 'Other' }[device.device_type] ?? device.device_type}
{device.ip_address} {device.model || '—'} {device.ros_version || '—'} {device.firmware_update_available && ( )} {device.last_seen ? new Date(device.last_seen).toLocaleString() : '—'}
e.stopPropagation()} > {canWrite && ( <> {deleteConfirm === device.id ? (
) : ( )} )}
)} {/* Discovered (unmanaged) MikroTik neighbors */} {discovered.length > 0 && (

Discovered Devices

— Mikrotik neighbors seen by your managed devices, not yet added to management
{(discovered as DiscoveredDevice[]).map((d, i) => ( ))}
Identity IP Address MAC Address Seen By Last Seen
{d.identity || '—'} {d.address ? ( d.address ) : ( Not detected — enter manually )} {d.mac_address || '—'} {d.seen_by} {new Date(d.discovered_at).toLocaleString()} {canWrite && ( )}
)} {showAddModal && ( setShowAddModal(false)} onSuccess={() => { setShowAddModal(false); queryClient.invalidateQueries({ queryKey: ['devices'] }); queryClient.invalidateQueries({ queryKey: ['devices-discovered'] }); }} /> )}
); }