diff --git a/backend/database.js b/backend/database.js index f891bef..c7c4ec4 100644 --- a/backend/database.js +++ b/backend/database.js @@ -1582,21 +1582,113 @@ function getLatestProtocols(limit = 20, siteUuid = null, agentUuid = null) { function getLatestCountries(limit = 15, siteUuid = null, agentUuid = null) { const d = getDB(); - const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM intel_ip_reputation`).get(); - if (!latest?.t) return []; if (agentUuid && AGENT_MAC_MAP[agentUuid]) { const macs = AGENT_MAC_MAP[agentUuid]; const placeholders = macs.map(() => '?').join(','); - return d.prepare(` - SELECT country AS country_name, SUM(download) AS download, SUM(upload) AS upload, COUNT(*) AS flow_count - FROM intel_ip_reputation - WHERE (mac_address IN (${placeholders}) OR local_ip IN (SELECT DISTINCT src_ip FROM flows WHERE src_mac IN (${placeholders}))) - AND country IS NOT NULL AND fetched_at = ? - GROUP BY country + + const rows = d.prepare(` + SELECT g.country AS country_name, + SUM(f.bytes_download) AS download, + SUM(f.bytes_upload) AS upload, + COUNT(*) AS flow_count + FROM ( + SELECT dst_ip, bytes_download, bytes_upload + FROM flows + WHERE src_mac IN (${placeholders}) + ORDER BY last_seen DESC, fetched_at DESC + LIMIT 500 + ) f + JOIN geoip_cache g ON f.dst_ip = g.ip_address + WHERE g.country IS NOT NULL + AND g.country != 'Local' + GROUP BY g.country ORDER BY download DESC LIMIT ? - `).all(...macs, ...macs, latest.t, limit); + `).all(...macs, limit); + + // Build dynamic name-to-code mapping from bandwidth_countries + const nameToCodeMap = {}; + try { + const mappingRows = d.prepare("SELECT DISTINCT country_code, country_name FROM bandwidth_countries WHERE country_code IS NOT NULL").all(); + for (const r of mappingRows) { + if (r.country_name) { + nameToCodeMap[r.country_name.toLowerCase().trim()] = r.country_code; + } + } + } catch (err) { + console.error('[DB] Failed to build country code mapping:', err); + } + + // Fallback/standard mapping + const fallbackMap = { + 'indonesia': 'ID', + 'united states': 'US', + 'united kingdom': 'GB', + 'great britain': 'GB', + 'singapore': 'SG', + 'hong kong': 'HK', + 'japan': 'JP', + 'canada': 'CA', + 'australia': 'AU', + 'germany': 'DE', + 'france': 'FR', + 'india': 'IN', + 'china': 'CN', + 'south korea': 'KR', + 'russia': 'RU', + 'russian federation': 'RU', + 'brazil': 'BR', + 'italy': 'IT', + 'spain': 'ES', + 'netherlands': 'NL', + 'the netherlands': 'NL', + 'switzerland': 'CH', + 'sweden': 'SE', + 'norway': 'NO', + 'finland': 'FI', + 'denmark': 'DK', + 'ireland': 'IE', + 'belgium': 'BE', + 'austria': 'AT', + 'malaysia': 'MY', + 'thailand': 'TH', + 'vietnam': 'VN', + 'philippines': 'PH', + 'taiwan': 'TW', + 'new zealand': 'NZ', + 'south africa': 'ZA', + 'mexico': 'MX', + 'argentina': 'AR', + 'chile': 'CL', + 'colombia': 'CO', + 'turkey': 'TR', + 'saudi arabia': 'SA', + 'united arab emirates': 'AE', + 'egypt': 'EG', + 'israel': 'IL', + 'ukraine': 'UA', + 'poland': 'PL', + 'romania': 'RO', + 'greece': 'GR', + 'hungary': 'HU', + 'bangladesh': 'BD', + 'seychelles': 'SC', + 'luxembourg': 'LU', + 'pakistan': 'PK' + }; + + return rows.map(r => { + const normalizedName = r.country_name.toLowerCase().trim(); + const code = nameToCodeMap[normalizedName] || fallbackMap[normalizedName] || null; + return { + country_code: code, + country_name: r.country_name, + download: r.download ?? 0, + upload: r.upload ?? 0, + flow_count: r.flow_count ?? 0 + }; + }); } const countryLatest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries`).get(); diff --git a/backend/tests/test_agent_globe_traffic.js b/backend/tests/test_agent_globe_traffic.js new file mode 100644 index 0000000..8c9ff5b --- /dev/null +++ b/backend/tests/test_agent_globe_traffic.js @@ -0,0 +1,45 @@ +const db = require('../database'); + +function runTest() { + console.log('=== STARTING TDD TEST FOR AGENT GLOBE TRAFFIC ==='); + + const agentUuid = '8A-V3-PB-85'; + console.log(`- Fetching countries for agent: ${agentUuid}`); + const countries = db.getLatestCountries(15, null, agentUuid); + + if (!Array.isArray(countries)) { + throw new Error('Countries result should be an array'); + } + + console.log(`- Retrieved ${countries.length} country stats`); + if (countries.length === 0) { + throw new Error('No country records found for agent flows. Verify if sample flows/geoip cache is set up.'); + } + + // Check each record has country_code and country_name, and valid values + for (const r of countries) { + console.log(` Country: ${r.country_name} (${r.country_code}) | Download: ${r.download} | Upload: ${r.upload} | Flows: ${r.flow_count}`); + + if (!r.country_name) { + throw new Error('Record has missing country_name'); + } + if (!r.country_code) { + throw new Error(`Record for ${r.country_name} is missing country_code!`); + } + if (r.country_code.length !== 2) { + throw new Error(`Invalid country_code format for ${r.country_name}: ${r.country_code}`); + } + if (typeof r.download !== 'number' || typeof r.upload !== 'number') { + throw new Error(`Invalid download/upload type for ${r.country_name}`); + } + } + + console.log('\n=== ALL AGENT GLOBE TRAFFIC TESTS PASSED SUCCESSFULLY! ==='); +} + +try { + runTest(); +} catch (err) { + console.error('\nāŒ TEST FAILED:', err.message); + process.exit(1); +} diff --git a/src/components/ui/DeviceDetailModal.tsx b/src/components/ui/DeviceDetailModal.tsx index d21cabe..1ddf791 100644 --- a/src/components/ui/DeviceDetailModal.tsx +++ b/src/components/ui/DeviceDetailModal.tsx @@ -313,7 +313,7 @@ export function DeviceDetailModal({ ip, deviceLabel, macAddress, onClose }: Prop data.flows.length === 0 ? : ( {data.flows.map((f, i) => { - const exp = explainAppOrPort(f.app_label, f.domain, f.dst_port || 0); + const exp = explainAppOrPort(f.app_label ?? null, f.domain ?? null, f.dst_port || 0); return ( diff --git a/src/components/ui/GlobeMap.tsx b/src/components/ui/GlobeMap.tsx index c0d235f..52e407d 100644 --- a/src/components/ui/GlobeMap.tsx +++ b/src/components/ui/GlobeMap.tsx @@ -23,7 +23,22 @@ const COUNTRY_COORDS: Record = { "LU": [6.1296, 49.8153], "NL": [5.2913, 52.1326], "TR": [35.2433, 38.9637], - "UA": [31.1656, 48.3794] + "UA": [31.1656, 48.3794], + "RU": [105.3188, 61.5240], + "HU": [19.5033, 47.1625], + "BD": [90.3563, 23.6850], + "AU": [133.7751, -25.2744], + "IN": [78.9629, 20.5937], + "DE": [10.4515, 51.1657], + "FR": [2.2137, 46.2276], + "ES": [-3.7492, 40.4637], + "BR": [-51.9253, -14.2350], + "MY": [101.9758, 4.2105], + "TH": [100.9925, 15.8700], + "VN": [108.2772, 14.0583], + "PH": [121.7740, 12.8797], + "TW": [120.9605, 23.6978], + "GR": [21.8243, 39.0742] }; // Jakarta, Indonesia as Origin diff --git a/src/lib/api.ts b/src/lib/api.ts index 0619b5c..2329cdc 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -129,6 +129,7 @@ export interface DeviceStat { download: number; upload: number; last_seen: string; + is_gateway_routed?: number | null; } export interface ThreatStat {