diff --git a/backend/src/routes/devices.ts b/backend/src/routes/devices.ts index fd1644c..3e91794 100644 --- a/backend/src/routes/devices.ts +++ b/backend/src/routes/devices.ts @@ -1873,32 +1873,66 @@ router.post('/:id/tools/capture', requireWrite, async (req: Request, res: Respon }); // POST /api/devices/:id/tools/btest — bandwidth test via RouterOS +// If target_device_id is provided: automatically enable/disable btest server on the target device. +// If only address is provided: manual mode — user must ensure the btest server is already running. router.post('/:id/tools/btest', requireWrite, async (req: Request, res: Response) => { - const { address, direction = 'both', duration = 5, protocol = 'tcp' } = req.body as { - address?: string; direction?: string; duration?: number; protocol?: string; + const { target_device_id, address, direction = 'both', duration = 5, protocol = 'tcp' } = req.body as { + target_device_id?: number; address?: string; direction?: string; duration?: number; protocol?: string; }; - if (!address) return res.status(400).json({ error: 'address is required' }); + if (!target_device_id && !address) { + return res.status(400).json({ error: 'target_device_id or address is required' }); + } const testSec = Math.min(Math.max(1, Number(duration) || 5), 30); const device = await getToolDevice(req.params.id); if (!device) return res.status(404).json({ error: 'Device not found' }); + // Resolve target: either a managed device (auto server management) or a manual IP + let targetAddress = address ?? ''; + let targetUser: string | undefined; + let targetPassword: string | undefined; + let serverClient: RouterOSClient | null = null; + + if (target_device_id) { + const target = await getToolDevice(String(target_device_id)); + if (!target) return res.status(404).json({ error: 'Target device not found' }); + targetAddress = target.ip_address; + targetUser = target.api_username; + targetPassword = decrypt(target.api_password_encrypted); + serverClient = new RouterOSClient( + target.ip_address, target.api_port, target.api_username, targetPassword, 15_000, 20_000 + ); + } + const client = new RouterOSClient( device.ip_address, device.api_port, device.api_username, decrypt(device.api_password_encrypted), 15_000, - (testSec + 10) * 1000 + (testSec + 15) * 1000 ); + try { + // Enable bandwidth-test server on target device before running test + if (serverClient) { + await serverClient.connect(); + await serverClient.execute('/tool/bandwidth-server/set', { enabled: 'yes', authenticate: 'yes' }); + serverClient.disconnect(); + } + await client.connect(); - const rows = await client.executeStreaming('/tool/bandwidth-test', { - address, + const btestParams: Record = { + address: targetAddress, direction: ['receive', 'transmit', 'both'].includes(direction) ? direction : 'both', duration: String(testSec), protocol: ['tcp', 'udp'].includes(protocol) ? protocol : 'tcp', - }, (testSec + 10) * 1000); + }; + // When testing against a managed device, authenticate with its RouterOS credentials + if (targetUser) btestParams['user'] = targetUser; + if (targetPassword) btestParams['password'] = targetPassword; + + const rows = await client.executeStreaming('/tool/bandwidth-test', btestParams, (testSec + 15) * 1000); + client.disconnect(); - // Return the final summary row (last row that has tx/rx total averages) const lastRow = rows.filter((r) => r['tx-total-average'] !== undefined || r['rx-total-average'] !== undefined).at(-1); const toMbps = (v: string | undefined) => v ? Math.round(parseInt(v) / 1_000_000) : 0; @@ -1908,12 +1942,23 @@ router.post('/:id/tools/btest', requireWrite, async (req: Request, res: Response direction, protocol, duration: testSec, + target_ip: targetAddress, raw: lastRow ?? rows.at(-1) ?? null, }); } catch (err) { return res.status(500).json({ error: (err as Error).message }); } finally { client.disconnect(); + // Always disable btest server on target when done (success or failure) + if (serverClient) { + try { + await serverClient.connect(); + await serverClient.execute('/tool/bandwidth-server/set', { enabled: 'no' }); + serverClient.disconnect(); + } catch { + // Best-effort — don't mask the original error + } + } } }); diff --git a/frontend/src/components/device-detail/ToolsTab.tsx b/frontend/src/components/device-detail/ToolsTab.tsx index e30c139..7d8c915 100644 --- a/frontend/src/components/device-detail/ToolsTab.tsx +++ b/frontend/src/components/device-detail/ToolsTab.tsx @@ -473,19 +473,35 @@ function PacketCaptureTool({ deviceId, interfaces }: { deviceId: number; interfa // ─── Bandwidth Test ─────────────────────────────────────────────────────────── function BandwidthTestTool({ deviceId }: { deviceId: number }) { - const [address, setAddress] = useState(''); + // 'managed' = pick a device from inventory; 'manual' = type an IP (server must be pre-enabled) + const [mode, setMode] = useState<'managed' | 'manual'>('managed'); + const [targetDeviceId, setTargetDeviceId] = useState(''); + const [manualAddress, setManualAddress] = useState(''); const [direction, setDirection] = useState('both'); const [duration, setDuration] = useState('5'); const [protocol, setProtocol] = useState('tcp'); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); - const [result, setResult] = useState<{ tx_mbps: number; rx_mbps: number; direction: string; protocol: string; duration: number } | null>(null); + const [result, setResult] = useState<{ tx_mbps: number; rx_mbps: number; direction: string; protocol: string; duration: number; target_ip: string } | null>(null); + + const { data: allDevices = [] } = useQuery({ + queryKey: ['devices'], + queryFn: () => devicesApi.list().then((r) => r.data), + }); + + // Exclude the current device from the target list + const targetDevices = allDevices.filter((d: { id: number; status?: string }) => d.id !== deviceId); + + const canRun = mode === 'managed' ? !!targetDeviceId : !!manualAddress.trim(); const run = async () => { - if (!address) return; + if (!canRun) return; setLoading(true); setError(''); setResult(null); try { - const { data } = await deviceToolsApi.btest(deviceId, { address, direction, duration: parseInt(duration), protocol }); + const body = mode === 'managed' + ? { target_device_id: parseInt(targetDeviceId), direction, duration: parseInt(duration), protocol } + : { address: manualAddress.trim(), direction, duration: parseInt(duration), protocol }; + const { data } = await deviceToolsApi.btest(deviceId, body); setResult(data); } catch (e: unknown) { const msg = (e as { response?: { data?: { error?: string } } })?.response?.data?.error; @@ -498,17 +514,49 @@ function BandwidthTestTool({ deviceId }: { deviceId: number }) { return (

- Runs RouterOS /tool/bandwidth-test to measure throughput between this device and a target IP. - The target must have the bandwidth-test server enabled. + Measures throughput between this device (client) and a target (server). When targeting a managed + device, the bandwidth-test server is automatically enabled and disabled for you.

+ + {/* Mode toggle */} +
+ + +
+
- setAddress(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && run()} - /> + {mode === 'managed' ? ( + + ) : ( + setManualAddress(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && run()} + /> + )}
+ + {mode === 'manual' && ( +

+ Manual mode: ensure the bandwidth-test server is running on the target device before starting. +

+ )} + {loading && (

Running {duration}s bandwidth test — please wait… @@ -534,13 +589,13 @@ function BandwidthTestTool({ deviceId }: { deviceId: number }) { {(result.direction === 'both' || result.direction === 'transmit') && (

{result.tx_mbps} Mbps
-
TX (Upload)
+
TX (Upload to {result.target_ip})
)} {(result.direction === 'both' || result.direction === 'receive') && (
{result.rx_mbps} Mbps
-
RX (Download)
+
RX (Download from {result.target_ip})
)}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 9b63454..6ed9310 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -337,9 +337,9 @@ export const deviceToolsApi = { api.post<{ success: boolean; message: string }>(`/devices/${id}/tools/wol`, body, { timeout: TOOL_TIMEOUT }), capture: (id: number, body: { interface?: string; filter_ip?: string; duration?: number }) => api.post(`/devices/${id}/tools/capture`, body, { timeout: 120_000, responseType: 'blob' }), - btest: (id: number, body: { address: string; direction?: string; duration?: number; protocol?: string }) => - api.post<{ tx_mbps: number; rx_mbps: number; direction: string; protocol: string; duration: number; raw: Record | null }>( - `/devices/${id}/tools/btest`, body, { timeout: 60_000 } + btest: (id: number, body: { target_device_id?: number; address?: string; direction?: string; duration?: number; protocol?: string }) => + api.post<{ tx_mbps: number; rx_mbps: number; direction: string; protocol: string; duration: number; target_ip: string; raw: Record | null }>( + `/devices/${id}/tools/btest`, body, { timeout: 75_000 } ), };