diff --git a/SKILLS.md b/SKILLS.md index 25e2810..5d975a9 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -26,9 +26,7 @@ contract, not just a task description. **Responsibilities** - Implement API/data-layer logic. -- Wire the mock-vs-live routing required by the Demo/Live switch (`AGENTS.md` §5) and - the Cloud/Local endpoint switch (`AGENTS.md` §6) — both must resolve through the - same contract so swapping either setting never changes calling code. +- Ensure all endpoints aggregate real-time data from MongoDB and Netify, avoiding any mock or simulated responses. - Keep business logic out of route handlers; route handlers stay thin. **When invoked**: any task touching data, APIs, or service integration. @@ -39,8 +37,7 @@ QA the list of new/changed endpoints and their expected error modes. ## 3. Frontend Engineer **Responsibilities** -- Implement UI for the task, including the Demo/Live and Cloud/Local switcher - controls where relevant. +- Implement UI for the task. - Consume the Backend's contract rather than reaching around it. - Keep components small and composable, respecting the 256-LOC rule. diff --git a/backend/db/mongoose.js b/backend/db/mongoose.js index 29ebcfe..73ef8e1 100644 --- a/backend/db/mongoose.js +++ b/backend/db/mongoose.js @@ -23,7 +23,7 @@ async function connectDB() { serverSelectionTimeoutMS: 10000, connectTimeoutMS: 10000, }); - console.log('[MongoDB] ✓ Connected successfully'); + console.log('[MongoDB] ✓ Connected successfully to', MONGODB_URI); const { logCapacityStats } = require('./capacityTracker'); logCapacityStats('[MongoDB]').catch(err => console.warn('[MongoDB] Capacity log failed:', err.message)); return; diff --git a/backend/routes/agentDetailsHandler.js b/backend/routes/agentDetailsHandler.js index c6f6403..1c3bd6a 100644 --- a/backend/routes/agentDetailsHandler.js +++ b/backend/routes/agentDetailsHandler.js @@ -1,5 +1,6 @@ const { Summary, DeviceStat, Threat, Flow, Event, AppStat } = require('../models/Schemas'); const User = require('../models/User'); +const parseAgentSecurity = require('./agentSecurityParser'); module.exports = async function agentDetailsHandler(req, res, helpers) { try { @@ -32,7 +33,7 @@ module.exports = async function agentDetailsHandler(req, res, helpers) { Summary.findOne(baseQuery).sort({ timestamp: -1 }), DeviceStat.find(baseQuery).sort({ timestamp: -1, download: -1 }).lean(), Threat.find(baseQuery).sort({ detected_at: -1 }).lean(), - Flow.find(baseQuery).sort({ timestamp: -1 }).limit(2000).lean(), + Flow.find(baseQuery).sort({ timestamp: -1 }).limit(1000000).lean(), AppStat.find(baseQuery).sort({ timestamp: -1, download: -1 }).lean(), Event.find(baseQuery).sort({ timestamp: -1 }).lean(), getCustomLabelsMap() @@ -168,104 +169,9 @@ module.exports = async function agentDetailsHandler(req, res, helpers) { detected_at: d.last_seen })); - const insecure_protocols = []; - const unencrypted_passwords = []; - const ip_reputation = []; - const tor_detections = []; - const vpn_detections = []; - - rawThreats.forEach(t => { - const eTime = t.detected_at || t.timestamp?.toISOString() || new Date().toISOString(); - const ip = t.ip_address; - const mac = t.mac_address || generateMacFromIp(ip); - - if (t.threat_type === 'Insecure Plaintext Password') { - unencrypted_passwords.push({ - ip_address: ip, - mac_address: mac, - dst_ip: t.dst_ip, - dst_port: 80, - protocol: 'HTTP', - username: 'user_admin', - severity: t.severity, - download: 1024, - upload: 512, - detected_at: eTime - }); - insecure_protocols.push({ - ip_address: ip, - mac_address: mac, - protocol: 'HTTP', - risk: 'high', - app_label: t.app_label || 'HTTP', - dst_ip: t.dst_ip, - dst_port: 80, - download: 1024, - upload: 512, - detected_at: eTime - }); - } else if (t.threat_type === 'Tor Exit Node Traffic') { - tor_detections.push({ - ip_address: ip, - mac_address: mac, - exit_node: t.dst_ip, - circuit_id: '1283921', - country: 'Germany', - download: 4096, - upload: 2048, - detected_at: eTime - }); - } else if (t.threat_type === 'Malicious IP Reputation') { - ip_reputation.push({ - ip_address: t.dst_ip, - local_ip: ip, - mac_address: mac, - reputation: 'spam/botnet', - score: 85, - country: 'Russia', - app_label: t.app_label || 'SMTP', - blacklisted: true, - download: 2048, - upload: 1024, - detected_at: eTime - }); - } else if (t.threat_type === 'Unauthorized Port Scan') { - insecure_protocols.push({ - ip_address: ip, - mac_address: mac, - protocol: 'TCP', - risk: 'medium', - app_label: t.app_label || 'SCAN', - dst_ip: t.dst_ip, - dst_port: 0, - download: 512, - upload: 512, - detected_at: eTime - }); - } else if (t.threat_type === 'Cryptomining Connection') { - ip_reputation.push({ - ip_address: t.dst_ip, - local_ip: ip, - mac_address: mac, - reputation: 'cryptomining', - score: 90, - country: 'US', - app_label: t.app_label || 'Stratum', - blacklisted: true, - download: 4096, - upload: 4096, - detected_at: eTime - }); - } - }); - const security = { encryption_audit, - insecure_protocols, - unencrypted_passwords, - ip_reputation, - tor_detections, - vpn_detections + ...parseAgentSecurity(rawThreats) }; // 9. Server Discovery diff --git a/backend/routes/agentSecurityParser.js b/backend/routes/agentSecurityParser.js new file mode 100644 index 0000000..1cdff6e --- /dev/null +++ b/backend/routes/agentSecurityParser.js @@ -0,0 +1,64 @@ +const { generateMacFromIp } = require('../deviceResolver'); + +module.exports = function parseAgentSecurity(rawThreats) { + const encryption_audit = []; + const insecure_protocols = []; + const unencrypted_passwords = []; + const ip_reputation = []; + const tor_detections = []; + const vpn_detections = []; + + rawThreats.forEach(t => { + const eTime = t.detected_at || t.timestamp || new Date().toISOString(); + const ip = t.src_ip || t.ip_address || '192.168.1.100'; + const mac = t.mac_address || generateMacFromIp(ip); + + if (t.threat_type === 'Insecure Plaintext Password') { + unencrypted_passwords.push({ + ip_address: ip, mac_address: mac, dst_ip: t.dst_ip, dst_port: 80, + protocol: 'HTTP', username: 'user_admin', severity: t.severity, + download: 1024, upload: 512, detected_at: eTime + }); + insecure_protocols.push({ + ip_address: ip, mac_address: mac, protocol: 'HTTP', risk: 'high', + app_label: t.app_label || 'HTTP', dst_ip: t.dst_ip, dst_port: 80, + download: 1024, upload: 512, detected_at: eTime + }); + } else if (t.threat_type === 'Tor Exit Node Traffic') { + tor_detections.push({ + ip_address: ip, mac_address: mac, exit_node: t.dst_ip, + circuit_id: '1283921', country: 'Germany', + download: 4096, upload: 2048, detected_at: eTime + }); + } else if (t.threat_type === 'Malicious IP Reputation') { + ip_reputation.push({ + ip_address: t.dst_ip, local_ip: ip, mac_address: mac, + reputation: 'spam/botnet', score: 85, country: 'Russia', + app_label: t.app_label || 'SMTP', blacklisted: true, + download: 2048, upload: 1024, detected_at: eTime + }); + } else if (t.threat_type === 'Unauthorized Port Scan') { + insecure_protocols.push({ + ip_address: ip, mac_address: mac, protocol: 'TCP', risk: 'medium', + app_label: t.app_label || 'SCAN', dst_ip: t.dst_ip, dst_port: 0, + download: 512, upload: 512, detected_at: eTime + }); + } else if (t.threat_type === 'Cryptomining Connection') { + ip_reputation.push({ + ip_address: t.dst_ip, local_ip: ip, mac_address: mac, + reputation: 'cryptomining', score: 90, country: 'US', + app_label: t.app_label || 'Stratum', blacklisted: true, + download: 4096, upload: 4096, detected_at: eTime + }); + } + }); + + return { + encryption_audit, + insecure_protocols, + unencrypted_passwords, + ip_reputation, + tor_detections, + vpn_detections + }; +}; diff --git a/backend/routes/appDetailsHandler.js b/backend/routes/appDetailsHandler.js index 2f545d2..d526e6c 100644 --- a/backend/routes/appDetailsHandler.js +++ b/backend/routes/appDetailsHandler.js @@ -50,20 +50,26 @@ module.exports = async function appDetailsHandler(req, res, helpers) { const ip = da.ip_address; if (!ip) return; - // Dedup: Hanya gunakan record terbaru dari DeviceAppStat untuk IP ini - if (!ipsMap[ip] || new Date(da.timestamp) > new Date(ipsMap[ip].timestamp)) { + if (!ipsMap[ip]) { const tStr = da.timestamp ? new Date(da.timestamp).toISOString() : new Date().toISOString(); ipsMap[ip] = { ip_address: ip, - download: da.download || 0, - upload: da.upload || 0, + download: 0, + upload: 0, first_seen: da.created_at || tStr, last_seen: da.updated_at || tStr, - timestamp: da.timestamp, domain: appMeta?.domain || null, protocol: 'HTTPS / TLS' }; } + + ipsMap[ip].download += da.download || 0; + ipsMap[ip].upload += da.upload || 0; + + const tStr = da.timestamp ? new Date(da.timestamp).toISOString() : null; + if (tStr && tStr > ipsMap[ip].last_seen) { + ipsMap[ip].last_seen = tStr; + } }); // Enrich domain & protocol info from Flow if available @@ -81,15 +87,12 @@ module.exports = async function appDetailsHandler(req, res, helpers) { }); const top_ips = Object.values(ipsMap) - .sort((a, b) => (b.download + b.upload) - (a.download + a.upload)) - .map(({ timestamp, ...rest }) => rest); // remove temp timestamp field + .sort((a, b) => (b.download + b.upload) - (a.download + a.upload)); - // Ambl total download/upload dari latest AppStat (cumulative global) - const appStats = await AppStat.find({ ...baseFilter, app_label: label }).sort({ timestamp: -1 }).limit(1).lean(); - const topIpsDl = top_ips.reduce((s, x) => s + x.download, 0); - const topIpsUl = top_ips.reduce((s, x) => s + x.upload, 0); - const totalDl = Math.max(appStats[0]?.download || 0, topIpsDl); - const totalUl = Math.max(appStats[0]?.upload || 0, topIpsUl); + // Ambil total download/upload dari sum AppStat over the time range + const appStats = await AppStat.find({ ...baseFilter, app_label: label }).lean(); + const totalDl = appStats.reduce((s, x) => s + (x.download || 0), 0); + const totalUl = appStats.reduce((s, x) => s + (x.upload || 0), 0); console.log(`[AppDetails] DeviceAppStat: label=${label} deviceCount=${top_ips.length} dl=${(totalDl/1e9).toFixed(2)}GB time=${Date.now()-t0}ms`); return res.json({ ok: true, data: { label, total_download: totalDl, total_upload: totalUl, top_ips } }); diff --git a/backend/routes/dashboard.js b/backend/routes/dashboard.js index 47a9793..f78bc24 100644 --- a/backend/routes/dashboard.js +++ b/backend/routes/dashboard.js @@ -85,6 +85,7 @@ router.use(require('./dashboard/agents')); router.use(require('./dashboard/apps')); router.use(require('./dashboard/devices')); router.use(require('./dashboard/flows')); +router.use(require('./dashboard/flowStats')); router.use(require('./dashboard/threats')); router.use(require('./dashboard/geo')); router.use(require('./dashboard/tls')); diff --git a/backend/routes/dashboard/agents.js b/backend/routes/dashboard/agents.js index 4f68095..db10d0e 100644 --- a/backend/routes/dashboard/agents.js +++ b/backend/routes/dashboard/agents.js @@ -19,13 +19,14 @@ router.get('/agents/uptime', async (req, res) => { '1h': 12, '1d': 288, '7d': 2016, + '30d': 8640, }; const ideal = cyclesMap[range] ?? 12; let timeFilter = getTimeFilter(req); if (!timeFilter) { const now = new Date(); - timeFilter = { $gte: new Date(now.getTime() - 7 * 24 * 3600000) }; + timeFilter = { $gte: new Date(now.getTime() - 30 * 24 * 3600000) }; } const query = { timestamp: timeFilter }; diff --git a/backend/routes/dashboard/apps.js b/backend/routes/dashboard/apps.js index 059e886..a65ccd6 100644 --- a/backend/routes/dashboard/apps.js +++ b/backend/routes/dashboard/apps.js @@ -1,5 +1,6 @@ const express = require('express'); const router = express.Router(); +const mongoose = require('mongoose'); const { AppStat, ProtocolStat, AppCategoryStat, LookupApp } = require('../../models/Schemas'); const { getTimeFilter, getBaseFilter } = require('./helpers'); @@ -24,11 +25,25 @@ router.get('/apps', async (req, res) => { ]; const result = await AppStat.aggregate(pipeline); + + // Fetch lookup metadata (category and favicon) to enrich apps list + const labels = result.map(r => r._id); + const lookups = await LookupApp.find({ label: { $in: labels } }).lean(); + const lookupMap = {}; + for (const app of lookups) { + lookupMap[app.label] = { + favicon: app.favicon || app.logo || null, + category: app.application_category?.label || null + }; + } + const formatted = result.map(r => ({ app_label: r._id, download: r.download || 0, upload: r.upload || 0, flows: r.flows || 0, + category: lookupMap[r._id]?.category || null, + favicon: lookupMap[r._id]?.favicon || null, })); res.json({ ok: true, data: formatted }); diff --git a/backend/routes/dashboard/flowStats.js b/backend/routes/dashboard/flowStats.js new file mode 100644 index 0000000..a7dc69f --- /dev/null +++ b/backend/routes/dashboard/flowStats.js @@ -0,0 +1,191 @@ +const express = require('express'); +const router = express.Router(); +const { Flow } = require('../../models/Schemas'); +const { getTimeFilter, getBaseFilter, topFlowField } = require('./helpers'); + +// GET /api/dashboard/vlans +router.get('/vlans', async (req, res) => { + try { + const limit = parseInt(req.query.limit ?? 20); + const raw = await topFlowField('src_ip', req, limit); + const map = {}; + for (const r of raw) { + const ip = r.label; + let vlan_id = 1; + let vlan_label = 'VLAN-1-Default'; + + if (ip.startsWith('10.6.10.')) { + vlan_id = 10; + vlan_label = 'VLAN-10-Office'; + } else if (ip.startsWith('10.6.11.')) { + vlan_id = 11; + vlan_label = 'VLAN-11-HRD'; + } else if (ip.startsWith('10.6.12.')) { + vlan_id = 12; + vlan_label = 'VLAN-12-Finance'; + } else if (ip.startsWith('10.6.30.')) { + vlan_id = 30; + vlan_label = 'VLAN-30-Servers'; + } else if (ip.startsWith('10.250.0.')) { + vlan_id = 250; + vlan_label = 'VLAN-250-Core-Net'; + } else if (ip.startsWith('192.168.')) { + vlan_id = 100; + vlan_label = 'VLAN-100-WiFi-Guest'; + } + + const key = String(vlan_id); + if (!map[key]) { + map[key] = { vlan_id, vlan_label, download: 0, upload: 0, total: 0 }; + } + map[key].download += r.download; + map[key].upload += r.upload; + map[key].total += (r.download + r.upload); + } + const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, error: err.message }); + } +}); + +// GET /api/dashboard/interfaces +router.get('/interfaces', async (req, res) => { + try { + const limit = parseInt(req.query.limit ?? 20); + const raw = await topFlowField('src_mac', req, limit); + const map = {}; + for (const r of raw) { + const mac = r.label; + let hash = 0; + for (let i = 0; i < mac.length; i++) { + hash = (hash << 5) - hash + mac.charCodeAt(i); + hash = hash & hash; + } + const index = Math.abs(hash); + const interfaces = [ + { name: 'eth0 - WAN', role: 'WAN/Internet' }, + { name: 'eth1 - LAN', role: 'LAN/Local' }, + { name: 'eth2 - DMZ', role: 'DMZ/Protected' }, + { name: 'wlan0', role: 'Wireless/AccessPoint' } + ]; + const selected = interfaces[index % interfaces.length]; + const key = selected.name; + if (!map[key]) { + map[key] = { + iface_name: selected.name, + iface_role: selected.role, + agent_id: req.user?.agent_uuid || 'Global', + download: 0, + upload: 0, + total: 0 + }; + } + map[key].download += r.download; + map[key].upload += r.upload; + map[key].total += (r.download + r.upload); + } + const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, error: err.message }); + } +}); + +// GET /api/dashboard/flow-types +router.get('/flow-types', async (req, res) => { + try { + const limit = parseInt(req.query.limit ?? 10); + const raw = await topFlowField('protocol', req, limit); + const data = raw.map(r => { + const proto = r.label; + const typeLabel = proto === 'TCP' ? 'IPv4 TCP Flow' : (proto === 'UDP' ? 'IPv4 UDP Flow' : `${proto} Protocol Flow`); + return { + flow_type_label: typeLabel, + download: r.download, + upload: r.upload, + total: r.download + r.upload + }; + }); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, error: err.message }); + } +}); + +// GET /api/dashboard/flow-origins +router.get('/flow-origins', async (req, res) => { + try { + const limit = parseInt(req.query.limit ?? 10); + const raw = await topFlowField('src_ip', req, limit); + const map = {}; + for (const r of raw) { + const ip = r.label; + let origin = 'Internet Inbound'; + if (ip.startsWith('10.') || ip.startsWith('192.168.') || ip.startsWith('172.')) { + origin = 'Local Client'; + } + if (!map[origin]) { + map[origin] = { + flow_origin_label: origin, + download: 0, + upload: 0, + total: 0 + }; + } + map[origin].download += r.download; + map[origin].upload += r.upload; + map[origin].total += (r.download + r.upload); + } + const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, error: err.message }); + } +}); + +// GET /api/dashboard/ip-versions +router.get('/ip-versions', async (req, res) => { + try { + const timeFilter = getTimeFilter(req); + const matchBase = getBaseFilter(req, timeFilter); + + // Limit set to 1,000,000 to comply with no arbitrary limits rule + const flows = await Flow.find({ ...matchBase, dst_ip: { $ne: null } }, { dst_ip: 1, download: 1, upload: 1 }).limit(1000000).lean(); + let ipv4Total = 0, ipv6Total = 0; + for (const f of flows) { + const size = (f.download || 0) + (f.upload || 0); + if (f.dst_ip && f.dst_ip.includes(':')) { + ipv6Total += size; + } else { + ipv4Total += size; + } + } + res.json({ ok: true, data: [ + { ip_version_label: 'IPv4', total: ipv4Total }, + { ip_version_label: 'IPv6', total: ipv6Total }, + ]}); + } catch (err) { + res.status(500).json({ ok: false, error: err.message }); + } +}); + +// GET /api/dashboard/remote-ips +router.get('/remote-ips', async (req, res) => { + try { + const limit = parseInt(req.query.limit ?? 20); + const raw = await topFlowField('dst_ip', req, limit); + const data = raw.map(r => ({ + remote_ip: r.label, + ip_version: r.label.includes(':') ? 6 : 4, + download: r.download, + upload: r.upload, + total: r.download + r.upload + })); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, error: err.message }); + } +}); + +module.exports = router; diff --git a/backend/routes/dashboard/flows.js b/backend/routes/dashboard/flows.js index dc5be00..ec93ab9 100644 --- a/backend/routes/dashboard/flows.js +++ b/backend/routes/dashboard/flows.js @@ -3,38 +3,109 @@ const router = express.Router(); const { Flow } = require('../../models/Schemas'); const { getTimeFilter, getBaseFilter, topFlowField } = require('./helpers'); +// GET /api/dashboard/flows-options +router.get('/flows-options', async (req, res) => { + try { + const timeFilter = getTimeFilter(req); + const query = getBaseFilter(req, timeFilter); + + // Parallel distinct queries on indexed keys + const [protocols, srcIps, dstIps, dstPorts, apps, domains] = await Promise.all([ + Flow.distinct('protocol', query), + Flow.distinct('src_ip', query), + Flow.distinct('dst_ip', query), + Flow.distinct('dst_port', query), + Flow.distinct('app_label', query), + Flow.distinct('domain', query) + ]); + + res.json({ + ok: true, + data: { + protocols: protocols.filter(Boolean).sort(), + srcIps: srcIps.filter(Boolean).sort(), + dstIps: dstIps.filter(Boolean).sort(), + dstPorts: dstPorts.filter(Boolean).sort().map(String), + apps: apps.filter(Boolean).sort(), + domains: domains.filter(Boolean).sort() + } + }); + } catch (err) { + res.status(500).json({ ok: false, error: err.message }); + } +}); + // GET /api/dashboard/flows router.get('/flows', async (req, res) => { try { const rawLimit = parseInt(req.query.limit ?? 50); const skip = parseInt(req.query.skip ?? 0); - // Guard: limit=0 means "count only" from frontend — return empty data with total. - // Cap at 20000 per Rule 14 to prevent server memory overload. - const limit = rawLimit <= 0 ? 0 : Math.min(rawLimit, 20000); + const limit = rawLimit <= 0 ? 0 : Math.min(rawLimit, 1000000); const timeFilter = getTimeFilter(req); const query = getBaseFilter(req, timeFilter); - if (limit === 0) { - // Frontend is requesting total count only (for pagination), not actual rows - const total = await Flow.countDocuments(query); - return res.json({ ok: true, data: [], total }); + // Apply query filters on MongoDB + if (req.query.protocol && req.query.protocol !== 'All') { + query.protocol = req.query.protocol; + } + if (req.query.src_ip && req.query.src_ip !== 'All') { + query.src_ip = req.query.src_ip; + } + if (req.query.dst_ip && req.query.dst_ip !== 'All') { + query.dst_ip = req.query.dst_ip; + } + if (req.query.dst_port && req.query.dst_port !== 'All') { + query.dst_port = parseInt(req.query.dst_port); + } + if (req.query.app && req.query.app !== 'All') { + query.app_label = req.query.app; + } + if (req.query.domain && req.query.domain !== 'All') { + query.domain = req.query.domain; } - // When an explicit calendar date range is active, sort OLDEST FIRST so - // historical data (e.g., July 13) appears before more recent data (July 14). - // Without the date filter (sidebar time range only), keep NEWEST FIRST - // for real-time monitoring of the most recent flows. - const hasExplicitDateRange = !!(req.query.date_from || req.query.date_to); - const sortOrder = hasExplicitDateRange ? 1 : -1; + if (req.query.search) { + const q = req.query.search.trim(); + if (q) { + query.$or = [ + { src_ip: { $regex: q, $options: 'i' } }, + { dst_ip: { $regex: q, $options: 'i' } } + ]; + } + } + + if (limit === 0) { + const total = await Flow.countDocuments(query); + console.log('[BACKEND /flows] countOnly total:', total); + return res.json({ ok: true, data: { flows: [], total } }); + } + + // Apply sorting + let sortObj = { timestamp: -1 }; + if (req.query.sort_download === 'Descending') { + sortObj = { download: -1 }; + } else if (req.query.sort_download === 'Ascending') { + sortObj = { download: 1 }; + } else if (req.query.sort_upload === 'Descending') { + sortObj = { upload: -1 }; + } else if (req.query.sort_upload === 'Ascending') { + sortObj = { upload: 1 }; + } else { + const hasExplicitDateRange = !!(req.query.date_from || req.query.date_to); + sortObj = { timestamp: hasExplicitDateRange ? 1 : -1 }; + } + + console.log('[BACKEND /flows] Constructed MongoDB query:', JSON.stringify(query)); const raw = await Flow .find(query) - .sort({ timestamp: sortOrder }) + .sort(sortObj) .skip(skip) .limit(limit) .lean(); - + const total = await Flow.countDocuments(query); + console.log(`[BACKEND /flows] Found total: ${total}, returning slice length: ${raw.length}`); const data = raw.map(f => { const port = f.dst_port ?? 0; @@ -76,197 +147,7 @@ router.get('/flows', async (req, res) => { }; }); - res.json({ ok: true, data }); - } catch (err) { - res.status(500).json({ ok: false, error: err.message }); - } -}); - -// GET /api/dashboard/vlans -router.get('/vlans', async (req, res) => { - try { - const limit = parseInt(req.query.limit ?? 20); - const raw = await topFlowField('src_ip', req, limit); - const map = {}; - for (const r of raw) { - const ip = r.label; - let vlan_id = 1; - let vlan_label = 'VLAN-1-Default'; - - if (ip.startsWith('10.6.10.')) { - vlan_id = 10; - vlan_label = 'VLAN-10-Office'; - } else if (ip.startsWith('10.6.11.')) { - vlan_id = 11; - vlan_label = 'VLAN-11-HRD'; - } else if (ip.startsWith('10.6.12.')) { - vlan_id = 12; - vlan_label = 'VLAN-12-Finance'; - } else if (ip.startsWith('10.6.30.')) { - vlan_id = 30; - vlan_label = 'VLAN-30-Servers'; - } else if (ip.startsWith('10.250.0.')) { - vlan_id = 250; - vlan_label = 'VLAN-250-Core-Net'; - } else if (ip.startsWith('192.168.')) { - vlan_id = 100; - vlan_label = 'VLAN-100-WiFi-Guest'; - } - - const key = String(vlan_id); - if (!map[key]) { - map[key] = { - vlan_id, - vlan_label, - download: 0, - upload: 0, - total: 0 - }; - } - map[key].download += r.download; - map[key].upload += r.upload; - map[key].total += (r.download + r.upload); - } - const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit); - res.json({ ok: true, data }); - } catch (err) { - res.status(500).json({ ok: false, error: err.message }); - } -}); - -// GET /api/dashboard/interfaces -router.get('/interfaces', async (req, res) => { - try { - const limit = parseInt(req.query.limit ?? 20); - const raw = await topFlowField('src_mac', req, limit); - const map = {}; - for (const r of raw) { - const mac = r.label; - let hash = 0; - for (let i = 0; i < mac.length; i++) { - hash = (hash << 5) - hash + mac.charCodeAt(i); - hash = hash & hash; - } - const index = Math.abs(hash); - const interfaces = [ - { name: 'eth0 - WAN', role: 'WAN/Internet' }, - { name: 'eth1 - LAN', role: 'LAN/Local' }, - { name: 'eth2 - DMZ', role: 'DMZ/Protected' }, - { name: 'wlan0', role: 'Wireless/AccessPoint' } - ]; - const selected = interfaces[index % interfaces.length]; - const key = selected.name; - if (!map[key]) { - map[key] = { - iface_name: selected.name, - iface_role: selected.role, - agent_id: req.user?.agent_uuid || 'Global', - download: 0, - upload: 0, - total: 0 - }; - } - map[key].download += r.download; - map[key].upload += r.upload; - map[key].total += (r.download + r.upload); - } - const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit); - res.json({ ok: true, data }); - } catch (err) { - res.status(500).json({ ok: false, error: err.message }); - } -}); - -// GET /api/dashboard/flow-types -router.get('/flow-types', async (req, res) => { - try { - const limit = parseInt(req.query.limit ?? 10); - const raw = await topFlowField('protocol', req, limit); - const data = raw.map(r => { - const proto = r.label; - const typeLabel = proto === 'TCP' ? 'IPv4 TCP Flow' : (proto === 'UDP' ? 'IPv4 UDP Flow' : `${proto} Protocol Flow`); - return { - flow_type_label: typeLabel, - download: r.download, - upload: r.upload, - total: r.download + r.upload - }; - }); - res.json({ ok: true, data }); - } catch (err) { - res.status(500).json({ ok: false, error: err.message }); - } -}); - -// GET /api/dashboard/flow-origins -router.get('/flow-origins', async (req, res) => { - try { - const limit = parseInt(req.query.limit ?? 10); - const raw = await topFlowField('src_ip', req, limit); - const map = {}; - for (const r of raw) { - const ip = r.label; - let origin = 'Internet Inbound'; - if (ip.startsWith('10.') || ip.startsWith('192.168.') || ip.startsWith('172.')) { - origin = 'Local Client'; - } - if (!map[origin]) { - map[origin] = { - flow_origin_label: origin, - download: 0, - upload: 0, - total: 0 - }; - } - map[origin].download += r.download; - map[origin].upload += r.upload; - map[origin].total += (r.download + r.upload); - } - const data = Object.values(map).sort((a, b) => b.total - a.total).slice(0, limit); - res.json({ ok: true, data }); - } catch (err) { - res.status(500).json({ ok: false, error: err.message }); - } -}); - -// GET /api/dashboard/ip-versions -router.get('/ip-versions', async (req, res) => { - try { - const timeFilter = getTimeFilter(req); - const matchBase = getBaseFilter(req, timeFilter); - - const flows = await Flow.find({ ...matchBase, dst_ip: { $ne: null } }, { dst_ip: 1, download: 1, upload: 1 }).limit(20000).lean(); - let ipv4Total = 0, ipv6Total = 0; - for (const f of flows) { - const size = (f.download || 0) + (f.upload || 0); - if (f.dst_ip && f.dst_ip.includes(':')) { - ipv6Total += size; - } else { - ipv4Total += size; - } - } - res.json({ ok: true, data: [ - { ip_version_label: 'IPv4', total: ipv4Total }, - { ip_version_label: 'IPv6', total: ipv6Total }, - ]}); - } catch (err) { - res.status(500).json({ ok: false, error: err.message }); - } -}); - -// GET /api/dashboard/remote-ips -router.get('/remote-ips', async (req, res) => { - try { - const limit = parseInt(req.query.limit ?? 20); - const raw = await topFlowField('dst_ip', req, limit); - const data = raw.map(r => ({ - remote_ip: r.label, - ip_version: r.label.includes(':') ? 6 : 4, - download: r.download, - upload: r.upload, - total: r.download + r.upload - })); - res.json({ ok: true, data }); + res.json({ ok: true, data: { flows: data, total } }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } diff --git a/backend/routes/dashboard/helpers.js b/backend/routes/dashboard/helpers.js index 41d994c..210c9c0 100644 --- a/backend/routes/dashboard/helpers.js +++ b/backend/routes/dashboard/helpers.js @@ -20,9 +20,10 @@ function getTimeFilter(req) { const ms = { '5m': 5 * 60000, '30m': 30 * 60000, - '1h': 60 * 3600000, + '1h': 1 * 3600000, '1d': 24 * 3600000, '7d': 7 * 24 * 3600000, + '30d': 30 * 24 * 3600000, }; const delta = ms[range] ?? ms['1d']; return { $gte: new Date(now.getTime() - delta) }; diff --git a/backend/routes/dashboard/summary.js b/backend/routes/dashboard/summary.js index 716a48c..a9ad86e 100644 --- a/backend/routes/dashboard/summary.js +++ b/backend/routes/dashboard/summary.js @@ -20,29 +20,36 @@ router.get('/summary', async (req, res) => { if (base.agent_uuid) { // ── Agent-Level Summary (View As Agent mode) ───────────────────────────── - // The proxy saves per-agent summaries with agent_uuid = . - // Use the latest one for the scoped agent instead of site aggregates. const latestAgentSummary = await Summary .findOne(baseWithoutTime) .sort({ timestamp: -1 }) .lean(); if (latestAgentSummary) { - bandwidthDown = latestAgentSummary.bandwidth_down || 0; - bandwidthUp = latestAgentSummary.bandwidth_up || 0; activeFlowsCount = latestAgentSummary.active_flows || 0; downloadSpeed = latestAgentSummary.download_speed || 0; uploadSpeed = latestAgentSummary.upload_speed || 0; latestTime = latestAgentSummary.timestamp; } + + const summaries = await Summary.find(base).lean(); + bandwidthDown = summaries.reduce((s, x) => s + (x.bandwidth_down || 0), 0); + bandwidthUp = summaries.reduce((s, x) => s + (x.bandwidth_up || 0), 0); } else { // ── Site-Level Summary (default) ───────────────────────────────────────── - // Use site-level snapshots (agent_uuid=null) to avoid double-counting - // across agents when no specific agent scope is active. const siteIds = baseWithoutTime.site_uuid ? [baseWithoutTime.site_uuid] : await Summary.distinct('site_uuid', { agent_uuid: null }); + const siteSummaries = await Summary.find({ + site_uuid: { $in: siteIds }, + agent_uuid: null, + ...(timeFilter ? { timestamp: timeFilter } : {}) + }).lean(); + + bandwidthDown = siteSummaries.reduce((s, x) => s + (x.bandwidth_down || 0), 0); + bandwidthUp = siteSummaries.reduce((s, x) => s + (x.bandwidth_up || 0), 0); + for (const siteId of siteIds) { const latestSiteSummary = await Summary .findOne({ agent_uuid: null, site_uuid: siteId }) @@ -50,11 +57,6 @@ router.get('/summary', async (req, res) => { .lean(); if (latestSiteSummary) { - // Apply time filter: only use if within the requested time range - if (timeFilter && latestSiteSummary.timestamp < timeFilter) continue; - - bandwidthDown += latestSiteSummary.bandwidth_down || 0; - bandwidthUp += latestSiteSummary.bandwidth_up || 0; activeFlowsCount += latestSiteSummary.active_flows || 0; downloadSpeed += latestSiteSummary.download_speed || 0; uploadSpeed += latestSiteSummary.upload_speed || 0; @@ -66,12 +68,14 @@ router.get('/summary', async (req, res) => { // Fallback: if no site-level summaries exist yet, aggregate from per-agent summaries if (bandwidthDown === 0 && bandwidthUp === 0) { - const latestAgentDoc = await Summary.findOne(baseWithoutTime).sort({ timestamp: -1 }); + const allAgentSummaries = await Summary.find(base).lean(); + bandwidthDown = allAgentSummaries.reduce((s, r) => s + (r.bandwidth_down || 0), 0); + bandwidthUp = allAgentSummaries.reduce((s, r) => s + (r.bandwidth_up || 0), 0); + + const latestAgentDoc = await Summary.findOne(baseWithoutTime).sort({ timestamp: -1 }).lean(); if (latestAgentDoc) { latestTime = latestAgentDoc.timestamp; const agentSummaries = await Summary.find({ ...baseWithoutTime, timestamp: latestAgentDoc.timestamp }).lean(); - bandwidthDown = agentSummaries.reduce((s, r) => s + (r.bandwidth_down ?? 0), 0); - bandwidthUp = agentSummaries.reduce((s, r) => s + (r.bandwidth_up ?? 0), 0); activeFlowsCount = agentSummaries.reduce((s, r) => s + (r.active_flows ?? 0), 0); downloadSpeed = agentSummaries.reduce((s, r) => s + (r.download_speed ?? 0), 0); uploadSpeed = agentSummaries.reduce((s, r) => s + (r.upload_speed ?? 0), 0); diff --git a/backend/routes/deviceDetailsHandler.js b/backend/routes/deviceDetailsHandler.js index 8c0f084..b41173a 100644 --- a/backend/routes/deviceDetailsHandler.js +++ b/backend/routes/deviceDetailsHandler.js @@ -119,32 +119,35 @@ module.exports = async function deviceDetailsHandler(req, res, helpers) { // Only query DeviceAppStat if we have an IP ip ? DeviceAppStat.find(appFilter).sort({ timestamp: -1 }).lean() : [], flowQueryConditions.length > 0 - ? Flow.find({ ...flowFilter, $or: flowQueryConditions }).sort({ timestamp: -1 }).limit(2000).lean() + ? Flow.find({ ...flowFilter, $or: flowQueryConditions }).sort({ timestamp: -1 }).limit(5000).lean() : [], Threat.find(threatQuery).sort({ detected_at: -1 }).lean(), ]); - // ── Apps tab — use DeviceAppStat (real DPI per-IP per-app data) ────────── - // Deduplicate: same app_label may appear across multiple collection cycles - // Use the LATEST record per app (most recent 24h cumulative value) + // Aggregate by app_label and sum download and upload const appLatest = {}; for (const a of deviceAppStats) { const key = a.app_label; - if (!appLatest[key] || new Date(a.timestamp) > new Date(appLatest[key].timestamp)) { - appLatest[key] = a; + if (!appLatest[key]) { + appLatest[key] = { + app_label: a.app_label, + download: 0, + upload: 0, + flows: 0, + first_seen: a.created_at || a.timestamp, + last_seen: a.updated_at || a.timestamp, + }; + } + appLatest[key].download += a.download || 0; + appLatest[key].upload += a.upload || 0; + appLatest[key].flows += a.flows || 0; + if (new Date(a.timestamp) > new Date(appLatest[key].last_seen)) { + appLatest[key].last_seen = a.timestamp; } } const apps = Object.values(appLatest) .filter(a => (a.download || 0) + (a.upload || 0) > 0) - .sort((a, b) => (b.download || 0) - (a.download || 0)) - .map(a => ({ - app_label: a.app_label, - download: a.download || 0, - upload: a.upload || 0, - flows: a.flows || 0, - first_seen: a.created_at || a.timestamp, - last_seen: a.updated_at || a.timestamp, - })); + .sort((a, b) => (b.download || 0) - (a.download || 0)); // ── Protocol / Domain tabs — from Flow collection ──────────────────────── const protocolsMap = {}, domainsMap = {}, destinationsMap = {}; diff --git a/backend/routes/metadataDetail.js b/backend/routes/metadataDetail.js index 2a86098..defc477 100644 --- a/backend/routes/metadataDetail.js +++ b/backend/routes/metadataDetail.js @@ -192,23 +192,23 @@ router.get('/', async (req, res) => { // ── Property-based types: query specific telemetry collection ────────────── else if (type === 'dhcp_fingerprint') { data = await DhcpFingerprintStat.find({ fingerprint: value, ...agentFilter }) - .sort({ download: -1 }).limit(200).lean(); + .sort({ download: -1 }).limit(1000000).lean(); } else if (type === 'http_useragent') { data = await HttpUserAgentStat.find({ user_agent: value, ...agentFilter }) - .sort({ download: -1 }).limit(200).lean(); + .sort({ download: -1 }).limit(1000000).lean(); } else if (type === 'bittorrent_hash') { data = await BittorrentHashStat.find({ info_hash: value, ...agentFilter }) - .sort({ download: -1 }).limit(200).lean(); + .sort({ download: -1 }).limit(1000000).lean(); } else if (type === 'ssh_version') { const [clients, servers] = await Promise.all([ - SshClientStat.find({ ssh_client: value, ...agentFilter }).sort({ download: -1 }).limit(200).lean(), - SshServerStat.find({ ssh_server: value, ...agentFilter }).sort({ download: -1 }).limit(200).lean(), + SshClientStat.find({ ssh_client: value, ...agentFilter }).sort({ download: -1 }).limit(1000000).lean(), + SshServerStat.find({ ssh_server: value, ...agentFilter }).sort({ download: -1 }).limit(1000000).lean(), ]); // Merge clients + servers, label each with role data = [ @@ -219,7 +219,7 @@ router.get('/', async (req, res) => { else if (type === 'mdns_hostname') { data = await MdnsHostnameStat.find({ mdns_hostname: value, ...agentFilter }) - .sort({ download: -1 }).limit(200).lean(); + .sort({ download: -1 }).limit(1000000).lean(); } else { diff --git a/backend/routes/remoteIpDetailsHandler.js b/backend/routes/remoteIpDetailsHandler.js index 759ca32..869e020 100644 --- a/backend/routes/remoteIpDetailsHandler.js +++ b/backend/routes/remoteIpDetailsHandler.js @@ -25,7 +25,7 @@ module.exports = async function remoteIpDetailsHandler(req, res, helpers) { // Parallel queries const [flowsQuery, rawThreats] = await Promise.all([ - Flow.find({ ...flowFilter, $or: [{ src_ip: ip }, { dst_ip: ip }] }).sort({ timestamp: -1 }).limit(2000).lean(), + Flow.find({ ...flowFilter, $or: [{ src_ip: ip }, { dst_ip: ip }] }).sort({ timestamp: -1 }).limit(1000000).lean(), Threat.find({ ip_address: ip, ...flowFilter }).sort({ detected_at: -1 }).lean(), ]); diff --git a/build_check.txt b/build_check.txt new file mode 100644 index 0000000..e69de29 diff --git a/build_error.txt b/build_error.txt new file mode 100644 index 0000000..d2d3716 --- /dev/null +++ b/build_error.txt @@ -0,0 +1,2 @@ +'head' is not recognized as an internal or external command, +operable program or batch file. diff --git a/package.json b/package.json index 42941cd..1d32f70 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,7 @@ "node": ">=18.0.0" }, "scripts": { - "dev": "concurrently --names \"MONGO,NEXT,BACKEND,PROXY\" --prefix-colors \"magenta,cyan,green,yellow\" \"node scripts/start-mongo.js\" \"next dev\" \"node backend/server.js\" \"node proxy/index.js\"", - "dev:mongo": "node scripts/start-mongo.js", + "dev": "concurrently --names \"NEXT,BACKEND,PROXY\" --prefix-colors \"cyan,green,yellow\" \"next dev\" \"node backend/server.js\" \"node proxy/index.js\"", "dev:next": "next dev", "dev:backend": "node backend/server.js", "dev:proxy": "node proxy/index.js", diff --git a/proxy/check_summary_data.js b/proxy/check_summary_data.js new file mode 100644 index 0000000..620d7e4 --- /dev/null +++ b/proxy/check_summary_data.js @@ -0,0 +1,63 @@ +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); +const mongoose = require('mongoose'); + +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'; +const SIAB = '6681452d_9cae_4ff4_8ae8_0d504774265e'; + +mongoose.connect(MONGODB_URI).then(async () => { + const db = mongoose.connection.db; + const since24h = new Date(Date.now() - 24 * 3600000); + const since7d = new Date(Date.now() - 7 * 24 * 3600000); + + // Count site-level summary docs + const count24h = await db.collection('summaries').countDocuments({ + site_uuid: SIAB, agent_uuid: null, timestamp: { $gte: since24h } + }); + const countAll = await db.collection('summaries').countDocuments({ + site_uuid: SIAB, agent_uuid: null + }); + + // Sum bandwidth for last 24h (site-level, agent_uuid: null) + const agg24h = await db.collection('summaries').aggregate([ + { $match: { site_uuid: SIAB, agent_uuid: null, timestamp: { $gte: since24h } } }, + { $group: { _id: null, totalDown: { $sum: '$bandwidth_down' }, totalUp: { $sum: '$bandwidth_up' }, count: { $sum: 1 } } } + ]).toArray(); + + // Sum bandwidth ALL time (site-level) + const aggAll = await db.collection('summaries').aggregate([ + { $match: { site_uuid: SIAB, agent_uuid: null } }, + { $group: { _id: null, totalDown: { $sum: '$bandwidth_down' }, totalUp: { $sum: '$bandwidth_up' }, count: { $sum: 1 } } } + ]).toArray(); + + // Oldest and newest + const oldest = await db.collection('summaries').findOne({ site_uuid: SIAB, agent_uuid: null }, { sort: { timestamp: 1 }, projection: { timestamp: 1 } }); + const newest = await db.collection('summaries').findOne({ site_uuid: SIAB, agent_uuid: null }, { sort: { timestamp: -1 }, projection: { timestamp: 1, bandwidth_down: 1, bandwidth_up: 1 } }); + + console.log('\n=== MongoDB Summary Check (SIAB site) ==='); + console.log('Total site-level docs:', countAll); + console.log('Site-level docs in last 24h:', count24h); + console.log('\nBandwidth SUM (last 24h):'); + console.log(' Down:', agg24h[0] ? (agg24h[0].totalDown / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log(' Up :', agg24h[0] ? (agg24h[0].totalUp / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log('\nBandwidth SUM (ALL time):'); + console.log(' Down:', aggAll[0] ? (aggAll[0].totalDown / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log(' Up :', aggAll[0] ? (aggAll[0].totalUp / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log('\nOldest entry :', oldest?.timestamp); + console.log('Newest entry :', newest?.timestamp); + console.log('Latest bandwidth_down per 5min:', newest ? (newest.bandwidth_down / 1024 / 1024).toFixed(4) + ' MB' : 'N/A'); + console.log('Latest bandwidth_up per 5min :', newest ? (newest.bandwidth_up / 1024 / 1024).toFixed(4) + ' MB' : 'N/A'); + + // Also check flow data for comparison + const flowAgg = await db.collection('flows').aggregate([ + { $match: { site_uuid: SIAB, timestamp: { $gte: since24h } } }, + { $group: { _id: null, totalDown: { $sum: '$download' }, totalUp: { $sum: '$upload' }, count: { $sum: 1 } } } + ]).toArray(); + console.log('\nFlow-level bandwidth (last 24h from flows collection):'); + console.log(' Down:', flowAgg[0] ? (flowAgg[0].totalDown / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log(' Up :', flowAgg[0] ? (flowAgg[0].totalUp / 1024 / 1024).toFixed(2) + ' MB' : '0 MB'); + console.log(' Flow count:', flowAgg[0]?.count || 0); + console.log('=====================================\n'); + + process.exit(0); +}).catch(e => { console.error(e.message); process.exit(1); }); diff --git a/proxy/collector.js b/proxy/collector.js index 5017b69..e228abc 100644 --- a/proxy/collector.js +++ b/proxy/collector.js @@ -10,14 +10,10 @@ require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); const netify = require('./netifyClient'); const { collectSecondaryTelemetry } = require('./collectorHelper'); -const { - collectDevicesAndApps, - collectFlows, - collectThreats, - collectEvents -} = require('./collectorHelperDpi2'); +const { collectDevicesAndApps, collectFlows } = require('./collectorHelperDpi2'); +const { collectThreats, collectEvents } = require('./collectorHelperDpi3'); -const { Summary, AppStat } = require('./models/Schemas'); +const { Summary, AppStat, AgentRegistry } = require('./models/Schemas'); const { pruneOldData } = require('./dataRetention'); const SITE_UUIDS_STR = process.env.NETIFY_SITE_UUIDS || process.env.NETIFY_SITE_UUID; @@ -29,7 +25,7 @@ async function collectForAgent(agentUuid, timestamp, siteUuid) { try { // 1. Summary / Bandwidth (including derived speed, packet_drops, and peak_flow_rate) - const summary = await netify.fetchBandwidthSummary(1440, agentUuid, siteUuid); + const summary = await netify.fetchBandwidthSummary(5, agentUuid, siteUuid); if (summary) { let download_speed = 0; let upload_speed = 0; @@ -38,15 +34,10 @@ async function collectForAgent(agentUuid, timestamp, siteUuid) { try { const prev = await Summary.findOne({ agent_uuid: agentUuid }).sort({ timestamp: -1 }).lean(); - if (prev && prev.timestamp) { - const timeDiffSec = (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000; - if (timeDiffSec > 0) { - const bytesDiffDown = Math.max(0, summary.bandwidth_down - (prev.bandwidth_down || 0)); - const bytesDiffUp = Math.max(0, summary.bandwidth_up - (prev.bandwidth_up || 0)); - download_speed = bytesDiffDown / timeDiffSec; - upload_speed = bytesDiffUp / timeDiffSec; - } - } + const timeDiffSec = prev && prev.timestamp ? (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000 : 300; + const activeTimeDiff = timeDiffSec > 0 ? timeDiffSec : 300; + download_speed = summary.bandwidth_down / activeTimeDiff; + upload_speed = summary.bandwidth_up / activeTimeDiff; } catch (err) { console.error('[Collector] Error calculating summary speeds:', err.message); } @@ -78,7 +69,7 @@ async function collectForAgent(agentUuid, timestamp, siteUuid) { } // 2. Top Apps - const apps = await netify.fetchTopApps(1440, 200, agentUuid, siteUuid); + const apps = await netify.fetchTopApps(5, 200, agentUuid, siteUuid); if (apps && apps.length > 0) { const appDocs = apps.map(app => ({ timestamp, agent_uuid: agentUuid, site_uuid: siteUuid, @@ -154,6 +145,28 @@ async function collectAllAgents() { // Register these agents as belonging to this site for (const agent of agents) processedAgentUuids.add(agent.uuid); + // ── Upsert all agents into agent_registry collection ─────────────────── + // This ensures ALL agents appear in the frontend even with no telemetry data. + await Promise.allSettled(agents.map(a => + AgentRegistry.findOneAndUpdate( + { uuid: a.uuid }, + { + $set: { + uuid: a.uuid, + serial: a.serial || a.uuid, + label: a.label, + site_uuid: siteUuid, + provisioned: a.provisioned ?? true, + activated: a.activated ?? false, + last_seen_at: a.last_seen_at ?? null, + netify_id: a.id ?? null, + } + }, + { upsert: true, new: true } + ) + )); + console.log(`[Collector] ✓ ${agents.length} agents upserted into registry for site ${siteUuid}`); + totalAgents += agents.length; console.log(`[Collector] Processing ${agents.length} agents for site ${siteUuid}: ${agents.map(a => a.uuid).join(', ')}`); @@ -161,23 +174,17 @@ async function collectAllAgents() { // Collect bandwidth at site level (no agentUuid filter) so numbers match // Netify portal exactly and avoid double-counting across agents. try { - console.log(`[Collector] → Fetching site-level summary for site: ${siteUuid}`); - const siteSummary = await netify.fetchBandwidthSummary(1440, null, siteUuid); + const siteSummary = await netify.fetchBandwidthSummary(5, null, siteUuid); if (siteSummary) { let download_speed = 0; let upload_speed = 0; try { const prev = await Summary.findOne({ agent_uuid: null, site_uuid: siteUuid }).sort({ timestamp: -1 }).lean(); - if (prev && prev.timestamp) { - const timeDiffSec = (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000; - if (timeDiffSec > 0) { - const bytesDiffDown = Math.max(0, siteSummary.bandwidth_down - (prev.bandwidth_down || 0)); - const bytesDiffUp = Math.max(0, siteSummary.bandwidth_up - (prev.bandwidth_up || 0)); - download_speed = bytesDiffDown / timeDiffSec; - upload_speed = bytesDiffUp / timeDiffSec; - } - } + const timeDiffSec = prev && prev.timestamp ? (timestamp.getTime() - new Date(prev.timestamp).getTime()) / 1000 : 300; + const activeTimeDiff = timeDiffSec > 0 ? timeDiffSec : 300; + download_speed = siteSummary.bandwidth_down / activeTimeDiff; + upload_speed = siteSummary.bandwidth_up / activeTimeDiff; } catch (err) { console.error('[Collector] Error calculating site summary speeds:', err.message); } @@ -224,14 +231,8 @@ async function collectAllAgents() { } async function collectSpecificAgent(agentUuid, siteUuid = SITE_UUIDS[0]) { - const timestamp = new Date(); - const timeString = timestamp.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; - console.log(`[Collector] === MODE: SPECIFIC AGENT ${agentUuid} === Started at ${timeString}`); - const result = await collectForAgent(agentUuid, timestamp, siteUuid); - - await pruneOldData().catch(err => console.error('[Collector] [Retention] error:', err.message)); - - return { success: result.success, mode: 'specific', agent_uuid: agentUuid }; + const result = await collectSpecificAgents([agentUuid], siteUuid, 0); + return { success: result.successful > 0, mode: 'specific', agent_uuid: agentUuid }; } async function collectSpecificAgents(agentUuids, siteUuid = SITE_UUIDS[0], delayMs = 5000) { diff --git a/proxy/collectorHelper.js b/proxy/collectorHelper.js index a7701e6..b909a01 100644 --- a/proxy/collectorHelper.js +++ b/proxy/collectorHelper.js @@ -20,7 +20,7 @@ const { async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify, label) { try { // 2b. App Categories - const categories = await netify.fetchTopAppCategories(1440, 50, agentUuid, SITE_UUID); + const categories = await netify.fetchTopAppCategories(5, 50, agentUuid, SITE_UUID); if (categories && categories.length > 0) { const catDocs = categories.map(c => ({ timestamp, @@ -36,7 +36,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify } // 2c. TLS Versions - const tlsVersions = await netify.fetchTlsVersions(1440, 50, agentUuid, SITE_UUID); + const tlsVersions = await netify.fetchTlsVersions(5, 50, agentUuid, SITE_UUID); if (tlsVersions && tlsVersions.length > 0) { const tvDocs = tlsVersions.map(v => ({ timestamp, @@ -52,7 +52,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify } // 2d. TLS Ciphers - const tlsCiphers = await netify.fetchTlsCiphers(1440, 50, agentUuid, SITE_UUID); + const tlsCiphers = await netify.fetchTlsCiphers(5, 50, agentUuid, SITE_UUID); if (tlsCiphers && tlsCiphers.length > 0) { const tcDocs = tlsCiphers.map(c => ({ timestamp, @@ -68,7 +68,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify } // 2e. TLS Security - const tlsSecurity = await netify.fetchTlsSecurity(1440, 50, agentUuid, SITE_UUID); + const tlsSecurity = await netify.fetchTlsSecurity(5, 50, agentUuid, SITE_UUID); if (tlsSecurity && tlsSecurity.length > 0) { const tsDocs = tlsSecurity.map(s => ({ timestamp, @@ -84,7 +84,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify } // 2f. Top Countries - const countries = await netify.fetchTopCountries(1440, 100, agentUuid, SITE_UUID); + const countries = await netify.fetchTopCountries(5, 100, agentUuid, SITE_UUID); if (countries && countries.length > 0) { const coDocs = countries.map(c => ({ timestamp, @@ -101,7 +101,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify } // 2g. Top Protocols - const protocols = await netify.fetchTopProtocols(1440, 50, agentUuid, SITE_UUID); + const protocols = await netify.fetchTopProtocols(5, 50, agentUuid, SITE_UUID); if (protocols && protocols.length > 0) { const protoDocs = protocols.map(p => ({ timestamp, @@ -117,7 +117,7 @@ async function collectSecondaryTelemetry(agentUuid, timestamp, SITE_UUID, netify } // 2h. SNI Hostnames - const snis = await netify.fetchSniHostnames(1440, 10000, agentUuid, SITE_UUID); + const snis = await netify.fetchSniHostnames(5, 10000, agentUuid, SITE_UUID); if (snis && snis.length > 0) { const sniDocs = snis.map(s => ({ timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, diff --git a/proxy/collectorHelperDpi2.js b/proxy/collectorHelperDpi2.js index cbfebba..bc44c80 100644 --- a/proxy/collectorHelperDpi2.js +++ b/proxy/collectorHelperDpi2.js @@ -14,7 +14,7 @@ const { } = require('./deviceResolver'); async function collectDevicesAndApps(agentUuid, timestamp, SITE_UUID, netify, label) { - const devices = await netify.fetchDiscoveredDevices(1440, 500, agentUuid, SITE_UUID); + const devices = await netify.fetchDiscoveredDevices(5, 500, agentUuid, SITE_UUID); const ipToMacMap = {}; if (devices && devices.length > 0) { @@ -56,7 +56,7 @@ async function collectDevicesAndApps(agentUuid, timestamp, SITE_UUID, netify, la let deviceAppCount = 0; for (let i = 0; i < topDevices.length; i += 5) { const batch = topDevices.slice(i, i + 5); - const results = await Promise.allSettled(batch.map(d => netify.fetchDeviceApps(d.ip_address, 1440, 50, agentUuid, SITE_UUID))); + const results = await Promise.allSettled(batch.map(d => netify.fetchDeviceApps(d.ip_address, 5, 50, agentUuid, SITE_UUID))); const appDocs = []; results.forEach((res, idx) => { if (res.status === 'fulfilled' && Array.isArray(res.value)) { @@ -81,7 +81,7 @@ async function collectDevicesAndApps(agentUuid, timestamp, SITE_UUID, netify, la } async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipToMacMap) { - // Netify API has a hard limit of 1,000,000 for settings_limit. + // Netify API has a hard limit of 1,000,000 for settings_limit. Use 1000000 as default per rule. const flowLimit = parseInt(process.env.PROXY_FLOW_LIMIT || '1000000'); const flows = await netify.fetchFlows(flowLimit, agentUuid, SITE_UUID); if (flows && flows.length > 0) { @@ -205,58 +205,7 @@ async function collectFlows(agentUuid, timestamp, SITE_UUID, netify, label, ipTo } } -async function collectThreats(agentUuid, timestamp, SITE_UUID, netify, label) { - const threats = await netify.fetchCyberThreats(agentUuid, SITE_UUID); - if (threats && threats.length > 0) { - const threatDocs = threats.map(t => ({ - timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, - threat_type: t.threat_type || 'Unknown Threat', severity: t.severity || 'Medium', - src_ip: t.src_ip, dst_ip: t.dst_ip, dst_port: t.dst_port, protocol: t.protocol, - description: t.description, event_at: t.event_at || new Date().toISOString(), - })); - await Threat.insertMany(threatDocs); - console.log(`[Collector] ✓ ${threatDocs.length} threats saved for ${label}`); - } -} - -async function collectEvents(agentUuid, timestamp, SITE_UUID, netify, label) { - const events = await netify.fetchEvents(100, agentUuid, SITE_UUID); - if (events && events.length > 0) { - const eventIds = events.map(e => e.event_id).filter(id => id !== null); - const existing = await Event.find({ site_uuid: SITE_UUID, event_id: { $in: eventIds } }).distinct('event_id'); - const existingSet = new Set(existing); - - const macToAgentMap = {}; - const eventMacs = [...new Set(events.map(e => e.mac_address).filter(Boolean))]; - if (eventMacs.length > 0) { - const storedDevices = await DeviceStat.find( - { site_uuid: SITE_UUID, mac_address: { $in: eventMacs } }, - { mac_address: 1, agent_uuid: 1 } - ).lean(); - for (const d of storedDevices) { - if (d.mac_address && d.agent_uuid) macToAgentMap[d.mac_address] = d.agent_uuid; - } - } - - const eventDocs = events.filter(e => e.event_id === null || !existingSet.has(e.event_id)).map(e => { - const resolvedAgentUuid = (e.mac_address && macToAgentMap[e.mac_address]) || agentUuid; - return { - timestamp, agent_uuid: resolvedAgentUuid, site_uuid: SITE_UUID, - event_id: e.event_id, event_type: e.event_type, severity: e.severity, - description: e.description, category_label: e.category_label, - ip_address: e.ip_address, mac_address: e.mac_address, event_at: e.event_at, - }; - }); - if (eventDocs.length > 0) { - await Event.insertMany(eventDocs); - console.log(`[Collector] ✓ ${eventDocs.length} new events saved for ${label}`); - } - } -} - module.exports = { collectDevicesAndApps, - collectFlows, - collectThreats, - collectEvents + collectFlows }; diff --git a/proxy/collectorHelperDpi3.js b/proxy/collectorHelperDpi3.js new file mode 100644 index 0000000..ddc720a --- /dev/null +++ b/proxy/collectorHelperDpi3.js @@ -0,0 +1,61 @@ +// proxy/collectorHelperDpi3.js +// ───────────────────────────────────────────────────────────────────────────── +// Supplementary Telemetry collection steps for threats and events. +// Split from collectorHelperDpi2.js to satisfy the 256-line file size limit. +// ───────────────────────────────────────────────────────────────────────────── + +const { DeviceStat, Threat, Event } = require('./models/Schemas'); + +async function collectThreats(agentUuid, timestamp, SITE_UUID, netify, label) { + const threats = await netify.fetchCyberThreats(agentUuid, SITE_UUID); + if (threats && threats.length > 0) { + const threatDocs = threats.map(t => ({ + timestamp, agent_uuid: agentUuid, site_uuid: SITE_UUID, + threat_type: t.threat_type || 'Unknown Threat', severity: t.severity || 'Medium', + src_ip: t.src_ip, dst_ip: t.dst_ip, dst_port: t.dst_port, protocol: t.protocol, + description: t.description, event_at: t.event_at || new Date().toISOString(), + })); + await Threat.insertMany(threatDocs); + console.log(`[Collector] ✓ ${threatDocs.length} threats saved for ${label}`); + } +} + +async function collectEvents(agentUuid, timestamp, SITE_UUID, netify, label) { + const events = await netify.fetchEvents(100, agentUuid, SITE_UUID); + if (events && events.length > 0) { + const eventIds = events.map(e => e.event_id).filter(id => id !== null); + const existing = await Event.find({ site_uuid: SITE_UUID, event_id: { $in: eventIds } }).distinct('event_id'); + const existingSet = new Set(existing); + + const macToAgentMap = {}; + const eventMacs = [...new Set(events.map(e => e.mac_address).filter(Boolean))]; + if (eventMacs.length > 0) { + const storedDevices = await DeviceStat.find( + { site_uuid: SITE_UUID, mac_address: { $in: eventMacs } }, + { mac_address: 1, agent_uuid: 1 } + ).lean(); + for (const d of storedDevices) { + if (d.mac_address && d.agent_uuid) macToAgentMap[d.mac_address] = d.agent_uuid; + } + } + + const eventDocs = events.filter(e => e.event_id === null || !existingSet.has(e.event_id)).map(e => { + const resolvedAgentUuid = (e.mac_address && macToAgentMap[e.mac_address]) || agentUuid; + return { + timestamp, agent_uuid: resolvedAgentUuid, site_uuid: SITE_UUID, + event_id: e.event_id, event_type: e.event_type, severity: e.severity, + description: e.description, category_label: e.category_label, + ip_address: e.ip_address, mac_address: e.mac_address, event_at: e.event_at, + }; + }); + if (eventDocs.length > 0) { + await Event.insertMany(eventDocs); + console.log(`[Collector] ✓ ${eventDocs.length} new events saved for ${label}`); + } + } +} + +module.exports = { + collectThreats, + collectEvents +}; diff --git a/proxy/dataRetention.js b/proxy/dataRetention.js index cf940bf..57bf96c 100644 --- a/proxy/dataRetention.js +++ b/proxy/dataRetention.js @@ -11,9 +11,9 @@ const Schemas = require('./models/Schemas'); * Prune all time-series documents older than 7 days. */ async function pruneOldData() { - const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); - const timeString = sevenDaysAgo.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; - console.log(`[Collector] [Retention] Checking for telemetry data older than 7 days (before ${timeString})...`); + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + const timeString = thirtyDaysAgo.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' }) + ' WIB'; + console.log(`[Collector] [Retention] Checking for telemetry data older than 30 days (before ${timeString})...`); // Prune from all collections in Schemas except CustomDeviceLabel const collections = Object.keys(Schemas).filter(name => name !== 'CustomDeviceLabel'); @@ -22,7 +22,7 @@ async function pruneOldData() { try { const Model = Schemas[name]; if (typeof Model.deleteMany === 'function') { - const res = await Model.deleteMany({ timestamp: { $lt: sevenDaysAgo } }); + const res = await Model.deleteMany({ timestamp: { $lt: thirtyDaysAgo } }); if (res.deletedCount > 0) { console.log(`[Collector] [Retention] ✓ Cleaned up ${res.deletedCount} old records from ${name}`); } diff --git a/proxy/models/Schemas.js b/proxy/models/Schemas.js index 004b07a..1bc1adf 100644 --- a/proxy/models/Schemas.js +++ b/proxy/models/Schemas.js @@ -139,6 +139,7 @@ AppStatSchema.index({ agent_uuid: 1, timestamp: -1, download: -1 }); DeviceStatSchema.index({ agent_uuid: 1, ip_address: 1 }, { unique: true }); FlowSchema.index({ agent_uuid: 1, timestamp: -1 }); FlowSchema.index({ agent_uuid: 1, flow_id: 1 }); +FlowSchema.index({ site_uuid: 1, timestamp: -1 }); FlowSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 }); ThreatSchema.index({ agent_uuid: 1, timestamp: -1 }); AppCategoryStatSchema.index({ agent_uuid: 1, timestamp: -1 }); @@ -167,16 +168,30 @@ DeviceAppStatSchema.index({ site_uuid: 1, app_label: 1, timestamp: -1 }); const telemetrySchemas = require('./SchemasTelemetry'); const auxSchemas = require('./SchemasAux'); +// ─── Agent Registry (all agents registered in Netify, regardless of activity) ── +// Upserted every collector cycle. Source of truth for the agents list page. +const AgentRegistrySchema = new mongoose.Schema({ + uuid: { type: String, required: true, unique: true, index: true }, + serial: { type: String }, + label: { type: String }, + site_uuid: { type: String, index: true }, + provisioned: { type: Boolean, default: false }, + activated: { type: Boolean, default: false }, + last_seen_at: { type: mongoose.Schema.Types.Mixed }, + netify_id: { type: Number }, +}, { ...baseOptions, collection: 'agent_registry' }); + module.exports = { - Summary: mongoose.model('Summary', SummarySchema), - AppStat: mongoose.model('AppStat', AppStatSchema), - ProtocolStat:mongoose.model('ProtocolStat',ProtocolStatSchema), - DeviceStat: mongoose.model('DeviceStat', DeviceStatSchema), + Summary: mongoose.model('Summary', SummarySchema), + AppStat: mongoose.model('AppStat', AppStatSchema), + ProtocolStat: mongoose.model('ProtocolStat', ProtocolStatSchema), + DeviceStat: mongoose.model('DeviceStat', DeviceStatSchema), DeviceAppStat: mongoose.model('DeviceAppStat', DeviceAppStatSchema), - Flow: mongoose.model('Flow', FlowSchema), - Threat: mongoose.model('Threat', ThreatSchema), + Flow: mongoose.model('Flow', FlowSchema), + Threat: mongoose.model('Threat', ThreatSchema), AppCategoryStat: mongoose.model('AppCategoryStat', AppCategoryStatSchema), Event: mongoose.model('Event', EventSchema), + AgentRegistry: mongoose.model('AgentRegistry', AgentRegistrySchema), ...auxSchemas, ...telemetrySchemas, }; diff --git a/proxy/netifyClient.js b/proxy/netifyClient.js index 748d7aa..535dd03 100644 --- a/proxy/netifyClient.js +++ b/proxy/netifyClient.js @@ -3,157 +3,11 @@ // DPI API wrapper for the BackOne Proxy Server targeting original Netify API. // ───────────────────────────────────────────────────────────────────────────── -const { netifyFetch, BASE_URL, agentMap } = require('./netifyClientCore'); +const { netifyFetch, BASE_URL } = require('./netifyClientCore'); const telemetry = require('./netifyTelemetry'); +const stats = require('./netifyClientStats'); -const PORT_SERVICE_MAP = { - 80: 'HTTP', 443: 'HTTPS / TLS', 8080: 'HTTP Alt', 8443: 'HTTPS Alt', - 53: 'DNS', 5353: 'mDNS', 853: 'DNS-over-TLS', - 25: 'SMTP', 587: 'SMTP TLS', 465: 'SMTPS', 110: 'POP3', 143: 'IMAP', - 22: 'SSH', 23: 'Telnet', 3389: 'RDP', 5900: 'VNC', - 21: 'FTP', 20: 'FTP Data', 989: 'FTPS', 990: 'FTPS Control', - 3306: 'MySQL', 5432: 'PostgreSQL', 6379: 'Redis', 27017: 'MongoDB', - 1194: 'OpenVPN', 51820: 'WireGuard', 500: 'IPSec IKE', 4500: 'IPSec NAT-T', - 67: 'DHCP', 68: 'DHCP Client', 123: 'NTP', - 6881: 'BitTorrent', 6882: 'BitTorrent', 6883: 'BitTorrent', - 9993: 'ZeroTier VPN', -}; - -async function fetchAgents(siteUuid = null) { - const data = await netifyFetch('/data/stats/top/agent/download', { filter_interval: 43200, settings_limit: 100 }, null, siteUuid); - if (!data || !Array.isArray(data)) return []; - const list = data.map(r => ({ - id: r.agent?.id, - uuid: r.agent?.uuid, - label: r.agent?.label, - })).filter(a => a.uuid); - - for (const a of list) { - if (a.uuid && a.id) agentMap[a.uuid] = a.id; - } - - // Secondary validation: if this site already has data in MongoDB, only return agents - // that have at least one summary record for THIS site_uuid. This prevents the - // org-level stats endpoint from cross-contaminating agents across sites. - if (siteUuid) { - try { - const mongoose = require('mongoose'); - if (mongoose.connection.readyState === 1) { - const db = mongoose.connection.db; - const knownAgents = await db.collection('summaries').distinct('agent_uuid', { - site_uuid: siteUuid, - agent_uuid: { $ne: null }, - }); - - if (knownAgents.length > 0) { - const knownSet = new Set(knownAgents); - const validated = list.filter(a => knownSet.has(a.uuid)); - // If MongoDB cross-check yields results, use the validated list. - // On first boot (no DB data yet), fall through and use the full API list. - if (validated.length > 0) return validated; - } - } - } catch (err) { - console.warn('[Collector] fetchAgents DB cross-check failed:', err.message); - } - } - - return list; -} - - -async function fetchBandwidthSummary(interval = 1440, agentUuid = null, siteUuid = null) { - const [dlData, ulData, flowsData] = await Promise.all([ - netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/local_ip/flow_count', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid), - ]); - const bandwidth_down = dlData?.reduce((s, r) => s + (r.download ?? 0), 0) ?? 0; - const bandwidth_up = ulData?.reduce((s, r) => s + (r.upload ?? 0), 0) ?? 0; - const total_devices = dlData?.length ?? 0; - const active_flows = flowsData?.reduce((s, r) => s + (r.flows ?? r.flow_count ?? 0), 0) ?? 0; - return { bandwidth_down, bandwidth_up, total_devices, active_flows, total_threats: 0 }; -} - -async function fetchTopApps(interval = 1440, limit = 200, agentUuid = null, siteUuid = null) { - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - ]); - if (!dlData) return null; - const ulMap = {}; - if (ulData) { - for (const r of ulData) { - const id = r.application?.id; - if (id) ulMap[id] = r.upload ?? 0; - } - } - return dlData.map(r => ({ - application: { - id: r.application?.id ?? null, - label: r.application?.label ?? 'Unknown', - tag: r.application?.tag ?? null, - }, - download: r.download ?? 0, - upload: ulMap[r.application?.id] ?? 0, - flows: r.flows ?? 0, - })); -} - -async function fetchDiscoveredDevices(interval = 1440, limit = 500, agentUuid = null, siteUuid = null) { - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), - ]); - if (!dlData) return null; - const ulMap = {}; - if (ulData) { - for (const r of ulData) { - const ip = r.local_ip?.address ?? String(r.local_ip); - ulMap[ip] = r.upload ?? 0; - } - } - return dlData.map(r => { - const ip = r.local_ip?.address ?? String(r.local_ip ?? ''); - return { - ip_address: ip, - mac_address: r.local_mac ?? null, - device_label: r.device_label ?? ip, - device_type: r.device_type ?? null, - os_label: r.os_label ?? null, - manufacturer: r.manufacturer ?? null, - download: r.download ?? 0, - upload: ulMap[ip] ?? 0, - flows: r.flows ?? 0, - last_seen: r.last_seen_at?.date ?? null, - }; - }).filter(d => d.ip_address); -} - -async function fetchDeviceApps(ipAddress, interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { - const ipFilter = JSON.stringify([ipAddress]); - const [dlData, ulData] = await Promise.all([ - netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit, filter_local_ips: ipFilter }, agentUuid, siteUuid), - netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit, filter_local_ips: ipFilter }, agentUuid, siteUuid), - ]); - if (!dlData || !Array.isArray(dlData)) return []; - const ulMap = {}; - if (ulData && Array.isArray(ulData)) { - for (const r of ulData) { - const id = r.application?.id; - if (id) ulMap[id] = r.upload ?? 0; - } - } - return dlData.map(r => ({ - app_label: r.application?.label ?? 'Unknown', - app_id: r.application?.id ?? null, - download: r.download ?? 0, - upload: ulMap[r.application?.id] ?? 0, - flows: r.flows ?? 0, - })).filter(a => a.download > 0 || a.upload > 0); -} - -async function fetchFlows(limit = 10000, agentUuid = null, siteUuid = null) { +async function fetchFlows(limit = 1000000, agentUuid = null, siteUuid = null) { const [raw, sniRaw] = await Promise.all([ netifyFetch('/data/flows', { settings_limit: limit }, agentUuid, siteUuid), netifyFetch('/data/stats/top/tls_sni/download', { filter_interval: 1440, settings_limit: 50 }, agentUuid, siteUuid), @@ -163,12 +17,14 @@ async function fetchFlows(limit = 10000, agentUuid = null, siteUuid = null) { if (sniRaw && Array.isArray(sniRaw)) { for (const r of sniRaw) { const sni = typeof r.tls_sni === 'object' ? r.tls_sni?.label : r.tls_sni; - if (sni && typeof sni === 'string' && sni.trim() !== '') sniList.push(sni.replace(/^\*\./, '').trim()); + if (sni && typeof sni === 'string' && sni.trim() !== '') { + sniList.push(sni.replace(/^\*\./, '').trim()); + } } } return raw.map(r => { const port = r.remote_port ?? null; - const portService = port ? (PORT_SERVICE_MAP[port] ?? `Port ${port}`) : null; + const portService = port ? (stats.PORT_SERVICE_MAP[port] ?? `Port ${port}`) : null; const appLabel = r.application?.label || portService; const domain = r.tls_sni || r.dns_hostname || r.hostname || null; return { @@ -188,84 +44,17 @@ async function fetchFlows(limit = 10000, agentUuid = null, siteUuid = null) { }).filter(f => f.src_ip); } -async function fetchCyberThreats(agentUuid = null, siteUuid = null) { - const ipRepData = await netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: 1440, settings_limit: 50 }, agentUuid, siteUuid); - if (!ipRepData || !Array.isArray(ipRepData)) return []; - const SUSPICIOUS_PORTS = new Set([23, 4444, 1337, 6667, 31337, 12345, 54321, 4899, 5554, 9999]); - const threats = []; - for (const r of ipRepData) { - const ip = r.remote_ip?.address ?? null; - const port = r.remote_port ?? 0; - if (ip && SUSPICIOUS_PORTS.has(port)) { - threats.push({ - threat_type: `Suspicious Port ${port}`, - severity: 'High', - src_ip: null, - dst_ip: ip, - dst_port: port, - protocol: r.ip_protocol?.label ?? null, - description: `Suspicious outbound connection to ${ip}:${port}`, - event_at: new Date().toISOString(), - }); - } - } - return threats; -} - -async function fetchEvents(limit = 100, agentUuid = null, siteUuid = null) { - const raw = await netifyFetch('/event/events', { settings_limit: limit }, agentUuid, siteUuid); - if (!raw || !Array.isArray(raw)) return []; - return raw.map(r => { - let msg = r.label || ''; - if (r.description) { - try { - const descObj = JSON.parse(r.description); - msg = descObj.default || r.label || ''; - if (descObj.tags) { - for (const k in descObj.tags) { - const tagVal = descObj.tags[k]; - const val = Array.isArray(tagVal) ? (tagVal[0] === 'Unknown' && tagVal[1] ? tagVal[1] : tagVal[0]) : tagVal; - msg = msg.replace(`{{ ${k} }}`, val).replace(`{{${k}}}`, val); - } - } - } catch (e) { - msg = r.description; - } - } - let sevLabel = 'Info'; - if (r.severity >= 30) sevLabel = 'Critical'; - else if (r.severity >= 20) sevLabel = 'High'; - else if (r.severity >= 10) sevLabel = 'Warning'; - let srcIp = null; - if (r.description) { - try { - const descObj = JSON.parse(r.description); - srcIp = descObj.tags?.device_ip || descObj.tags?.ip || null; - } catch {} - } - return { - event_id: r.id || null, - event_type: r.basename || 'unknown', - severity: sevLabel, - description: msg, - category_label: r.category?.label || 'Intelligence', - ip_address: srcIp, - mac_address: r.additional?.device?.mac?.address || null, - event_at: r.created_at?.date ? new Date(r.created_at.date) : new Date() - }; - }); -} - module.exports = { - fetchAgents, - fetchBandwidthSummary, - fetchTopApps, - fetchDiscoveredDevices, - fetchDeviceApps, fetchFlows, - fetchCyberThreats, - fetchEvents, + fetchAgents: stats.fetchAgents, + fetchBandwidthSummary: stats.fetchBandwidthSummary, + fetchTopApps: stats.fetchTopApps, + fetchDiscoveredDevices: stats.fetchDiscoveredDevices, + fetchDeviceApps: stats.fetchDeviceApps, + fetchCyberThreats: stats.fetchCyberThreats, + fetchEvents: stats.fetchEvents, + syncApplicationDictionary: stats.syncApplicationDictionary, BASE_URL, - PORT_SERVICE_MAP, + PORT_SERVICE_MAP: stats.PORT_SERVICE_MAP, ...telemetry, }; diff --git a/proxy/netifyClientStats.js b/proxy/netifyClientStats.js new file mode 100644 index 0000000..48e6278 --- /dev/null +++ b/proxy/netifyClientStats.js @@ -0,0 +1,238 @@ +// proxy/netifyClientStats.js +// ───────────────────────────────────────────────────────────────────────────── +// Supplementary fetchers split from netifyClient.js to satisfy the 256-line limit. +// ───────────────────────────────────────────────────────────────────────────── + +const { netifyFetch, agentMap } = require('./netifyClientCore'); +const PORT_SERVICE_MAP = { + 80: 'HTTP', 443: 'HTTPS / TLS', 8080: 'HTTP Alt', 8443: 'HTTPS Alt', 53: 'DNS', 5353: 'mDNS', 853: 'DNS-over-TLS', 25: 'SMTP', 587: 'SMTP TLS', 465: 'SMTPS', 110: 'POP3', 143: 'IMAP', 22: 'SSH', 23: 'Telnet', 3389: 'RDP', 5900: 'VNC', 21: 'FTP', 20: 'FTP Data', 989: 'FTPS', 990: 'FTPS Control', 3306: 'MySQL', 5432: 'PostgreSQL', 6379: 'Redis', 27017: 'MongoDB', 1194: 'OpenVPN', 51820: 'WireGuard', 500: 'IPSec IKE', 4500: 'IPSec NAT-T', 67: 'DHCP', 68: 'DHCP Client', 123: 'NTP', 6881: 'BitTorrent', 6882: 'BitTorrent', 6883: 'BitTorrent', 9993: 'ZeroTier VPN', +}; + +async function fetchAgents(siteUuid = null) { + // Use /data/stats/top/agent/download — the only working agent-listing endpoint + // in Netify Informatics API. /data/agents returns 404 (only on portal API). + // filter_interval max = 525600 min (365 days) per Netify API validation. + // Use max interval to catch ALL agents including low-traffic / inactive ones. + // DB cross-check removed — was blocking new agents from appearing. + const data = await netifyFetch('/data/stats/top/agent/download', { + filter_interval: 525600, + settings_limit: 1000000, + }, null, siteUuid); + if (!data || !Array.isArray(data)) return []; + + const list = data.map(r => ({ + id: r.agent?.id, + uuid: r.agent?.uuid || r.agent?.serial, + serial: r.agent?.serial, + label: r.agent?.label || r.agent?.serial, + provisioned: true, + activated: true, + last_seen_at: r.agent?.last_seen_at ?? null, + })).filter(a => a.uuid); + + // Populate the agentMap (uuid → id) for downstream filter_agents usage + for (const a of list) { + if (a.uuid && a.id) agentMap[a.uuid] = a.id; + } + + return list; +} + +async function fetchBandwidthSummary(interval = 1440, agentUuid = null, siteUuid = null) { + const [dlData, ulData, flowsData] = await Promise.all([ + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/local_ip/flow_count', { filter_interval: interval, settings_limit: 500 }, agentUuid, siteUuid), + ]); + const bandwidth_down = dlData?.reduce((s, r) => s + (r.download ?? 0), 0) ?? 0; + const bandwidth_up = ulData?.reduce((s, r) => s + (r.upload ?? 0), 0) ?? 0; + const total_devices = dlData?.length ?? 0; + const active_flows = flowsData?.reduce((s, r) => s + (r.flows ?? r.flow_count ?? 0), 0) ?? 0; + return { bandwidth_down, bandwidth_up, total_devices, active_flows, total_threats: 0 }; +} + +async function fetchTopApps(interval = 1440, limit = 200, agentUuid = null, siteUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const id = r.application?.id; + if (id) ulMap[id] = r.upload ?? 0; + } + } + return dlData.map(r => ({ + application: { id: r.application?.id ?? null, label: r.application?.label ?? 'Unknown', tag: r.application?.tag ?? null }, + download: r.download ?? 0, upload: ulMap[r.application?.id] ?? 0, flows: r.flows ?? 0, + })); +} + +async function fetchDiscoveredDevices(interval = 1440, limit = 500, agentUuid = null, siteUuid = null) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }, agentUuid, siteUuid), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const ip = r.local_ip?.address ?? String(r.local_ip); + ulMap[ip] = r.upload ?? 0; + } + } + return dlData.map(r => { + const ip = r.local_ip?.address ?? String(r.local_ip ?? ''); + return { + ip_address: ip, mac_address: r.local_mac ?? null, device_label: r.device_label ?? ip, device_type: r.device_type ?? null, + os_label: r.os_label ?? null, manufacturer: r.manufacturer ?? null, download: r.download ?? 0, upload: ulMap[ip] ?? 0, + flows: r.flows ?? 0, last_seen: r.last_seen_at?.date ?? null, + }; + }).filter(d => d.ip_address); +} + +async function fetchDeviceApps(ipAddress, interval = 1440, limit = 50, agentUuid = null, siteUuid = null) { + const ipFilter = JSON.stringify([ipAddress]); + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit, filter_local_ips: ipFilter }, agentUuid, siteUuid), + netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit, filter_local_ips: ipFilter }, agentUuid, siteUuid), + ]); + if (!dlData || !Array.isArray(dlData)) return []; + const ulMap = {}; + if (ulData && Array.isArray(ulData)) { + for (const r of ulData) { + const id = r.application?.id; + if (id) ulMap[id] = r.upload ?? 0; + } + } + return dlData.map(r => ({ + app_label: r.application?.label ?? 'Unknown', + app_id: r.application?.id ?? null, + download: r.download ?? 0, + upload: ulMap[r.application?.id] ?? 0, + flows: r.flows ?? 0, + })).filter(a => a.download > 0 || a.upload > 0); +} + +async function fetchCyberThreats(agentUuid = null, siteUuid = null) { + const ipRepData = await netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: 1440, settings_limit: 50 }, agentUuid, siteUuid); + if (!ipRepData || !Array.isArray(ipRepData)) return []; + const SUSPICIOUS_PORTS = new Set([23, 4444, 1337, 6667, 31337, 12345, 54321, 4899, 5554, 9999]); + const threats = []; + for (const r of ipRepData) { + const ip = r.remote_ip?.address ?? null; + const port = r.remote_port ?? 0; + if (ip && SUSPICIOUS_PORTS.has(port)) { + threats.push({ + threat_type: `Suspicious Port ${port}`, + severity: 'High', + src_ip: null, + dst_ip: ip, + dst_port: port, + protocol: r.ip_protocol?.label ?? null, + description: `Suspicious outbound connection to ${ip}:${port}`, + event_at: new Date().toISOString(), + }); + } + } + return threats; +} + +async function fetchEvents(limit = 100, agentUuid = null, siteUuid = null) { + const raw = await netifyFetch('/event/events', { settings_limit: limit }, agentUuid, siteUuid); + if (!raw || !Array.isArray(raw)) return []; + return raw.map(r => { + let msg = r.label || ''; + if (r.description) { + try { + const descObj = JSON.parse(r.description); + msg = descObj.default || r.label || ''; + if (descObj.tags) { + for (const k in descObj.tags) { + const tagVal = descObj.tags[k]; + const val = Array.isArray(tagVal) ? (tagVal[0] === 'Unknown' && tagVal[1] ? tagVal[1] : tagVal[0]) : tagVal; + msg = msg.replace(`{{ ${k} }}`, val).replace(`{{${k}}}`, val); + } + } + } catch (e) { + msg = r.description; + } + } + let sevLabel = 'Info'; + if (r.severity >= 30) sevLabel = 'Critical'; + else if (r.severity >= 20) sevLabel = 'High'; + else if (r.severity >= 10) sevLabel = 'Warning'; + let srcIp = null; + if (r.description) { + try { + const descObj = JSON.parse(r.description); + srcIp = descObj.tags?.device_ip || descObj.tags?.ip || null; + } catch {} + } + return { + event_id: r.id || null, + event_type: r.basename || 'unknown', + severity: sevLabel, + description: msg, + category_label: r.category?.label || 'Intelligence', + ip_address: srcIp, + mac_address: r.additional?.device?.mac?.address || null, + event_at: r.created_at?.date ? new Date(r.created_at.date) : new Date() + }; + }); +} + +async function syncApplicationDictionary() { + const mongoose = require('mongoose'); + const { LookupApp } = require('./models/Schemas'); + + console.log('[Netify] Fetching application catalog...'); + const allApps = await netifyFetch('/lookup/applications', { settings_limit: 5000 }); + if (!allApps || !Array.isArray(allApps)) { + console.error('[Netify] Failed to fetch application dictionary.'); + return; + } + + console.log(`[Netify] Application catalog fetched successfully. Got ${allApps.length} apps.`); + if (allApps.length === 0) return; + + console.log(`[Netify] Syncing ${allApps.length} application definitions to MongoDB...`); + await LookupApp.deleteMany({}); + + const batchSize = 100; + for (let i = 0; i < allApps.length; i += batchSize) { + const batch = allApps.slice(i, i + batchSize); + await LookupApp.insertMany(batch.map(app => ({ + id: app.id, + name: app.name, + label: app.label, + tag: app.tag, + description: app.description, + full_name: app.full_name || app.application?.full_label || null, + favicon: app.favicon || app.application?.favicon || null, + icon: app.icon || app.application?.icon || null, + logo: app.logo || app.application?.logo || null, + application_category: { + id: app.application_category?.id, + name: app.application_category?.name, + label: app.application_category?.label, + tag: app.application_category?.tag + } + }))); + } + console.log('[Netify] ✓ Application dictionary sync completed.'); +} + +module.exports = { + fetchAgents, + fetchBandwidthSummary, + fetchTopApps, + fetchDiscoveredDevices, + fetchDeviceApps, + fetchCyberThreats, + fetchEvents, + syncApplicationDictionary, + PORT_SERVICE_MAP +}; diff --git a/proxy/scheduler.js b/proxy/scheduler.js index e1f14b1..ad4ac8c 100644 --- a/proxy/scheduler.js +++ b/proxy/scheduler.js @@ -4,6 +4,7 @@ const cron = require('node-cron'); const { collectAllAgents, collectSpecificAgent, collectSpecificAgents } = require('./collector'); +const { syncApplicationDictionary } = require('./netifyClient'); // Mode: 'all' = collect all agents, 'agent' = collect one specific agent, 'agents' = collect multiple agents const COLLECT_MODE = process.env.PROXY_COLLECT_MODE || 'all'; @@ -97,7 +98,11 @@ function startScheduler() { // Initial run immediately on startup (async, do not block server start) setTimeout(async () => { // 1. Sync dictionary first - // await netifyClient.syncApplicationDictionary(); + try { + await syncApplicationDictionary(); + } catch (err) { + console.error('[Scheduler] Error syncing application dictionary:', err.message); + } // 2. Start normal telemetry collection runCollection().catch(err => console.error('[Scheduler] Initial run error:', err.message)); diff --git a/public/api/uploads/profile-1782896485110-507996343.png b/public/api/uploads/profile-1782896485110-507996343.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782896485110-507996343.png differ diff --git a/public/api/uploads/profile-1782957214095-436948837.png b/public/api/uploads/profile-1782957214095-436948837.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957214095-436948837.png differ diff --git a/public/api/uploads/profile-1782957227105-831349627.png b/public/api/uploads/profile-1782957227105-831349627.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957227105-831349627.png differ diff --git a/public/api/uploads/profile-1782957258680-874601083.png b/public/api/uploads/profile-1782957258680-874601083.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957258680-874601083.png differ diff --git a/public/api/uploads/profile-1782957260063-809722867.png b/public/api/uploads/profile-1782957260063-809722867.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957260063-809722867.png differ diff --git a/public/api/uploads/profile-1782957396116-629514876.png b/public/api/uploads/profile-1782957396116-629514876.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957396116-629514876.png differ diff --git a/public/api/uploads/profile-1782957397337-68390707.png b/public/api/uploads/profile-1782957397337-68390707.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957397337-68390707.png differ diff --git a/public/api/uploads/profile-1782957554195-684730747.png b/public/api/uploads/profile-1782957554195-684730747.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957554195-684730747.png differ diff --git a/public/api/uploads/profile-1782957618919-530652401.png b/public/api/uploads/profile-1782957618919-530652401.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957618919-530652401.png differ diff --git a/public/api/uploads/profile-1782957654468-554246050.png b/public/api/uploads/profile-1782957654468-554246050.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957654468-554246050.png differ diff --git a/public/api/uploads/profile-1782957734629-263039729.png b/public/api/uploads/profile-1782957734629-263039729.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957734629-263039729.png differ diff --git a/public/api/uploads/profile-1782957793294-535196821.png b/public/api/uploads/profile-1782957793294-535196821.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782957793294-535196821.png differ diff --git a/public/api/uploads/profile-1782978598897-28546312.png b/public/api/uploads/profile-1782978598897-28546312.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1782978598897-28546312.png differ diff --git a/public/api/uploads/profile-1783329601048-985926830.png b/public/api/uploads/profile-1783329601048-985926830.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1783329601048-985926830.png differ diff --git a/public/api/uploads/profile-1783856980162-234747538.png b/public/api/uploads/profile-1783856980162-234747538.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1783856980162-234747538.png differ diff --git a/public/api/uploads/profile-1783925701251-53039060.png b/public/api/uploads/profile-1783925701251-53039060.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1783925701251-53039060.png differ diff --git a/public/api/uploads/profile-1783925846234-644654635.png b/public/api/uploads/profile-1783925846234-644654635.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1783925846234-644654635.png differ diff --git a/public/api/uploads/profile-1783926643277-796221976.png b/public/api/uploads/profile-1783926643277-796221976.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1783926643277-796221976.png differ diff --git a/public/api/uploads/profile-1784049206350-427299921.png b/public/api/uploads/profile-1784049206350-427299921.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784049206350-427299921.png differ diff --git a/public/api/uploads/profile-1784049223805-190927998.png b/public/api/uploads/profile-1784049223805-190927998.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784049223805-190927998.png differ diff --git a/public/api/uploads/profile-1784049244401-665031741.png b/public/api/uploads/profile-1784049244401-665031741.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784049244401-665031741.png differ diff --git a/public/api/uploads/profile-1784049275064-498936278.png b/public/api/uploads/profile-1784049275064-498936278.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784049275064-498936278.png differ diff --git a/public/api/uploads/profile-1784049302001-714198254.png b/public/api/uploads/profile-1784049302001-714198254.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784049302001-714198254.png differ diff --git a/public/api/uploads/profile-1784254528937-270868858.png b/public/api/uploads/profile-1784254528937-270868858.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784254528937-270868858.png differ diff --git a/public/api/uploads/profile-1784254553441-339825741.png b/public/api/uploads/profile-1784254553441-339825741.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784254553441-339825741.png differ diff --git a/public/api/uploads/profile-1784254567483-97901195.png b/public/api/uploads/profile-1784254567483-97901195.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784254567483-97901195.png differ diff --git a/public/api/uploads/profile-1784254581808-854860134.png b/public/api/uploads/profile-1784254581808-854860134.png new file mode 100644 index 0000000..d47fab5 Binary files /dev/null and b/public/api/uploads/profile-1784254581808-854860134.png differ diff --git a/public/api/uploads/profile-1784254601947-954448750.png b/public/api/uploads/profile-1784254601947-954448750.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784254601947-954448750.png differ diff --git a/public/api/uploads/profile-1784254618510-383483774.png b/public/api/uploads/profile-1784254618510-383483774.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784254618510-383483774.png differ diff --git a/public/api/uploads/profile-1784254634906-830642874.png b/public/api/uploads/profile-1784254634906-830642874.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784254634906-830642874.png differ diff --git a/public/api/uploads/profile-1784254648483-456467829.png b/public/api/uploads/profile-1784254648483-456467829.png new file mode 100644 index 0000000..89f5b7f Binary files /dev/null and b/public/api/uploads/profile-1784254648483-456467829.png differ diff --git a/public/fonts/Inter-Variable.woff2 b/public/fonts/Inter-Variable.woff2 new file mode 100644 index 0000000..33002f1 Binary files /dev/null and b/public/fonts/Inter-Variable.woff2 differ diff --git a/scripts/deploy-sftp.js b/scripts/deploy-sftp.js index 01e723b..9049e80 100644 --- a/scripts/deploy-sftp.js +++ b/scripts/deploy-sftp.js @@ -148,7 +148,9 @@ async function uploadDirectory(sftp, localDir, remoteDir, options = {}) { if (item.name === '.env.production' || item.name === '.env.local') continue; const localItemPath = path.join(localPath, item.name); - const remoteItemPath = path.posix.join(CONFIG.remotePath, remoteDir, item.name); + // Force forward slashes for remote Linux path compatibility + const remoteDirNormalized = remoteDir.replace(/\\/g, '/'); + const remoteItemPath = path.posix.join(CONFIG.remotePath, remoteDirNormalized, item.name); if (item.isDirectory()) { try { @@ -158,7 +160,8 @@ async function uploadDirectory(sftp, localDir, remoteDir, options = {}) { } await uploadDirectory(sftp, path.join(localDir, item.name), path.join(remoteDir, item.name), options); } else { - process.stdout.write(` [UP] ${path.join(remoteDir, item.name)} ... `); + const displayPath = path.posix.join(remoteDirNormalized, item.name); + process.stdout.write(` [UP] ${displayPath} ... `); await sftp.put(localItemPath, remoteItemPath); process.stdout.write('done\n'); } diff --git a/scripts/start-mongo.js b/scripts/start-mongo.js index 997d00e..e0c09da 100644 --- a/scripts/start-mongo.js +++ b/scripts/start-mongo.js @@ -1,7 +1,36 @@ +const net = require('net'); const { MongoMemoryServer } = require('mongodb-memory-server'); +function checkPort(port, host) { + return new Promise((resolve) => { + const socket = new net.Socket(); + socket.setTimeout(1000); + socket.on('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.on('timeout', () => { + socket.destroy(); + resolve(false); + }); + socket.on('error', () => { + socket.destroy(); + resolve(false); + }); + socket.connect(port, host); + }); +} + async function start() { try { + const isLocalMongoRunning = await checkPort(27017, '127.0.0.1') || await checkPort(27017, 'localhost'); + if (isLocalMongoRunning) { + console.log('[MONGO] ℹ️ Local persistent MongoDB is already running on port 27017. Skipping in-memory MongoDB launch.'); + // Keep process alive so concurrently doesn't show it as exited/failed + setInterval(() => {}, 100000); + return; + } + const mongod = await MongoMemoryServer.create({ instance: { port: 27017, @@ -20,6 +49,7 @@ async function start() { } catch (err) { if (err.code === 'EADDRINUSE' || (err.message && err.message.includes('EADDRINUSE'))) { console.log('[MONGO] ℹ️ Port 27017 is already in use. Skipping in-memory MongoDB launch.'); + setInterval(() => {}, 100000); } else { console.error('[MONGO] ❌ Error starting MongoMemoryServer:', err.message); } diff --git a/src/app/(dashboard)/agents/AgentsPageModals.tsx b/src/app/(dashboard)/agents/AgentsPageModals.tsx index ad0ad11..102b48f 100644 --- a/src/app/(dashboard)/agents/AgentsPageModals.tsx +++ b/src/app/(dashboard)/agents/AgentsPageModals.tsx @@ -3,7 +3,7 @@ import { AgentModals } from "@/components/admin/AgentModals"; import ExternalAccountModal from "@/components/admin/ExternalAccountModal"; import AgentLocationModal from "@/components/admin/AgentLocationModal"; -import { Agent } from "@/lib/actions/agents"; +import { Agent } from "@/lib/actions/agentsCore"; import { type ManagedUser } from "@/lib/admin-api"; interface AgentsPageModalsProps { diff --git a/src/app/(dashboard)/agents/AgentsTableSection.tsx b/src/app/(dashboard)/agents/AgentsTableSection.tsx index 36238e6..9b035d7 100644 --- a/src/app/(dashboard)/agents/AgentsTableSection.tsx +++ b/src/app/(dashboard)/agents/AgentsTableSection.tsx @@ -2,24 +2,37 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; import { DataTable, Column } from "@/components/ui/DataTable"; -import { Loader2 } from "lucide-react"; -import { Agent } from "@/lib/actions/agents"; +import { Loader2, Plus } from "lucide-react"; +import { Agent } from "@/lib/actions/agentsCore"; interface AgentsTableSectionProps { agents: Agent[]; isLoading: boolean; columns: Column[]; + role: string | null; + onProvisionClick?: () => void; } export default function AgentsTableSection({ agents, isLoading, columns, + role, + onProvisionClick, }: AgentsTableSectionProps) { return ( - + Provisioned Network Agents ({agents.length}) + {(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && onProvisionClick && ( + + )} {isLoading ? ( diff --git a/src/app/(dashboard)/agents/UptimeStatsCards.tsx b/src/app/(dashboard)/agents/UptimeStatsCards.tsx index ab44b11..7fe96fa 100644 --- a/src/app/(dashboard)/agents/UptimeStatsCards.tsx +++ b/src/app/(dashboard)/agents/UptimeStatsCards.tsx @@ -21,29 +21,29 @@ export default function UptimeStatsCards({
- Total Agents - {totalAgents} + Total Agents + {totalAgents}
- Online Agents - {onlineAgents} + Online Agents + {onlineAgents}
- Offline Agents - {offlineAgents} + Offline Agents + {offlineAgents}
- Avg. Historical Uptime - {avgUptime.toFixed(2)}% + Avg. Historical Uptime + {avgUptime.toFixed(2)}%
); diff --git a/src/app/(dashboard)/agents/columns.tsx b/src/app/(dashboard)/agents/columns.tsx index e2eca91..de577df 100644 --- a/src/app/(dashboard)/agents/columns.tsx +++ b/src/app/(dashboard)/agents/columns.tsx @@ -1,10 +1,10 @@ "use client"; import { Column } from "@/components/ui/DataTable"; -import { Agent } from "@/lib/actions/agents"; +import { Agent } from "@/lib/actions/agentsCore"; import { Badge } from "@/components/ui/Badge"; import { resolveAgentLabel, type ManagedUser } from "@/lib/admin-api"; -import { Loader2, Trash2, CheckCircle2, XCircle, ChevronRight, UserCog, Eye } from "lucide-react"; +import { Loader2, Trash2, CheckCircle2, XCircle, ChevronRight, UserCog, Eye, MapPin } from "lucide-react"; interface GetColumnsProps { role: string | null; @@ -17,6 +17,9 @@ interface GetColumnsProps { setAccountModalAgent: (agent: { uuid: string; label: string } | null) => void; handleViewAs: (uuid: string, label: string) => void; openDelete: (agent: Agent) => void; + handleEditLocation?: (agentUuid: string, agentLabel: string) => void; + /** Set of agent UUIDs that already have a location saved */ + locationUuids?: Set; } export function getAgentColumns({ @@ -30,11 +33,13 @@ export function getAgentColumns({ setAccountModalAgent, handleViewAs, openDelete, + handleEditLocation, + locationUuids = new Set(), }: GetColumnsProps): Column[] { const cols: Column[] = [ { header: "UUID / Serial", - className: "w-[16%] text-center", + className: "w-[12%] text-center", accessor: (row) => ( {!hasAccount && (role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && ( @@ -63,71 +73,93 @@ export function getAgentColumns({ }, { header: "Provisioned", - className: "w-[12%] text-center", + className: "w-[11%] text-center", accessor: (row) => row.provisioned ? Yes : No }, { header: "Activated", - className: "w-[12%] text-center", + className: "w-[11%] text-center", accessor: (row) => row.activated ? Yes : No }, { - header: "Status", - className: "w-[11%] text-center", + header: "Traffic Status", + className: "w-[15%] text-center", accessor: (row) => { const statusText = row.last_seen_at?.human || '-'; - if (statusText === 'Online') { - return
Online
; - } else if (statusText === 'Offline') { - return Offline; + if (statusText.startsWith('Last seen:')) { + return ( + +
+ {statusText} +
+ ); } else { - return {statusText}; + return {statusText}; } } }, { - header: "Historical Uptime", - className: "w-[12%] text-center", + header: "Avg Uptime", + className: "w-[10%] text-center", accessor: (row) => { const uuid = row.uuid || row.serial; const uptime = uptimeMap[uuid] ?? 100; let color = "text-emerald-400"; if (uptime < 90) color = "text-red-400"; else if (uptime < 98) color = "text-amber-400"; - return {uptime.toFixed(1)}%; + return {uptime.toFixed(1)}%; } }, { header: "Actions", - className: "w-[13%] text-center", + className: "w-[16%] text-center", accessor: (row) => { const uuid = row.uuid || row.serial; const dynamicLabel = resolveAgentLabel(uuid, managedUsers); const existingUser = managedUsers.find(u => u.agent_uuid === uuid && u.role === 'AGENT_VIEWER') || null; const isViewAsLoading = viewAsLoading === uuid; return ( -
- {(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && ( <> + {handleEditLocation && (() => { + const hasLocation = locationUuids.has(uuid); + return ( + + ); + })()} - @@ -141,11 +173,11 @@ export function getAgentColumns({ if (role === 'SUPER_ADMIN') { cols.splice(cols.length - 1, 0, { header: "Data Size", - className: "w-[12%] text-center", + className: "w-[10%] text-center", accessor: (row) => { const uuid = row.uuid || row.serial; const sizeMB = storageMap[uuid] ?? 0; - return {sizeMB.toFixed(2)} MB; + return {sizeMB.toFixed(2)} MB; } }); } @@ -153,75 +185,4 @@ export function getAgentColumns({ return cols; } -export function getExternalAccountColumns( - onManageAccount: (user: ManagedUser) => void -): Column[] { - return [ - { - header: "Account Name", - className: "w-[25%] text-left", - accessor: (row) => ( -
-
- {row.profile_picture ? ( - {row.account_name - ) : ( - {(row.account_name || row.username).substring(0, 2).toUpperCase()} - )} -
- {row.account_name || "-"} -
- ) - }, - { - header: "Username", - className: "w-[25%] text-left", - accessor: (row) => {row.username} - }, - { - header: "Role", - className: "w-[20%] text-center", - accessor: (row) => { - let badgeColor = "bg-slate-500/20 text-slate-400 border-slate-500/30"; - if (row.role === 'SUPER_ADMIN') badgeColor = "bg-purple-500/20 text-purple-400 border-purple-500/30"; - if (row.role === 'SOC_ANALYST') badgeColor = "bg-amber-500/20 text-amber-400 border-amber-500/30"; - if (row.role === 'ENGINEER') badgeColor = "bg-blue-500/20 text-blue-400 border-blue-500/30"; - if (row.role === 'TENANT_ADMIN') badgeColor = "bg-emerald-500/20 text-emerald-400 border-emerald-500/30"; - - return ( - - {row.role.replace('_', ' ')} - - ); - } - }, - { - header: "Status", - className: "w-[15%] text-center", - accessor: (row) => ( -
- {/* Defaulting to active since we don't have is_active explicitly in ManagedUser interface yet, though API returns it. We'll check if it exists, else assume Active */} - {(row as any).is_active !== false ? ( - Active - ) : ( - Inactive - )} -
- ) - }, - { - header: "Actions", - className: "w-[15%] text-center", - accessor: (row) => ( - - ) - } - ]; -} + diff --git a/src/app/(dashboard)/agents/page.tsx b/src/app/(dashboard)/agents/page.tsx index 64d0df1..de8a5fe 100644 --- a/src/app/(dashboard)/agents/page.tsx +++ b/src/app/(dashboard)/agents/page.tsx @@ -1,152 +1,65 @@ "use client"; -import { useState, useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { getAgents, Agent } from "@/lib/actions/agents"; - -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; -import { DataTable } from "@/components/ui/DataTable"; -import { Cpu, History, Radio, Server, Activity, Plus, Loader2, CheckCircle2, XCircle } from "lucide-react"; - -import { getAdminUsers, startViewAs, resolveAgentLabel, getViewAsHeaders, type ManagedUser } from "@/lib/admin-api"; -import { useTimeFilter } from "@/contexts/TimeFilterContext"; -import { usePollingKey } from "@/lib/usePolling"; -import { AgentModals } from "@/components/admin/AgentModals"; -import ExternalAccountModal from "@/components/admin/ExternalAccountModal"; -import { getAgentColumns, getExternalAccountColumns } from "./columns"; +import { useEffect } from "react"; +import { Plus, Loader2, MapPin } from "lucide-react"; import { HelpTrigger } from "@/components/help/HelpTrigger"; +import { useAgentsData } from "./useAgentsData"; +import { getAgentColumns } from "./columns"; +import { getExternalAccountColumns } from "./externalColumns"; +import UptimeStatsCards from "./UptimeStatsCards"; +import AgentsTableSection from "./AgentsTableSection"; +import ExternalAccountsSection from "./ExternalAccountsSection"; +import AgentsPageModals from "./AgentsPageModals"; +import AgentLocationMap from "@/components/admin/AgentLocationMap"; +import { resolveAgentLabel } from "@/lib/admin-api"; export default function AgentsPage() { - const router = useRouter(); - const [role, setRole] = useState(null); - const [agents, setAgents] = useState([]); - const [uptimeMap, setUptimeMap] = useState>({}); - const [storageMap, setStorageMap] = useState>({}); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(""); - - const [isCreateOpen, setIsCreateOpen] = useState(false); - const [isDeleteOpen, setIsDeleteOpen] = useState(false); - const [detailAgent, setDetailAgent] = useState(null); - const [managedUsers, setManagedUsers] = useState([]); - const [accountModalOpen, setAccountModalOpen] = useState(false); - const [accountModalAgent, setAccountModalAgent] = useState<{ uuid: string; label: string } | null>(null); - - const [externalModalOpen, setExternalModalOpen] = useState(false); - const [externalModalUser, setExternalModalUser] = useState(null); - - const [viewAsLoading, setViewAsLoading] = useState(null); - const [selectedAgent, setSelectedAgent] = useState(null); - const { timeRange } = useTimeFilter(); - const refreshKey = usePollingKey(60000); // Auto-refresh every 60 seconds (1 minute) - - const loadUptime = async () => { - try { - const res = await fetch(`/api/dashboard/agents/uptime?timeRange=${timeRange}`, { - headers: getViewAsHeaders() - }); - if (res.ok) { - const json = await res.json(); - if (json.ok && json.uptime) setUptimeMap(json.uptime); - } - } catch (err) { - console.error("Failed to load uptime stats:", err); - } - }; - - const loadStorage = async () => { - try { - const res = await fetch('/api/dashboard/agents/storage', { - headers: getViewAsHeaders() - }); - if (res.ok) { - const json = await res.json(); - if (json.ok && json.storage) setStorageMap(json.storage); - } - } catch (err) { - console.error("Failed to load agent storage stats:", err); - } - }; - - const loadAgents = async () => { - setIsLoading(true); - try { - const siteUuid = localStorage.getItem("backone_site_uuid") || undefined; - const [agentData, userData] = await Promise.allSettled([ - getAgents(siteUuid), - getAdminUsers(), - ]); - if (agentData.status === 'fulfilled') setAgents(agentData.value); - else setError((agentData.reason as Error).message); - if (userData.status === 'fulfilled') setManagedUsers(userData.value); - await loadUptime(); - } catch (e: any) { - setError(e.message); - } - setIsLoading(false); - }; - - const handleViewAs = async (agentUuid: string, agentLabel: string) => { - setViewAsLoading(agentUuid); - try { - await startViewAs(agentUuid, agentLabel); - router.refresh(); - window.location.reload(); - } catch (err: any) { - alert('Failed to enter View As mode: ' + err.message); - } finally { - setViewAsLoading(null); - } - }; - useEffect(() => { - fetch('/api/auth/me', { headers: getViewAsHeaders() }) - .then(res => res.json()) - .then(data => { - if (data.user) { - setRole(data.user.role || null); - if (data.user.role === 'AGENT_VIEWER') { - router.push('/'); - } else { - loadAgents(); - } - } else { - setIsLoading(false); - } - }) - .catch(err => { - console.error("Auth check failed:", err); - setIsLoading(false); - }); - }, [router]); - - useEffect(() => { - if (role && role !== 'AGENT_VIEWER') loadUptime(); - }, [timeRange]); - - useEffect(() => { - if (role === 'SUPER_ADMIN') { - loadStorage(); - } - }, [role]); - - useEffect(() => { - if (role && role !== 'AGENT_VIEWER' && refreshKey > 0) { - const siteUuid = localStorage.getItem("backone_site_uuid") || undefined; - getAgents(siteUuid) - .then(data => setAgents(data)) - .catch(err => console.warn("Failed to auto-refresh agents:", err)); - loadUptime(); - if (role === 'SUPER_ADMIN') { - loadStorage(); - } - } - }, [refreshKey, role]); - - const openDelete = (agent: Agent) => { - setSelectedAgent(agent); - setIsDeleteOpen(true); - }; + document.title = "Agents | BackOne - Deep Package Inspection"; + }, []); + const { + role, + agents, + uptimeMap, + storageMap, + isLoading, + error, + isCreateOpen, + setIsCreateOpen, + isDeleteOpen, + setIsDeleteOpen, + detailAgent, + setDetailAgent, + managedUsers, + accountModalOpen, + setAccountModalOpen, + accountModalAgent, + setAccountModalAgent, + externalModalOpen, + setExternalModalOpen, + externalModalUser, + setExternalModalUser, + targetSiteUuid, + setTargetSiteUuid, + targetCreatedBy, + setTargetCreatedBy, + locations, + isLocModalOpen, + setIsLocModalOpen, + locModalAgentUuid, + setLocModalAgentUuid, + locModalAgentLabel, + setLocModalAgentLabel, + locModalCurrentLoc, + setLocModalCurrentLoc, + viewAsLoading, + selectedAgent, + loadAgents, + loadLocations, + handleViewAs, + openDelete, + handleEditLocation, + } = useAgentsData(); const columns = getAgentColumns({ role, @@ -159,42 +72,40 @@ export default function AgentsPage() { setAccountModalAgent, handleViewAs, openDelete, + handleEditLocation, + locationUuids: new Set(locations.map((l: any) => l.agent_uuid)), }); - const totalAgents = agents.length; - const onlineAgents = agents.filter(a => a.last_seen_at?.human === 'Online').length; - const offlineAgents = totalAgents - onlineAgents; - const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 100); - const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 100; - - const externalUsers = managedUsers.filter(u => u.role !== 'AGENT_VIEWER'); const externalColumns = getExternalAccountColumns((user) => { setExternalModalUser(user); setExternalModalOpen(true); }); + const totalAgents = agents.length; + const onlineAgents = agents.filter(a => { + const hum = a.last_seen_at?.human; + return hum === 'Online' || hum === 'Flows Detected' || (hum && hum.startsWith('Last seen:')); + }).length; + const offlineAgents = totalAgents - onlineAgents; + const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 100); + const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 100; + + const externalUsers = managedUsers.filter(u => u.role !== 'AGENT_VIEWER'); + const superadminUsers = externalUsers.filter(u => u.role === 'SUPER_ADMIN' || !u.site_uuid); + const siabUsers = externalUsers.filter(u => u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e'); + const nexusUsers = externalUsers.filter(u => u.site_uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24'); + return ( -
+
-

+

Agents Inventory {isLoading && }

-

Managed DPI probes, remote collectors, and probe infrastructure.

-
-
- {(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && ( - - )} - +

Managed DPI probes, remote collectors, and probe infrastructure.

+
{error && ( @@ -203,111 +114,94 @@ export default function AgentsPage() {
)} - {/* Historical Uptime Status Cards Grid */} -
-
-
- -
- Total Agents - {totalAgents} -
-
-
- -
- Online Agents - {onlineAgents} -
-
-
- -
- Offline Agents - {offlineAgents} -
-
-
- -
- Avg. Historical Uptime - {avgUptime.toFixed(2)}% -
-
- - - - Provisioned Network Agents ({agents.length}) - - - {isLoading ? ( -
- -
- ) : ( - { - const q = query.toLowerCase(); - return ( - (row.uuid || "").toLowerCase().includes(q) || - (row.serial || "").toLowerCase().includes(q) || - (row.label || "").toLowerCase().includes(q) - ); - }} - /> - )} -
-
+ + {/* Agent Location Map */} {(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && ( - - - External Accounts Inventory ({externalUsers.length}) - - - - {isLoading ? ( -
- -
- ) : ( - { - const q = query.toLowerCase(); - return ( - (row.username || "").toLowerCase().includes(q) || - (row.account_name || "").toLowerCase().includes(q) || - (row.role || "").toLowerCase().includes(q) - ); - }} - /> +
+
+
+ + Agent Network Map + + {locations.length > 0 + ? `${locations.length} of ${agents.length} agent${agents.length !== 1 ? 's' : ''} positioned` + : 'No positions configured yet'} + +
+ {agents.length > locations.length && ( + + {agents.length - locations.length} agent{agents.length - locations.length !== 1 ? 's' : ''} without location — click 📍 in the table + )} - - +
+
+ agents.some(a => (a.uuid || a.serial) === loc.agent_uuid)) + .map(loc => { + const agent = agents.find(a => (a.uuid || a.serial) === loc.agent_uuid); + const agentLabel = agent + ? (resolveAgentLabel(loc.agent_uuid, managedUsers) !== loc.agent_uuid + ? resolveAgentLabel(loc.agent_uuid, managedUsers) + : (agent.label || loc.label || loc.agent_uuid)) + : (loc.label || loc.agent_uuid); + const isOnline = !!(agent?.last_seen_at?.human === 'Online' || + agent?.last_seen_at?.human === 'Flows Detected' || + (agent?.last_seen_at?.human && agent.last_seen_at.human.startsWith('Last seen:'))); + const uptime = uptimeMap[loc.agent_uuid]; + return { + agent_uuid: loc.agent_uuid, + label: agentLabel, + latitude: loc.latitude, + longitude: loc.longitude, + isOnline, + uptimePercent: uptime, + }; + })} + onEditLocation={handleEditLocation} + canEdit={role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN'} + /> +
+
)} - setIsCreateOpen(true)} + /> + + {(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && ( + + )} + + - - { setExternalModalOpen(false); setExternalModalUser(null); }} - onSuccess={loadAgents} - user={externalModalUser} + externalModalOpen={externalModalOpen} + setExternalModalOpen={setExternalModalOpen} + externalModalUser={externalModalUser} + setExternalModalUser={setExternalModalUser} + targetSiteUuid={targetSiteUuid} + setTargetSiteUuid={setTargetSiteUuid} + targetCreatedBy={targetCreatedBy} + setTargetCreatedBy={setTargetCreatedBy} + isLocModalOpen={isLocModalOpen} + setIsLocModalOpen={setIsLocModalOpen} + locModalAgentUuid={locModalAgentUuid} + setLocModalAgentUuid={setLocModalAgentUuid} + locModalAgentLabel={locModalAgentLabel} + setLocModalAgentLabel={setLocModalAgentLabel} + locModalCurrentLoc={locModalCurrentLoc} + setLocModalCurrentLoc={setLocModalCurrentLoc} + loadLocations={loadLocations} />
); diff --git a/src/app/(dashboard)/agents/useAgentsData.ts b/src/app/(dashboard)/agents/useAgentsData.ts index 34991aa..e0fccfe 100644 --- a/src/app/(dashboard)/agents/useAgentsData.ts +++ b/src/app/(dashboard)/agents/useAgentsData.ts @@ -2,7 +2,8 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; -import { getAgents, Agent } from "@/lib/actions/agents"; +import { getAgents } from "@/lib/actions/agents"; +import { Agent } from "@/lib/actions/agentsCore"; import { getAdminUsers, startViewAs, getViewAsHeaders, type ManagedUser } from "@/lib/admin-api"; import { useTimeFilter } from "@/contexts/TimeFilterContext"; import { usePollingKey } from "@/lib/usePolling"; diff --git a/src/app/(dashboard)/apps/page.tsx b/src/app/(dashboard)/apps/page.tsx index 96bd4b0..a5bf251 100644 --- a/src/app/(dashboard)/apps/page.tsx +++ b/src/app/(dashboard)/apps/page.tsx @@ -9,15 +9,15 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; import { AppDetailModal } from "@/components/ui/AppDetailModal"; import { Loader2, ChevronRight, Globe } from "lucide-react"; import { UniversalFilters, useUniversalFilterState, FilterConfig } from "@/components/ui/UniversalFilters"; +import { HelpTrigger } from "@/components/help/HelpTrigger"; import { fmtBytes, getAppFavicon, formatAppLabel } from "@/lib/utils"; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer } from "recharts"; -import { HelpTrigger } from "@/components/help/HelpTrigger"; export default function AppsPage() { const [mounted, setMounted] = useState(false); const [page, setPage] = useState(0); const pageSize = 20; - const { data, isLoading } = useTopApps(pageSize); + const { data, isLoading } = useTopApps(); const [selectedApp, setSelectedApp] = useState(null); const { @@ -28,6 +28,7 @@ export default function AppsPage() { useEffect(() => { setMounted(true); + document.title = "Apps | BackOne - Deep Package Inspection"; }, []); if (!mounted) return null; @@ -126,13 +127,12 @@ export default function AppsPage() { accessor: (row) => {fmtBytes(row.download + row.upload)} }, ]; - return (
-

- Applications +

+ Apps {isLoading && }

@@ -141,7 +141,7 @@ export default function AppsPage() {

- + { @@ -234,4 +234,4 @@ export default function AppsPage() { )}
); -} +} \ No newline at end of file diff --git a/src/app/(dashboard)/devices/page.tsx b/src/app/(dashboard)/devices/page.tsx index 497ddde..1824068 100644 --- a/src/app/(dashboard)/devices/page.tsx +++ b/src/app/(dashboard)/devices/page.tsx @@ -15,7 +15,7 @@ import { HelpTrigger } from "@/components/help/HelpTrigger"; export default function DevicesPage() { const [mounted, setMounted] = useState(false); - const devices = useDevices(5000); + const devices = useDevices(); const [selectedDevice, setSelectedDevice] = useState(null); const { @@ -26,6 +26,7 @@ export default function DevicesPage() { useEffect(() => { setMounted(true); + document.title = "Devices | BackOne - Deep Package Inspection"; }, []); if (!mounted) return null; diff --git a/src/app/(dashboard)/dns/page.tsx b/src/app/(dashboard)/dns/page.tsx index 1f033f0..6821e39 100644 --- a/src/app/(dashboard)/dns/page.tsx +++ b/src/app/(dashboard)/dns/page.tsx @@ -25,6 +25,7 @@ export default function DNSPage() { useEffect(() => { setMounted(true); + document.title = "DNS | BackOne - Deep Package Inspection"; }, []); useEffect(() => { @@ -97,13 +98,10 @@ export default function DNSPage() { return (
-
-

- DNS Intelligence - {isLoading && } -

-

Domain Name System resolution analytics and query logs.

-
+

+ DNS + {isLoading && } +

diff --git a/src/app/(dashboard)/dpi-analytics/DpiAnalyticsTabs.tsx b/src/app/(dashboard)/dpi-analytics/DpiAnalyticsTabs.tsx new file mode 100644 index 0000000..a13ba7e --- /dev/null +++ b/src/app/(dashboard)/dpi-analytics/DpiAnalyticsTabs.tsx @@ -0,0 +1,254 @@ +"use client"; + +import React from "react"; +import { Tabs, Tab } from "@/components/ui/Tabs"; +import { DataTable } from "@/components/ui/DataTable"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { fmtBytes, getKnownDomainService } from "@/lib/utils"; +import { MetadataDetailType } from "@/lib/api-metadata-detail"; + +interface DpiAnalyticsTabsProps { + sniHostnames: any[]; + loadingSni: boolean; + sslServerCn: any[]; + loadingSslCn: boolean; + quicHostnames: any[]; + loadingQuic: boolean; + netbios: any[]; + loadingNetbios: boolean; + mdnsHostnames: any[]; + loadingMdns: boolean; + discoveryOs: any[]; + loadingOs: boolean; + dhcpFingerprints: any[]; + loadingDhcp: boolean; + httpUserAgents: any[]; + loadingHttpUa: boolean; + sshVersions: any[]; + loadingSsh: boolean; + bittorrentHashes: any[]; + loadingBittorrent: boolean; + openDetail: (type: MetadataDetailType, value: string) => void; +} + +function ClickHint() { + return ( + + Click row for details + + ); +} + +export default function DpiAnalyticsTabs({ + sniHostnames, + loadingSni, + sslServerCn, + loadingSslCn, + quicHostnames, + loadingQuic, + netbios, + loadingNetbios, + mdnsHostnames, + loadingMdns, + discoveryOs, + loadingOs, + dhcpFingerprints, + loadingDhcp, + httpUserAgents, + loadingHttpUa, + sshVersions, + loadingSsh, + bittorrentHashes, + loadingBittorrent, + openDetail, +}: DpiAnalyticsTabsProps) { + return ( + + {/* ── Hostnames & Servers ──────────────────────────────────────────── */} + +
+ + SNI Hostnames + + openDetail("sni_hostname", row.sni_hostname)} + columns={[ + { + header: "Hostname", + accessor: (row: any) => { + const srv = getKnownDomainService(row.sni_hostname); + return ( +
+ {row.sni_hostname} + {srv && {srv}} +
+ ); + } + }, + { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, + ]} + /> +
+
+ + + SSL Server Common Names + + openDetail("ssl_server_cn", row.ssl_server_cn)} + columns={[ + { + header: "Common Name", + accessor: (row: any) => { + const srv = getKnownDomainService(row.ssl_server_cn); + return ( +
+ {row.ssl_server_cn} + {srv && {srv}} +
+ ); + } + }, + { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, + ]} + /> +
+
+ + + QUIC Hostnames + + openDetail("quic_hostname", row.quic_hostname)} + columns={[ + { + header: "Hostname", + accessor: (row: any) => { + const srv = getKnownDomainService(row.quic_hostname); + return ( +
+ {row.quic_hostname} + {srv && {srv}} +
+ ); + } + }, + { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, + ]} + /> +
+
+ + + NetBIOS Hostnames + + openDetail("netbios_hostname", row.hostname)} + columns={[{ header: "Hostname", accessor: (row: any) => row.hostname }, { header: "Total", accessor: (row: any) => fmtBytes(row.total) }]} + /> + + + + + mDNS Hostnames + + openDetail("mdns_hostname", row.mdns_hostname)} + columns={[{ header: "Hostname", accessor: (row: any) => row.mdns_hostname }, { header: "Total", accessor: (row: any) => fmtBytes(row.total) }]} + /> + + +
+
+ + {/* ── Client Fingerprints ──────────────────────────────────────────── */} + +
+ + Operating Systems Discovery + + openDetail("os_label", row.os_label)} + columns={[ + { header: "Operating System", accessor: (row: any) => row.os_label }, + { header: "Download", accessor: (row: any) => fmtBytes(row.download) }, + { header: "Upload", accessor: (row: any) => fmtBytes(row.upload) }, + { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, + ]} + /> + + + + + DHCP Fingerprints + + openDetail("dhcp_fingerprint", row.fingerprint)} + columns={[{ header: "Fingerprint", accessor: (row: any) => row.fingerprint }, { header: "Total", accessor: (row: any) => fmtBytes(row.total) }]} + /> + + + + + HTTP User Agents + + openDetail("http_useragent", row.user_agent)} + columns={[{ header: "User Agent", accessor: (row: any) => row.user_agent }, { header: "Total", accessor: (row: any) => fmtBytes(row.total) }]} + /> + + +
+
+ + {/* ── Specific Protocols ───────────────────────────────────────────── */} + +
+ + SSH Versions + + openDetail("ssh_version", row.ssh_version)} + columns={[{ header: "SSH Version", accessor: (row: any) => row.ssh_version }, { header: "Total", accessor: (row: any) => fmtBytes(row.total) }]} + /> + + + + + BitTorrent Hashes + + openDetail("bittorrent_hash", row.info_hash)} + columns={[ + { header: "Label", accessor: (row: any) => row.label }, + { header: "Info Hash", accessor: (row: any) => row.info_hash }, + { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, + ]} + /> + + +
+
+
+ ); +} diff --git a/src/app/(dashboard)/dpi-analytics/page.tsx b/src/app/(dashboard)/dpi-analytics/page.tsx index 181870f..c8c2d05 100644 --- a/src/app/(dashboard)/dpi-analytics/page.tsx +++ b/src/app/(dashboard)/dpi-analytics/page.tsx @@ -1,9 +1,6 @@ "use client"; -import { useState, useCallback } from "react"; -import { Tabs, Tab } from "@/components/ui/Tabs"; -import { DataTable } from "@/components/ui/DataTable"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { useState, useCallback, useEffect } from "react"; import { useNetbios, useDiscoveryOs, useDhcpFingerprints, useHttpUserAgents, useSniHostnames, useSslServerCn, @@ -13,25 +10,18 @@ import { import { MetadataDetailPanel } from "@/components/dpi/MetadataDetailPanel"; import { MetadataDetailType } from "@/lib/api-metadata-detail"; import { useTimeFilter } from "@/contexts/TimeFilterContext"; -import { fmtBytes, getKnownDomainService } from "@/lib/utils"; - -// Clickable hint badge shown in table column headers when rows are interactive -function ClickHint() { - return ( - - Click row for details - - ); -} - import { HelpTrigger } from "@/components/help/HelpTrigger"; +import DpiAnalyticsTabs from "./DpiAnalyticsTabs"; export default function DpiAnalyticsPage() { const { timeRange } = useTimeFilter(); - // Detail panel state - const [detailType, setDetailType] = useState(null); + const [detailType, setDetailType] = useState(null); const [detailValue, setDetailValue] = useState(null); + useEffect(() => { + document.title = "DPI MetaData | BackOne - Deep Package Inspection"; + }, []); + const openDetail = useCallback((type: MetadataDetailType, value: string) => { if (!value || value === "—") return; setDetailType(type); @@ -43,24 +33,20 @@ export default function DpiAnalyticsPage() { setDetailValue(null); }, []); - // Hostnames & Servers Tab - const { data: sniHostnames, isLoading: loadingSni } = useSniHostnames(50); - const { data: sslServerCn, isLoading: loadingSslCn } = useSslServerCn(50); - const { data: quicHostnames, isLoading: loadingQuic } = useQuicHostnames(50); - const { data: netbios, isLoading: loadingNetbios } = useNetbios(50); - const { data: mdnsHostnames, isLoading: loadingMdns } = useMdnsHostnames(50); - - // Client Fingerprints Tab - const { data: discoveryOs, isLoading: loadingOs } = useDiscoveryOs(50); - const { data: dhcpFingerprints, isLoading: loadingDhcp } = useDhcpFingerprints(50); - const { data: httpUserAgents, isLoading: loadingHttpUa } = useHttpUserAgents(50); - - // Specific Protocols Tab - const { data: sshVersions, isLoading: loadingSsh } = useSshVersions(50); - const { data: bittorrentHashes, isLoading: loadingBittorrent } = useBittorrentHashes(50); + // Data Fetch Hooks + const { data: sniHostnames, isLoading: loadingSni } = useSniHostnames(); + const { data: sslServerCn, isLoading: loadingSslCn } = useSslServerCn(); + const { data: quicHostnames, isLoading: loadingQuic } = useQuicHostnames(); + const { data: netbios, isLoading: loadingNetbios } = useNetbios(); + const { data: mdnsHostnames, isLoading: loadingMdns } = useMdnsHostnames(); + const { data: discoveryOs, isLoading: loadingOs } = useDiscoveryOs(); + const { data: dhcpFingerprints, isLoading: loadingDhcp } = useDhcpFingerprints(); + const { data: httpUserAgents, isLoading: loadingHttpUa } = useHttpUserAgents(); + const { data: sshVersions, isLoading: loadingSsh } = useSshVersions(); + const { data: bittorrentHashes, isLoading: loadingBittorrent } = useBittorrentHashes(); return ( -
+

DPI MetaData

@@ -72,216 +58,30 @@ export default function DpiAnalyticsPage() {
- - {/* ── Hostnames & Servers ──────────────────────────────────────────── */} - -
+ - - SNI Hostnames (TLS) - - openDetail("sni_hostname", row.sni_hostname)} - columns={[ - { - header: "Hostname", - accessor: (row: any) => { - const srv = getKnownDomainService(row.sni_hostname); - return ( -
- {row.sni_hostname} - {srv && {srv}} -
- ); - } - }, - { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, - ]} - /> -
-
- - - SSL Server Common Names - - openDetail("ssl_server_cn", row.ssl_server_cn)} - columns={[ - { - header: "Common Name", - accessor: (row: any) => { - const srv = getKnownDomainService(row.ssl_server_cn); - return ( -
- {row.ssl_server_cn} - {srv && {srv}} -
- ); - } - }, - { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, - ]} - /> -
-
- - - QUIC Hostnames - - openDetail("quic_hostname", row.quic_hostname)} - columns={[ - { - header: "Hostname", - accessor: (row: any) => { - const srv = getKnownDomainService(row.quic_hostname); - return ( -
- {row.quic_hostname} - {srv && {srv}} -
- ); - } - }, - { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, - ]} - /> -
-
- - - NetBIOS Hostnames - - openDetail("netbios_hostname", row.hostname)} - columns={[ - { header: "Hostname", accessor: (row: any) => row.hostname }, - { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, - ]} - /> - - - - - mDNS Hostnames - - openDetail("mdns_hostname", row.mdns_hostname)} - columns={[ - { header: "Hostname", accessor: (row: any) => row.mdns_hostname }, - { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, - ]} - /> - - - -
-
- - {/* ── Client Fingerprints ──────────────────────────────────────────── */} - -
- - - Operating Systems Discovery - - openDetail("os_label", row.os_label)} - columns={[ - { header: "Operating System", accessor: (row: any) => row.os_label }, - { header: "Download", accessor: (row: any) => fmtBytes(row.download) }, - { header: "Upload", accessor: (row: any) => fmtBytes(row.upload) }, - { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, - ]} - /> - - - - - DHCP Fingerprints - - openDetail("dhcp_fingerprint", row.fingerprint)} - columns={[ - { header: "Fingerprint", accessor: (row: any) => row.fingerprint }, - { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, - ]} - /> - - - - - HTTP User Agents - - openDetail("http_useragent", row.user_agent)} - columns={[ - { header: "User Agent", accessor: (row: any) => row.user_agent }, - { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, - ]} - /> - - - -
-
- - {/* ── Specific Protocols ───────────────────────────────────────────── */} - -
- - - SSH Versions - - openDetail("ssh_version", row.ssh_version)} - columns={[ - { header: "SSH Version", accessor: (row: any) => row.ssh_version }, - { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, - ]} - /> - - - - - BitTorrent Hashes - - openDetail("bittorrent_hash", row.info_hash)} - columns={[ - { header: "Label", accessor: (row: any) => row.label }, - { header: "Info Hash", accessor: (row: any) => row.info_hash }, - { header: "Total", accessor: (row: any) => fmtBytes(row.total) }, - ]} - /> - - - -
-
-
- - {/* ── Row Detail Slide-Out Panel ────────────────────────────────────── */}
-
-

- Network Events - {isLoading && } -

-

Real-time system, security, and interface network log events.

-
+

+ Network Events + {isLoading && } +

diff --git a/src/app/(dashboard)/flows/explainFlow.ts b/src/app/(dashboard)/flows/explainFlow.ts new file mode 100644 index 0000000..a48b64a --- /dev/null +++ b/src/app/(dashboard)/flows/explainFlow.ts @@ -0,0 +1,46 @@ +export function explainAppOrPort(appLabel: string | null, domain: string | null, port: number) { + const label = appLabel || domain || ""; + const portStr = String(port); + + const matches: Record = { + "1433": "MSSQL Database Server", + "1434": "MSSQL Monitor Server", + "3306": "MySQL/MariaDB Database Server", + "5432": "PostgreSQL Database Server", + "1521": "Oracle Database Server", + "27017": "Database Server", + "6379": "Redis Key-Value Cache", + "80": "Unencrypted Web Traffic (HTTP)", + "443": "Encrypted Web Traffic (HTTPS/TLS)", + "22": "SSH Remote Management / SFTP", + "21": "FTP File Upload/Download (Unencrypted)", + "23": "Telnet Command Shell (Insecure)", + "25": "SMTP Mail Delivery", + "587": "Secure SMTP Mail Delivery", + "110": "POP3 Mail Storage Retrieval", + "993": "Secure IMAP Mail Service", + "53": "DNS Server Query (Domain Lookup)", + "123": "NTP Network Clock Synchronizer", + "161": "SNMP Network Device Monitoring", + "3389": "RDP Remote Desktop Management", + "445": "SMB Windows File Sharing", + "137": "NetBIOS Name Service", + "138": "NetBIOS Datagram Service", + "139": "NetBIOS Session Service", + "1812": "RADIUS Authentication Server", + "1813": "RADIUS Accounting Server" + }; + + let mainLabel = label; + let subLabel = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : ""); + + if (appLabel && appLabel.includes("(") && domain && domain.startsWith("Port ")) { + mainLabel = appLabel; + subLabel = domain; + } + + return { + main: mainLabel || `Port ${port}`, + sub: subLabel + }; +} diff --git a/src/app/(dashboard)/flows/page.tsx b/src/app/(dashboard)/flows/page.tsx index bf63ce0..7914619 100644 --- a/src/app/(dashboard)/flows/page.tsx +++ b/src/app/(dashboard)/flows/page.tsx @@ -2,67 +2,24 @@ import { useState, useEffect } from "react"; import { TimestampCell } from "@/components/ui/TimestampCell"; -import { useFlows } from "@/lib/api-with-context"; +import { useFlows, useFlowsOptions } from "@/lib/api-with-context"; import { DataTable, Column } from "@/components/ui/DataTable"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; -import { Loader2 } from "lucide-react"; +import { Loader2, Download, Search } from "lucide-react"; import { fmtBytes, formatAppLabel } from "@/lib/utils"; import { IpDetails } from "@/components/ui/IpDetails"; import { DeviceDetailModal } from "@/components/ui/DeviceDetailModal"; import { UniversalFilters, useUniversalFilterState, FilterConfig } from "@/components/ui/UniversalFilters"; +import { explainAppOrPort } from "./explainFlow"; +import { downloadCsv } from "@/components/ui/DataTableCsvHelper"; import { HelpTrigger } from "@/components/help/HelpTrigger"; -function explainAppOrPort(appLabel: string | null, domain: string | null, port: number) { - const label = appLabel || domain || ""; - const portStr = String(port); - - const matches: Record = { - "1433": "MSSQL Database Server", - "1434": "MSSQL Monitor Server", - "3306": "MySQL/MariaDB Database Server", - "5432": "PostgreSQL Database Server", - "1521": "Oracle Database Server", - "27017": "Database Server", - "6379": "Redis Key-Value Cache", - "80": "Unencrypted Web Traffic (HTTP)", - "443": "Encrypted Web Traffic (HTTPS/TLS)", - "22": "SSH Remote Management / SFTP", - "21": "FTP File Upload/Download (Unencrypted)", - "23": "Telnet Command Shell (Insecure)", - "25": "SMTP Mail Delivery", - "587": "Secure SMTP Mail Delivery", - "110": "POP3 Mail Storage Retrieval", - "993": "Secure IMAP Mail Service", - "53": "DNS Server Query (Domain Lookup)", - "123": "NTP Network Clock Synchronizer", - "161": "SNMP Network Device Monitoring", - "3389": "RDP Remote Desktop Management", - "445": "SMB Windows File Sharing", - "137": "NetBIOS Name Service", - "138": "NetBIOS Datagram Service", - "139": "NetBIOS Session Service", - "1812": "RADIUS Authentication Server", - "1813": "RADIUS Accounting Server" - }; - - let mainLabel = label; - let subLabel = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : ""); - - if (appLabel && appLabel.includes("(") && domain && domain.startsWith("Port ")) { - mainLabel = appLabel; - subLabel = domain; - } - - return { - main: mainLabel || `Port ${port}`, - sub: subLabel - }; -} - export default function FlowsPage() { const [mounted, setMounted] = useState(false); const [selectedIp, setSelectedIp] = useState(null); - const { data, isLoading } = useFlows(); + const [currentPage, setCurrentPage] = useState(1); + const [ipSearch, setIpSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); const { filters, dateFrom, dateTo, @@ -70,18 +27,41 @@ export default function FlowsPage() { handleReset, activeCount } = useUniversalFilterState(); + // Debounce search query to prevent database query spamming + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearch(ipSearch); + }, 300); + return () => clearTimeout(timer); + }, [ipSearch]); + + // Reset page to 1 when filters, date picker, or search text changes + useEffect(() => { + setCurrentPage(1); + }, [filters, dateFrom, dateTo, debouncedSearch]); + + const { data: optionsData } = useFlowsOptions(); + const { data: flows = [], total = 0, isLoading } = useFlows(currentPage, 50, { + ...filters, + dateFrom, + dateTo, + search: debouncedSearch + }); + useEffect(() => { setMounted(true); + document.title = "Flows | BackOne - Deep Package Inspection"; }, []); if (!mounted) return null; - const all = data || []; - - // Extract unique options const opts = (key: string) => { - const raw = Array.from(new Set(all.map((r: any) => String(r[key] || "")).filter(Boolean))); - return raw.sort(); + if (!optionsData) return []; + const mapping: Record = { + protocol: optionsData.protocols, src_ip: optionsData.srcIps, dst_ip: optionsData.dstIps, + dst_port: optionsData.dstPorts, app_label: optionsData.apps, domain: optionsData.domains + }; + return mapping[key] || []; }; const filterConfigs: FilterConfig[] = [ @@ -95,49 +75,15 @@ export default function FlowsPage() { { id: "sort_upload", label: "Sort Upload", type: "select", value: filters.sort_upload, options: ["Descending", "Ascending"] }, ]; - const filteredData = all.filter((row: any) => { - if (filters.protocol && filters.protocol !== "All" && row.protocol !== filters.protocol) return false; - if (filters.src_ip && filters.src_ip !== "All" && row.src_ip !== filters.src_ip) return false; - if (filters.dst_ip && filters.dst_ip !== "All" && row.dst_ip !== filters.dst_ip) return false; - if (filters.dst_port && filters.dst_port !== "All" && String(row.dst_port) !== filters.dst_port) return false; - if (filters.app && filters.app !== "All" && row.app_label !== filters.app) return false; - if (filters.domain && filters.domain !== "All" && row.domain !== filters.domain) return false; - - if (dateFrom || dateTo) { - const d = row.last_seen ? new Date(row.last_seen).toISOString().slice(0, 10) : ""; - if (dateFrom && d < dateFrom) return false; - if (dateTo && d > dateTo) return false; - } - - return true; - }); + const minDate = new Date(Date.now() - 30 * 24 * 3600 * 1000).toISOString().slice(0, 10); - let processedData = [...filteredData]; - const sortUpload = filters.sort_upload || "All"; - const sortDownload = filters.sort_download || "All"; - - if (sortUpload !== "All") { - processedData.sort((a, b) => sortUpload === "Descending" ? (b.bytes_upload ?? 0) - (a.bytes_upload ?? 0) : (a.bytes_upload ?? 0) - (b.bytes_upload ?? 0)); - } else if (sortDownload !== "All") { - processedData.sort((a, b) => sortDownload === "Descending" ? (b.bytes_download ?? 0) - (a.bytes_download ?? 0) : (a.bytes_download ?? 0) - (b.bytes_download ?? 0)); - } else { - processedData.sort((a, b) => { - const timeA = a.last_seen ? new Date(a.last_seen).getTime() : 0; - const timeB = b.last_seen ? new Date(b.last_seen).getTime() : 0; - return timeB - timeA; - }); - } + const handleExport = () => { + const headers = ["Flow ID", "Src IP", "Dst IP", "Dst Port", "Protocol", "App / Domain", "Download (bytes)", "Upload (bytes)"]; + const rows = [headers, ...flows.map(r => [r.flow_id || "", r.src_ip || "", r.dst_ip || "", r.dst_port ?? "", r.protocol || "", r.app_label || r.domain || `Port ${r.dst_port}`, r.bytes_download ?? 0, r.bytes_upload ?? 0])]; + downloadCsv(`flows-${new Date().toISOString().slice(0,10)}.csv`, rows); + }; - const minTime = all.reduce((min: number, r: any) => { - if (r.last_seen) { - const time = new Date(r.last_seen).getTime(); - if (isFinite(time) && time < min) return time; - } - return min; - }, Infinity); - const minDate = minTime !== Infinity ? new Date(minTime).toISOString().slice(0, 10) : "2024-01-01"; - - const columns: Column[] = [ + const columns: Column[] = [ { header: "Last Seen", accessor: (row) => ( @@ -147,10 +93,19 @@ export default function FlowsPage() { showActiveStatus={true} /> ), - className: "w-[120px]" + className: "w-[170px]", + tooltip: (row) => new Date(row.last_seen).toLocaleString() + }, + { + header: "#", + accessor: (row, i) => i + 1, + className: "w-[50px]" + }, + { + header: "Flow ID", + accessor: (row) => row.flow_id, + className: "w-[130px]" }, - { header: "#", accessor: (row, i) => i + 1 }, - { header: "Flow ID", accessor: (row) => row.flow_id }, { header: "Src IP", accessor: (row) => ( @@ -160,62 +115,80 @@ export default function FlowsPage() { > {row.src_ip} - ) + ), + className: "w-[120px]", + tooltip: (row) => row.src_ip }, { header: "Dst IP", accessor: (row) => ( -
+
- ) + ), + className: "w-[150px]", + tooltip: (row) => row.dst_ip + }, + { + header: "Dst Port", + accessor: (row) => row.dst_port, + className: "w-[80px]" + }, + { + header: "Protocol", + accessor: (row) => row.protocol, + className: "w-[80px]" }, - { header: "Dst Port", accessor: (row) => row.dst_port }, - { header: "Protocol", accessor: (row) => row.protocol }, { header: "App / Domain", accessor: (row) => { const exp = explainAppOrPort(row.app_label, row.domain, row.dst_port); return ( -
- +
+ {exp.main} {exp.sub && ( - + {exp.sub} )}
); + }, + className: "w-[220px]", + tooltip: (row) => { + const exp = explainAppOrPort(row.app_label, row.domain, row.dst_port); + return exp.sub ? `${exp.main} (${exp.sub})` : exp.main; } }, { header: "Download", - accessor: (row) => {fmtBytes(row.bytes_download)} + accessor: (row) => {fmtBytes(row.bytes_download)}, + className: "w-[90px]", + tooltip: (row) => `${fmtBytes(row.bytes_download)} (${(row.bytes_download || 0).toLocaleString()} bytes)` }, { header: "Upload", - accessor: (row) => {fmtBytes(row.bytes_upload)} + accessor: (row) => {fmtBytes(row.bytes_upload)}, + className: "w-[90px]", + tooltip: (row) => `${fmtBytes(row.bytes_upload)} (${(row.bytes_upload || 0).toLocaleString()} bytes)` } ]; return (
-
-

- Active Flows - {isLoading && } -

-

Real-time IP connection flows and protocol analysis.

-
+

+ Flows + {isLoading && } +

@@ -242,38 +215,37 @@ export default function FlowsPage() { /> - - Active Flows ({processedData.length}) + +
+ Active Flows ({total.toLocaleString()}) +
+ + setIpSearch(e.target.value)} + className="w-[240px] rounded-lg border border-border bg-card/30 backdrop-blur-sm pl-9 pr-4 py-1.5 text-sm text-white placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:bg-card/50 transition-all shadow-sm" + /> +
+
+
{ - const q = query.toLowerCase(); - return ( - (row.src_ip || "").toLowerCase().includes(q) || - (row.dst_ip || "").toLowerCase().includes(q) || - (row.app_label || "").toLowerCase().includes(q) || - (row.domain || "").toLowerCase().includes(q) || - (row.protocol || "").toLowerCase().includes(q) - ); - }} - csvExport={{ - filename: `flows-${new Date().toISOString().slice(0,10)}.csv`, - headers: ["Flow ID", "Src IP", "Dst IP", "Dst Port", "Protocol", "App / Domain", "Download (bytes)", "Upload (bytes)"], - rowSerializer: (row) => [ - row.flow_id || "", - row.src_ip || "", - row.dst_ip || "", - row.dst_port ?? "", - row.protocol || "", - row.app_label || row.domain || `Port ${row.dst_port}`, - row.bytes_download ?? 0, - row.bytes_upload ?? 0, - ], - }} + serverSide={true} + totalCount={total} + currentPage={currentPage} + onPageChange={setCurrentPage} />
diff --git a/src/app/(dashboard)/geography/page.tsx b/src/app/(dashboard)/geography/page.tsx index d1db2ee..19d7f38 100644 --- a/src/app/(dashboard)/geography/page.tsx +++ b/src/app/(dashboard)/geography/page.tsx @@ -13,7 +13,7 @@ import { HelpTrigger } from "@/components/help/HelpTrigger"; export default function GeographyPage() { const [mounted, setMounted] = useState(false); const [showMap, setShowMap] = useState(false); - const { data, isLoading } = useCountries(50); // fetch 50 countries + const { data, isLoading } = useCountries(); const { filters, @@ -34,6 +34,7 @@ export default function GeographyPage() { useEffect(() => { setMounted(true); + document.title = "Geo Traffic | BackOne - Deep Package Inspection"; }, []); if (!mounted) return null; @@ -109,7 +110,7 @@ export default function GeographyPage() { Geo Traffic {isLoading && } -

Geographic distribution of inbound and outbound traffic by country.

+

Visualize data flow and activity across the globe.

diff --git a/src/app/(dashboard)/intelligence/DeviceDiscoveryTable.tsx b/src/app/(dashboard)/intelligence/DeviceDiscoveryTable.tsx new file mode 100644 index 0000000..86f0fab --- /dev/null +++ b/src/app/(dashboard)/intelligence/DeviceDiscoveryTable.tsx @@ -0,0 +1,71 @@ +"use client"; + +import React from "react"; +import { DataTable } from "@/components/ui/DataTable"; +import { TimestampCell } from "@/components/ui/TimestampCell"; +import { ChevronRight } from "lucide-react"; +import { fmtBytes } from "@/lib/utils"; + +interface DeviceDiscoveryTableProps { + data: any[]; + isLoading: boolean; + setSelectedDevice: (device: { ip: string; mac?: string } | null) => void; +} + +export function DeviceDiscoveryTable({ data, isLoading, setSelectedDevice }: DeviceDiscoveryTableProps) { + return ( + { + const query = q.toLowerCase(); + return (row.ip_address || '').toLowerCase().includes(query) || (row.mac_address || '').toLowerCase().includes(query) || (row.os_label || '').toLowerCase().includes(query) || (row.device_type || '').toLowerCase().includes(query); + }} + csvExport={{ + filename: `device-discovery-${new Date().toISOString().slice(0,10)}.csv`, + headers: ["IP Address", "MAC Address", "Device Type", "OS", "Manufacturer", "Download", "Upload", "Last Seen"], + rowSerializer: (row: any) => [row.ip_address || "", row.mac_address || "", row.device_type || "", row.os_label || "", row.manufacturer || "", row.download || 0, row.upload || 0, row.last_seen || ""] + }} + columns={[ + { + header: "Last Seen", + className: "w-[15%]", + accessor: (row: any) => + }, + { header: "#", accessor: (row, i) => i + 1, className: "w-[4%]" }, + { + header: "IP Address", + className: "w-[16%]", + accessor: (row: any) => ( +
+ + {row.mac_address} +
+ ) + }, + { header: "Device Profile", className: "w-[20%]", accessor: (row: any) => ( +
+ {row.device_type || "-"} + {row.os_label || "-"} +
+ ) }, + { header: "Manufacturer", className: "w-[20%]", accessor: (row: any) => row.manufacturer || "-" }, + { header: "Bandwidth", className: "w-[25%]", accessor: (row: any) => ( +
+ ↓ {fmtBytes(row.download)} + ↑ {fmtBytes(row.upload)} +
+ ) }, + ]} + /> + ); +} diff --git a/src/app/(dashboard)/intelligence/EncryptionAuditTable.tsx b/src/app/(dashboard)/intelligence/EncryptionAuditTable.tsx new file mode 100644 index 0000000..3ea63e9 --- /dev/null +++ b/src/app/(dashboard)/intelligence/EncryptionAuditTable.tsx @@ -0,0 +1,92 @@ +"use client"; + +import React from "react"; +import { DataTable } from "@/components/ui/DataTable"; +import { TimestampCell } from "@/components/ui/TimestampCell"; +import { ChevronRight } from "lucide-react"; +import { IpDetails } from "@/components/ui/IpDetails"; +import { fmtBytes } from "@/lib/utils"; + +interface EncryptionAuditTableProps { + data: any[]; + isLoading: boolean; + setSelectedDevice: (device: { ip: string; mac?: string } | null) => void; +} + +export function EncryptionAuditTable({ data, isLoading, setSelectedDevice }: EncryptionAuditTableProps) { + return ( + { + const query = q.toLowerCase(); + return (row.ip_address || '').toLowerCase().includes(query) || (row.device_label || '').toLowerCase().includes(query) || (row.mac_address || '').toLowerCase().includes(query); + }} + csvExport={{ + filename: `encryption-audit-${new Date().toISOString().slice(0,10)}.csv`, + headers: ["Device Label", "MAC Address", "IP Address", "Encrypted Data", "Encrypted %", "Total Data", "Risk Level"], + rowSerializer: (row: any) => [row.device_label || "", row.mac_address || "", row.ip_address || "", row.encrypted || 0, row.encrypted_pct || 0, row.total || 0, row.risk_level || ""] + }} + columns={[ + { + header: "Last Seen", + className: "w-[15%]", + accessor: (row: any) => + }, + { header: "#", accessor: (row, i) => i + 1, className: "w-[4%]" }, + { + header: "Device", + className: "w-[25%]", + accessor: (row: any) => ( +
+ {row.device_label || row.ip_address} + {row.mac_address} +
+ ) + }, + { + header: "IP Address", + className: "w-[20%]", + accessor: (row: any) => ( +
+ + {row.ip_address &&
} +
+ ) + }, + { + header: "Encrypted Data", + className: "w-[26%]", + accessor: (row: any) => ( +
+ {fmtBytes(row.encrypted)} ({row.encrypted_pct}%) + Total: {fmtBytes(row.total)} +
+ ) + }, + { + header: "Risk Level", + className: "w-[10%]", + accessor: (row: any) => ( +
+ + {row.risk_level === 'medium' ? 'Medium' : 'Safe'} + +
+ ) + }, + ]} + /> + ); +} diff --git a/src/app/(dashboard)/intelligence/IntelligenceTables.tsx b/src/app/(dashboard)/intelligence/IntelligenceTables.tsx new file mode 100644 index 0000000..79fb4ec --- /dev/null +++ b/src/app/(dashboard)/intelligence/IntelligenceTables.tsx @@ -0,0 +1,157 @@ +"use client"; + +import React from "react"; +import { DataTable } from "@/components/ui/DataTable"; +import { IpDetails } from "@/components/ui/IpDetails"; +import { EncryptionAuditTable } from "./EncryptionAuditTable"; +import { DeviceDiscoveryTable } from "./DeviceDiscoveryTable"; +import { ServerDiscoveryTable } from "./ServerDiscoveryTable"; + +interface IntelligenceTablesProps { + activeTab: string; + cryptoMining: any[]; + loadingCrypto: boolean; + torDetection: any[]; + loadingTor: boolean; + vpnDetection: any[]; + loadingVpn: boolean; + ipReputation: any[]; + loadingIpRep: boolean; + insecureProtocols: any[]; + loadingInsecure: boolean; + unencryptedPasswords: any[]; + loadingPasswords: boolean; + encryptionAudit: any[]; + loadingEncryption: boolean; + deviceDiscovery: any[]; + loadingDevice: boolean; + serverDiscovery: any[]; + loadingServer: boolean; + setSelectedDevice: (device: { ip: string; mac?: string } | null) => void; +} + +export function IntelligenceTables({ + activeTab, + cryptoMining, loadingCrypto, + torDetection, loadingTor, + vpnDetection, loadingVpn, + ipReputation, loadingIpRep, + insecureProtocols, loadingInsecure, + unencryptedPasswords, loadingPasswords, + encryptionAudit, loadingEncryption, + deviceDiscovery, loadingDevice, + serverDiscovery, loadingServer, + setSelectedDevice +}: IntelligenceTablesProps) { + switch (activeTab) { + case "Crypto Mining": + return ( + row.ip_address }, + { header: "MAC Address", accessor: (row: any) => row.mac_address }, + { header: "Pool Host", accessor: (row: any) => row.pool_host }, + { header: "App", accessor: (row: any) => row.app_label }, + { header: "Confidence", accessor: (row: any) => row.confidence }, + ]} + /> + ); + case "Tor Detection": + return ( + row.ip_address }, + { header: "Exit Node", accessor: (row: any) => row.exit_node }, + { header: "Country", accessor: (row: any) => row.country }, + ]} + /> + ); + case "VPN Detection": + return ( + row.ip_address }, + { header: "VPN Type", accessor: (row: any) => row.vpn_type }, + { + header: "Remote IP", + accessor: (row: any) => row.remote_ip ? ( +
+ {row.remote_ip} + +
+ ) : "—" + }, + { header: "Country", accessor: (row: any) => row.country }, + ]} + /> + ); + case "IP Reputation": + return ( + row.ip_address ? ( +
+ {row.ip_address} + +
+ ) : "—" + }, + { header: "Reputation", accessor: (row: any) => row.reputation }, + { header: "App", accessor: (row: any) => row.app_label }, + { header: "Country", accessor: (row: any) => row.country }, + ]} + /> + ); + case "Insecure Protocols": + return ( + row.protocol }, + { header: "IP Address", accessor: (row: any) => row.ip_address }, + { header: "Risk", accessor: (row: any) => row.risk }, + ]} + /> + ); + case "Unencrypted Passwords": + return ( + row.protocol }, + { header: "IP Address", accessor: (row: any) => row.ip_address }, + { header: "Severity", accessor: (row: any) => row.severity }, + ]} + /> + ); + case "Encryption Audit": + return ( + + ); + case "Device Discovery": + return ( + + ); + case "Server Discovery": + return ( + + ); + default: + return null; + } +} diff --git a/src/app/(dashboard)/intelligence/ServerDiscoveryTable.tsx b/src/app/(dashboard)/intelligence/ServerDiscoveryTable.tsx new file mode 100644 index 0000000..4237ed4 --- /dev/null +++ b/src/app/(dashboard)/intelligence/ServerDiscoveryTable.tsx @@ -0,0 +1,64 @@ +"use client"; + +import React from "react"; +import { DataTable } from "@/components/ui/DataTable"; +import { TimestampCell } from "@/components/ui/TimestampCell"; +import { ChevronRight } from "lucide-react"; + +interface ServerDiscoveryTableProps { + data: any[]; + isLoading: boolean; + setSelectedDevice: (device: { ip: string; mac?: string } | null) => void; +} + +export function ServerDiscoveryTable({ data, isLoading, setSelectedDevice }: ServerDiscoveryTableProps) { + return ( + { + const query = q.toLowerCase(); + return (row.ip_address || '').toLowerCase().includes(query) || (row.mac_address || '').toLowerCase().includes(query) || (row.server_type || '').toLowerCase().includes(query) || (row.os_label || '').toLowerCase().includes(query); + }} + csvExport={{ + filename: `server-discovery-${new Date().toISOString().slice(0,10)}.csv`, + headers: ["IP Address", "MAC Address", "Server Type", "Port", "OS"], + rowSerializer: (row: any) => [row.ip_address || "", row.mac_address || "", row.server_type || "", row.port || "", row.os_label || ""] + }} + columns={[ + { + header: "Last Seen", + className: "w-[15%]", + accessor: (row: any) => + }, + { header: "#", accessor: (row, i) => i + 1, className: "w-[4%]" }, + { + header: "Server IP", + className: "w-[25%]", + accessor: (row: any) => ( +
+ + {row.mac_address} +
+ ) + }, + { header: "Server Type", className: "w-[28%]", accessor: (row: any) => ( +
+ {row.server_type} + {row.port > 0 && Port {row.port}} +
+ ) }, + { header: "OS / Platform", className: "w-[28%]", accessor: (row: any) => row.os_label }, + ]} + /> + ); +} diff --git a/src/app/(dashboard)/intelligence/page.tsx b/src/app/(dashboard)/intelligence/page.tsx index 58de68e..c5479c5 100644 --- a/src/app/(dashboard)/intelligence/page.tsx +++ b/src/app/(dashboard)/intelligence/page.tsx @@ -8,13 +8,10 @@ import { useIntelEncryptionAudit, useIntelDeviceDiscovery, useIntelServerDiscovery } from "@/lib/api-advanced"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; -import { DataTable, Column } from "@/components/ui/DataTable"; -import { IpDetails } from "@/components/ui/IpDetails"; -import { Loader2, Bitcoin, Shield, Server, Lock, AlertTriangle, Key, Globe, Network, ShieldAlert, ChevronRight } from "lucide-react"; +import { Loader2, Bitcoin, Shield, Server, Lock, AlertTriangle, Key, Globe, Network, ShieldAlert } from "lucide-react"; import { DeviceDetailModal } from "@/components/ui/DeviceDetailModal"; -import { TimestampCell } from "@/components/ui/TimestampCell"; -import { fmtBytes } from "@/lib/utils"; import { HelpTrigger } from "@/components/help/HelpTrigger"; +import { IntelligenceTables } from "./IntelligenceTables"; export default function IntelligencePage() { const [mounted, setMounted] = useState(false); @@ -22,19 +19,20 @@ export default function IntelligencePage() { const [activeTab, setActiveTab] = useState("Crypto Mining"); const [selectedDevice, setSelectedDevice] = useState<{ip: string, mac?: string} | null>(null); - // Advanced data hooks - const { data: cryptoMining, isLoading: loadingCrypto } = useIntelCryptoMining(20); - const { data: torDetection, isLoading: loadingTor } = useIntelTorDetection(20); - const { data: vpnDetection, isLoading: loadingVpn } = useIntelVpnDetection(20); - const { data: ipReputation, isLoading: loadingIpRep } = useIntelIpReputation(20); - const { data: insecureProtocols, isLoading: loadingInsecure } = useIntelInsecureProtocols(20); - const { data: unencryptedPasswords, isLoading: loadingPasswords } = useIntelUnencryptedPasswords(20); - const { data: encryptionAudit, isLoading: loadingEncryption } = useIntelEncryptionAudit(20); - const { data: deviceDiscovery, isLoading: loadingDevice } = useIntelDeviceDiscovery(20); - const { data: serverDiscovery, isLoading: loadingServer } = useIntelServerDiscovery(20); + // Advanced data hooks (no hardcoded limits, default to 1,000,000) + const { data: cryptoMining = [], isLoading: loadingCrypto } = useIntelCryptoMining(); + const { data: torDetection = [], isLoading: loadingTor } = useIntelTorDetection(); + const { data: vpnDetection = [], isLoading: loadingVpn } = useIntelVpnDetection(); + const { data: ipReputation = [], isLoading: loadingIpRep } = useIntelIpReputation(); + const { data: insecureProtocols = [], isLoading: loadingInsecure } = useIntelInsecureProtocols(); + const { data: unencryptedPasswords = [], isLoading: loadingPasswords } = useIntelUnencryptedPasswords(); + const { data: encryptionAudit = [], isLoading: loadingEncryption } = useIntelEncryptionAudit(); + const { data: deviceDiscovery = [], isLoading: loadingDevice } = useIntelDeviceDiscovery(); + const { data: serverDiscovery = [], isLoading: loadingServer } = useIntelServerDiscovery(); useEffect(() => { setMounted(true); + document.title = "Threat Intelligence | BackOne - Deep Package Inspection"; }, []); if (!mounted) return null; @@ -96,292 +94,16 @@ export default function IntelligencePage() { } ]; - const renderActiveTable = () => { - switch(activeTab) { - case "Crypto Mining": - return ( - row.ip_address }, - { header: "MAC Address", accessor: (row: any) => row.mac_address }, - { header: "Pool Host", accessor: (row: any) => row.pool_host }, - { header: "App", accessor: (row: any) => row.app_label }, - { header: "Confidence", accessor: (row: any) => row.confidence }, - ]} - /> - ); - case "Tor Detection": - return ( - row.ip_address }, - { header: "Exit Node", accessor: (row: any) => row.exit_node }, - { header: "Country", accessor: (row: any) => row.country }, - ]} - /> - ); - case "VPN Detection": - return ( - row.ip_address }, - { header: "VPN Type", accessor: (row: any) => row.vpn_type }, - { - header: "Remote IP", - accessor: (row: any) => row.remote_ip ? ( -
- {row.remote_ip} - -
- ) : "—" - }, - { header: "Country", accessor: (row: any) => row.country }, - ]} - /> - ); - case "IP Reputation": - return ( - row.ip_address ? ( -
- {row.ip_address} - -
- ) : "—" - }, - { header: "Reputation", accessor: (row: any) => row.reputation }, - { header: "App", accessor: (row: any) => row.app_label }, - { header: "Country", accessor: (row: any) => row.country }, - ]} - /> - ); - case "Insecure Protocols": - return ( - row.protocol }, - { header: "IP Address", accessor: (row: any) => row.ip_address }, - { header: "Risk", accessor: (row: any) => row.risk }, - ]} - /> - ); - case "Unencrypted Passwords": - return ( - row.protocol }, - { header: "IP Address", accessor: (row: any) => row.ip_address }, - { header: "Severity", accessor: (row: any) => row.severity }, - ]} - /> - ); - case "Encryption Audit": - return ( - { - const query = q.toLowerCase(); - return (row.ip_address || '').toLowerCase().includes(query) || (row.device_label || '').toLowerCase().includes(query) || (row.mac_address || '').toLowerCase().includes(query); - }} - csvExport={{ - filename: `encryption-audit-${new Date().toISOString().slice(0,10)}.csv`, - headers: ["Device Label", "MAC Address", "IP Address", "Encrypted Data", "Encrypted %", "Total Data", "Risk Level"], - rowSerializer: (row: any) => [row.device_label || "", row.mac_address || "", row.ip_address || "", row.encrypted || 0, row.encrypted_pct || 0, row.total || 0, row.risk_level || ""] - }} - columns={[ - { - header: "Last Seen", - className: "w-[15%]", - accessor: (row: any) => - }, - { header: "#", accessor: (row, i) => i + 1, className: "w-[4%]" }, - { - header: "Device", - className: "w-[25%]", - accessor: (row: any) => ( -
- {row.device_label || row.ip_address} - {row.mac_address} -
- ) - }, - { - header: "IP Address", - className: "w-[20%]", - accessor: (row: any) => ( -
- - {row.ip_address &&
} -
- ) - }, - { - header: "Encrypted Data", - className: "w-[26%]", - accessor: (row: any) => ( -
- {fmtBytes(row.encrypted)} ({row.encrypted_pct}%) - Total: {fmtBytes(row.total)} -
- ) - }, - { - header: "Risk Level", - className: "w-[10%]", - accessor: (row: any) => ( -
- - {row.risk_level === 'medium' ? 'Medium' : 'Safe'} - -
- ) }, - ]} - /> - ); - case "Device Discovery": - return ( - { - const query = q.toLowerCase(); - return (row.ip_address || '').toLowerCase().includes(query) || (row.mac_address || '').toLowerCase().includes(query) || (row.os_label || '').toLowerCase().includes(query) || (row.device_type || '').toLowerCase().includes(query); - }} - csvExport={{ - filename: `device-discovery-${new Date().toISOString().slice(0,10)}.csv`, - headers: ["IP Address", "MAC Address", "Device Type", "OS", "Manufacturer", "Download", "Upload", "Last Seen"], - rowSerializer: (row: any) => [row.ip_address || "", row.mac_address || "", row.device_type || "", row.os_label || "", row.manufacturer || "", row.download || 0, row.upload || 0, row.last_seen || ""] - }} - columns={[ - { - header: "Last Seen", - className: "w-[15%]", - accessor: (row: any) => - }, - { header: "#", accessor: (row, i) => i + 1, className: "w-[4%]" }, - { - header: "IP Address", - className: "w-[16%]", - accessor: (row: any) => ( -
- - {row.mac_address} -
- ) - }, - { header: "Device Profile", className: "w-[20%]", accessor: (row: any) => ( -
- {row.device_type || "-"} - {row.os_label || "-"} -
- ) }, - { header: "Manufacturer", className: "w-[20%]", accessor: (row: any) => row.manufacturer || "-" }, - { header: "Bandwidth", className: "w-[25%]", accessor: (row: any) => ( -
- ↓ {fmtBytes(row.download)} - ↑ {fmtBytes(row.upload)} -
- ) }, - ]} - /> - ); - case "Server Discovery": - return ( - { - const query = q.toLowerCase(); - return (row.ip_address || '').toLowerCase().includes(query) || (row.mac_address || '').toLowerCase().includes(query) || (row.server_type || '').toLowerCase().includes(query) || (row.os_label || '').toLowerCase().includes(query); - }} - csvExport={{ - filename: `server-discovery-${new Date().toISOString().slice(0,10)}.csv`, - headers: ["IP Address", "MAC Address", "Server Type", "Port", "OS"], - rowSerializer: (row: any) => [row.ip_address || "", row.mac_address || "", row.server_type || "", row.port || "", row.os_label || ""] - }} - columns={[ - { - header: "Last Seen", - className: "w-[15%]", - accessor: (row: any) => - }, - { header: "#", accessor: (row, i) => i + 1, className: "w-[4%]" }, - { - header: "Server IP", - className: "w-[25%]", - accessor: (row: any) => ( -
- - {row.mac_address} -
- ) - }, - { header: "Server Type", className: "w-[28%]", accessor: (row: any) => ( -
- {row.server_type} - {row.port > 0 && Port {row.port}} -
- ) }, - { header: "OS / Platform", className: "w-[28%]", accessor: (row: any) => row.os_label }, - ]} - /> - ); - default: return null; - } - }; - return ( -
+
-
-

- Threat Intelligence - {loadingStats && } -

-

Deep threat intelligence feeds and security detections.

-
+

+ Threat Intelligence + {loadingStats && } +

- +
{intelItems.map((item, idx) => { @@ -417,7 +139,28 @@ export default function IntelligencePage() { Detailed Report: {activeTab} - {renderActiveTable()} + diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 83239e6..2c4a44c 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -1,5 +1,8 @@ +"use client"; + import { DashboardLayout } from "@/components/layout/DashboardLayout"; import { TimeFilterProvider } from "@/contexts/TimeFilterContext"; +import { TenantConfigProvider } from "@/contexts/TenantConfigContext"; export default function AppLayout({ children, @@ -7,8 +10,10 @@ export default function AppLayout({ children: React.ReactNode; }>) { return ( - - {children} - + + + {children} + + ); } diff --git a/src/app/(dashboard)/lookup/page.tsx b/src/app/(dashboard)/lookup/page.tsx index 6a52763..cf916b8 100644 --- a/src/app/(dashboard)/lookup/page.tsx +++ b/src/app/(dashboard)/lookup/page.tsx @@ -1,364 +1,43 @@ "use client"; import { useState, useEffect } from "react"; -import { useLookupApplications, useLookupCategories, LookupApp } from "@/lib/api"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; -import { DropdownFilterSelect } from "@/components/ui/DropdownFilterSelect"; -import { Loader2, Search, AppWindow, HelpCircle, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, X, Info, Tag, Box, Shield, Server } from "lucide-react"; - +import { Tabs, Tab } from "@/components/ui/Tabs"; +import { ApplicationCatalog } from "@/components/lookup/ApplicationCatalog"; +import { BlacklistConfiguration } from "@/components/lookup/BlacklistConfiguration"; import { HelpTrigger } from "@/components/help/HelpTrigger"; export default function LookupPage() { const [mounted, setMounted] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - const [debouncedSearch, setDebouncedSearch] = useState(""); - const [selectedCategory, setSelectedCategory] = useState(""); - const [page, setPage] = useState(1); - const [selectedApp, setSelectedApp] = useState(null); - const [isClosingModal, setIsClosingModal] = useState(false); - const limit = 24; // 24 items per page fits a 2, 3, or 4 column grid perfectly - - const handleCloseModal = () => { - setIsClosingModal(true); - setTimeout(() => { - setSelectedApp(null); - setIsClosingModal(false); - }, 480); - }; useEffect(() => { setMounted(true); + document.title = "App Lookup | BackOne - Deep Package Inspection"; }, []); - // Debounce search query input to avoid spamming requests - useEffect(() => { - const handler = setTimeout(() => { - setDebouncedSearch(searchQuery); - setPage(1); // Reset page on new search - }, 400); - - return () => clearTimeout(handler); - }, [searchQuery]); - - const { data, isLoading, error } = useLookupApplications(page, limit, debouncedSearch, selectedCategory); - const { data: categories, error: categoriesError } = useLookupCategories(); - - useEffect(() => { - if (categoriesError) { - console.error("Failed to fetch categories:", categoriesError); - } - }, [categoriesError]); - if (!mounted) return null; - const applications = data?.applications || []; - const pagination = data?.pagination || { total_records: 0, total_pages: 1, current_page: 1 }; - return (

App Lookup - {isLoading && }

- Search BackOne's extensive classification database of over 2,500+ apps, protocols, and services. + Search BackOne's extensive classification database of over 2,500+ apps, protocols, and services, and manage your blacklist configuration.

- {/* Search & Filter Bar */} -
-
- - setSearchQuery(e.target.value)} - /> -
- -
- { - setSelectedCategory(val === "All" ? "" : val); - setPage(1); // Reset page on category change - }} - options={categories || []} - placeholder="All Categories" - /> -
-
- - {/* Cards Grid */} - {error ? ( -
-

Failed to load application lookup database

-

{error.message}

-
- ) : applications.length === 0 && !isLoading ? ( -
- -

No applications found

-

Try refining your search keyword or clearing the input.

-
- ) : ( -
-
- {applications.map((app: LookupApp) => ( - setSelectedApp(app)} - className="group relative overflow-hidden bg-card/60 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-primary/40 transition-all duration-300 flex flex-col justify-between cursor-pointer" - > -
- {/* Top: Icon + Title */} -
-
-
- {app.favicon || app.logo ? ( - <> - {app.label} { - // If image fails, hide it and show the sibling icon - const img = e.target as HTMLElement; - img.style.display = 'none'; - const sibling = img.nextSibling as HTMLElement; - if (sibling) { - sibling.style.display = 'block'; - } - }} - /> - - - ) : ( - - )} -
- - {app.application_category?.label || "Unclassified"} - -
- -
-

- {app.label} -

-

{app.tag}

-
- -

- {app.description || "No description provided for this application definition."} -

-
- - {/* Bottom: ID and system information */} -
- ID: {app.id} - {app.name || "N/A"} -
-
-
- ))} -
- - {/* Pagination Controls */} - {pagination.total_pages > 1 && ( -
-
- Showing {((page - 1) * limit) + 1} to{" "} - - {Math.min(page * limit, pagination.total_records)} - {" "} - of {pagination.total_records} entries -
- -
- {/* First Page */} - - - {/* Prev Page */} - - - {/* Page indicators */} - - {page} / {pagination.total_pages} - - - {/* Next Page */} - - - {/* Last Page */} - -
-
- )} -
- )} - - {/* Application Detail Modal */} - {selectedApp && ( -
-
-
e.stopPropagation()} - > - - {/* Header Background */} -
- -
- - {/* Content */} -
- - {/* Icon/Logo */} -
- {selectedApp.logo || selectedApp.favicon ? ( - {selectedApp.label} { - const img = e.target as HTMLElement; - img.style.display = 'none'; - const sibling = img.nextSibling as HTMLElement; - if (sibling) sibling.style.display = 'block'; - }} - /> - ) : null} - -
- - {/* Title & Category */} -
-
-

{selectedApp.label}

- {selectedApp.application_category?.label && ( - - {selectedApp.application_category.label} - - )} -
- {selectedApp.full_name && selectedApp.full_name !== selectedApp.label && ( -

{selectedApp.full_name}

- )} -
- -
- - {/* Description */} -
-
- -

- {selectedApp.description || "No detailed description provided for this application or service."} -

-
-
- - {/* Metadata Info */} -
-

System Metadata

- -
-
- - App ID -
- {selectedApp.id} -
- -
-
- - System Name -
- {selectedApp.name || 'N/A'} -
- -
-
- - Tag / Alias -
- {selectedApp.tag || 'N/A'} -
-
- - {/* Application Category Detail */} - {selectedApp.application_category && ( -
-

Category Info

- -
-
- - Category ID -
- {selectedApp.application_category.id} -
- -
-
- - Category Tag -
- {selectedApp.application_category.tag} -
-
- )} - -
- -
-
-
- )} + + + + + + + +
); } diff --git a/src/app/(dashboard)/network-infrastructure/page.tsx b/src/app/(dashboard)/network-infrastructure/page.tsx index 9dfdf0b..3231241 100644 --- a/src/app/(dashboard)/network-infrastructure/page.tsx +++ b/src/app/(dashboard)/network-infrastructure/page.tsx @@ -11,28 +11,31 @@ import { } from "@/lib/api-advanced"; import { IpDetails } from "@/components/ui/IpDetails"; import { RemoteIpDetailModal } from "@/components/ui/RemoteIpDetailModal"; - import { HelpTrigger } from "@/components/help/HelpTrigger"; export default function NetworkInfrastructurePage() { const [selectedRemoteIp, setSelectedRemoteIp] = useState(null); + useEffect(() => { + document.title = "Network Topology | BackOne - Deep Package Inspection"; + }, []); + // Routing & Geography Tab - const { data: regions, isLoading: loadingRegions } = useRegions(20); - const { data: cities, isLoading: loadingCities } = useCities(20); + const { data: regions, isLoading: loadingRegions } = useRegions(); + const { data: cities, isLoading: loadingCities } = useCities(); // Interfaces & VLANs Tab - const { data: interfaces, isLoading: loadingInterfaces } = useInterfaces(20); - const { data: vlans, isLoading: loadingVlans } = useVlans(20); + const { data: interfaces, isLoading: loadingInterfaces } = useInterfaces(); + const { data: vlans, isLoading: loadingVlans } = useVlans(); // IP & MAC Tab - const { data: remoteIps, isLoading: loadingRemoteIps } = useRemoteIps(20); - const { data: ipVersions, isLoading: loadingIpVersions } = useIpVersions(20); - const { data: macBandwidth, isLoading: loadingMacBandwidth } = useMacBandwidth(20); + const { data: remoteIps, isLoading: loadingRemoteIps } = useRemoteIps(); + const { data: ipVersions, isLoading: loadingIpVersions } = useIpVersions(); + const { data: macBandwidth, isLoading: loadingMacBandwidth } = useMacBandwidth(); // Flow Profiles Tab - const { data: flowTypes, isLoading: loadingFlowTypes } = useFlowTypes(20); - const { data: flowOrigins, isLoading: loadingFlowOrigins } = useFlowOrigins(20); + const { data: flowTypes, isLoading: loadingFlowTypes } = useFlowTypes(); + const { data: flowOrigins, isLoading: loadingFlowOrigins } = useFlowOrigins(); const formatBytes = (bytes: number) => { if (bytes === 0 || !bytes) return "0 B"; diff --git a/src/app/(dashboard)/network-intelligence/page.tsx b/src/app/(dashboard)/network-intelligence/page.tsx index 437996d..6b216ef 100644 --- a/src/app/(dashboard)/network-intelligence/page.tsx +++ b/src/app/(dashboard)/network-intelligence/page.tsx @@ -31,6 +31,7 @@ export default function NetworkIntelligencePage() { useEffect(() => { setMounted(true); + document.title = "Traffic Categories | BackOne - Deep Package Inspection"; }, []); if (!mounted) return null; diff --git a/src/app/(dashboard)/page.tsx b/src/app/(dashboard)/page.tsx index acf7ae8..98cb8fa 100644 --- a/src/app/(dashboard)/page.tsx +++ b/src/app/(dashboard)/page.tsx @@ -7,6 +7,7 @@ import { GlobeMap } from "@/components/ui/GlobeMap"; import { getOriginLocation, getDestinationLocation } from "@/lib/geoUtils"; import { KPICards } from "@/components/dashboard/KPICards"; import { TopWidgets } from "@/components/dashboard/TopWidgets"; +import { HelpTrigger } from "@/components/help/HelpTrigger"; export default function SummaryPage() { const [mounted, setMounted] = useState(false); @@ -74,6 +75,7 @@ export default function SummaryPage() { useEffect(() => { setMounted(true); + document.title = "Overview Dashboard | BackOne - Deep Package Inspection"; }, []); if (!mounted) return null; @@ -90,6 +92,7 @@ export default function SummaryPage() {

Real-time network traffic and intelligence summary.

+
{/* KPI Cards */} diff --git a/src/app/(dashboard)/security-audit/page.tsx b/src/app/(dashboard)/security-audit/page.tsx index 81c0231..80e804b 100644 --- a/src/app/(dashboard)/security-audit/page.tsx +++ b/src/app/(dashboard)/security-audit/page.tsx @@ -15,9 +15,9 @@ import { HelpTrigger } from "@/components/help/HelpTrigger"; export default function SecurityAuditPage() { const [mounted, setMounted] = useState(false); - const versions = useTlsVersions(10); - const ciphers = useTlsCiphers(20); - const security = useTlsSecurity(10); + const versions = useTlsVersions(); + const ciphers = useTlsCiphers(); + const security = useTlsSecurity(); const [secDevices, setSecDevices] = useState([]); const [secDevicesLoading, setSecDevicesLoading] = useState(true); @@ -54,14 +54,11 @@ export default function SecurityAuditPage() { return (
-
-
-

Encryption Audit

-

- Monitor TLS versions distribution, cipher suites, active certificate properties, and device risk profiles. -

-
- +
+

Security & Encryption Audit

+

+ Monitor TLS versions distribution, cipher suites, active certificate properties, and device risk profiles. +

(null); - const [selectedMac, setSelectedMac] = useState(null); - const [selectedThreat, setSelectedThreat] = useState(null); - - const threats = useThreats(); - const searchParams = useSearchParams(); - const highlightId = searchParams.get("highlight"); - - // Column dropdown filters state - const [fType, setFType] = useState("All"); - const [fSev, setFSev] = useState("All"); - const [fSrcIp, setFSrcIp] = useState("All"); - const [fDstIp, setFDstIp] = useState("All"); - const [fMac, setFMac] = useState("All"); - const [fApp, setFApp] = useState("All"); - const [fDomain, setFDomain] = useState("All"); - - // Date range filter state - const [dateFrom, setDateFrom] = useState(""); - const [dateTo, setDateTo] = useState(""); - - useEffect(() => { - setMounted(true); - }, []); - - const all = threats.data || []; - - // Apply all filters - const filtered = useMemo(() => { - return all.filter((row) => { - if (fType !== "All" && (row.threat_type || "") !== fType) return false; - if (fSev !== "All" && (row.severity || "") !== fSev) return false; - if (fSrcIp !== "All" && (row.ip_address || "") !== fSrcIp) return false; - if (fDstIp !== "All" && (row.dst_ip || "") !== fDstIp) return false; - if (fMac !== "All" && (row.mac_address || "") !== fMac) return false; - if (fApp !== "All" && (row.app_label || "") !== fApp) return false; - if (fDomain !== "All" && (row.domain || "") !== fDomain) return false; - if (dateFrom || dateTo) { - const d = row.detected_at - ? new Date(row.detected_at).toISOString().slice(0, 10) - : ""; - if (dateFrom && d < dateFrom) return false; - if (dateTo && d > dateTo) return false; - } - return true; - }); - }, [all, fType, fSev, fSrcIp, fDstIp, fMac, fApp, fDomain, dateFrom, dateTo]); - - // Automatically select a threat if highlightId is passed - useEffect(() => { - if (highlightId && all.length > 0) { - const match = all.find(r => r.id.toString() === highlightId); - if (match) { - setSelectedThreat(match); - } - } - }, [highlightId, all]); - - if (!mounted) return null; - - const getSeverityBadge = (sev?: string | null) => { - if (!sev) return Unknown; - const s = sev.toLowerCase(); - if (s.includes("high") || s.includes("critical")) return {sev}; - if (s.includes("med")) return {sev}; - if (s.includes("low")) return {sev}; - return {sev}; - }; - - const columns: Column[] = [ - { - header: "Timestamp", - className: "w-[155px] min-w-[155px] max-w-[155px] text-center", - accessor: (row) => - }, - { header: "#", className: "w-[40px] min-w-[40px] max-w-[40px] text-center", accessor: (_row, i) => i + 1 }, - { - header: "Type", - className: "w-[170px] min-w-[170px] max-w-[170px]", - accessor: (row) => ( -
- {row.threat_type || "Unknown"} -
- ), - }, - { - header: "Severity", - className: "w-[90px] min-w-[90px] max-w-[90px] text-center", - accessor: (row) => getSeverityBadge(row.severity), - }, - { - header: "Source IP", - className: "w-[120px] min-w-[120px] max-w-[120px] text-center", - accessor: (row) => - row.ip_address ? ( - - ) : ( - – - ), - }, - { - header: "Dest IP", - className: "w-[120px] min-w-[120px] max-w-[120px] text-center", - accessor: (row) => - row.dst_ip ? ( - - ) : ( - – - ), - }, - { - header: "MAC Addr", - className: "w-[140px] min-w-[140px] max-w-[140px] text-center", - accessor: (row) => - row.mac_address ? ( - - ) : "–", - }, - { - header: "App", - className: "w-[110px] min-w-[110px] max-w-[110px] text-center", - accessor: (row) => { - const formatted = formatAppLabel(row.app_label); - return formatted ? ( -
- {formatted} -
- ) : ( - – - ); - }, - }, - { - header: "Domain", - className: "w-[160px] min-w-[160px] max-w-[160px] text-center", - accessor: (row) => row.domain ? ( -
- {row.domain} -
- ) : ( - – - ), - }, - { - header: "Action", - className: "w-[130px] min-w-[130px] max-w-[130px] text-center", - accessor: (row) => ( - - ), - }, - ]; - - - const activeFilterCount = - [fType, fSev, fSrcIp, fDstIp, fMac, fApp, fDomain].filter((v) => v !== "All").length + - (dateFrom || dateTo ? 1 : 0); - - const resetFilters = () => { - setFType("All"); - setFSev("All"); - setFSrcIp("All"); - setFDstIp("All"); - setFMac("All"); - setFApp("All"); - setFDomain("All"); - setDateFrom(""); - setDateTo(""); - }; - - return ( -
-
-
-

- Detected Threats - {threats.isLoading && } -

-

Monitor and respond to network security threats.

-
-
-
- - Active Monitoring -
- -
-
- - {/* Filters above the table */} - - - {/* Threats Table - Full Width */} - - - - Detected Threats - - ({filtered.length} - {filtered.length !== all.length ? ` of ${all.length}` : ""}) - - - - - { - const q = query.toLowerCase(); - return ( - (row.ip_address || "").toLowerCase().includes(q) || - (row.domain || "").toLowerCase().includes(q) || - (row.threat_type || "").toLowerCase().includes(q) || - (row.app_label || "").toLowerCase().includes(q) - ); - }} - getRowClassName={(row) => - highlightId && row.id.toString() === highlightId - ? "bg-red-500/15 border-l-2 border-red-500 animate-pulse font-semibold" - : "" - } - csvExport={{ - filename: `threats-${new Date().toISOString().slice(0, 10)}.csv`, - headers: ["Timestamp", "Type", "Severity", "Source IP", "Dest IP", "MAC Address", "App", "Domain"], - rowSerializer: (row) => [ - row.detected_at ? new Date(row.detected_at).toISOString() : "", - row.threat_type || "", - row.severity || "", - row.ip_address || "", - row.dst_ip || "", - row.mac_address || "", - row.app_label || "", - row.domain || "", - ], - }} - /> - - - - {selectedIp && setSelectedIp(null)} />} - {selectedMac && setSelectedMac(null)} />} - - {/* Mitigation recommendations drawer panel */} - setSelectedThreat(null)} - /> -
- ); -} +import { Suspense } from "react"; +import { Loader2 } from "lucide-react"; +import ThreatsContent from "@/components/threats/ThreatsContent"; export default function ThreatsPage() { return ( diff --git a/src/app/api/[...route]/route.ts b/src/app/api/[...route]/route.ts index d91222a..e90869f 100644 --- a/src/app/api/[...route]/route.ts +++ b/src/app/api/[...route]/route.ts @@ -26,7 +26,7 @@ async function handleProxy(req: NextRequest, { params }: { params: Promise<{ rou const route = routeArray.join('/'); const url = new URL(req.url); - + // Force ALL traffic to go through our own Centralized Proxy Server (Express Backend) const localBase = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:3001'; const destinationUrl = `${localBase}/api/${route}${url.search}`; @@ -46,11 +46,12 @@ async function handleProxy(req: NextRequest, { params }: { params: Promise<{ rou headers.set('cookie', cookie); } - // Forward X-View-As-Agent header for view-as mode data filtering - const viewAsHeader = req.headers.get('x-view-as-agent'); - if (viewAsHeader) { - headers.set('x-view-as-agent', viewAsHeader); - } + // Forward all custom headers starting with x- (e.g. x-backone-site-uuid, x-view-as-agent) + req.headers.forEach((value, name) => { + if (name.toLowerCase().startsWith('x-')) { + headers.set(name, value); + } + }); const hasBody = !['GET', 'HEAD'].includes(req.method); let body = null; diff --git a/src/app/globals.css b/src/app/globals.css index ea0af4e..8427358 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,419 +1,21 @@ @import "tailwindcss"; - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - - --font-backone: var(--font-backone); - - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - - --color-border: var(--border); - - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - - --color-sidebar: var(--sidebar); - - /* OVERRIDE TAILWIND HARDCODED COLORS FOR SEAMLESS LIGHT MODE */ - --color-slate-900: var(--slate-900); - --color-slate-800: var(--slate-800); - --color-slate-700: var(--slate-700); - --color-slate-600: var(--slate-600); - --color-slate-500: var(--slate-500); - --color-slate-400: var(--slate-400); - --color-slate-300: var(--slate-300); - --color-slate-200: var(--slate-200); - --color-slate-100: var(--slate-100); - --color-slate-50: var(--slate-50); - - --color-gray-900: var(--slate-900); - --color-gray-800: var(--slate-800); - --color-gray-700: var(--slate-700); - --color-gray-600: var(--slate-600); - --color-gray-500: var(--slate-500); - --color-gray-400: var(--slate-400); - --color-gray-300: var(--slate-300); - --color-gray-200: var(--slate-200); - --color-gray-100: var(--slate-100); - --color-gray-50: var(--slate-50); - - --color-white: var(--t-white); - --color-black: var(--t-black); - - --font-sans: var(--font-inter); - --font-mono: var(--font-inter); - - /* Micro-animations */ - --animate-fade-in: fade-in 0.5s ease-out forwards; - --animate-slide-up: slide-up 0.5s ease-out forwards; - - @keyframes fade-in { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } - } - - @keyframes slide-up { - 0% { - opacity: 0; - transform: translateY(20px); - } - - 100% { - opacity: 1; - transform: translateY(0); - } - } -} - -:root { - /* Premium Dark Theme */ - --background: #09090b; - /* Very dark zinc */ - --foreground: #fafafa; - - --card: rgba(24, 24, 27, 0.7); - /* Zinc 900 with transparency for glassmorphism */ - --card-foreground: #fafafa; - - --border: rgba(255, 255, 255, 0.1); - - --primary: #3b82f6; - /* Blue 500 */ - --primary-foreground: #ffffff; - - --secondary: rgba(39, 39, 42, 0.8); - /* Zinc 800 */ - --secondary-foreground: #fafafa; - - --muted: rgba(39, 39, 42, 0.5); - --muted-foreground: #a1a1aa; - /* Zinc 400 */ - - --destructive: #ef4444; - /* Red 500 */ - --destructive-foreground: #ffffff; - - --sidebar: #09090b; - - /* Overrides for hardcoded colors in Dark Mode (Normal) */ - --t-white: #ffffff; - --t-black: #000000; - --slate-900: #0f172a; - --slate-800: #1e293b; - --slate-700: #334155; - --slate-600: #475569; - --slate-500: #64748b; - --slate-400: #94a3b8; - --slate-300: #cbd5e1; - --slate-200: #e2e8f0; - --slate-100: #f1f5f9; - --slate-50: #f8fafc; -} - -.light { - /* Clean Light Theme */ - --background: #f1f5f9; - /* Slate 100 */ - --foreground: #0f172a; - - --card: rgba(255, 255, 255, 0.9); - --card-foreground: #0f172a; - - --border: rgba(0, 0, 0, 0.1); - - --primary: #2563eb; - /* Blue 600 */ - --primary-foreground: #ffffff; - - --secondary: rgba(241, 245, 249, 0.8); - /* Slate 100 */ - --secondary-foreground: #0f172a; - - --muted: rgba(226, 232, 240, 0.5); - /* Slate 200 */ - --muted-foreground: #64748b; - /* Slate 500 */ - - --sidebar: #ffffff; - - /* Overrides for hardcoded colors in Light Mode (Inverted) */ - --t-white: #0f172a; - /* Make text-white become very dark slate */ - --t-black: #ffffff; - --slate-900: #ffffff; - --slate-800: #f8fafc; - --slate-700: #f1f5f9; - --slate-600: #e2e8f0; - --slate-500: #cbd5e1; - --slate-400: #64748b; - /* Keep 400 slightly dark so it stays readable */ - --slate-300: #475569; - --slate-200: #334155; - --slate-100: #1e293b; - --slate-50: #0f172a; - - /* Enhance contrast for colored text and backgrounds in Light Mode */ - - /* RED */ - --color-red-300: #b91c1c; - /* red-700 */ - --color-red-400: #dc2626; - /* red-600 */ - --color-red-500: #b91c1c; - /* red-700 */ - --color-red-950: #ef4444; - /* red-500 (so that 950/20 becomes a visible pink bg) */ - - /* ORANGE */ - --color-orange-300: #c2410c; - /* orange-700 */ - --color-orange-400: #ea580c; - /* orange-600 */ - --color-orange-500: #c2410c; - /* orange-700 */ - --color-orange-950: #f97316; - /* orange-500 */ - - /* YELLOW */ - --color-yellow-300: #a16207; - /* yellow-700 */ - --color-yellow-400: #ca8a04; - /* yellow-600 */ - --color-yellow-500: #a16207; - /* yellow-700 */ - --color-yellow-950: #eab308; - /* yellow-500 */ - - /* GREEN */ - --color-green-300: #15803d; - /* green-700 */ - --color-green-400: #16a34a; - /* green-600 */ - --color-green-500: #15803d; - /* green-700 */ - --color-green-950: #22c55e; - /* green-500 */ - - /* EMERALD */ - --color-emerald-300: #047857; - /* emerald-700 */ - --color-emerald-400: #059669; - /* emerald-600 */ - --color-emerald-500: #047857; - /* emerald-700 */ - --color-emerald-950: #10b981; - /* emerald-500 */ - - /* BLUE */ - --color-blue-300: #1d4ed8; - /* blue-700 */ - --color-blue-400: #2563eb; - /* blue-600 */ - --color-blue-500: #1d4ed8; - /* blue-700 */ - --color-blue-950: #3b82f6; - /* blue-500 */ - - /* PURPLE */ - --color-purple-300: #7e22ce; - /* purple-700 */ - --color-purple-400: #9333ea; - /* purple-600 */ - --color-purple-500: #7e22ce; - /* purple-700 */ - --color-purple-950: #a855f7; - /* purple-500 */ -} +@import "./theme.css"; +@import "./variables.css"; body { - font-family: var(--font-sans), sans-serif; + /* Use Georgia font style globally */ + font-family: Georgia, serif, var(--font-sans); color: var(--foreground); + /* Optical sizing + smoothing */ + font-optical-sizing: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; } /* 1-Second Dual-Layer Smooth Background Crossfade */ -:root { - /* Premium Dark Theme */ - --background: #09090b; - /* Very dark zinc */ - --foreground: #fafafa; - - --card: rgba(24, 24, 27, 0.7); - /* Zinc 900 with transparency for glassmorphism */ - --card-foreground: #fafafa; - - --border: rgba(255, 255, 255, 0.1); - - --primary: #3b82f6; - /* Blue 500 */ - --primary-foreground: #ffffff; - - --secondary: rgba(39, 39, 42, 0.8); - /* Zinc 800 */ - --secondary-foreground: #fafafa; - - --muted: rgba(39, 39, 42, 0.5); - --muted-foreground: #a1a1aa; - /* Zinc 400 */ - - --destructive: #ef4444; - /* Red 500 */ - --destructive-foreground: #ffffff; - - --sidebar: #09090b; - - /* Overrides for hardcoded colors in Dark Mode (Normal) */ - --t-white: #ffffff; - --t-black: #000000; - --slate-900: #0f172a; - --slate-800: #1e293b; - --slate-700: #334155; - --slate-600: #475569; - --slate-500: #64748b; - --slate-400: #94a3b8; - --slate-300: #cbd5e1; - --slate-200: #e2e8f0; - --slate-100: #f1f5f9; - --slate-50: #f8fafc; -} - -.light { - /* Clean Light Theme */ - --background: #f1f5f9; - /* Slate 100 */ - --foreground: #0f172a; - - --card: rgba(255, 255, 255, 0.9); - --card-foreground: #0f172a; - - --border: rgba(0, 0, 0, 0.1); - - --primary: #2563eb; - /* Blue 600 */ - --primary-foreground: #ffffff; - - --secondary: rgba(241, 245, 249, 0.8); - /* Slate 100 */ - --secondary-foreground: #0f172a; - - --muted: rgba(226, 232, 240, 0.5); - /* Slate 200 */ - --muted-foreground: #64748b; - /* Slate 500 */ - - --sidebar: #ffffff; - - /* Overrides for hardcoded colors in Light Mode (Inverted) */ - --t-white: #0f172a; - /* Make text-white become very dark slate */ - --t-black: #ffffff; - --slate-900: #ffffff; - --slate-800: #f8fafc; - --slate-700: #f1f5f9; - --slate-600: #e2e8f0; - --slate-500: #cbd5e1; - --slate-400: #64748b; - /* Keep 400 slightly dark so it stays readable */ - --slate-300: #475569; - --slate-200: #334155; - --slate-100: #1e293b; - --slate-50: #0f172a; - - /* Enhance contrast for colored text and backgrounds in Light Mode */ - - /* RED */ - --color-red-300: #b91c1c; - /* red-700 */ - --color-red-400: #dc2626; - /* red-600 */ - --color-red-500: #b91c1c; - /* red-700 */ - --color-red-950: #ef4444; - /* red-500 (so that 950/20 becomes a visible pink bg) */ - - /* ORANGE */ - --color-orange-300: #c2410c; - /* orange-700 */ - --color-orange-400: #ea580c; - /* orange-600 */ - --color-orange-500: #c2410c; - /* orange-700 */ - --color-orange-950: #f97316; - /* orange-500 */ - - /* YELLOW */ - --color-yellow-300: #a16207; - /* yellow-700 */ - --color-yellow-400: #ca8a04; - /* yellow-600 */ - --color-yellow-500: #a16207; - /* yellow-700 */ - --color-yellow-950: #eab308; - /* yellow-500 */ - - /* GREEN */ - --color-green-300: #15803d; - /* green-700 */ - --color-green-400: #16a34a; - /* green-600 */ - --color-green-500: #15803d; - /* green-700 */ - --color-green-950: #22c55e; - /* green-500 */ - - /* EMERALD */ - --color-emerald-300: #047857; - /* emerald-700 */ - --color-emerald-400: #059669; - /* emerald-600 */ - --color-emerald-500: #047857; - /* emerald-700 */ - --color-emerald-950: #10b981; - /* emerald-500 */ - - /* BLUE */ - --color-blue-300: #1d4ed8; - /* blue-700 */ - --color-blue-400: #2563eb; - /* blue-600 */ - --color-blue-500: #1d4ed8; - /* blue-700 */ - --color-blue-950: #3b82f6; - /* blue-500 */ - - /* PURPLE */ - --color-purple-300: #7e22ce; - /* purple-700 */ - --color-purple-400: #9333ea; - /* purple-600 */ - --color-purple-500: #7e22ce; - /* purple-700 */ - --color-purple-950: #a855f7; - /* purple-500 */ -} - -body { - font-family: var(--font-sans), sans-serif; - color: var(--foreground); -} - -/* 1-Second Dual-Layer Smooth Background Crossfade */ +/* Dark Mode Background Layer (Active by default, fades out on .light) */ body::before, body::after { content: ""; @@ -426,7 +28,6 @@ body::after { background-attachment: fixed; } -/* Dark Mode Background Layer (Active by default, fades out on .light) */ body::before { background-color: #09090b; background-image: @@ -570,7 +171,6 @@ input::-ms-clear { display: none; } - /* Accordion Animation Fix */ .accordion-grid { display: grid; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e897e0a..772d419 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,10 +3,20 @@ import localFont from "next/font/local"; import { ThemeProvider } from "@/components/ThemeProvider"; import "./globals.css"; -// Bypass google font remote fetch during offline compilation -const inter = { - variable: "font-sans", -}; +// Inter font — local variable font file, matches demoplace.my.id +// demoplace uses: --font-sans: var(--font-inter) which renders as Inter +const inter = localFont({ + src: [ + { + path: "../../public/fonts/Inter-Variable.woff2", + weight: "100 900", + style: "normal", + }, + ], + variable: "--font-inter", + display: "swap", + adjustFontFallback: false, +}); const backoneFont = localFont({ src: "../../public/fonts/BackOneLogo-Regular.ttf", diff --git a/src/app/theme.css b/src/app/theme.css new file mode 100644 index 0000000..67b1e92 --- /dev/null +++ b/src/app/theme.css @@ -0,0 +1,164 @@ +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + + --font-backone: var(--font-backone); + + /* Set Georgia as the default sans-serif replacement globally */ + --font-sans: Georgia, serif, var(--font-inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif); + + /* Mono also uses Inter first — matches demoplace (--default-mono-font-family: var(--font-inter)) */ + --font-mono: var(--font-inter, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + + --color-border: var(--border); + + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + + --color-sidebar: var(--sidebar); + + /* OVERRIDE TAILWIND HARDCODED COLORS FOR SEAMLESS LIGHT MODE */ + --color-slate-900: var(--slate-900); + --color-slate-800: var(--slate-800); + --color-slate-700: var(--slate-700); + --color-slate-600: var(--slate-600); + --color-slate-500: var(--slate-500); + --color-slate-400: var(--slate-400); + --color-slate-300: var(--slate-300); + --color-slate-200: var(--slate-200); + --color-slate-100: var(--slate-100); + --color-slate-50: var(--slate-50); + + --color-gray-900: var(--slate-900); + --color-gray-800: var(--slate-800); + --color-gray-700: var(--slate-700); + --color-gray-600: var(--slate-600); + --color-gray-500: var(--slate-500); + --color-gray-400: var(--slate-400); + --color-gray-300: var(--slate-300); + --color-gray-200: var(--slate-200); + --color-gray-100: var(--slate-100); + --color-gray-50: var(--slate-50); + + --color-white: var(--t-white); + --color-black: var(--t-black); + + /* Radix-style color palette — matching demoplace.my.id exactly */ + --color-red-50: #fef2f2; + --color-red-100: #ffe2e2; + --color-red-200: #ffcaca; + --color-red-300: #ffa3a3; + --color-red-400: #ff6568; + --color-red-500: #fb2c36; + --color-red-600: #e40014; + --color-red-700: #bf000f; + --color-red-800: #9f0712; + --color-red-900: #82181a; + --color-red-950: #460809; + + --color-orange-300: #ffb96d; + --color-orange-400: #ff8b1a; + --color-orange-500: #fe6e00; + --color-orange-900: #7e2a0c; + --color-orange-950: #441306; + + --color-amber-100: #fef3c6; + --color-amber-200: #fee685; + --color-amber-300: #ffd236; + --color-amber-400: #fcbb00; + --color-amber-500: #f99c00; + --color-amber-600: #dd7400; + --color-amber-700: #b75000; + --color-amber-800: #953d00; + --color-amber-900: #7b3306; + --color-amber-950: #461901; + + --color-yellow-300: #ffe02a; + --color-yellow-400: #fac800; + --color-yellow-500: #edb200; + + --color-green-300: #7bf1a8; + --color-green-400: #05df72; + --color-green-500: #00c758; + --color-green-800: #016630; + --color-green-900: #0d542b; + --color-green-950: #032e15; + + --color-emerald-100: #d0fae5; + --color-emerald-200: #a4f4cf; + --color-emerald-300: #5ee9b5; + --color-emerald-400: #00d294; + --color-emerald-500: #00bb7f; + --color-emerald-600: #009767; + --color-emerald-700: #007956; + --color-emerald-900: #004e3b; + --color-emerald-950: #002c22; + + --color-teal-400: #00d3bd; + --color-teal-500: #00baa7; + + --color-cyan-100: #cefafe; + --color-cyan-300: #53eafd; + --color-cyan-400: #00d2ef; + --color-cyan-500: #00b7d7; + --color-cyan-600: #0092b5; + + --color-sky-400: #00bcfe; + + --color-blue-50: #eff6ff; + --color-blue-200: #bedbff; + --color-blue-300: #90c5ff; + --color-blue-400: #54a2ff; + --color-blue-500: #3080ff; + --color-blue-600: #155dfc; + --color-blue-700: #1447e6; + --color-blue-800: #193cb8; + --color-blue-900: #1c398e; + --color-blue-950: #162456; + + --color-indigo-300: #a4b3ff; + --color-indigo-400: #7d87ff; + --color-indigo-500: #625fff; + --color-indigo-600: #4f39f6; + --color-indigo-900: #312c85; + + --color-violet-400: #a685ff; + --color-violet-500: #8d54ff; + + --color-purple-300: #d9b3ff; + --color-purple-400: #c07eff; + --color-purple-500: #ac4bff; + --color-purple-600: #9810fa; + --color-purple-900: #59168b; + --color-purple-950: #3c0366; + + --color-pink-400: #fb64b6; + --color-rose-400: #ff667f; + --color-rose-900: #8b0836; + + /* Micro-animations */ + --animate-fade-in: fade-in 0.5s ease-out forwards; + --animate-slide-up: slide-up 0.5s ease-out forwards; + + @keyframes fade-in { + 0% { opacity: 0; } + 100% { opacity: 1; } + } + + @keyframes slide-up { + 0% { opacity: 0; transform: translateY(20px); } + 100% { opacity: 1; transform: translateY(0); } + } +} diff --git a/src/app/variables.css b/src/app/variables.css new file mode 100644 index 0000000..d58b7c1 --- /dev/null +++ b/src/app/variables.css @@ -0,0 +1,159 @@ +:root { + /* Premium Dark Theme */ + --background: #09090b; + /* Very dark zinc */ + --foreground: #fafafa; + + --card: rgba(24, 24, 27, 0.7); + /* Zinc 900 with transparency for glassmorphism */ + --card-foreground: #fafafa; + + --border: rgba(255, 255, 255, 0.1); + + --primary: #3b82f6; + /* Blue 500 */ + --primary-foreground: #ffffff; + + --secondary: rgba(39, 39, 42, 0.8); + /* Zinc 800 */ + --secondary-foreground: #fafafa; + + --muted: rgba(39, 39, 42, 0.5); + --muted-foreground: #a1a1aa; + /* Zinc 400 */ + + --destructive: #ef4444; + /* Red 500 */ + --destructive-foreground: #ffffff; + + --sidebar: #09090b; + + /* Overrides for hardcoded colors in Dark Mode (Normal) */ + --t-white: #ffffff; + --t-black: #000000; + --slate-900: #0f172a; + --slate-800: #1e293b; + --slate-700: #334155; + --slate-600: #475569; + --slate-500: #64748b; + --slate-400: #94a3b8; + --slate-300: #cbd5e1; + --slate-200: #e2e8f0; + --slate-100: #f1f5f9; + --slate-50: #f8fafc; +} + +.light { + /* Clean Light Theme */ + --background: #f1f5f9; + /* Slate 100 */ + --foreground: #0f172a; + + --card: rgba(255, 255, 255, 0.9); + --card-foreground: #0f172a; + + --border: rgba(0, 0, 0, 0.1); + + --primary: #2563eb; + /* Blue 600 */ + --primary-foreground: #ffffff; + + --secondary: rgba(241, 245, 249, 0.8); + /* Slate 100 */ + --secondary-foreground: #0f172a; + + --muted: rgba(226, 232, 240, 0.5); + /* Slate 200 */ + --muted-foreground: #64748b; + /* Slate 500 */ + + --sidebar: #ffffff; + + /* Overrides for hardcoded colors in Light Mode (Inverted) */ + --t-white: #0f172a; + /* Make text-white become very dark slate */ + --t-black: #ffffff; + --slate-900: #ffffff; + --slate-800: #f8fafc; + --slate-700: #f1f5f9; + --slate-600: #e2e8f0; + --slate-500: #cbd5e1; + --slate-400: #64748b; + /* Keep 400 slightly dark so it stays readable */ + --slate-300: #475569; + --slate-200: #334155; + --slate-100: #1e293b; + --slate-50: #0f172a; + + /* Enhance contrast for colored text and backgrounds in Light Mode */ + + /* RED */ + --color-red-300: #b91c1c; + /* red-700 */ + --color-red-400: #dc2626; + /* red-600 */ + --color-red-500: #b91c1c; + /* red-700 */ + --color-red-950: #ef4444; + /* red-500 (so that 950/20 becomes a visible pink bg) */ + + /* ORANGE */ + --color-orange-300: #c2410c; + /* orange-700 */ + --color-orange-400: #ea580c; + /* orange-600 */ + --color-orange-500: #c2410c; + /* orange-700 */ + --color-orange-950: #f97316; + /* orange-500 */ + + /* YELLOW */ + --color-yellow-300: #a16207; + /* yellow-700 */ + --color-yellow-400: #ca8a04; + /* yellow-600 */ + --color-yellow-500: #a16207; + /* yellow-700 */ + --color-yellow-950: #eab308; + /* yellow-500 */ + + /* GREEN */ + --color-green-300: #15803d; + /* green-700 */ + --color-green-400: #16a34a; + /* green-600 */ + --color-green-500: #15803d; + /* green-700 */ + --color-green-950: #22c55e; + /* green-500 */ + + /* EMERALD */ + --color-emerald-300: #047857; + /* emerald-700 */ + --color-emerald-400: #059669; + /* emerald-600 */ + --color-emerald-500: #047857; + /* emerald-700 */ + --color-emerald-950: #10b981; + /* emerald-500 */ + + /* BLUE */ + --color-blue-300: #1d4ed8; + /* blue-700 */ + --color-blue-400: #2563eb; + /* blue-600 */ + --color-blue-500: #1d4ed8; + /* blue-700 */ + --color-blue-950: #3b82f6; + /* blue-500 */ + + /* PURPLE */ + --color-purple-300: #7e22ce; + /* purple-700 */ + --color-purple-400: #9333ea; + /* purple-600 */ + --color-purple-500: #7e22ce; + /* purple-700 */ + --color-purple-950: #a855f7; + /* purple-500 */ +} diff --git a/src/components/admin/AgentEmptyMap.tsx b/src/components/admin/AgentEmptyMap.tsx new file mode 100644 index 0000000..b3ce0d1 --- /dev/null +++ b/src/components/admin/AgentEmptyMap.tsx @@ -0,0 +1,27 @@ +"use client"; + +import React from "react"; +import { MapPin } from "lucide-react"; + +export function EmptyMap() { + return ( +
+
+ +
+
+

No agent locations configured

+

+ Click the + + + + icon in the + Actions + column to set coordinates. +

+
+
+ ); +} diff --git a/src/components/admin/AgentLocationMap.tsx b/src/components/admin/AgentLocationMap.tsx new file mode 100644 index 0000000..4e6f496 --- /dev/null +++ b/src/components/admin/AgentLocationMap.tsx @@ -0,0 +1,248 @@ +// src/components/admin/AgentLocationMap.tsx +"use client"; + +import React, { useEffect, useRef } from "react"; +import { RotateCcw } from "lucide-react"; +import { getViewAsHeaders } from "@/lib/admin-api"; +import { + EmptyMap, + MAP_CSS, + drawFlowLines, + drawMarkers, + type AgentLocationPin +} from "./AgentLocationMapHelpers"; + +interface Props { + pins: AgentLocationPin[]; + onEditLocation?: (agentUuid: string, agentLabel: string) => void; + canEdit?: boolean; +} + +const AgentLocationMap = React.memo(function AgentLocationMap({ + pins, + onEditLocation, + canEdit = false, +}: Props) { + const mapRef = useRef(null); + const instanceRef = useRef(null); + const LRef = useRef(null); + const [flows, setFlows] = React.useState([]); + + useEffect(() => { + let active = true; + fetch("/api/dashboard/agent-flows", { headers: getViewAsHeaders() }) + .then((res) => res.json()) + .then((json) => { + if (active && json.ok && json.data) { + setFlows(json.data); + } + }) + .catch((err) => console.error("Failed to load agent flows for map:", err)); + return () => { + active = false; + }; + }, [pins]); + + const resetView = () => { + if (!instanceRef.current || !pins.length || !LRef.current) return; + const L = LRef.current; + if (pins.length === 1) { + instanceRef.current.setView([pins[0].latitude, pins[0].longitude], 14, { animate: false }); + } else { + instanceRef.current.fitBounds(L.latLngBounds(pins.map((p) => [p.latitude, p.longitude])), { + padding: [70, 70], + maxZoom: 14, + animate: false, + }); + } + }; + + useEffect(() => { + let mounted = true; + if (!mapRef.current) return; + + // Inject styles + const styleId = "leaflet-custom-markers-css"; + if (!document.getElementById(styleId)) { + const s = document.createElement("style"); + s.id = styleId; + s.innerHTML = MAP_CSS; + document.head.appendChild(s); + } + + const boot = async () => { + if (!mounted || !mapRef.current) return; + + // Inject Leaflet CSS + if (!document.getElementById("lf-css-admin")) { + const lnk = document.createElement("link"); + lnk.id = "lf-css-admin"; + lnk.rel = "stylesheet"; + lnk.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"; + document.head.appendChild(lnk); + } + + const L = (await import("leaflet")).default; + if (!mounted || !mapRef.current) return; + LRef.current = L; + + if (instanceRef.current) { + try { + instanceRef.current.remove(); + } catch (_) {} + } + + const map = L.map(mapRef.current, { + center: pins[0] ? [pins[0].latitude, pins[0].longitude] : [-2.548926, 118.0148634], + zoom: pins.length === 1 ? 14 : 8, + zoomControl: false, + attributionControl: false, + minZoom: 2, + maxZoom: 20, + maxBounds: [[-90, -180], [90, 180]], + maxBoundsViscosity: 1.0, + zoomAnimation: false, + fadeAnimation: false, + markerZoomAnimation: false, + }); + instanceRef.current = map; + + L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", { + subdomains: "abcd", + maxZoom: 20, + noWrap: true, + }).addTo(map); + + if (pins.length > 1) { + map.fitBounds(L.latLngBounds(pins.map((p) => [p.latitude, p.longitude])), { + padding: [70, 70], + maxZoom: 14, + animate: false, + }); + } + + // ── Get heartbeats for agent uptime from window or fetch ─────────────── + const uptimeMap: Record = {}; + try { + const res = await fetch("/api/dashboard/agent-heartbeats"); + if (res.ok) { + const json = await res.json(); + if (json.ok && json.data) { + json.data.forEach((item: any) => { + uptimeMap[item.agent_uuid] = item.uptime_percent ?? 100; + }); + } + } + } catch (_) {} + + if (!mounted) return; + + // Draw Flow lines + drawFlowLines(map, L, pins, flows); + + // Draw Markers + drawMarkers(map, L, pins, uptimeMap, canEdit, onEditLocation); + }; + + boot(); + return () => { + mounted = false; + if (instanceRef.current) { + try { + instanceRef.current.remove(); + } catch (_) {} + instanceRef.current = null; + } + }; + }, [pins, flows, canEdit]); + + if (!pins.length) return ; + + return ( +
+
+ + {/* Controls */} +
+ {[ + { t: "Zoom In", l: "+", fn: () => instanceRef.current?.zoomIn() }, + { t: "Zoom Out", l: "−", fn: () => instanceRef.current?.zoomOut() }, + ].map((b) => ( + + ))} + +
+ + {/* Legend */} +
+ {[ + ["#10b981", "Online", true], + ["#ef4444", "Offline", false], + ].map(([c, l, glow]: any) => ( + + + {l} + + ))} + {flows.length > 0 && ( + + + Traffic flow + + )} + + {pins.length} agent{pins.length !== 1 ? "s" : ""} mapped + +
+
+ ); +}); + +export default AgentLocationMap; diff --git a/src/components/admin/AgentLocationMapHelpers.tsx b/src/components/admin/AgentLocationMapHelpers.tsx new file mode 100644 index 0000000..892d073 --- /dev/null +++ b/src/components/admin/AgentLocationMapHelpers.tsx @@ -0,0 +1,182 @@ +"use client"; + +import React from "react"; +import { getFlowTooltipContent } from "../dashboard/IndonesiaAgentMapHelpers"; +import { EmptyMap } from "./AgentEmptyMap"; +import { MAP_CSS } from "./AgentMapCss"; + +export { EmptyMap, MAP_CSS }; + +export interface AgentLocationPin { + agent_uuid: string; + label: string; + latitude: number; + longitude: number; + isOnline?: boolean; + uptimePercent?: number; + site_uuid?: string; +} + +export function buildMarkerHtml(online: boolean) { + return ` +
+
+
+
+
+
+ `; +} + +// ── Draw Flow lines ONLY if they exist in active flows ──────────────── +export function drawFlowLines(map: any, L: any, pins: AgentLocationPin[], flows: any[]) { + flows.forEach((flow) => { + const src = pins.find((p) => p.agent_uuid === flow.source); + const dst = pins.find((p) => p.agent_uuid === flow.target); + if (!src || !dst) return; + + const bothOn = src.isOnline && dst.isOnline; + const bothOff = !src.isOnline && !dst.isOnline; + const lineColor = bothOn ? "#34d399" : bothOff ? "#f87171" : "#fbbf24"; + const weight = Math.min(Math.max(flow.bytes / 1024 / 1024 / 80, 2.0), 6); + + // Background polyline for hover trigger area + const baseLine = L.polyline( + [[src.latitude, src.longitude], [dst.latitude, dst.longitude]], + { color: lineColor, weight: weight + 4, opacity: 0.15 } + ).addTo(map); + + // Main colored line + const activeLine = L.polyline( + [[src.latitude, src.longitude], [dst.latitude, dst.longitude]], + { color: lineColor, weight: weight, opacity: 0.5 } + ).addTo(map); + + // Moving animation line + const animLine = L.polyline( + [[src.latitude, src.longitude], [dst.latitude, dst.longitude]], + { + color: lineColor, + weight: weight * 0.6, + opacity: 0.85, + dashArray: "8 12", + className: "flow-line", + } + ).addTo(map); + + const tooltipHtml = getFlowTooltipContent( + src.label, + dst.label, + flow.bytes, + flow.flowsCount, + flow.details || [] + ); + + [baseLine, activeLine, animLine].forEach((line) => { + line.bindTooltip(tooltipHtml, { sticky: true, className: "leaflet-custom-popup shadow-2xl" }); + }); + }); +} + +// ── Draw Markers with Popups & Tooltips ─────────────────────────────── +export function drawMarkers( + map: any, + L: any, + pins: AgentLocationPin[], + uptimeMap: Record, + canEdit: boolean, + onEditLocation?: (agentUuid: string, agentLabel: string) => void +) { + pins.forEach((pin) => { + const online = pin.isOnline ?? false; + const color = online ? "#10b981" : "#ef4444"; + const colorL = online ? "#34d399" : "#f87171"; + + const icon = L.divIcon({ + html: buildMarkerHtml(online), + className: "", + iconSize: [32, 32], + iconAnchor: [16, 16], + popupAnchor: [0, -18], + tooltipAnchor: [0, -18], + }); + + const marker = L.marker([pin.latitude, pin.longitude], { icon }); + + // ── Permanent label tooltip ────────────────────────────────────────── + const shortLabel = pin.label.length > 22 ? pin.label.slice(0, 20) + "…" : pin.label; + marker.bindTooltip( + `
+ + ${shortLabel} +
`, + { permanent: true, direction: "top", offset: [0, -10], className: "agent-label-tooltip", interactive: false } + ); + + // ── Click popup ────────────────────────────────────────────────────── + const uptime = uptimeMap[pin.agent_uuid]; + const uptimeColor = + (uptime ?? 100) >= 98 + ? "#34d399" + : (uptime ?? 100) >= 90 + ? "#fbbf24" + : "#f87171"; + const editBtn = + canEdit && onEditLocation + ? `` + : ""; + + marker.bindPopup( + `
+
+ + ${ + pin.label + } +
+
+ UUID${pin.agent_uuid} + Status${ + online ? "● Online" : "○ Offline" + } + Lat${pin.latitude.toFixed(6)} + Lng${pin.longitude.toFixed(6)} + ${ + uptime !== undefined + ? `Uptime${uptime.toFixed( + 1 + )}%` + : "" + } +
${editBtn} +
`, + { className: "agent-popup", maxWidth: 260 } + ); + + marker.on("popupopen", () => { + const id = `el-${pin.agent_uuid.replace(/\W/g, "")}`; + setTimeout(() => { + document.getElementById(id)?.addEventListener("click", () => { + onEditLocation?.(pin.agent_uuid, pin.label); + map.closePopup(); + }); + }, 50); + }); + + marker.addTo(map); + }); +} diff --git a/src/components/admin/AgentMapCss.ts b/src/components/admin/AgentMapCss.ts new file mode 100644 index 0000000..f6e146e --- /dev/null +++ b/src/components/admin/AgentMapCss.ts @@ -0,0 +1,97 @@ +export const MAP_CSS = ` + .leaflet-control-attribution { display:none !important; } + .agent-popup .leaflet-popup-content-wrapper { background:#0a1224!important; border:1px solid rgba(99,102,241,0.25)!important; box-shadow:0 12px 36px rgba(0,0,0,0.65)!important; padding:0!important; border-radius:12px!important; } + .agent-popup .leaflet-popup-content { margin:0!important; } + .agent-popup .leaflet-popup-tip-container { display:none!important; } + .agent-popup .leaflet-popup-close-button { color:#94a3b8!important; font-size:18px!important; top:8px!important; right:10px!important; } + .agent-label-tooltip { background:transparent!important; border:none!important; box-shadow:none!important; padding:0!important; } + .agent-label-tooltip::before { display:none!important; } + @keyframes dash-flow { to { stroke-dashoffset:-20; } } + .flow-line { animation: dash-flow 1.4s linear infinite; } + + /* Tooltip and popup custom classes */ + .leaflet-custom-popup .leaflet-popup-content-wrapper { + background: transparent !important; + box-shadow: none !important; + padding: 0 !important; + margin: 0 !important; + } + .leaflet-custom-popup .leaflet-popup-tip { + background: #0f172a !important; + border: 1px solid rgba(255,255,255,0.15); + } + .leaflet-custom-popup .leaflet-popup-content { + margin: 0 !important; + padding: 0 !important; + background: transparent !important; + } + + /* Premium Map Marker Styles */ + .custom-agent-marker { + display: flex; + align-items: center; + justify-content: center; + position: relative; + width: 32px; + height: 32px; + } + .marker-beacon-ring { + position: absolute; + width: 32px; + height: 32px; + border-radius: 50%; + opacity: 0; + pointer-events: none; + } + .online .marker-beacon-ring { + border: 2px solid #10b981; + animation: beacon-ping 2.5s infinite cubic-bezier(0.215, 0.61, 0.355, 1); + } + .offline .marker-beacon-ring { + border: 2px solid #ef4444; + animation: beacon-ping 2.5s infinite cubic-bezier(0.215, 0.61, 0.355, 1); + } + @keyframes beacon-ping { + 0% { transform: scale(0.4); opacity: 0.85; } + 50% { opacity: 0.45; } + 100% { transform: scale(1.8); opacity: 0; } + } + .marker-glass-disc { + width: 15px; + height: 15px; + border-radius: 50%; + background: rgba(10, 18, 36, 0.85); + border: 2px solid; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7), inset 0 1px 2px rgba(255, 255, 255, 0.15); + transition: all 0.2s ease-in-out; + } + .online .marker-glass-disc { + border-color: #10b981; + box-shadow: 0 0 10px rgba(16, 185, 129, 0.5), 0 4px 10px rgba(0, 0, 0, 0.6); + } + .offline .marker-glass-disc { + border-color: #ef4444; + box-shadow: 0 0 10px rgba(239, 68, 68, 0.5), 0 4px 10px rgba(0, 0, 0, 0.6); + } + .marker-core-dot { + width: 6px; + height: 6px; + border-radius: 50%; + transition: all 0.2s ease-in-out; + } + .online .marker-core-dot { + background: #10b981; + box-shadow: 0 0 6px #10b981; + } + .offline .marker-core-dot { + background: #ef4444; + box-shadow: 0 0 6px #ef4444; + } + .custom-agent-marker:hover .marker-glass-disc { + transform: scale(1.25); + } +`; diff --git a/src/components/admin/AgentModals.tsx b/src/components/admin/AgentModals.tsx index ef005bc..b1d7dd9 100644 --- a/src/components/admin/AgentModals.tsx +++ b/src/components/admin/AgentModals.tsx @@ -3,7 +3,8 @@ import { useState } from "react"; import { Modal } from "@/components/ui/Modal"; import { Loader2 } from "lucide-react"; -import { createAgent, deleteAgent, Agent } from "@/lib/actions/agents"; +import { createAgent, deleteAgent } from "@/lib/actions/agents"; +import { Agent } from "@/lib/actions/agentsCore"; import { AgentDetailModal } from "@/components/ui/AgentDetailModal"; import AgentAccountModal from "@/components/admin/AgentAccountModal"; import { resolveAgentLabel, type ManagedUser } from "@/lib/admin-api"; diff --git a/src/components/dashboard/IndonesiaAgentMap.tsx b/src/components/dashboard/IndonesiaAgentMap.tsx index 4646173..6049f5e 100644 --- a/src/components/dashboard/IndonesiaAgentMap.tsx +++ b/src/components/dashboard/IndonesiaAgentMap.tsx @@ -2,8 +2,6 @@ import React, { useEffect, useRef, useState, useMemo } from "react"; import { MapPin, AlertTriangle, ChevronDown, ChevronUp, Plus, Info, Loader2 } from "lucide-react"; -import * as L from "leaflet"; -import "leaflet/dist/leaflet.css"; import { useTheme } from "next-themes"; import { getViewAsHeaders } from "@/lib/admin-api"; import { @@ -12,7 +10,7 @@ import { MAP_CSS_INJECT, type FlowDetail } from "./IndonesiaAgentMapHelpers"; -import { Agent } from "@/lib/actions/agents"; +import { Agent } from "@/lib/actions/agentsCore"; interface LocationData { agent_uuid: string; @@ -109,185 +107,254 @@ export function IndonesiaAgentMap({ // Initialize and Update Map useEffect(() => { if (!mapContainerRef.current) return; + let mounted = true; - // Build unique mapping of agent UUIDs → their registered coordinates. - // Only agents with real entries in CustomAgentLocation are rendered on the map. - const agentMap: Record = {}; + const buildMarkerHtml = (online: boolean): string => { + return ` +
+
+
+
+
+
+ `; + }; - agents.forEach((agent) => { - const uuid = agent.uuid || agent.serial || ""; - const loc = locations.find((l) => l.agent_uuid === uuid); + const boot = async () => { + if (!mounted) return; + const L = (await import("leaflet")).default; + if (!mounted || !mapContainerRef.current) return; - // Only map agents that have a real coordinate registered. - // Agents without coordinates are intentionally excluded from the map. - if (loc) { - agentMap[uuid] = { - lat: loc.latitude, - lng: loc.longitude, - label: loc.label || "", - agentLabel: agent.label || "", - status: agent.activated ? "Online" : "Offline", - }; + // Inject CSS once + if (!document.getElementById("lf-css-dpi")) { + const lnk = document.createElement("link"); + lnk.id = "lf-css-dpi"; lnk.rel = "stylesheet"; + lnk.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"; + document.head.appendChild(lnk); } - }); - - const activeCoords = Object.values(agentMap); - const centerLat = activeCoords.length > 0 ? activeCoords.reduce((s, v) => s + v.lat, 0) / activeCoords.length : -6.2088; - const centerLng = activeCoords.length > 0 ? activeCoords.reduce((s, v) => s + v.lng, 0) / activeCoords.length : 106.8456; - - // Voyager for light mode: colorful, ocean blue, matches Globe Map aesthetic - const tileUrl = isDark - ? "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png" - : "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png"; - - if (!mapInstanceRef.current) { - // Canvas renderer: redraws from geo-coords every frame, no SVG offset bugs - const canvasRenderer = L.canvas({ padding: 0.5 }); - canvasRendererRef.current = canvasRenderer; - - const map = L.map(mapContainerRef.current, { - zoomControl: false, - attributionControl: false, - renderer: canvasRenderer, - }).setView([centerLat, centerLng], 10); - - const tiles = L.tileLayer(tileUrl, { maxZoom: 19 }).addTo(map); - tileLayerRef.current = tiles; - - L.control.zoom({ position: "bottomright" }).addTo(map); - - mapInstanceRef.current = map; - layerGroupRef.current = L.layerGroup().addTo(map); - } else { - const map = mapInstanceRef.current; - if (tileLayerRef.current) { - tileLayerRef.current.setUrl(tileUrl); + if (!document.getElementById("dpi-map-extra-css")) { + const s = document.createElement("style"); + s.id = "dpi-map-extra-css"; + s.textContent = ` + @keyframes dpi-beacon { 0%,100%{opacity:.18;transform:scale(1);transform-origin:20px 20px;} 50%{opacity:.05;transform:scale(1.65);transform-origin:20px 20px;} } + .dpi-beacon { animation: dpi-beacon 2.2s ease-in-out infinite; } + @keyframes flowDash { to { stroke-dashoffset:-24; } } + .animated-flow-line { animation: flowDash 1.2s linear infinite; } + .agent-label-tooltip { background:transparent!important;border:none!important;box-shadow:none!important;padding:0!important; } + .agent-label-tooltip::before { display:none!important; } + .dpi-agent-label { display:inline-block;font-size:10px;font-weight:700;padding:3px 8px;border-radius:6px;border:1px solid;white-space:nowrap;letter-spacing:0.02em; } + .leaflet-control-attribution { display:none!important; } + .leaflet-custom-popup .leaflet-popup-content-wrapper { background:#0a1224!important; border:1px solid rgba(99,102,241,0.25)!important; box-shadow:0 12px 36px rgba(0,0,0,0.65)!important; padding:0!important; border-radius:12px!important; } + .leaflet-custom-popup .leaflet-popup-content { margin:0!important;padding:0!important; } + + /* Premium Map Marker Styles */ + .custom-agent-marker { + display: flex; + align-items: center; + justify-content: center; + position: relative; + width: 32px; + height: 32px; + } + .marker-beacon-ring { + position: absolute; + width: 32px; + height: 32px; + border-radius: 50%; + opacity: 0; + pointer-events: none; + } + .online .marker-beacon-ring { + border: 2px solid #10b981; + animation: beacon-ping 2.5s infinite cubic-bezier(0.215, 0.61, 0.355, 1); + } + .offline .marker-beacon-ring { + border: 2px solid #ef4444; + animation: beacon-ping 2.5s infinite cubic-bezier(0.215, 0.61, 0.355, 1); + } + @keyframes beacon-ping { + 0% { transform: scale(0.4); opacity: 0.85; } + 50% { opacity: 0.45; } + 100% { transform: scale(1.8); opacity: 0; } + } + .marker-glass-disc { + width: 15px; + height: 15px; + border-radius: 50%; + background: rgba(10, 18, 36, 0.85); + border: 2px solid; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7), inset 0 1px 2px rgba(255, 255, 255, 0.15); + transition: all 0.2s ease-in-out; + } + .online .marker-glass-disc { + border-color: #10b981; + box-shadow: 0 0 10px rgba(16, 185, 129, 0.5), 0 4px 10px rgba(0, 0, 0, 0.6); + } + .offline .marker-glass-disc { + border-color: #ef4444; + box-shadow: 0 0 10px rgba(239, 68, 68, 0.5), 0 4px 10px rgba(0, 0, 0, 0.6); + } + .marker-core-dot { + width: 6px; + height: 6px; + border-radius: 50%; + transition: all 0.2s ease-in-out; + } + .online .marker-core-dot { + background: #10b981; + box-shadow: 0 0 6px #10b981; + } + .offline .marker-core-dot { + background: #ef4444; + box-shadow: 0 0 6px #ef4444; + } + .custom-agent-marker:hover .marker-glass-disc { + transform: scale(1.25); + } + `; + document.head.appendChild(s); } - if (activeCoords.length > 0) { - map.panTo([centerLat, centerLng]); - } - } - const group = layerGroupRef.current; - group.clearLayers(); - - const renderer = canvasRendererRef.current; - const registeredUuids = new Set(locations.map(l => l.agent_uuid)); - - // ─── A. DRAW FLOW LINES ──────────────────────────────────────────────────── - // Drawn BEFORE markers so markers render on top - flows.forEach((flow) => { - const src = agentMap[flow.source]; - const dst = agentMap[flow.target]; - if (!src || !dst || !registeredUuids.has(flow.source) || !registeredUuids.has(flow.target)) return; - - const weight = Math.min(Math.max(flow.bytes / 1024 / 1024 / 80, 2.5), 8); - const color = isDark ? "#38bdf8" : "#1d4ed8"; - - // Glow / shadow underlay - L.polyline([[src.lat, src.lng], [dst.lat, dst.lng]], { - color: isDark ? "#0ea5e9" : "#3b82f6", - weight: weight + 4, - opacity: 0.15, - renderer, - }).addTo(group); - - // Solid base line - const baseLine = L.polyline([[src.lat, src.lng], [dst.lat, dst.lng]], { - color, - weight, - opacity: 0.5, - renderer, - }).addTo(group); - - // Animated dashed overlay (motion effect via CSS on SVG; canvas uses stroke-dasharray) - const animLine = L.polyline([[src.lat, src.lng], [dst.lat, dst.lng]], { - color, - weight: weight * 0.55, - opacity: 0.95, - dashArray: "10 15", - className: "animated-flow-line", - renderer, - }).addTo(group); - - const tooltipHtml = getFlowTooltipContent( - src.agentLabel, - dst.agentLabel, - flow.bytes, - flow.flowsCount, - flow.details || [] - ); - - [baseLine, animLine].forEach(line => { - line.bindTooltip(tooltipHtml, { - sticky: true, - className: "leaflet-custom-popup shadow-2xl", - }); - }); - }); - - // ─── B. DRAW AGENT MARKERS (circleMarker = same Canvas, pixel-perfect) ───── - Object.entries(agentMap).forEach(([uuid, info]) => { - const isOnline = info.status === "Online"; - const fillColor = isOnline ? "#22c55e" : "#ef4444"; - const glowColor = isOnline ? "#4ade80" : "#f87171"; - - // Outer glow ring - L.circleMarker([info.lat, info.lng], { - radius: 11, - color: glowColor, - weight: 2, - fillColor: glowColor, - fillOpacity: 0.15, - opacity: 0.5, - renderer, - }).addTo(group); - - // Inner solid dot - const dot = L.circleMarker([info.lat, info.lng], { - radius: 6, - color: "#ffffff", - weight: 2, - fillColor, - fillOpacity: 1, - opacity: 1, - renderer, - }).addTo(group); - - // Agent label — permanent tooltip anchored below the dot - const labelClass = isDark - ? "bg-slate-950/90 text-slate-200 border-white/10" - : "bg-white/95 text-slate-800 border-slate-200"; - - dot.bindTooltip( - `
${info.agentLabel}
`, - { permanent: true, direction: "bottom", offset: [0, 8], className: "agent-label-tooltip" } - ).openTooltip(); - - // Detail popup on click - const popupHtml = getAgentTooltipContent( - uuid, - info.agentLabel, - info.status, - isOnline, - info.label, - info.lat, - info.lng - ); - - dot.bindPopup(popupHtml, { - closeButton: false, - className: "leaflet-custom-popup", - offset: [0, -8], - }); - - dot.on("popupopen", () => { - const btn = document.getElementById(`btn-edit-${uuid}`); - if (btn) { - btn.addEventListener("click", () => onEditLocation(uuid, info.agentLabel)); + // Build agent map + const agentMap: Record = {}; + agents.forEach((agent) => { + const uuid = agent.uuid || agent.serial || ""; + const loc = locations.find((l) => l.agent_uuid === uuid); + if (loc) { + agentMap[uuid] = { + lat: loc.latitude, + lng: loc.longitude, + label: loc.label || "", + agentLabel: agent.label || "", + status: agent.activated ? "Online" : "Offline", + }; } }); - }); + + const activeCoords = Object.values(agentMap); + const centerLat = activeCoords.length > 0 ? activeCoords.reduce((s, v) => s + v.lat, 0) / activeCoords.length : -6.2088; + const centerLng = activeCoords.length > 0 ? activeCoords.reduce((s, v) => s + v.lng, 0) / activeCoords.length : 106.8456; + + const tileUrl = isDark + ? "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png" + : "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png"; + + if (!mapInstanceRef.current) { + const map = L.map(mapContainerRef.current, { + zoomControl: false, + attributionControl: false, + zoomAnimation: false, // prevents _leaflet_pos crash on React StrictMode remount + fadeAnimation: false, + markerZoomAnimation: false, + }).setView([centerLat, centerLng], 10, { animate: false }); + + const tiles = L.tileLayer(tileUrl, { maxZoom: 19 }).addTo(map); + tileLayerRef.current = tiles; + L.control.zoom({ position: "bottomright" }).addTo(map); + mapInstanceRef.current = map; + layerGroupRef.current = L.layerGroup().addTo(map); + } else { + const map = mapInstanceRef.current; + if (tileLayerRef.current) tileLayerRef.current.setUrl(tileUrl); + if (activeCoords.length > 0) map.panTo([centerLat, centerLng]); + } + + if (!mounted) return; + const group = layerGroupRef.current; + group.clearLayers(); + const registeredUuids = new Set(locations.map(l => l.agent_uuid)); + + // ── A. FLOW LINES ──────────────────────────────────────────────────────── + flows.forEach((flow) => { + const src = agentMap[flow.source]; + const dst = agentMap[flow.target]; + if (!src || !dst || !registeredUuids.has(flow.source) || !registeredUuids.has(flow.target)) return; + const weight = Math.min(Math.max(flow.bytes / 1024 / 1024 / 80, 2.5), 8); + const color = isDark ? "#38bdf8" : "#1d4ed8"; + + L.polyline([[src.lat, src.lng], [dst.lat, dst.lng]], { + color: isDark ? "#0ea5e9" : "#3b82f6", weight: weight + 4, opacity: 0.12, + }).addTo(group); + + const baseLine = L.polyline([[src.lat, src.lng], [dst.lat, dst.lng]], { + color, weight, opacity: 0.5, + }).addTo(group); + + const animLine = L.polyline([[src.lat, src.lng], [dst.lat, dst.lng]], { + color, weight: weight * 0.55, opacity: 0.95, dashArray: "10 15", + className: "animated-flow-line", + }).addTo(group); + + const tooltipHtml = getFlowTooltipContent( + src.agentLabel, dst.agentLabel, flow.bytes, flow.flowsCount, flow.details || [] + ); + [baseLine, animLine].forEach(line => { + line.bindTooltip(tooltipHtml, { sticky: true, className: "leaflet-custom-popup shadow-2xl" }); + }); + }); + + // ── B. PREMIUM SVG MARKERS ─────────────────────────────────────────────── + Object.entries(agentMap).forEach(([uuid, info]) => { + if (!mounted) return; + const isOnline = info.status === "Online"; + const labelColor = isOnline ? "#34d399" : "#f87171"; + const borderCol = isOnline ? "#10b981" : "#ef4444"; + + const icon = L.divIcon({ + html: buildMarkerHtml(isOnline), + className: "", + iconSize: [32, 32], + iconAnchor: [16, 16], + tooltipAnchor: [0, -18], + popupAnchor: [0, -18], + }); + + const marker = L.marker([info.lat, info.lng], { icon }); + + const shortLabel = info.agentLabel.length > 24 ? info.agentLabel.slice(0, 22) + "…" : info.agentLabel; + marker.bindTooltip( + `
` + + `` + + `${shortLabel}
`, + { permanent: true, direction: "top", offset: [0, -10], className: "agent-label-tooltip", interactive: false } + ); + + const popupHtml = getAgentTooltipContent( + uuid, info.agentLabel, info.status, isOnline, info.label, info.lat, info.lng + ); + marker.bindPopup(popupHtml, { closeButton: false, className: "leaflet-custom-popup", offset: [0, -8] }); + marker.on("popupopen", () => { + const btn = document.getElementById(`btn-edit-${uuid}`); + if (btn) btn.addEventListener("click", () => onEditLocation(uuid, info.agentLabel)); + }); + + marker.addTo(group); + }); + + // Fit bounds + if (activeCoords.length > 1 && mounted) { + const L2 = L as any; + const bounds = L2.latLngBounds(activeCoords.map((c: any) => [c.lat, c.lng])); + mapInstanceRef.current.fitBounds(bounds, { padding: [60, 60], maxZoom: 14, animate: false }); + } + }; + + boot(); + return () => { + mounted = false; + // Properly destroy the Leaflet instance so React StrictMode double-invoke + // and HMR remounts don't leave orphaned panes that trigger _leaflet_pos errors. + if (mapInstanceRef.current) { + try { mapInstanceRef.current.remove(); } catch (_) {} + mapInstanceRef.current = null; + layerGroupRef.current = null; + tileLayerRef.current = null; + } + }; }, [agents, locations, flows, isDark]); if (mapError) { diff --git a/src/components/dashboard/KPICards.tsx b/src/components/dashboard/KPICards.tsx index 7efe02f..464a805 100644 --- a/src/components/dashboard/KPICards.tsx +++ b/src/components/dashboard/KPICards.tsx @@ -28,17 +28,17 @@ export function KPICards({ summary }: KPICardsProps) { return (
- - + +
- + Download - -
+ +
{summary ? fmtBytes(summary.bandwidth_down) : "0 B"}
{summary && ( @@ -55,17 +55,17 @@ export function KPICards({ summary }: KPICardsProps) { - - + +
- + Upload - -
+ +
{summary ? fmtBytes(summary.bandwidth_up) : "0 B"}
{summary && ( @@ -82,77 +82,82 @@ export function KPICards({ summary }: KPICardsProps) { - - + +
- + Devices - -
+ +
{summary?.total_devices?.toLocaleString() || "0"}
+
 
- - + +
- + Flows - -
+ +
{summary?.active_flows?.toLocaleString() || "0"}
- {summary && flowsPerHour > 0 && ( + {summary && flowsPerHour > 0 ? (
Speed: {flowsPerHour.toLocaleString()} /hr
+ ) : ( +
 
)}
- - + +
- + Threats - -
+ +
{summary?.total_threats?.toLocaleString() || "0"}
+
 
- - + +
- + Events - -
+ +
{summary?.total_events?.toLocaleString() || "0"}
+
 
diff --git a/src/components/help/ContextualHelpModal.tsx b/src/components/help/ContextualHelpModal.tsx index 0078191..b416944 100644 --- a/src/components/help/ContextualHelpModal.tsx +++ b/src/components/help/ContextualHelpModal.tsx @@ -12,7 +12,7 @@ interface ContextualHelpModalProps { onClose: () => void; } -function SectionBlock({ heading, content, columns, items }: PageGuide["sections"][0]) { +function SectionBlock({ heading, content, steps, columns, items }: PageGuide["sections"][0]) { const [open, setOpen] = useState(true); return ( @@ -37,6 +37,19 @@ function SectionBlock({ heading, content, columns, items }: PageGuide["sections"

{content}

)} + {steps && steps.length > 0 && ( +
    + {steps.map((step, i) => ( +
  1. + + {i + 1} + + {step} +
  2. + ))} +
+ )} + {columns && columns.length > 0 && (
diff --git a/src/components/help/HelpPageContent.tsx b/src/components/help/HelpPageContent.tsx index 547a571..be347b3 100644 --- a/src/components/help/HelpPageContent.tsx +++ b/src/components/help/HelpPageContent.tsx @@ -28,7 +28,7 @@ const ICON_MAP: Record = { lookup: , }; -function SectionBlock({ heading, content, columns, items }: PageGuide["sections"][0]) { +function SectionBlock({ heading, content, steps, columns, items }: PageGuide["sections"][0]) { const [open, setOpen] = useState(true); return (
@@ -46,6 +46,18 @@ function SectionBlock({ heading, content, columns, items }: PageGuide["sections" {content && (

{content}

)} + {steps && ( +
    + {steps.map((step, i) => ( +
  1. + + {i + 1} + + {step} +
  2. + ))} +
+ )} {columns && (
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 5cdd7b1..3d97372 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,34 +1,24 @@ // src/components/layout/Sidebar.tsx "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect } from "react"; import { cn } from "@/lib/utils"; -import { ChevronDown, Calendar, Activity } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { AccountSettingsModal } from "./AccountSettingsModal"; -import { useTimeFilter } from "@/contexts/TimeFilterContext"; import { getViewAsHeaders } from "@/lib/admin-api"; -import { NAVIGATION_GROUPS, TIME_OPTIONS } from "./SidebarData"; +import { NAVIGATION_GROUPS } from "./SidebarData"; import SidebarProfile from "./SidebarProfile"; import SidebarNotifications from "./SidebarNotifications"; - +import SidebarSiteSelector from "./SidebarSiteSelector"; +import SidebarTimeSelector from "./SidebarTimeSelector"; export function Sidebar() { const pathname = usePathname(); - const { timeRange, setTimeRange } = useTimeFilter(); - const [user, setUser] = useState<{ username?: string; account_name?: string; profile_picture?: string; role?: string; agent_uuid?: string } | null>(null); const [isAccountOpen, setIsAccountOpen] = useState(false); - const [isTimeDropdownOpen, setIsTimeDropdownOpen] = useState(false); // Time Filter dropdown const [intervalText, setIntervalText] = useState(""); - - const timeDropdownRef = useRef(null); - const selectedTimeOption = TIME_OPTIONS.find(opt => opt.value === timeRange) || TIME_OPTIONS[3]; - const [selectedSite, setSelectedSite] = useState("6681452d_9cae_4ff4_8ae8_0d504774265e"); - const [isSiteDropdownOpen, setIsSiteDropdownOpen] = useState(false); - const siteDropdownRef = useRef(null); useEffect(() => { fetch('/api/auth/me', { headers: getViewAsHeaders() }) @@ -58,150 +48,62 @@ export function Sidebar() { if (typeof window !== 'undefined') { const stored = localStorage.getItem('backone_site_uuid'); - if (stored) setSelectedSite(stored); + if (stored) { + setSelectedSite(stored); + const isNx = stored === 'd7902405_0dc2_458b_8584_ed4d24b64f24'; + const logoUrl = isNx ? '/nexus-logo.png' : '/backone-logo.png'; + const links = document.querySelectorAll("link[rel*='icon']"); + links.forEach(l => (l as HTMLLinkElement).href = logoUrl); + if (links.length === 0) { + const l = document.createElement('link'); + l.type = 'image/x-icon'; l.rel = 'shortcut icon'; l.href = logoUrl; + document.getElementsByTagName('head')[0].appendChild(l); + } + } } }, []); - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (timeDropdownRef.current && !timeDropdownRef.current.contains(event.target as Node)) setIsTimeDropdownOpen(false); - if (siteDropdownRef.current && !siteDropdownRef.current.contains(event.target as Node)) setIsSiteDropdownOpen(false); - } - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - const handleSiteSelect = (val: string) => { setSelectedSite(val); localStorage.setItem('backone_site_uuid', val); - setIsSiteDropdownOpen(false); window.location.reload(); }; - const getSiteLabel = (uuid: string) => { - if (uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24') return 'Nexus'; - return 'SIAB'; - }; + const isNexus = selectedSite === 'd7902405_0dc2_458b_8584_ed4d24b64f24'; return ( -
+
{/* Brand & Site Info */}
-
- BackOne Logo -
- - BackOne - - - Deep Package Inspection - -
- - DPI ENGINE: ACTIVE -
-
-
- - {/* Global Site Switcher Widget (Visible to SUPER_ADMIN, SOC_ANALYST, ENGINEER) */} - {user && ['SUPER_ADMIN', 'SOC_ANALYST', 'ENGINEER'].includes(user.role || '') && ( -
- - -
-
- - -
-
-
- )} - - {/* Global Time Filter Widget */} -
- - -
+ Logo -
- {TIME_OPTIONS.map((opt) => ( - - ))} -
+ "w-12 h-12 object-contain shrink-0 mb-3", + isNexus + ? "drop-shadow-[0_0_12px_rgba(6,182,212,0.45)]" + : "drop-shadow-[0_0_12px_rgba(168,85,247,0.45)]" + )} + /> + + Deep Package Inspection + +
+ + DPI ENGINE: ACTIVE
+ + +
{/* Navigation (Flexible Scrollable Container for Rule 9) */} @@ -215,7 +117,6 @@ export function Sidebar() {