From 448736bc035cd2d46b68df2252a50f7cd5ec3a1e Mon Sep 17 00:00:00 2001 From: vanne Date: Wed, 1 Jul 2026 14:53:33 +0700 Subject: [PATCH] Fix devices bandwidth and MAC descriptors --- backend/database.js | 188 ++++++++++-------------- backend/netify.js | 4 +- backend/tests/test_devices_bandwidth.js | 74 ++++++++++ src/app/(dashboard)/devices/page.tsx | 22 ++- 4 files changed, 171 insertions(+), 117 deletions(-) create mode 100644 backend/tests/test_devices_bandwidth.js diff --git a/backend/database.js b/backend/database.js index 99c0d53..b209ee4 100644 --- a/backend/database.js +++ b/backend/database.js @@ -883,125 +883,99 @@ function getLatestDevices(limit = 100, siteUuid = null, agentUuid = null) { const intelList = d.prepare("SELECT * FROM intel_device_discovery").all(); const intelMap = new Map(intelList.map(i => [i.ip_address, i])); - if (agentUuid && AGENT_MAC_MAP[agentUuid]) { - const macs = AGENT_MAC_MAP[agentUuid]; - const placeholders = macs.map(() => '?').join(','); - - // Find latest fetched_at in flows - const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); - - // Get unique flow client IPs with bandwidth aggregation - let flowClients = []; - if (latestFlowFetch?.t) { - flowClients = d.prepare(` - SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload - FROM flows - WHERE src_mac IN (${placeholders}) AND fetched_at = ? - GROUP BY src_ip - ORDER BY flow_download DESC - `).all(...macs, latestFlowFetch.t); - } + // Get all devices in latest snapshot from devices table + const devices = d.prepare(`SELECT * FROM devices WHERE fetched_at = ?`).all(latest.t); - // Get all devices in latest snapshot - const devices = d.prepare(`SELECT * FROM devices WHERE fetched_at = ?`).all(latest.t); - const devicesMap = new Map(devices.map(dev => [dev.ip_address, dev])); - - const resolved = []; - const seenIps = new Set(); - - // First, add all devices that are explicitly in flows (active) - for (const fc of flowClients) { - if (!fc.src_ip) continue; - seenIps.add(fc.src_ip); - - const devInfo = devicesMap.get(fc.src_ip); - const intelInfo = intelMap.get(fc.src_ip); - - const dbLabel = devInfo ? devInfo.device_label : (intelInfo ? intelInfo.device_label : null); - const dbMan = devInfo ? devInfo.manufacturer : (intelInfo ? intelInfo.manufacturer : null); - const dbType = intelInfo ? intelInfo.device_type : null; - - const meta = resolveDeviceMetadata(fc.src_ip, fc.src_mac, dbLabel, dbMan, dbType); - - const isRouted = fc.src_mac === '04:f4:1c:ce:c2:e6' && fc.src_ip !== '10.6.50.25' && fc.src_ip !== '10.6.12.242'; - - resolved.push({ - id: devInfo ? devInfo.id : null, - site_uuid: devInfo ? devInfo.site_uuid : siteUuid, - fetched_at: latest.t, - mac_address: fc.src_mac, - ip_address: fc.src_ip, - device_label: meta.label, - device_type: meta.type, - os_label: meta.os, - manufacturer: meta.manufacturer, - download: fc.flow_download, - upload: fc.flow_upload, - total: fc.flow_download + fc.flow_upload, - is_gateway_routed: isRouted ? 1 : 0 - }); - } - - // Next, add other devices in devices table belonging to the agent's subnet (e.g. inactive/idle) - for (const dev of devices) { - if (seenIps.has(dev.ip_address)) continue; - - // Check if this device belongs to the agent - let isAgentDevice = macs.includes(dev.mac_address); - if (!isAgentDevice && dev.ip_address) { - const devOctets = dev.ip_address.split('.'); - if (devOctets.length === 4) { - const prefix3 = devOctets.slice(0, 3).join('.'); - isAgentDevice = flowClients.some(fc => fc.src_ip && fc.src_ip.startsWith(prefix3)); - } - } - - if (isAgentDevice) { - const intelInfo = intelMap.get(dev.ip_address); - const meta = resolveDeviceMetadata(dev.ip_address, dev.mac_address, dev.device_label, dev.manufacturer, intelInfo ? intelInfo.device_type : null); - - const isRouted = dev.mac_address === '04:f4:1c:ce:c2:e6' && dev.ip_address !== '10.6.50.25' && dev.ip_address !== '10.6.12.242'; - - resolved.push({ - ...dev, - device_label: meta.label, - device_type: meta.type, - os_label: meta.os, - manufacturer: meta.manufacturer, - download: dev.download || 0, - upload: dev.upload || 0, - total: dev.total || 0, - is_gateway_routed: isRouted ? 1 : 0 - }); - } - } - - // Sort by download desc - resolved.sort((a, b) => b.download - a.download); - return resolved.slice(0, limit); + // Get active flow bandwidth in latest flows snapshot + const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); + let flowsBandwidth = []; + if (latestFlowFetch?.t) { + flowsBandwidth = d.prepare(` + SELECT src_ip, src_mac, SUM(bytes_download) as flow_download, SUM(bytes_upload) as flow_upload + FROM flows + WHERE fetched_at = ? + GROUP BY src_ip + `).all(latestFlowFetch.t); } + const flowMap = new Map(flowsBandwidth.map(f => [f.src_ip, f])); - // Admin view: apply metadata parsing and gateway routed check for all devices - const rows = d.prepare(` - SELECT * FROM devices WHERE (@siteUuid IS NULL OR site_uuid = @siteUuid) AND fetched_at = @fetched_at ORDER BY download DESC LIMIT @limit - `).all({ fetched_at: latest.t, limit, siteUuid }); + const resolved = []; + const seenIps = new Set(); - return rows.map(dev => { - const intelInfo = intelMap.get(dev.ip_address); - const meta = resolveDeviceMetadata(dev.ip_address, dev.mac_address, dev.device_label, dev.manufacturer, intelInfo ? intelInfo.device_type : null); - - const isRouted = dev.mac_address === '04:f4:1c:ce:c2:e6' && dev.ip_address !== '10.6.50.25' && dev.ip_address !== '10.6.12.242'; + function processDevice(ip, mac, dbDev, flowInfo) { + const intelInfo = intelMap.get(ip); + const dbLabel = dbDev ? dbDev.device_label : (intelInfo ? intelInfo.device_label : null); + const dbMan = dbDev ? dbDev.manufacturer : (intelInfo ? intelInfo.manufacturer : null); + const dbType = intelInfo ? intelInfo.device_type : null; + + const meta = resolveDeviceMetadata(ip, mac, dbLabel, dbMan, dbType); + const isRouted = mac === '04:f4:1c:ce:c2:e6' && ip !== '10.6.50.25' && ip !== '10.6.12.242'; + + const download = flowInfo ? flowInfo.flow_download : (dbDev ? dbDev.download : 0); + const upload = flowInfo ? flowInfo.flow_upload : (dbDev ? dbDev.upload : 0); return { - ...dev, - mac_address: dev.mac_address, + id: dbDev ? dbDev.id : null, + site_uuid: dbDev ? dbDev.site_uuid : siteUuid, + fetched_at: latest.t, + mac_address: mac, + ip_address: ip, device_label: meta.label, device_type: meta.type, os_label: meta.os, manufacturer: meta.manufacturer, + download: download || 0, + upload: upload || 0, + total: (download || 0) + (upload || 0), is_gateway_routed: isRouted ? 1 : 0 }; - }); + } + + // 1. Process all devices in the devices table + for (const dev of devices) { + seenIps.add(dev.ip_address); + const flowInfo = flowMap.get(dev.ip_address); + resolved.push(processDevice(dev.ip_address, dev.mac_address, dev, flowInfo)); + } + + // 2. Process any active flow IPs that are NOT present in the devices table (e.g. dynamic client IPs) + for (const flow of flowsBandwidth) { + if (!flow.src_ip || seenIps.has(flow.src_ip)) continue; + seenIps.add(flow.src_ip); + resolved.push(processDevice(flow.src_ip, flow.src_mac, null, flow)); + } + + // 3. Filter list based on role (Admin vs Agent) + let filtered = resolved; + if (agentUuid && AGENT_MAC_MAP[agentUuid]) { + const macs = AGENT_MAC_MAP[agentUuid]; + filtered = resolved.filter(dev => { + // Direct MAC check + if (macs.includes(dev.mac_address)) { + // But if it is the routed gateway MAC, only keep if it belongs to the agent's subnet/IP range + if (dev.mac_address === '04:f4:1c:ce:c2:e6') { + return dev.ip_address.startsWith('10.6.'); + } + return true; + } + // Subnet check for client IPs + if (dev.ip_address) { + const octets = dev.ip_address.split('.'); + if (octets.length === 4) { + if (agentUuid === '8A-V3-PB-85') return octets[0] === '10' && octets[1] === '6'; + if (agentUuid === '2F-TF-1D-GK') return octets[0] === '10' && octets[1] === '6'; + if (agentUuid === 'F6-2V-DT-8A') return octets[0] === '10' && octets[1] === '6'; + } + } + return false; + }); + } else if (siteUuid) { + filtered = resolved.filter(dev => dev.site_uuid === siteUuid); + } + + // 4. Sort by download DESC + filtered.sort((a, b) => b.download - a.download); + return filtered.slice(0, limit); } function getLatestFlows(limit = 100, siteUuid = null, agentUuid = null) { diff --git a/backend/netify.js b/backend/netify.js index 077e86e..521f542 100644 --- a/backend/netify.js +++ b/backend/netify.js @@ -1756,8 +1756,8 @@ async function fetchDeviceDetails(ip) { return { ip, mac_address : mac, - total_download : sumRow?.total_download ?? 0, - total_upload : sumRow?.total_upload ?? 0, + total_download : deviceRow?.download || sumRow?.total_download || 0, + total_upload : deviceRow?.upload || sumRow?.total_upload || 0, flow_count : sumRow?.flow_count ?? 0, device_info, top_apps, diff --git a/backend/tests/test_devices_bandwidth.js b/backend/tests/test_devices_bandwidth.js new file mode 100644 index 0000000..c3bd696 --- /dev/null +++ b/backend/tests/test_devices_bandwidth.js @@ -0,0 +1,74 @@ +const { getLatestDevices } = require('../database'); + +function runTest() { + console.log('=== STARTING TDD TEST FOR DEVICES BANDWIDTH ==='); + + // Test 1: Admin View + console.log('\nRunning Test 1: Admin View...'); + const adminDevices = getLatestDevices(100, null, null); + if (!Array.isArray(adminDevices)) { + throw new Error('Admin devices should be an array'); + } + console.log(`- Retrieved ${adminDevices.length} devices for Admin`); + + // Verify descending sort + for (let i = 1; i < adminDevices.length; i++) { + if (adminDevices[i].download > adminDevices[i-1].download) { + throw new Error(`Admin devices not sorted correctly at index ${i}: ${adminDevices[i].download} > ${adminDevices[i-1].download}`); + } + } + console.log('- Verified sort order is descending'); + + // Find any active reconstructed flow client (which has null id because it is not in the devices table) + const activeReconstructed = adminDevices.find(d => d.id === null && d.download > 0 && d.ip_address.startsWith('10.6.')); + if (!activeReconstructed) { + throw new Error('Should find at least one active reconstructed flow client (IP starting with 10.6. and id = null) in Admin view'); + } + console.log(`- Active reconstructed device ${activeReconstructed.ip_address} has download: ${activeReconstructed.download} bytes (OK)`); + + // Verify specific historical device (e.g., 10.250.192.202) has non-zero fallback bandwidth + const historicalDevice = adminDevices.find(d => d.ip_address === '10.250.192.202'); + if (!historicalDevice) { + throw new Error('Historical device 10.250.192.202 should be present in Admin view'); + } + if (historicalDevice.download === 0) { + throw new Error(`Historical device 10.250.192.202 should have non-zero fallback download, got ${historicalDevice.download}`); + } + console.log(`- Historical device 10.250.192.202 has download: ${historicalDevice.download} bytes (OK)`); + + // Verify no duplicate IP addresses + const seenIps = new Set(); + for (const d of adminDevices) { + if (seenIps.has(d.ip_address)) { + throw new Error(`Duplicate IP address found: ${d.ip_address}`); + } + seenIps.add(d.ip_address); + } + console.log('- Verified no duplicate IP addresses exist in the output'); + + // Test 2: Agent View + console.log('\nRunning Test 2: Agent View...'); + const agentUuid = '8A-V3-PB-85'; // IFG LT.18 + const agentDevices = getLatestDevices(100, null, agentUuid); + if (!Array.isArray(agentDevices)) { + throw new Error('Agent devices should be an array'); + } + console.log(`- Retrieved ${agentDevices.length} devices for Agent ${agentUuid}`); + + // Verify only agent devices are returned (IP starts with 10.6.) + for (const d of agentDevices) { + if (d.ip_address && !d.ip_address.startsWith('10.6.')) { + throw new Error(`Non-agent device IP ${d.ip_address} found in Agent view`); + } + } + console.log('- Verified all returned devices belong to the agent\'s subnet (10.6.x.x)'); + + console.log('\n=== ALL TESTS PASSED SUCCESSFULLY! ==='); +} + +try { + runTest(); +} catch (error) { + console.error('\n❌ TEST FAILED:', error.message); + process.exit(1); +} diff --git a/src/app/(dashboard)/devices/page.tsx b/src/app/(dashboard)/devices/page.tsx index dbef73b..6305e9d 100644 --- a/src/app/(dashboard)/devices/page.tsx +++ b/src/app/(dashboard)/devices/page.tsx @@ -38,21 +38,27 @@ export default function DevicesPage() { }, { header: "MAC Address", - accessor: (row) => - row.mac_address ? ( -
- + accessor: (row) => { + const isRouted = row.is_gateway_routed === 1; + const hasMan = row.manufacturer && row.manufacturer !== "-" && row.manufacturer !== "Unknown"; + const info = hasMan ? row.manufacturer : (row.device_type && row.device_type !== "-" && row.device_type !== "Generic Client" ? row.device_type : ""); + + return row.mac_address ? ( +
+ {row.mac_address} - {row.is_gateway_routed === 1 && ( - - Via Routed Gateway + {(isRouted || info) && ( + + {isRouted ? "Via Routed Gateway" : ""} + {isRouted && info ? ` • ${info}` : (!isRouted ? info : "")} )}
) : ( "-" - ), + ); + }, }, { header: "Type", accessor: (row) => row.device_type || "-" }, { header: "OS", accessor: (row) => row.os_label || "-" },